Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search, view tracks websocket endpoints, service to like media. #489

Merged
merged 45 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
fbc89e9
Adds a generic get method
Nov 28, 2024
86beb1e
Adds a generic get method. Which abuses the _get method of spotipy
Nov 28, 2024
6ee3bcc
Add missing import
Nov 28, 2024
c39a817
Generic get => view_handler, adds a search endpoint
Nov 29, 2024
2bc9eb8
Merge branch 'dev' of https://github.com/fondberg/spotcast into gener…
Nov 29, 2024
df4727a
Fix syntax
Nov 29, 2024
891eb4a
Fix url
Nov 29, 2024
5f7decc
Fix mistakes
Nov 29, 2024
3dbd616
Add account in search handler
Nov 29, 2024
de2ff8f
Remove locale in search
Nov 29, 2024
104848f
song => track
Nov 29, 2024
adfe849
Add HREF to search result.
Nov 29, 2024
5cb754c
Use already existing search method. Added comments as explanation
Nov 30, 2024
c91a8ee
Remove unused partial import
Nov 30, 2024
9855836
Align search with new return time.
Nov 30, 2024
c699e59
Merge branch 'dev' of https://github.com/fondberg/spotcast into gener…
Dec 4, 2024
885eeaa
pr comments
Dec 7, 2024
f0fd2d5
Merge branch 'dev' of https://github.com/fondberg/spotcast into gener…
Dec 7, 2024
590eb15
Merge branch 'generic_get_ws' of https://github.com/mikevanes/spotcas…
Dec 7, 2024
1d7199b
Added description to view and search and trackshandler
Dec 10, 2024
3172652
href => uri
Dec 10, 2024
4f2bc8e
Adds a method to like a song.
Dec 10, 2024
62b8d97
Expire liked songs cache after adding liked songs
Dec 10, 2024
e2730ca
Adds websocket to retrieve liked media.
Dec 10, 2024
d55dfc6
Merge branch 'dev' into generic_get_ws
fcusson Dec 10, 2024
a2b2d5d
refactoring unit test
fcusson Dec 11, 2024
b6d66a7
full unit test on websocket handlers
fcusson Dec 11, 2024
55cf26d
added service definition
fcusson Dec 11, 2024
73f8e8f
Merge pull request #1 from fcusson/feature/generic_get_ws
Dec 11, 2024
acac78e
Adds documentation
Dec 12, 2024
39a8be6
typo
fcusson Dec 12, 2024
1b79898
moved config and option flow to config_flow.py for hassfest validation
fcusson Dec 12, 2024
1ff1a9e
tweaked github actions
fcusson Dec 12, 2024
78d4ce7
typo
fcusson Dec 12, 2024
3494ddf
reorg selector strings to fit documentation
fcusson Dec 12, 2024
c82db7f
fixed issue with selector definition
fcusson Dec 12, 2024
9cb29ce
fixed config flow and options flow test with new python module structure
fcusson Dec 12, 2024
d475bb7
added pylint to dev requirements
fcusson Dec 12, 2024
335c33e
fixed option flow for HASS2024.12
fcusson Dec 12, 2024
e8f66e0
bumped minimal version due to breaking change in 2024.12 for option f…
fcusson Dec 12, 2024
f9fd6c3
added pythonpath to linter definition
fcusson Dec 12, 2024
74fa79f
moved location of pythonpath definition
fcusson Dec 12, 2024
097d39d
typo
fcusson Dec 12, 2024
d85a0d3
Fix documentation
Dec 14, 2024
f3f6407
Merge pull request #2 from fcusson/feature/generic_get_ws
Dec 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions custom_components/spotcast/spotify/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
TokenError,
)


LOGGER = getLogger(__name__)


Expand All @@ -51,7 +52,7 @@ class SpotifyAccount:

Properties:
- id(str): the identifier of the account
- name(str): the dusplay name for the account
- name(str): the display name for the account
- profile(dict): the full profile dictionary of the account
- country(str): the country code where the account currently
is.
Expand Down Expand Up @@ -86,6 +87,7 @@ class SpotifyAccount:
- async_liked_songs
- async_repeat
- async_set_volume
- async_view

