[Deezer] Allow user to manually input arl token, temporary fix (#1090)
authorJonathan Bangert <jonathan@bangert.dk>
Mon, 19 Feb 2024 00:41:35 +0000 (01:41 +0100)
committerGitHub <noreply@github.com>
Mon, 19 Feb 2024 00:41:35 +0000 (01:41 +0100)
music_assistant/server/providers/deezer/__init__.py
music_assistant/server/providers/deezer/gw_client.py
music_assistant/server/providers/deezer/manifest.json
requirements_all.txt

index 9937a08ddaa593b5997b4c1de16ebf4c62109ab8..eaf572d86746e921607b40addc63bc84bc615814 100644 (file)
@@ -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 @@ DEEZER_APP_ID = app_var(6)
 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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         :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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
                 MediaType.PLAYLIST,
             ]
 
+        # Create a task for each media_type
         tasks = {}
 
         async with TaskGroup() as taskgroup:
@@ -249,27 +263,32 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
 
     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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
 
     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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
     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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         ]
 
     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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
 
     ### 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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         )
 
     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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         )
 
     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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         )
 
     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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         """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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         """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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         """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 @@ class DeezerProvider(MusicProvider):  # pylint: disable=W0223
         """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
 
index 1d16f119aa620be720c6163dc11af506e824ec0b..640744053bde8065731c809a03ab1379e1210aff 100644 (file)
@@ -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 @@ class GWClient:
 
     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):
index c0111f2ddc4a879f4743df68545b8e967625e24f..5e69128b393d425282047fde91792e3088cbe314 100644 (file)
@@ -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
 }
index d22372ec4ade80068b937f1a490ac151e79d67cd..66d4561cd736474ba4e90b649197d7e1629639d6 100644 (file)
@@ -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