Rewrite Deezer provider with GraphQL client#3900
Open
jdaberkow wants to merge 86 commits into
Open
Conversation
Replace the REST-based deezer-python-async client with the typed GraphQL client (deezer-python-gql) for all Deezer API operations except streaming and listen logging (which remain on the GW API). Key changes: - Auth: remove OAuth flow, use ARL-only authentication via GQL JWT - Library sync: cursor-paginated favorites via GQL (artists, albums, tracks, playlists) with favoritedAt → date_added mapping - Search: single GQL call replaces 4 parallel REST calls - Get-by-ID: all entity lookups via GQL with richer metadata - Favorites: add/remove via GQL mutations - Playlist CRUD: create, add/remove tracks via GQL mutations - Recommendations: Flow, mood/genre FlowConfigs, SmartTracklists, charts, hot tracks, new releases — replaces curated radios - Lyrics: add ProviderFeature.LYRICS with plain text and LRC sync - GW client: remove unused api_token parameter - Manifest: deezer-python-async → deezer-python-gql>=0.3.0
- Override browse() with 3 custom folders alongside standard library paths - Made For You: Moods, Genres, Your Top Tracks/Artists/Albums, SmartTracklists, Hot Tracks - Explore: Top Charts, Top Albums/Artists/Playlists, All Flows - Recently Played: mixed albums/playlists/artists - Add Recently Played to recommendations() - Replace Charts & Hot Tracks recommendation with directly rendered Recommended Tracks - Filter Recommendations folder from browse root (covered by custom folders) - Wire up GENRE_FLOW_PREFIX, FLOW_CONFIG_PREFIX, HOT_TRACKS, USER_TOP_TRACKS virtual playlists
- Set favorite flag from isFavorite on tracks, artists, albums, playlists - Auto-favorite own playlists (owner matches current user) - Add popularity from track.popularity and fansCount on artists/albums/playlists - Add label and copyright on albums - Add bio summary as description on artists - Add description on playlists - Extract flow config images for detail pages (visuals) - Fetch 4x flow track batches with deduplication - Handle Flow and FlowConfig nodes in recently played - Upgrade to deezer-python-gql 0.5.0
- Replace Any with TrackFields, AlbumFields, ArtistFields, PlaylistFields - Remove 20 defensive getattr/hasattr calls in favor of direct field access - Bump deezer-python-gql requirement to >=0.5.1 (fragment inheritance)
- Extract _parse_recently_played_edges to deduplicate browse + recs - DRY up _get_virtual_playlist: consolidate 3 identical flow config branches - Type _get_flow_config_image with Protocol instead of object+getattr - Deduplicate _get_hot_tracks (delegates to _get_recommended_tracks) - Remove dead GW methods: get_user_radio, get_home_flows, get_song_data - Add get_similar_track_ids public method on GWClient - Add httpx connection pooling (AsyncClient lifecycle in init/unload) - Fix _add_flow_configs to use _get_flow_config_image consistently
Replace with direct _get_recommended_tracks call — same API, clearer name.
- Merge _browse_mood_flows/_browse_genre_flows into _browse_flow_configs(category) - Consolidate 3 get_playlist_tracks prefix dispatches into loop - Remove HOT_TRACKS_PLAYLIST_ID (identical to RECOMMENDED_TRACKS_PLAYLIST_ID) - Single get_recommendations() API call instead of 3 separate calls - Shared cached _get_recently_played_items() for browse + recommendations - Shared cached _get_smart_tracklist_playlists() for browse + recommendations - Fix bug: _add_flow_configs used MOOD_FLOW_PREFIX for genres (now GENRE_FLOW_PREFIX)
…gle FLOW_CONFIG_PREFIX FlowConfig is Deezer's single API type for all personalized radio streams (moods, genres, and search results). The 3 separate prefixes were unnecessary since every downstream operation (track fetching, virtual playlist creation) is identical regardless of how the FlowConfig was discovered.
- _iter_favorites(): generic cursor-paginated favorites loop (artists, albums, tracks) - _flow_configs_to_playlists(): shared FlowConfig edge→playlist conversion - _apply_web_url(): extracted duplicate URL-patching from get_artist/get_album - Removed trivial _browse_recently_played wrapper (inlined at call site)
…p deezer-python-gql to 0.7.0 - Add audiobook browsing via GW page.get API (Discover Audiobooks with genre channels) - Split audiobook results from album search using check_audiobook_ids - Cache podcast episodes with @use_cache (3h TTL), apply fresh bookmarks on top - Fix all browse breadcrumb paths to use human-readable display names - Add Shaker (artist/track mix) browse support - Bump deezer-python-gql requirement from >=0.5.1 to >=0.7.0
- Remove httpx import and manual AsyncClient management - DeezerGQLClient now manages its own connection pool internally - Podcast episodes cached individually by ID for 30 days - Only uncached episodes are fetched from the API on sync
- Fix edge variable reuse across elif branches (charts, user charts) - Fix flow configs tuple type inference with explicit list() cast - Replace Protocol with concrete Union types for _get_flow_config_image - Use distinct variable names for flow_config/tracklist in _get_virtual_playlist - Remove unreachable statements (non-optional contributor edges, exhaustive match) - Fix similar tracks attribute access (ShorterResultsPlugin unwrap) - Fix search album edges: extract .node for proper Optional narrowing - Fix podcast episode variable shadowing across loops - Replace Sequence[Any] with concrete generated types - Update manifest pin to deezer-python-gql==0.8.3
- Paginate nested collections (album tracks, playlist tracks, artist albums/top tracks, audiobook chapters) instead of truncating at first page - Paginate podcast bookmark retrieval and reset stale state before overlay - Implement on_played() to sync podcast progress back to Deezer - Fix empty radio generator to prevent AttributeError during sync - Invalidate playlist track cache after add/remove mutations - Bump deezer-python-gql to 0.9.0 (pagination query variables) - Rewrite PR description to match actual branch scope
get_library_audiobooks() now calls the provider's paginated get_audiobook() method instead of bypassing it with a direct GQL client call limited to 200 chapters.
Deezer livestreams have no favorites API, so get_library_radios() was an empty generator. With the flag declared, radio sync would run, return no items, and mark previously-synced radios in_library=False — silently clearing favorites on subsequent syncs. Radios remain discoverable via search and playable. MA-local favorites still work without the flag.
- Add Personalized Playlists, Recommended Playlists, and Recommended Artist Playlists folders under Made For You in browse() - Split editorial and artist playlists into own recommendation sections (50 items each) replacing the old plain Recommended Artists section - Remove editorial playlists from Made for you recommendation folder - Extract _browse_made_for_you() dispatcher to stay within return limit
- Parse personal songs (negative SNG_ID) from GW API with full Artist/Album objects using prefixed IDs to avoid collisions - Yield personal artists/albums in get_library_artists/albums so they appear in library overview and search - Handle personal_artist_/personal_album_ prefixes in get_artist, get_album, get_artist_albums, get_artist_toptracks, get_album_tracks to avoid sending invalid IDs to the GQL API - Mark personal items as favorites so they appear in library
…bums Return Album instead of ItemMapping for track.album in parse_track so the core tracks controller can display 'Appears on' albums without needing to resolve ItemMappings (which it currently doesn't do).
- Populate album artists from track MAIN contributors in parse_track - Filter audiobooks from library albums using check_audiobook_ids - Yield audiobooks from favorite albums in get_library_audiobooks - Add audiobook chapter streaming with seek support - Use album favorite mutations for audiobooks (deprecated endpoints fail)
Deduplicate identical bookmark fetching logic that existed in both media.py (_fetch_all_bookmarks) and streaming.py (get_resume_position). The new fetch_all_bookmarks() helper in helpers.py is now used by both callers, reducing code duplication and ensuring consistent behavior.
Addresses reviewer concern about double caching. The two-layer strategy is intentional (outer=navigation speed, inner=avoid refetching details) but 24h was too aggressive. Reduced to 1h so new episodes appear faster. Added docstring explaining the caching design.
Replace 6 redundant get_personal_songs API calls with a single cached _get_personal_songs() method (24h TTL). Also fixes a pre-existing bug where only the first 500 songs were fetched — now paginates until exhausted so users with 500+ uploads get all their songs.
…:music-assistant/server into feature/deezer-gql-replacement
Define BROWSE_* constants in helpers.py for all path segments used in browse routing. Replaces hardcoded string literals in browse.py with constants to prevent silent routing breakage from typos. Also fixes inconsistent casing: 'Made for you' → 'Made For You'.
- Reject non-numeric entity IDs when parsing Deezer share URLs - Add defensive ValueError handling in get_track for invalid IDs - Delegate browse personal songs to media_manager's paginated method to remove 500-item cap
…:music-assistant/server into feature/deezer-gql-replacement
- Bump deezer-python-gql to 0.17.0 (adds media.rights.sub.available) - Add _track_available() to detect unplayable/non-subscriber tracks - Set ProviderMapping.available=False for unavailable tracks - Extract year from track.release_date for inline Album objects - Accept GetTrackTrack in parse_track type union
OzGav
reviewed
May 26, 2026
Contributor
OzGav
left a comment
There was a problem hiding this comment.
Just a couple of other small items to consider. Otherwise LGTM. One of the other guys will have a look at this and we have the models PR question to go. Thanks for all your work on this re-write!
- Move constants to dedicated constants.py module - Remove hasattr check for raw_episodes (type is known) - Remove internal-workings docstring lines per MA convention
fc331d0 to
455c37d
Compare
Contributor
|
Thanks. If you are happy with everything then press the ready for review button and one of the others will have a look soon. |
Member
Author
Thank you very much for your detailed reviews! Alright. From my side everything is good to go. |
…n deserialization The RecommendationFolder.items field now has a custom deserializer (music-assistant/models#239) that discriminates Union types by media_type, so full Playlist/Artist/Album objects survive the to_dict()/from_dict() roundtrip correctly. The ItemMapping conversion workaround is no longer needed. Resolves review issue #18.
OzGav
reviewed
May 30, 2026
| item_id="top_albums", | ||
| provider=self.instance_id, | ||
| path=f"{base}Top Albums", | ||
| name="Top Albums", |
Contributor
There was a problem hiding this comment.
You could make these constants like you did above?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full rewrite of the Deezer provider, replacing the REST-based
deezer-pythondependency withdeezer-python-gql- a typed async GraphQL client for Deezer's Pipe API.Core architecture
aiohttp.ClientSession(self.mass.http_session) for connection pooling with JWT auto-refreshNew capabilities
on_played)ProviderFeature.SIMILAR_ARTISTSis_dynamic): Flow, FlowConfig, and Shaker playlists return fresh tracks on each playback - enables endless radio-style queue refilldeezer.com/{type}/{id}) are now resolved from search/paste, with Deezer added toPROVIDERS_WITH_SHAREABLE_URLSpersonal_song.getList)Dependency change
deezer-python(REST, unmaintained)deezer-python-gql(async GraphQL, Pydantic models, auto-generated typed client, aiohttp backend)GW client changes
FILESIZE_MP3_MISCfallback)Breaking changes
None. Drop-in replacement. Existing ARL token configuration is unchanged.
Related