deezer-python-gql 0.5.1__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deezer_python_gql-0.7.0/PKG-INFO +256 -0
- deezer_python_gql-0.7.0/README.md +228 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql/base_client.py +32 -0
- deezer_python_gql-0.7.0/deezer_python_gql.egg-info/PKG-INFO +256 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/pyproject.toml +1 -1
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/tests/test_client.py +538 -1
- deezer_python_gql-0.5.1/PKG-INFO +0 -166
- deezer_python_gql-0.5.1/README.md +0 -138
- deezer_python_gql-0.5.1/deezer_python_gql.egg-info/PKG-INFO +0 -166
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/LICENSE +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/MANIFEST.in +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql/__init__.py +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql/py.typed +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql.egg-info/SOURCES.txt +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql.egg-info/dependency_links.txt +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql.egg-info/not-zip-safe +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql.egg-info/requires.txt +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/deezer_python_gql.egg-info/top_level.txt +0 -0
- {deezer_python_gql-0.5.1 → deezer_python_gql-0.7.0}/setup.cfg +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: deezer-python-gql
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: Async typed Python client for Deezer's Pipe GraphQL API.
|
|
5
|
+
Author-email: Julian Daberkow <jdaberkow@users.noreply.github.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Platform: any
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: httpx>=0.27.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: codespell==2.4.2; extra == "test"
|
|
18
|
+
Requires-Dist: mypy==1.19.1; extra == "test"
|
|
19
|
+
Requires-Dist: pre-commit==4.5.1; extra == "test"
|
|
20
|
+
Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
|
|
21
|
+
Requires-Dist: pytest==9.0.2; extra == "test"
|
|
22
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
24
|
+
Requires-Dist: ruff==0.15.7; extra == "test"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# deezer-python-gql
|
|
30
|
+
|
|
31
|
+
Async typed Python client for Deezer's Pipe GraphQL API.
|
|
32
|
+
|
|
33
|
+
Built with [ariadne-codegen](https://github.com/mirumee/ariadne-codegen) — all
|
|
34
|
+
client methods and response models are generated from the GraphQL schema and
|
|
35
|
+
`.graphql` query files.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv add deezer-python-gql
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
|
|
48
|
+
from deezer_python_gql import DeezerGQLClient
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
client = DeezerGQLClient(arl="YOUR_ARL_COOKIE")
|
|
52
|
+
|
|
53
|
+
# Current user
|
|
54
|
+
me = await client.get_me()
|
|
55
|
+
print(me)
|
|
56
|
+
|
|
57
|
+
# Track with media URLs, lyrics, and contributors
|
|
58
|
+
track = await client.get_track(track_id="3135556")
|
|
59
|
+
print(track.title, track.duration)
|
|
60
|
+
|
|
61
|
+
# Album with paginated track list
|
|
62
|
+
album = await client.get_album(album_id="302127")
|
|
63
|
+
print(album.display_title, album.tracks_count)
|
|
64
|
+
|
|
65
|
+
# Artist with top tracks and discography
|
|
66
|
+
artist = await client.get_artist(artist_id="27")
|
|
67
|
+
print(artist.name, artist.fans_count)
|
|
68
|
+
|
|
69
|
+
# Playlist with tracks
|
|
70
|
+
playlist = await client.get_playlist(playlist_id="53362031")
|
|
71
|
+
print(playlist.title, playlist.estimated_tracks_count)
|
|
72
|
+
|
|
73
|
+
# Unified search across all entity types
|
|
74
|
+
results = await client.search(query="Daft Punk")
|
|
75
|
+
print(len(results.tracks.edges), "tracks found")
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Available Queries
|
|
81
|
+
|
|
82
|
+
### Content Retrieval
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
| --------------------------------------------- | --------------------------------------------------------------- |
|
|
86
|
+
| `get_me()` | Current authenticated user |
|
|
87
|
+
| `get_track(track_id)` | Full track details — ISRC, media tokens, lyrics, contributors |
|
|
88
|
+
| `get_album(album_id)` | Album with cover, label, paginated tracks, fallback |
|
|
89
|
+
| `get_artist(artist_id)` | Artist with bio, top tracks, albums (ordered by release date) |
|
|
90
|
+
| `get_playlist(playlist_id)` | Playlist with owner, picture, paginated tracks |
|
|
91
|
+
| `get_livestream(livestream_id)` | Livestream (radio station) with streaming URLs and codec info |
|
|
92
|
+
| `get_podcast(podcast_id)` | Podcast with paginated episodes and rights info |
|
|
93
|
+
| `get_podcast_episode(podcast_episode_id)` | Single episode with media URL, codec, and parent podcast ref |
|
|
94
|
+
| `get_audiobook(audiobook_id)` | Audiobook with paginated chapters, contributors, and fallback |
|
|
95
|
+
| `get_audiobook_chapter(audiobook_chapter_id)` | Chapter with media token, estimated sizes, and streaming rights |
|
|
96
|
+
|
|
97
|
+
### Search & Discovery
|
|
98
|
+
|
|
99
|
+
| Method | Description |
|
|
100
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
101
|
+
| `search(query, ...)` | Unified search across tracks, albums, artists, playlists, podcasts, livestreams |
|
|
102
|
+
| `search_flows(query)` | Discover all available Deezer flows via search |
|
|
103
|
+
| `get_similar_tracks(track_id, nb)` | Recommended tracks based on a given track |
|
|
104
|
+
| `get_artist_mix(artist_ids, limit)` | Track mix blended from given artists |
|
|
105
|
+
| `get_track_mix(track_ids, limit)` | Track mix blended around given tracks |
|
|
106
|
+
| `get_flow()` | User's default Flow with tracks |
|
|
107
|
+
| `get_flow_batch()` | 4 batches of Flow tracks in one request (via GraphQL aliases) |
|
|
108
|
+
| `get_flow_configs(moods_first, genres_first)` | Mood & genre flow config lists for discovery |
|
|
109
|
+
| `get_flow_config_tracks(flow_config_id)` | Tracks for a specific mood/genre flow config |
|
|
110
|
+
| `get_made_for_me(first)` | "Made For You" SmartTracklist & Flow items |
|
|
111
|
+
| `get_smart_tracklist(smart_tracklist_id, first)` | Smart tracklist with paginated tracks |
|
|
112
|
+
| `get_charts(country_code, ...)` | Country charts — tracks, albums, artists, playlists |
|
|
113
|
+
| `get_recommendations(playlists_first, ...)` | Personalized recommendations across categories |
|
|
114
|
+
| `get_recently_played(first)` | Recently played mixed content (albums, playlists, artists...) |
|
|
115
|
+
| `get_user_charts()` | Personal top tracks, artists, and albums |
|
|
116
|
+
|
|
117
|
+
### Library & Favorites
|
|
118
|
+
|
|
119
|
+
| Method | Description |
|
|
120
|
+
| --------------------------------------------- | ----------------------------------------------------------- |
|
|
121
|
+
| `get_favorite_artists(first, after)` | Paginated favorite artists |
|
|
122
|
+
| `get_favorite_albums(first, after)` | Paginated favorite albums |
|
|
123
|
+
| `get_favorite_tracks(first, after)` | Paginated favorite tracks |
|
|
124
|
+
| `get_favorite_playlists(first, after)` | Paginated favorite playlists |
|
|
125
|
+
| `get_favorite_podcasts(first, after)` | Paginated favorite podcasts |
|
|
126
|
+
| `get_favorite_audiobooks()` | Favorite audiobook IDs with dates (via deprecated endpoint) |
|
|
127
|
+
| `get_podcast_episode_bookmarks(first, after)` | Bookmarked podcast episodes with playback position |
|
|
128
|
+
| `get_user_playlists(first, after)` | User's own playlists (not just favorites) |
|
|
129
|
+
|
|
130
|
+
### Music Together (Collaborative Playlists)
|
|
131
|
+
|
|
132
|
+
| Method | Description |
|
|
133
|
+
| ------------------------------------------ | --------------------------------------------------------- |
|
|
134
|
+
| `get_music_together_groups(first)` | User's Music Together groups |
|
|
135
|
+
| `get_music_together_group(group_id, mood)` | Single group with members, suggested & curated tracklists |
|
|
136
|
+
| `get_music_together_affinity(group_id)` | Group member affinity scores and discovery content |
|
|
137
|
+
|
|
138
|
+
## Available Mutations
|
|
139
|
+
|
|
140
|
+
### Favorites Management
|
|
141
|
+
|
|
142
|
+
| Method | Description |
|
|
143
|
+
| ---------------------------------------------- | ----------------------------------------------------------- |
|
|
144
|
+
| `add_artist_to_favorite(artist_id)` | Add artist to favorites |
|
|
145
|
+
| `remove_artist_from_favorite(artist_id)` | Remove artist from favorites |
|
|
146
|
+
| `add_album_to_favorite(album_id)` | Add album to favorites |
|
|
147
|
+
| `remove_album_from_favorite(album_id)` | Remove album from favorites |
|
|
148
|
+
| `add_track_to_favorite(track_id)` | Add track to favorites |
|
|
149
|
+
| `remove_track_from_favorite(track_id)` | Remove track from favorites |
|
|
150
|
+
| `add_playlist_to_favorite(playlist_id)` | Add playlist to favorites |
|
|
151
|
+
| `remove_playlist_from_favorite(playlist_id)` | Remove playlist from favorites |
|
|
152
|
+
| `add_podcast_to_favorite(podcast_id)` | Add podcast to favorites |
|
|
153
|
+
| `remove_podcast_from_favorite(podcast_id)` | Remove podcast from favorites |
|
|
154
|
+
| `add_audiobook_to_favorite(audiobook_id)` | Add audiobook to favorites (deprecated but functional) |
|
|
155
|
+
| `remove_audiobook_from_favorite(audiobook_id)` | Remove audiobook from favorites (deprecated but functional) |
|
|
156
|
+
|
|
157
|
+
### Playlist Management
|
|
158
|
+
|
|
159
|
+
| Method | Description |
|
|
160
|
+
| ------------------------------------------------ | ------------------------------------------------- |
|
|
161
|
+
| `create_playlist(title, ...)` | Create a new playlist |
|
|
162
|
+
| `update_playlist(playlist_id, ...)` | Update playlist title, description, or visibility |
|
|
163
|
+
| `delete_playlist(playlist_id)` | Delete a playlist |
|
|
164
|
+
| `add_tracks_to_playlist(playlist_id, track_ids)` | Add tracks to a playlist |
|
|
165
|
+
| `remove_tracks_from_playlist(playlist_id, ...)` | Remove tracks from a playlist |
|
|
166
|
+
|
|
167
|
+
### Podcast Episode Management
|
|
168
|
+
|
|
169
|
+
| Method | Description |
|
|
170
|
+
| ------------------------------------------------ | ------------------------------------------------- |
|
|
171
|
+
| `bookmark_podcast_episode(episode_id, offset)` | Bookmark episode with playback position (seconds) |
|
|
172
|
+
| `unbookmark_podcast_episode(episode_id)` | Remove bookmark from episode |
|
|
173
|
+
| `mark_as_played_podcast_episode(episode_id)` | Mark episode as played |
|
|
174
|
+
| `mark_as_not_played_podcast_episode(episode_id)` | Mark episode as not played |
|
|
175
|
+
|
|
176
|
+
### Music Together
|
|
177
|
+
|
|
178
|
+
| Method | Description |
|
|
179
|
+
| ------------------------------------------------- | ------------------------------------------- |
|
|
180
|
+
| `music_together_create_group(name, ...)` | Create a new Music Together group |
|
|
181
|
+
| `music_together_join_group(group_id)` | Join an existing group |
|
|
182
|
+
| `music_together_leave_group(group_id)` | Leave a group |
|
|
183
|
+
| `music_together_refresh_suggested_tracklist(...)` | Refresh the suggested tracklist for a group |
|
|
184
|
+
| `music_together_update_group_settings(...)` | Update group settings (name, family mode) |
|
|
185
|
+
| `music_together_generate_group_name()` | Generate a random group name |
|
|
186
|
+
|
|
187
|
+
### Utilities
|
|
188
|
+
|
|
189
|
+
| Method | Description |
|
|
190
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
191
|
+
| `check_audiobook_ids(album_ids)` | Batch-check which album IDs are audiobooks (single GraphQL call) |
|
|
192
|
+
|
|
193
|
+
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
194
|
+
|
|
195
|
+
## Development
|
|
196
|
+
|
|
197
|
+
Requires **Python 3.12+** and [uv](https://docs.astral.sh/uv/).
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# Install all dependencies (including codegen tooling)
|
|
201
|
+
make setup
|
|
202
|
+
|
|
203
|
+
# Re-generate the typed client from schema + queries
|
|
204
|
+
make generate
|
|
205
|
+
|
|
206
|
+
# Run linters and type checks
|
|
207
|
+
make lint
|
|
208
|
+
|
|
209
|
+
# Run tests
|
|
210
|
+
make test
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Adding a new query
|
|
214
|
+
|
|
215
|
+
1. Create a `.graphql` file in `queries/`.
|
|
216
|
+
2. Run `make generate` to produce the typed client method and response models.
|
|
217
|
+
3. Add tests in `tests/`.
|
|
218
|
+
|
|
219
|
+
## Exploring the API
|
|
220
|
+
|
|
221
|
+
To run ad-hoc GraphQL queries against the live Pipe API during development:
|
|
222
|
+
|
|
223
|
+
1. Create a `.env` file (already gitignored) with your ARL cookie:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
echo 'DEEZER_ARL=your_arl_cookie_value' > .env
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
2. Run queries:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Run a .graphql file
|
|
233
|
+
uv run python scripts/explore.py queries/get_me.graphql
|
|
234
|
+
|
|
235
|
+
# Run an inline query
|
|
236
|
+
uv run python scripts/explore.py -q '{ me { id } }'
|
|
237
|
+
|
|
238
|
+
# With variables
|
|
239
|
+
uv run python scripts/explore.py -q 'query($id: String!) { track(trackId: $id) { title } }' \
|
|
240
|
+
-v '{"id": "3135556"}'
|
|
241
|
+
|
|
242
|
+
# Via make
|
|
243
|
+
make explore Q=queries/get_me.graphql
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The script handles JWT auth automatically — no manual token management needed.
|
|
247
|
+
|
|
248
|
+
## Authentication
|
|
249
|
+
|
|
250
|
+
The Pipe API uses short-lived JWTs obtained from an ARL cookie. The base client
|
|
251
|
+
handles token acquisition and refresh automatically — you only need to supply a
|
|
252
|
+
valid ARL value.
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
Apache-2.0
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# deezer-python-gql
|
|
2
|
+
|
|
3
|
+
Async typed Python client for Deezer's Pipe GraphQL API.
|
|
4
|
+
|
|
5
|
+
Built with [ariadne-codegen](https://github.com/mirumee/ariadne-codegen) — all
|
|
6
|
+
client methods and response models are generated from the GraphQL schema and
|
|
7
|
+
`.graphql` query files.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv add deezer-python-gql
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
|
|
20
|
+
from deezer_python_gql import DeezerGQLClient
|
|
21
|
+
|
|
22
|
+
async def main():
|
|
23
|
+
client = DeezerGQLClient(arl="YOUR_ARL_COOKIE")
|
|
24
|
+
|
|
25
|
+
# Current user
|
|
26
|
+
me = await client.get_me()
|
|
27
|
+
print(me)
|
|
28
|
+
|
|
29
|
+
# Track with media URLs, lyrics, and contributors
|
|
30
|
+
track = await client.get_track(track_id="3135556")
|
|
31
|
+
print(track.title, track.duration)
|
|
32
|
+
|
|
33
|
+
# Album with paginated track list
|
|
34
|
+
album = await client.get_album(album_id="302127")
|
|
35
|
+
print(album.display_title, album.tracks_count)
|
|
36
|
+
|
|
37
|
+
# Artist with top tracks and discography
|
|
38
|
+
artist = await client.get_artist(artist_id="27")
|
|
39
|
+
print(artist.name, artist.fans_count)
|
|
40
|
+
|
|
41
|
+
# Playlist with tracks
|
|
42
|
+
playlist = await client.get_playlist(playlist_id="53362031")
|
|
43
|
+
print(playlist.title, playlist.estimated_tracks_count)
|
|
44
|
+
|
|
45
|
+
# Unified search across all entity types
|
|
46
|
+
results = await client.search(query="Daft Punk")
|
|
47
|
+
print(len(results.tracks.edges), "tracks found")
|
|
48
|
+
|
|
49
|
+
asyncio.run(main())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Available Queries
|
|
53
|
+
|
|
54
|
+
### Content Retrieval
|
|
55
|
+
|
|
56
|
+
| Method | Description |
|
|
57
|
+
| --------------------------------------------- | --------------------------------------------------------------- |
|
|
58
|
+
| `get_me()` | Current authenticated user |
|
|
59
|
+
| `get_track(track_id)` | Full track details — ISRC, media tokens, lyrics, contributors |
|
|
60
|
+
| `get_album(album_id)` | Album with cover, label, paginated tracks, fallback |
|
|
61
|
+
| `get_artist(artist_id)` | Artist with bio, top tracks, albums (ordered by release date) |
|
|
62
|
+
| `get_playlist(playlist_id)` | Playlist with owner, picture, paginated tracks |
|
|
63
|
+
| `get_livestream(livestream_id)` | Livestream (radio station) with streaming URLs and codec info |
|
|
64
|
+
| `get_podcast(podcast_id)` | Podcast with paginated episodes and rights info |
|
|
65
|
+
| `get_podcast_episode(podcast_episode_id)` | Single episode with media URL, codec, and parent podcast ref |
|
|
66
|
+
| `get_audiobook(audiobook_id)` | Audiobook with paginated chapters, contributors, and fallback |
|
|
67
|
+
| `get_audiobook_chapter(audiobook_chapter_id)` | Chapter with media token, estimated sizes, and streaming rights |
|
|
68
|
+
|
|
69
|
+
### Search & Discovery
|
|
70
|
+
|
|
71
|
+
| Method | Description |
|
|
72
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
73
|
+
| `search(query, ...)` | Unified search across tracks, albums, artists, playlists, podcasts, livestreams |
|
|
74
|
+
| `search_flows(query)` | Discover all available Deezer flows via search |
|
|
75
|
+
| `get_similar_tracks(track_id, nb)` | Recommended tracks based on a given track |
|
|
76
|
+
| `get_artist_mix(artist_ids, limit)` | Track mix blended from given artists |
|
|
77
|
+
| `get_track_mix(track_ids, limit)` | Track mix blended around given tracks |
|
|
78
|
+
| `get_flow()` | User's default Flow with tracks |
|
|
79
|
+
| `get_flow_batch()` | 4 batches of Flow tracks in one request (via GraphQL aliases) |
|
|
80
|
+
| `get_flow_configs(moods_first, genres_first)` | Mood & genre flow config lists for discovery |
|
|
81
|
+
| `get_flow_config_tracks(flow_config_id)` | Tracks for a specific mood/genre flow config |
|
|
82
|
+
| `get_made_for_me(first)` | "Made For You" SmartTracklist & Flow items |
|
|
83
|
+
| `get_smart_tracklist(smart_tracklist_id, first)` | Smart tracklist with paginated tracks |
|
|
84
|
+
| `get_charts(country_code, ...)` | Country charts — tracks, albums, artists, playlists |
|
|
85
|
+
| `get_recommendations(playlists_first, ...)` | Personalized recommendations across categories |
|
|
86
|
+
| `get_recently_played(first)` | Recently played mixed content (albums, playlists, artists...) |
|
|
87
|
+
| `get_user_charts()` | Personal top tracks, artists, and albums |
|
|
88
|
+
|
|
89
|
+
### Library & Favorites
|
|
90
|
+
|
|
91
|
+
| Method | Description |
|
|
92
|
+
| --------------------------------------------- | ----------------------------------------------------------- |
|
|
93
|
+
| `get_favorite_artists(first, after)` | Paginated favorite artists |
|
|
94
|
+
| `get_favorite_albums(first, after)` | Paginated favorite albums |
|
|
95
|
+
| `get_favorite_tracks(first, after)` | Paginated favorite tracks |
|
|
96
|
+
| `get_favorite_playlists(first, after)` | Paginated favorite playlists |
|
|
97
|
+
| `get_favorite_podcasts(first, after)` | Paginated favorite podcasts |
|
|
98
|
+
| `get_favorite_audiobooks()` | Favorite audiobook IDs with dates (via deprecated endpoint) |
|
|
99
|
+
| `get_podcast_episode_bookmarks(first, after)` | Bookmarked podcast episodes with playback position |
|
|
100
|
+
| `get_user_playlists(first, after)` | User's own playlists (not just favorites) |
|
|
101
|
+
|
|
102
|
+
### Music Together (Collaborative Playlists)
|
|
103
|
+
|
|
104
|
+
| Method | Description |
|
|
105
|
+
| ------------------------------------------ | --------------------------------------------------------- |
|
|
106
|
+
| `get_music_together_groups(first)` | User's Music Together groups |
|
|
107
|
+
| `get_music_together_group(group_id, mood)` | Single group with members, suggested & curated tracklists |
|
|
108
|
+
| `get_music_together_affinity(group_id)` | Group member affinity scores and discovery content |
|
|
109
|
+
|
|
110
|
+
## Available Mutations
|
|
111
|
+
|
|
112
|
+
### Favorites Management
|
|
113
|
+
|
|
114
|
+
| Method | Description |
|
|
115
|
+
| ---------------------------------------------- | ----------------------------------------------------------- |
|
|
116
|
+
| `add_artist_to_favorite(artist_id)` | Add artist to favorites |
|
|
117
|
+
| `remove_artist_from_favorite(artist_id)` | Remove artist from favorites |
|
|
118
|
+
| `add_album_to_favorite(album_id)` | Add album to favorites |
|
|
119
|
+
| `remove_album_from_favorite(album_id)` | Remove album from favorites |
|
|
120
|
+
| `add_track_to_favorite(track_id)` | Add track to favorites |
|
|
121
|
+
| `remove_track_from_favorite(track_id)` | Remove track from favorites |
|
|
122
|
+
| `add_playlist_to_favorite(playlist_id)` | Add playlist to favorites |
|
|
123
|
+
| `remove_playlist_from_favorite(playlist_id)` | Remove playlist from favorites |
|
|
124
|
+
| `add_podcast_to_favorite(podcast_id)` | Add podcast to favorites |
|
|
125
|
+
| `remove_podcast_from_favorite(podcast_id)` | Remove podcast from favorites |
|
|
126
|
+
| `add_audiobook_to_favorite(audiobook_id)` | Add audiobook to favorites (deprecated but functional) |
|
|
127
|
+
| `remove_audiobook_from_favorite(audiobook_id)` | Remove audiobook from favorites (deprecated but functional) |
|
|
128
|
+
|
|
129
|
+
### Playlist Management
|
|
130
|
+
|
|
131
|
+
| Method | Description |
|
|
132
|
+
| ------------------------------------------------ | ------------------------------------------------- |
|
|
133
|
+
| `create_playlist(title, ...)` | Create a new playlist |
|
|
134
|
+
| `update_playlist(playlist_id, ...)` | Update playlist title, description, or visibility |
|
|
135
|
+
| `delete_playlist(playlist_id)` | Delete a playlist |
|
|
136
|
+
| `add_tracks_to_playlist(playlist_id, track_ids)` | Add tracks to a playlist |
|
|
137
|
+
| `remove_tracks_from_playlist(playlist_id, ...)` | Remove tracks from a playlist |
|
|
138
|
+
|
|
139
|
+
### Podcast Episode Management
|
|
140
|
+
|
|
141
|
+
| Method | Description |
|
|
142
|
+
| ------------------------------------------------ | ------------------------------------------------- |
|
|
143
|
+
| `bookmark_podcast_episode(episode_id, offset)` | Bookmark episode with playback position (seconds) |
|
|
144
|
+
| `unbookmark_podcast_episode(episode_id)` | Remove bookmark from episode |
|
|
145
|
+
| `mark_as_played_podcast_episode(episode_id)` | Mark episode as played |
|
|
146
|
+
| `mark_as_not_played_podcast_episode(episode_id)` | Mark episode as not played |
|
|
147
|
+
|
|
148
|
+
### Music Together
|
|
149
|
+
|
|
150
|
+
| Method | Description |
|
|
151
|
+
| ------------------------------------------------- | ------------------------------------------- |
|
|
152
|
+
| `music_together_create_group(name, ...)` | Create a new Music Together group |
|
|
153
|
+
| `music_together_join_group(group_id)` | Join an existing group |
|
|
154
|
+
| `music_together_leave_group(group_id)` | Leave a group |
|
|
155
|
+
| `music_together_refresh_suggested_tracklist(...)` | Refresh the suggested tracklist for a group |
|
|
156
|
+
| `music_together_update_group_settings(...)` | Update group settings (name, family mode) |
|
|
157
|
+
| `music_together_generate_group_name()` | Generate a random group name |
|
|
158
|
+
|
|
159
|
+
### Utilities
|
|
160
|
+
|
|
161
|
+
| Method | Description |
|
|
162
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
163
|
+
| `check_audiobook_ids(album_ids)` | Batch-check which album IDs are audiobooks (single GraphQL call) |
|
|
164
|
+
|
|
165
|
+
All methods return fully-typed Pydantic models generated from the GraphQL schema.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
Requires **Python 3.12+** and [uv](https://docs.astral.sh/uv/).
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Install all dependencies (including codegen tooling)
|
|
173
|
+
make setup
|
|
174
|
+
|
|
175
|
+
# Re-generate the typed client from schema + queries
|
|
176
|
+
make generate
|
|
177
|
+
|
|
178
|
+
# Run linters and type checks
|
|
179
|
+
make lint
|
|
180
|
+
|
|
181
|
+
# Run tests
|
|
182
|
+
make test
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Adding a new query
|
|
186
|
+
|
|
187
|
+
1. Create a `.graphql` file in `queries/`.
|
|
188
|
+
2. Run `make generate` to produce the typed client method and response models.
|
|
189
|
+
3. Add tests in `tests/`.
|
|
190
|
+
|
|
191
|
+
## Exploring the API
|
|
192
|
+
|
|
193
|
+
To run ad-hoc GraphQL queries against the live Pipe API during development:
|
|
194
|
+
|
|
195
|
+
1. Create a `.env` file (already gitignored) with your ARL cookie:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
echo 'DEEZER_ARL=your_arl_cookie_value' > .env
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
2. Run queries:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# Run a .graphql file
|
|
205
|
+
uv run python scripts/explore.py queries/get_me.graphql
|
|
206
|
+
|
|
207
|
+
# Run an inline query
|
|
208
|
+
uv run python scripts/explore.py -q '{ me { id } }'
|
|
209
|
+
|
|
210
|
+
# With variables
|
|
211
|
+
uv run python scripts/explore.py -q 'query($id: String!) { track(trackId: $id) { title } }' \
|
|
212
|
+
-v '{"id": "3135556"}'
|
|
213
|
+
|
|
214
|
+
# Via make
|
|
215
|
+
make explore Q=queries/get_me.graphql
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The script handles JWT auth automatically — no manual token management needed.
|
|
219
|
+
|
|
220
|
+
## Authentication
|
|
221
|
+
|
|
222
|
+
The Pipe API uses short-lived JWTs obtained from an ARL cookie. The base client
|
|
223
|
+
handles token acquisition and refresh automatically — you only need to supply a
|
|
224
|
+
valid ARL value.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
Apache-2.0
|
|
@@ -257,3 +257,35 @@ class DeezerBaseClient:
|
|
|
257
257
|
logger.debug("JWT acquired, expires at %s", self._jwt_expires_at)
|
|
258
258
|
|
|
259
259
|
return self._jwt
|
|
260
|
+
|
|
261
|
+
async def check_audiobook_ids(self, album_ids: list[str]) -> set[str]:
|
|
262
|
+
"""Check which album IDs are also valid audiobooks on Deezer.
|
|
263
|
+
|
|
264
|
+
Uses GraphQL aliases to batch-check many IDs in a single request.
|
|
265
|
+
Returns the subset of IDs that are audiobooks (i.e., the audiobook
|
|
266
|
+
query returns non-null for them).
|
|
267
|
+
|
|
268
|
+
:param album_ids: List of Deezer album/audiobook IDs to check.
|
|
269
|
+
"""
|
|
270
|
+
if not album_ids:
|
|
271
|
+
return set()
|
|
272
|
+
|
|
273
|
+
# Query displayTitle alongside id — querying only { id } echoes back the
|
|
274
|
+
# input without validating that the ID is actually an audiobook.
|
|
275
|
+
parts = [
|
|
276
|
+
f'a{i}: audiobook(audiobookId: "{aid}") {{ id displayTitle }}'
|
|
277
|
+
for i, aid in enumerate(album_ids)
|
|
278
|
+
]
|
|
279
|
+
query = "{ " + " ".join(parts) + " }"
|
|
280
|
+
|
|
281
|
+
resp = await self.execute(query)
|
|
282
|
+
data = self.get_data(resp)
|
|
283
|
+
|
|
284
|
+
audiobook_ids: set[str] = set()
|
|
285
|
+
for i, aid in enumerate(album_ids):
|
|
286
|
+
node = data.get(f"a{i}")
|
|
287
|
+
# The API echoes back {id} for any valid album, so we must check
|
|
288
|
+
# a real audiobook field like displayTitle to distinguish.
|
|
289
|
+
if node is not None and node.get("displayTitle") is not None:
|
|
290
|
+
audiobook_ids.add(aid)
|
|
291
|
+
return audiobook_ids
|