From: Giel Janssens Date: Mon, 1 Apr 2024 15:08:04 +0000 (+0200) Subject: Soundcloudpy pip (#1191) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=70516f3ad5eacc1e68a219cc7050dda4a116797a;p=music-assistant-server.git Soundcloudpy pip (#1191) --- diff --git a/music_assistant/server/providers/soundcloud/__init__.py b/music_assistant/server/providers/soundcloud/__init__.py index f3dd1e06..5d2385f6 100644 --- a/music_assistant/server/providers/soundcloud/__init__.py +++ b/music_assistant/server/providers/soundcloud/__init__.py @@ -6,6 +6,8 @@ import asyncio import time from typing import TYPE_CHECKING +from soundcloudpy import SoundcloudAsyncAPI + from music_assistant.common.helpers.util import parse_title_and_version from music_assistant.common.models.config_entries import ConfigEntry, ConfigValueType from music_assistant.common.models.enums import ConfigEntryType, ProviderFeature @@ -31,8 +33,6 @@ from music_assistant.server.helpers.audio import ( ) from music_assistant.server.models.music_provider import MusicProvider -from .soundcloudpy.asyncsoundcloudpy import SoundcloudAsyncAPI - CONF_CLIENT_ID = "client_id" CONF_AUTHORIZATION = "authorization" diff --git a/music_assistant/server/providers/soundcloud/manifest.json b/music_assistant/server/providers/soundcloud/manifest.json index dbef8b6c..c7e67910 100644 --- a/music_assistant/server/providers/soundcloud/manifest.json +++ b/music_assistant/server/providers/soundcloud/manifest.json @@ -3,11 +3,8 @@ "domain": "soundcloud", "name": "Soundcloud", "description": "Support for the Soundcloud streaming provider in Music Assistant.", - "codeowners": [ - "@domanchi", - "@gieljnssns" - ], - "requirements": [], + "codeowners": ["@domanchi", "@gieljnssns"], + "requirements": ["soundcloudpy==0.1.0"], "documentation": "https://music-assistant.io/music-providers/soundcloud/", "multi_instance": true } diff --git a/music_assistant/server/providers/soundcloud/soundcloudpy/asyncsoundcloudpy.py b/music_assistant/server/providers/soundcloud/soundcloudpy/asyncsoundcloudpy.py deleted file mode 100644 index 81709c04..00000000 --- a/music_assistant/server/providers/soundcloud/soundcloudpy/asyncsoundcloudpy.py +++ /dev/null @@ -1,366 +0,0 @@ -""" -Async helpers for connecting to the Soundcloud API. - -This file is based on soundcloudpy from Naím Rodríguez https://github.com/naim-prog -Original package https://github.com/naim-prog/soundcloud-py -""" # noqa: INP001 - -from __future__ import annotations - -from typing import TYPE_CHECKING - -BASE_URL = "https://api-v2.soundcloud.com" - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from aiohttp.client import ClientSession - -# TODO: Fix docstring -# TODO: Add annotations - - -class SoundcloudAsyncAPI: - """Soundcloud.""" - - session: ClientSession | None = None - - def __init__(self, auth_token: str, client_id: str, http_session: ClientSession) -> None: - """Initialize SoundcloudAsyncAPI.""" - self.o_auth = auth_token - self.client_id = client_id - self.http_session = http_session - self.headers = None - self.app_version = None - self.firefox_version = None - self.request_timeout: float = 8.0 - - async def get(self, url, headers=None, params=None): - """Async get.""" - async with self.http_session.get(url=url, params=params, headers=headers) as response: - return await response.json() - - async def login(self) -> None: - """Login to soundcloud.""" - if len(self.client_id) != 32: - msg = "Not valid client id" - raise ValueError(msg) - - # To get the last version of Firefox to prevent some type of deprecated version - json_versions = await self.get( - # self, - url="https://product-details.mozilla.org/1.0/firefox_versions.json", - ) - - self.firefox_version = dict(json_versions).get("LATEST_FIREFOX_VERSION") - - #: Default headers that work properly for the API - #: User-Agent as if it was requested through Firefox Browser - self.headers = { - "Authorization": self.o_auth, - "Accept": "application/json", - "User-Agent": ( - f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{self.firefox_version})" - f" Gecko/20100101 Firefox/{self.firefox_version}" - ), - } - - # Version of soundcloud app - app_json = await self.get(url="https://soundcloud.com/versions.json") - self.app_version = dict(app_json).get("app") - - # ---------------- USER ---------------- - - async def get_account_details(self): - """Get account details.""" - return await self.get(url=f"{BASE_URL}/me", headers=self.headers) - - async def get_user_details(self, user_id): - """:param user_id: id of the user requested""" - return await self.get( - url=f"{BASE_URL}/users/{user_id}?client_id={self.client_id}", - headers=self.headers, - ) - - async def get_following(self, own_user_id, limit=120): - """:param limit: max numbers of following accounts""" - return await self.get( - f"{BASE_URL}/users/{own_user_id}/followings?client_id={self.client_id}" - f"&limit={limit}&offset=0&linked_partitioning=1&app_version={self.app_version}", - headers=self.headers, - ) - - async def get_recommended_users(self, limit=5): - """:param limit: max numbers of follower accounts to get""" - return await self.get( - f"{BASE_URL}/me/suggested/users/who_to_follow?view=recommended-first&client_id=" - f"{self.client_id}&limit={limit}&offset=0&linked_partitioning=1&app_version=" - f"{self.app_version}", - headers=self.headers, - ) - - # ---------------- TRACKS ---------------- - - async def get_last_track_info(self, limit=1): - """:param limit: number of last tracks reproduced""" - return await self.get( - f"{BASE_URL}/me/play-history/tracks?client_id={self.client_id}&limit={limit}", - headers=self.headers, - ) - - async def get_track_likes_users(self, track_id): - """:param track_id: track id""" - return await self.get( - f"{BASE_URL}/tracks/{track_id}/likers?client_id={self.client_id}", - headers=self.headers, - ) - - async def get_track_details(self, track_id): - """:param track_id: track id""" - return await self.get( - f"{BASE_URL}/tracks?ids={track_id}&client_id={self.client_id}", - headers=self.headers, - ) - - async def get_tracks_liked(self, limit: int = 0) -> AsyncGenerator[int, None]: - """Obtain the authenticated user's liked tracks. - - :param limit: number of tracks to get. if 0, will fetch all tracks. - :returns: list of track ids liked by the current user - """ - query_limit = limit - if query_limit == 0: - # NOTE(2023-11-11): At the time of writing, soundcloud does not look like it caps - # the limit. However, we still implement pagination for future proofing. - query_limit = 100 - - num_items = 0 - async for track in self._paginated_query( - "/me/track_likes/ids", params={"limit": str(query_limit)} - ): - num_items += 1 - if limit > 0 and num_items >= limit: - return - - yield track - - async def get_track_by_genre_recent(self, genre, limit=10): - """Get track by genre recent. - - :param genre: string of the genre to get tracks - :param limit: limit of playlists to get - """ - return await self.get( - f"{BASE_URL}/recent-tracks/{genre}?client_id={self.client_id}" - f"&limit={limit}&offset=0&linked_partitioning=1&app_version={self.app_version}", - headers=self.headers, - ) - - async def get_track_by_genre_popular(self, genre, limit=10): - """Get track by genre popular. - - :param genre: string of the genre to get tracks - :param limit: limit of playlists to get - """ - return await self.get( - f"{BASE_URL}/search/tracks?q=&filter.genre_or_tag={genre}&sort=popular&client_id=" - f"{self.client_id}&limit={limit}&offset=0&linked_partitioning=1&app_version=" - f"{self.app_version}", - headers=self.headers, - ) - - async def get_tracks_from_user(self, user_id, limit=10): - """Get tracks from user. - - :param user_id: id of the user to get his tracks - :param limit: number of tracks to get from user - """ - return await self.get( - f"{BASE_URL}/users/{user_id}/tracks?representation=&client_id=" - f"{self.client_id}&limit={limit}&offset=0&linked_partitioning=1&app_version=" - f"{self.app_version}", - headers=self.headers, - ) - - async def get_popular_tracks_user(self, user_id, limit=12): - """Get popular track from user. - - :param user_id: id of the user to get his tracks - :param limit: number of tracks to get from user - """ - return await self.get( - f"{BASE_URL}/users/{user_id}/toptracks?client_id={self.client_id}" - f"&limit={limit}&offset=0&linked_partitioning=1&app_version={self.app_version}", - headers=self.headers, - ) - - # ---------------- PLAYLISTS ---------------- - - async def get_account_playlists(self): - """Get account playlists, albums and stations.""" - # NOTE: This returns all track lists in reverse chronological order (most recent first). - async for playlist in self._paginated_query("/me/library/all"): - yield playlist - - async def get_playlist_details(self, playlist_id): - """:param playlist_id: playlist id""" - return await self.get( - f"{BASE_URL}/playlists/{playlist_id}?representation=full&client_id={self.client_id}", - headers=self.headers, - ) - - async def get_playlists_by_genre(self, genre, limit=10): - """Get playlists by genre. - - :param genre: string of the genre to get tracks - :param limit: limit of playlists to get - """ - return await self.get( - f"{BASE_URL}/playlists/discovery?tag={genre}&client_id=" - f"{self.client_id}&limit={limit}&offset=0&linked_partitioning=1&" - f"app_version={self.app_version}", - headers=self.headers, - ) - - async def get_playlists_from_user(self, user_id, limit=10): - """Get playlists from user. - - :param user_id: user id to get his playlists - :param limit: limit of playlists to get - """ - return await self.get( - f"{BASE_URL}/users/{user_id}/playlists_without_albums?client_id=" - f"{self.client_id}&limit={limit}&offset=0&linked_partitioning=1&" - f"app_version={self.app_version}", - headers=self.headers, - ) - - # ---------------- MISCELLANEOUS ---------------- - - async def get_recommended(self, track_id: str, limit: int = 10): - """:param track_id: track id to get recommended tracks from this""" - return await self.get( - f"{BASE_URL}/tracks/{track_id}/related?client_id={self.client_id}", - headers=self.headers, - ) - - async def get_stream_url(self, track_id): - """:param track_id: track id""" - full_json = await self.get_track_details(track_id) - media_url = full_json[0]["media"]["transcodings"][0]["url"] - track_auth = full_json[0]["track_authorization"] - stream_url = f"{media_url}?client_id={self.client_id}&track_authorization={track_auth}" - req = await self.get( - stream_url, - headers=self.headers, - ) - - return dict(req).get("url") - - async def get_comments_track(self, track_id, limit=100): - """Get track comments. - - :param track_id: track id for get the comments from - :param limit: limit of tracks to get - :add: with "next_href" in the return json you can keep getting more comments than limit - """ - return await self.get( - f"{BASE_URL}/tracks/{track_id}/comments?threaded=0&filter_replies=" - f"1&client_id={self.client_id}&limit={limit}&offset=0&linked_partitioning=" - f"1&app_version={self.app_version}", - headers=self.headers, - ) - - async def get_mixed_selection(self, limit=5): - """:param limit: limit of recommended playlists make for you""" - return await self.get( - f"{BASE_URL}/mixed-selections?variant_ids=&client_id=" - f"{self.client_id}&limit={limit}&app_version={self.app_version}", - headers=self.headers, - ) - - async def search(self, query_string, limit=25): - """Search. - - :param query_string: string to search on soundcloud for tracks - :param limit: limit of recommended playlists make for you - """ - return await self.get( - f"{BASE_URL}/search?q={query_string}&variant_ids=&facet=model&client_id=" - f"{self.client_id}&limit={limit}&offset=0&app_version={self.app_version}", - headers=self.headers, - ) - - async def get_subscribe_feed(self, limit=10): - """Get subscribe feed.""" - return await self.get( - f"{BASE_URL}/stream?offset=10&limit={limit}&promoted_playlist=true&client_id" - f"={self.client_id}&app_version={self.app_version}", - headers=self.headers, - ) - - async def get_album_from_user(self, user_id, limit=5): - """Get album from user. - - :param user_id: user to get the albums from - :param limit: numbers of albums to get from the user - """ - return await self.get( - f"{BASE_URL}/users/{user_id}/albums?client_id={self.client_id}" - f"&limit={limit}&offset=0&linked_partitioning=1&app_version={self.app_version}", - headers=self.headers, - ) - - async def get_all_feed_user(self, user_id, limit=20): - """Get all feed from user. - - :param user_id: user to get the albums from - :param limit: numbers of items to get from the user's feed - """ - return await self.get( - f"{BASE_URL}/stream/users/{user_id}?client_id={self.client_id}" - f"&limit={limit}&offset=0&linked_partitioning=1&app_version={self.app_version}", - headers=self.headers, - ) - - async def _paginated_query( - self, - path: str, - params: dict[str, str] | None = None, - ) -> AsyncGenerator[list[dict[str, any]], None]: - """Paginate response queries. - - Soundcloud paginates its queries using the same pattern. As such, we leverage the - same pattern to implement a pagination pattern to iterate over their APIs. - - :param path: endpoint to query - :param params: key-value pairs to use as query parameters when constructing the initial URL - """ - if params is None: - params = {} - - url = f"{BASE_URL}{path}?client_id={self.client_id}&app_version={self.app_version}" - for k, v in params.items(): - url += f"&{k}={v}" - - while True: - response = await self.get(url, headers=self.headers) - - # Sanity check. - if "collection" not in response: - msg = "Unexpected Soundcloud API response" - raise RuntimeError(msg) - - for item in response["collection"]: - yield item - - # Handle case when results requested exceeds number of actual results. - if int(params.get("limit", 0)) and len(response["collection"]) < int(params["limit"]): - return - - try: - url = response["next_href"] - if not url: - return - except KeyError: - return diff --git a/pyproject.toml b/pyproject.toml index 3e07a3ad..3d7f2e54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ server = [ "music-assistant-frontend==2.4.1", "pillow==10.2.0", "pyatv==0.14.5", + "soundcloudpy==0.1.0", "unidecode==1.3.8", "xmltodict==0.13.0", "orjson==3.9.15", diff --git a/requirements_all.txt b/requirements_all.txt index 87de7d65..023294ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,6 +35,7 @@ shortuuid==1.0.13 snapcast==2.3.6 soco==0.30.2 sonos-websocket==0.1.3 +soundcloudpy==0.1.0 tidalapi==0.7.5 unidecode==1.3.8 xmltodict==0.13.0