From 2aa56c67d8259dd42d2fb2f90642c972abe73fe0 Mon Sep 17 00:00:00 2001 From: Jonathan Bangert Date: Mon, 19 Feb 2024 01:41:35 +0100 Subject: [PATCH] [Deezer] Allow user to manually input arl token, temporary fix (#1090) --- .../server/providers/deezer/__init__.py | 137 ++++++++++-------- .../server/providers/deezer/gw_client.py | 17 +-- .../server/providers/deezer/manifest.json | 4 +- requirements_all.txt | 2 +- 4 files changed, 83 insertions(+), 77 deletions(-) diff --git a/music_assistant/server/providers/deezer/__init__.py b/music_assistant/server/providers/deezer/__init__.py index 9937a08dd..eaf572d86 100644 --- a/music_assistant/server/providers/deezer/__init__.py +++ b/music_assistant/server/providers/deezer/__init__.py @@ -89,6 +89,7 @@ class DeezerCredentials: CONF_ACCESS_TOKEN = "access_token" +CONF_ARL_TOKEN = "arl_token" CONF_ACTION_AUTH = "auth" DEEZER_AUTH_URL = "https://connect.deezer.com/oauth/auth.php" RELAY_URL = "https://deezer.oauth.jonathanbangert.com/" @@ -98,7 +99,7 @@ class DeezerCredentials: DEEZER_APP_SECRET = app_var(7) -async def update_access_token( +async def get_access_token( app_id: str, app_secret: str, code: str, http_session: ClientSession ) -> str: """Update the access_token.""" @@ -134,15 +135,14 @@ async def get_config_entries( values: dict[str, ConfigValueType] | None = None, ) -> tuple[ConfigEntry, ...]: """Return Config entries to setup this provider.""" - # If the action is to launch oauth flow + # Action is to launch oauth flow if action == CONF_ACTION_AUTH: - # We use the AuthenticationHelper to authenticate + # Use the AuthenticationHelper to authenticate async with AuthenticationHelper(mass, values["session_id"]) as auth_helper: # type: ignore - callback_url = auth_helper.callback_url url = f"{DEEZER_AUTH_URL}?app_id={DEEZER_APP_ID}&redirect_uri={RELAY_URL}\ -&perms={DEEZER_PERMS}&state={callback_url}" +&perms={DEEZER_PERMS}&state={auth_helper.callback_url}" code = (await auth_helper.authenticate(url))["code"] - values[CONF_ACCESS_TOKEN] = await update_access_token( # type: ignore + values[CONF_ACCESS_TOKEN] = await get_access_token( # type: ignore DEEZER_APP_ID, DEEZER_APP_SECRET, code, mass.http_session ) @@ -157,6 +157,14 @@ async def get_config_entries( action_label="Authenticate with Deezer", value=values.get(CONF_ACCESS_TOKEN) if values else None, ), + ConfigEntry( + key=CONF_ARL_TOKEN, + type=ConfigEntryType.SECURE_STRING, + label="Arl token", + required=True, + description="See https://www.dumpmedia.com/deezplus/deezer-arl.html", + value=values.get(CONF_ARL_TOKEN) if values else None, + ), ) @@ -165,26 +173,30 @@ class DeezerProvider(MusicProvider): # pylint: disable=W0223 client: deezer.Client gw_client: GWClient - creds: DeezerCredentials + credentials: DeezerCredentials user: deezer.User async def handle_setup(self) -> None: """Set up the Deezer provider.""" - self.creds = DeezerCredentials( + self.credentials = DeezerCredentials( app_id=DEEZER_APP_ID, app_secret=DEEZER_APP_SECRET, access_token=self.config.get_value(CONF_ACCESS_TOKEN), # type: ignore ) self.client = deezer.Client( - app_id=self.creds.app_id, - app_secret=self.creds.app_secret, - access_token=self.creds.access_token, + app_id=self.credentials.app_id, + app_secret=self.credentials.app_secret, + access_token=self.credentials.access_token, ) self.user = await self.client.get_user() - self.gw_client = GWClient(self.mass.http_session, self.config.get_value(CONF_ACCESS_TOKEN)) + self.gw_client = GWClient( + self.mass.http_session, + self.config.get_value(CONF_ACCESS_TOKEN), + self.config.get_value(CONF_ARL_TOKEN), + ) await self.gw_client.setup() @property @@ -200,6 +212,7 @@ async def search( :param search_query: Search query. :param media_types: A list of media_types to include. All types if None. """ + # If no media_types are provided, search for all types if not media_types: media_types = [ MediaType.ARTIST, @@ -208,6 +221,7 @@ async def search( MediaType.PLAYLIST, ] + # Create a task for each media_type tasks = {} async with TaskGroup() as taskgroup: @@ -249,27 +263,32 @@ async def search( async def get_library_artists(self) -> AsyncGenerator[Artist, None]: """Retrieve all library artists from Deezer.""" - for artist in await self.client.get_user_artists(): + async for artist in await self.client.get_user_artists(): yield self.parse_artist(artist=artist) async def get_library_albums(self) -> AsyncGenerator[Album, None]: """Retrieve all library albums from Deezer.""" - for album in await self.client.get_user_albums(): + async for album in await self.client.get_user_albums(): yield self.parse_album(album=album) async def get_library_playlists(self) -> AsyncGenerator[Playlist, None]: """Retrieve all library playlists from Deezer.""" - for playlist in await self.user.get_playlists(): + async for playlist in await self.user.get_playlists(): yield self.parse_playlist(playlist=playlist) async def get_library_tracks(self) -> AsyncGenerator[Track, None]: """Retrieve all library tracks from Deezer.""" - for track in await self.client.get_user_tracks(): + async for track in await self.client.get_user_tracks(): yield self.parse_track(track=track, user_country=self.gw_client.user_country) async def get_artist(self, prov_artist_id: str) -> Artist: """Get full artist details by id.""" - return self.parse_artist(artist=await self.client.get_artist(artist_id=int(prov_artist_id))) + try: + return self.parse_artist( + artist=await self.client.get_artist(artist_id=int(prov_artist_id)) + ) + except deezer.exceptions.DeezerErrorResponse as error: + self.logger.warning("Failed getting artist: %s", error) async def get_album(self, prov_album_id: str) -> Album: """Get full album details by id.""" @@ -280,30 +299,34 @@ async def get_album(self, prov_album_id: str) -> Album: async def get_playlist(self, prov_playlist_id: str) -> Playlist: """Get full playlist details by id.""" - return self.parse_playlist( - playlist=await self.client.get_playlist(playlist_id=int(prov_playlist_id)), - ) + try: + return self.parse_playlist( + playlist=await self.client.get_playlist(playlist_id=int(prov_playlist_id)), + ) + except deezer.exceptions.DeezerErrorResponse as error: + self.logger.warning("Failed getting playlist: %s", error) async def get_track(self, prov_track_id: str) -> Track: """Get full track details by id.""" - return self.parse_track( - track=await self.client.get_track(track_id=int(prov_track_id)), - user_country=self.gw_client.user_country, - ) + try: + return self.parse_track( + track=await self.client.get_track(track_id=int(prov_track_id)), + user_country=self.gw_client.user_country, + ) + except deezer.exceptions.DeezerErrorResponse as error: + self.logger.warning("Failed getting track: %s", error) async def get_album_tracks(self, prov_album_id: str) -> list[AlbumTrack]: - """Get all tracks in a album.""" + """Get all tracks in an album.""" album = await self.client.get_album(album_id=int(prov_album_id)) - result = [] - for count, deezer_track in enumerate(await album.get_tracks(), start=1): - result.append( - self.parse_track( - track=deezer_track, - user_country=self.gw_client.user_country, - extra_init_kwargs={"disc_number": 0, "track_number": count}, - ) + return [ + self.parse_track( + track=deezer_track, + user_country=self.gw_client.user_country, + extra_init_kwargs={"disc_number": 0, "track_number": count + 1}, ) - return result + for count, deezer_track in enumerate(await album.get_tracks()) + ] async def get_playlist_tracks( self, prov_playlist_id: str @@ -322,18 +345,14 @@ async def get_playlist_tracks( async def get_artist_albums(self, prov_artist_id: str) -> list[Album]: """Get albums by an artist.""" artist = await self.client.get_artist(artist_id=int(prov_artist_id)) - albums = [] - for album in await artist.get_albums(): - albums.append(self.parse_album(album=album)) - return albums + return [self.parse_album(album=album) async for album in await artist.get_albums()] async def get_artist_toptracks(self, prov_artist_id: str) -> list[Track]: """Get top 50 tracks of an artist.""" artist = await self.client.get_artist(artist_id=int(prov_artist_id)) - top_tracks = await artist.get_top(limit=50) return [ self.parse_track(track=track, user_country=self.gw_client.user_country) - async for track in top_tracks + async for track in await artist.get_top(limit=50) ] async def library_add(self, prov_item_id: str, media_type: MediaType) -> bool: @@ -390,14 +409,14 @@ async def recommendations(self) -> list[Track]: ] async def add_playlist_tracks(self, prov_playlist_id: str, prov_track_ids: list[str]) -> None: - """Add tra ck(s) to playlist.""" + """Add track(s) to playlist.""" playlist = await self.client.get_playlist(int(prov_playlist_id)) await playlist.add_tracks(tracks=[int(i) for i in prov_track_ids]) async def remove_playlist_tracks( self, prov_playlist_id: str, positions_to_remove: tuple[int, ...] ) -> None: - """Remove track(s) to playlist.""" + """Remove track(s) from playlist.""" playlist_track_ids = [] async for track in self.get_playlist_tracks(prov_playlist_id): if track.position in positions_to_remove: @@ -513,7 +532,7 @@ def parse_metadata_artist(self, artist: deezer.Artist) -> MediaItemMetadata: ### PARSING FUNCTIONS ### def parse_artist(self, artist: deezer.Artist) -> Artist: - """Parse the deezer-python artist to a MASS artist.""" + """Parse the deezer-python artist to a Music Assistant artist.""" return Artist( item_id=str(artist.id), provider=self.domain, @@ -531,7 +550,7 @@ def parse_artist(self, artist: deezer.Artist) -> Artist: ) def parse_album(self, album: deezer.Album) -> Album: - """Parse the deezer-python album to a MASS album.""" + """Parse the deezer-python album to a Music Assistant album.""" return Album( album_type=AlbumType(album.type), item_id=str(album.id), @@ -558,7 +577,7 @@ def parse_album(self, album: deezer.Album) -> Album: ) def parse_playlist(self, playlist: deezer.Playlist) -> Playlist: - """Parse the deezer-python playlist to a MASS playlist.""" + """Parse the deezer-python playlist to a Music Assistant playlist.""" creator = self.get_playlist_creator(playlist) return Playlist( item_id=str(playlist.id), @@ -582,7 +601,7 @@ def parse_playlist(self, playlist: deezer.Playlist) -> Playlist: ) def get_playlist_creator(self, playlist: deezer.Playlist): - """See https://twitter.com/Un10cked/status/1682709413889540097.""" + """On playlists, the creator is called creator, elsewhere it's called user.""" if hasattr(playlist, "creator"): return playlist.creator return playlist.user @@ -593,7 +612,7 @@ def parse_track( user_country: str, extra_init_kwargs: dict[str, Any] | None = None, ) -> Track | PlaylistTrack | AlbumTrack: - """Parse the deezer-python track to a MASS track.""" + """Parse the deezer-python track to a Music Assistant track.""" if hasattr(track, "artist"): artist = ItemMapping( media_type=MediaType.ARTIST, @@ -658,11 +677,9 @@ async def search_and_parse_tracks( """Search for tracks and parse them.""" deezer_tracks = await self.client.search(query=query, limit=limit) tracks = [] - index = 0 - async for track in deezer_tracks: + async for index, track in enumerate(deezer_tracks): tracks.append(self.parse_track(track, user_country)) - index += 1 - if index >= limit: + if index == limit: return tracks return tracks @@ -670,11 +687,9 @@ async def search_and_parse_artists(self, query: str, limit: int = 20) -> list[Ar """Search for artists and parse them.""" deezer_artist = await self.client.search_artists(query=query, limit=limit) artists = [] - index = 0 - async for artist in deezer_artist: + async for index, artist in enumerate(deezer_artist): artists.append(self.parse_artist(artist)) - index += 1 - if index >= limit: + if index == limit: return artists return artists @@ -682,11 +697,9 @@ async def search_and_parse_albums(self, query: str, limit: int = 20) -> list[Alb """Search for album and parse them.""" deezer_albums = await self.client.search_albums(query=query, limit=limit) albums = [] - index = 0 - async for album in deezer_albums: + async for index, album in enumerate(deezer_albums): albums.append(self.parse_album(album)) - index += 1 - if index >= limit: + if index == limit: return albums return albums @@ -694,11 +707,9 @@ async def search_and_parse_playlists(self, query: str, limit: int = 20) -> list[ """Search for playlists and parse them.""" deezer_playlists = await self.client.search_playlists(query=query, limit=limit) playlists = [] - index = 0 - async for playlist in deezer_playlists: + async for index, playlist in enumerate(deezer_playlists): playlists.append(self.parse_playlist(playlist)) - index += 1 - if index >= limit: + if index == limit: return playlists return playlists diff --git a/music_assistant/server/providers/deezer/gw_client.py b/music_assistant/server/providers/deezer/gw_client.py index 1d16f119a..640744053 100644 --- a/music_assistant/server/providers/deezer/gw_client.py +++ b/music_assistant/server/providers/deezer/gw_client.py @@ -27,6 +27,7 @@ class DeezerGWError(BaseException): class GWClient: """The GWClient class can be used to perform actions not being of the official API.""" + _arl_token: str _api_token: str _gw_csrf_token: str | None _license: str | None @@ -37,22 +38,16 @@ class GWClient: ] user_country: str - def __init__(self, session: ClientSession, api_token: str) -> None: + def __init__(self, session: ClientSession, api_token: str, arl_token: str) -> None: """Provide an aiohttp ClientSession and the deezer api_token.""" self._api_token = api_token + self._arl_token = arl_token self.session = session - async def _get_cookie(self) -> None: - await self.session.get( - "https://api.deezer.com/platform/generic/track/3135556", - headers={"Authorization": f"Bearer {self._api_token}", "User-Agent": USER_AGENT_HEADER}, - ) - json_response = await self._gw_api_call("user.getArl", False, http_method="GET") - arl = json_response.get("results") - + async def _set_cookie(self) -> None: cookie = Morsel() - cookie.set("arl", arl, arl) + cookie.set("arl", self._arl_token, self._arl_token) cookie.domain = ".deezer.com" cookie.path = "/" cookie.httponly = {"HttpOnly": True} @@ -85,7 +80,7 @@ async def _update_user_data(self) -> None: async def setup(self) -> None: """Call this to let the client get its cookies, license and tokens.""" - await self._get_cookie() + await self._set_cookie() await self._update_user_data() async def _get_license(self): diff --git a/music_assistant/server/providers/deezer/manifest.json b/music_assistant/server/providers/deezer/manifest.json index c0111f2dd..5e69128b3 100644 --- a/music_assistant/server/providers/deezer/manifest.json +++ b/music_assistant/server/providers/deezer/manifest.json @@ -3,8 +3,8 @@ "domain": "deezer", "name": "Deezer", "description": "Support for the Deezer streaming provider in Music Assistant.", - "codeowners": ["@Un10ck3d", "@micha91"], + "codeowners": ["@arctixdev", "@micha91"], "documentation": "https://music-assistant.github.io/music-providers/deezer/", - "requirements": ["git+https://github.com/music-assistant/deezer-python-async@v0.1.2", "pycryptodome==3.20.0"], + "requirements": ["git+https://github.com/music-assistant/deezer-python-async@v0.1.3", "pycryptodome==3.20.0"], "multi_instance": true } diff --git a/requirements_all.txt b/requirements_all.txt index d22372ec4..66d4561cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ cryptography==42.0.2 defusedxml==0.7.1 faust-cchardet>=2.1.18 git+https://github.com/MarvinSchenkel/pytube.git -git+https://github.com/music-assistant/deezer-python-async@v0.1.2 +git+https://github.com/music-assistant/deezer-python-async@v0.1.3 hass-client==1.0.1 ifaddr==0.2.0 jellyfin_apiclient_python==1.9.2