Functions:
- async_from_config_entry
Expand Down Expand Up @@ -885,7 +887,7 @@ async def async_categories(
async def async_category_playlists(
self,
category_id: str,
limit: int = None,
limit: int,
mikevanes marked this conversation as resolved.
Show resolved Hide resolved
) -> list[str]:
"""Fetches the playlist associated with a browse category

Expand Down Expand Up @@ -914,6 +916,47 @@ async def async_category_playlists(

return playlists

async def async_view(
self,
url: str,
limit: int,
locale: str,
) -> list:
"""Fetches a view based on url.

Args:
- url(str): The url of the view to fetch (e.g., 'made-for-x').
- limit(int): The maximum number of playlists to retrieve.
- locale(str): The locale for the request (optional).

Returns:
- list: A list of playlists.
"""

# Internal method, so that the _get method can be called
# As the pager parameters do not allign with the _get parameters
# Spotipy _get is defined as: def _get(self, url, args=None, payload=None, **kwargs)
def fetch_page(limit, offset):
fcusson marked this conversation as resolved.
Show resolved Hide resolved
params = {
"content_limit": limit,
"locale": locale,
"platform": "web",
"types": "album,playlist,artist,show,station",
"limit": limit,
"offset": offset,
}

return self._internal_cont._get(url, None, **params)

await self.async_ensure_tokens_valid()

return await self._async_pager(
function=fetch_page,
limit=25, # This is the max amount per call
max_items=limit,
sub_layer="content",
)

async def _async_get_count(
self,
function: callable,
Expand Down
10 changes: 10 additions & 0 deletions custom_components/spotcast/websocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
player_handler,
cast_devices_handler,
categories_handler,
view_handler,
search_handler
)

WEBSOCKET_ENDPOINTS = MappingProxyType({
Expand Down Expand Up @@ -40,6 +42,14 @@
"handler": categories_handler.async_get_categories,
"schema": categories_handler.SCHEMA,
},
view_handler.ENDPOINT: {
"handler": view_handler.async_view_handler,
"schema": view_handler.SCHEMA,
},
search_handler.ENDPOINT: {
"handler": search_handler.async_search_handler,
"schema": search_handler.SCHEMA,
},
})


Expand Down
76 changes: 76 additions & 0 deletions custom_components/spotcast/websocket/search_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import voluptuous as vol
from custom_components.spotcast.spotify.search_query import SearchQuery
from homeassistant.helpers import config_validation as cv
from homeassistant.core import HomeAssistant
from homeassistant.components.websocket_api import ActiveConnection

from custom_components.spotcast.utils import get_account_entry, search_account
from custom_components.spotcast.spotify.account import SpotifyAccount
from custom_components.spotcast.websocket.utils import websocket_wrapper
from custom_components.spotcast.spotify.utils import select_image_url

ENDPOINT = "spotcast/search"
SCHEMA = vol.Schema(
{
vol.Required("id"): cv.positive_int,
vol.Required("type"): ENDPOINT,
vol.Required("query"): cv.string,
vol.Optional("searchType"): cv.string, # Playlist or song, default playlist
vol.Optional("limit"): cv.positive_int,
vol.Optional("account"): cv.string,
}
)


@websocket_wrapper
async def async_search_handler(
hass: HomeAssistant, connection: ActiveConnection, msg: dict
):
"""Searches for playlists or tracks.

Args:
- hass (HomeAssistant): The Home Assistant instance.
- connection (ActiveConnection): The active WebSocket connection.
- msg (dict): The message received through the WebSocket API.
"""
account_id = msg.get("account")
query = msg.get("query")
searchType = msg.get("searchType", "playlist")
limit = msg.get("limit", 10)

account: SpotifyAccount

if account_id is None:
entry = get_account_entry(hass)
account_id = entry.entry_id
account = await SpotifyAccount.async_from_config_entry(hass, entry)
else:
account = search_account(hass, account_id)

query = SearchQuery(search=query, item_type=searchType)

results = await account.async_search(
query,
limit,
)

formatted_results = [
{
"id": result.get("id", None),
"name": result["name"],
"href": result["href"],
"icon": result["images"][0]["url"]
if "images" in result and len(result["images"]) > 0
else None,
}
for result in results
]

connection.send_result(
msg["id"],
{
"total": len(formatted_results),
"account": account_id,
f"{searchType}s": formatted_results,
},
)
79 changes: 79 additions & 0 deletions custom_components/spotcast/websocket/view_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.core import HomeAssistant
from homeassistant.components.websocket_api import ActiveConnection

from custom_components.spotcast.utils import get_account_entry, search_account
from custom_components.spotcast.spotify.account import SpotifyAccount
from custom_components.spotcast.websocket.utils import websocket_wrapper
from custom_components.spotcast.spotify.utils import select_image_url

ENDPOINT = "spotcast/view"
SCHEMA = vol.Schema(
{
vol.Required("id"): cv.positive_int,
vol.Required("type"): ENDPOINT,
vol.Required("url"): cv.string,
vol.Optional("limit"): cv.positive_int,
vol.Optional("locale"): cv.string,
vol.Optional("platform"): cv.string,
vol.Optional("types"): cv.string,
vol.Optional("account"): cv.string,
}
)


@websocket_wrapper
async def async_view_handler(
hass: HomeAssistant, connection: ActiveConnection, msg: dict
):
"""Gets a list of playlists from a specified account.

Args:
- hass (HomeAssistant): The Home Assistant instance.
- connection (ActiveConnection): The active WebSocket connection.
- msg (dict): The message received through the WebSocket API.
"""
account_id = msg.get("account")
url = msg.get("url")
limit = msg.get("limit", 10)
locale = msg.get("locale", "en_US")

account: SpotifyAccount

if account_id is None:
entry = get_account_entry(hass)
account_id = entry.entry_id
account = await SpotifyAccount.async_from_config_entry(hass, entry)
else:
account = search_account(hass, account_id)

# prepend views/ to the url
url = f"views/{url}"

raw_playlists = await account.async_view(
url=url,
limit=limit,
locale=locale,
)

formatted_playlists = [
{
"id": playlist.get("id", None),
"name": playlist["name"],
"href": playlist["href"],
"icon": playlist["images"][0]["url"]
if "images" in playlist and len(playlist["images"]) > 0
else None,
}
for playlist in raw_playlists
]

connection.send_result(
msg["id"],
{
"total": len(formatted_playlists),
"account": account_id,
"playlists": formatted_playlists,
},
)