Skip to content

Commit

Permalink
[Deezer] Allow user to manually input arl token, temporary fix (#1090)
Browse files Browse the repository at this point in the history
  • Loading branch information
arctixdev authored Feb 19, 2024
1 parent 728eb4b commit 2aa56c6
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 77 deletions.
137 changes: 74 additions & 63 deletions music_assistant/server/providers/deezer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand All @@ -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."""
Expand Down Expand Up @@ -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
)

Expand All @@ -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,
),
)


Expand All @@ -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
Expand All @@ -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,
Expand All @@ -208,6 +221,7 @@ async def search(
MediaType.PLAYLIST,
]

# Create a task for each media_type
tasks = {}

async with TaskGroup() as taskgroup:
Expand Down Expand Up @@ -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."""
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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),
Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -658,47 +677,39 @@ 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

async def search_and_parse_artists(self, query: str, limit: int = 20) -> list[Artist]:
"""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

async def search_and_parse_albums(self, query: str, limit: int = 20) -> list[Album]:
"""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

async def search_and_parse_playlists(self, query: str, limit: int = 20) -> list[Playlist]:
"""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

Expand Down
17 changes: 6 additions & 11 deletions music_assistant/server/providers/deezer/gw_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions music_assistant/server/providers/deezer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected].2", "pycryptodome==3.20.0"],
"requirements": ["git+https://github.com/music-assistant/[email protected].3", "pycryptodome==3.20.0"],
"multi_instance": true
}
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected].2
git+https://github.com/music-assistant/[email protected].3
hass-client==1.0.1
ifaddr==0.2.0
jellyfin_apiclient_python==1.9.2
Expand Down

0 comments on commit 2aa56c6

Please sign in to comment.