deezer-python-gql 0.19.0__tar.gz → 0.21.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.19.0/deezer_python_gql.egg-info → deezer_python_gql-0.21.0}/PKG-INFO +22 -5
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/README.md +17 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/base_client.py +15 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/__init__.py +22 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/base_client.py +15 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/client.py +67 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/fragments.py +12 -0
- deezer_python_gql-0.21.0/deezer_python_gql/generated/get_family.py +34 -0
- deezer_python_gql-0.21.0/deezer_python_gql/generated/move_track_in_playlist.py +21 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0/deezer_python_gql.egg-info}/PKG-INFO +22 -5
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/SOURCES.txt +2 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/requires.txt +4 -4
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/pyproject.toml +10 -5
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/tests/test_client.py +62 -1
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/LICENSE +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/MANIFEST.in +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/__init__.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/add_album_to_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/add_artist_to_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/add_audiobook_to_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/add_playlist_to_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/add_podcast_to_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/add_track_to_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/add_tracks_to_playlist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/ban_artist_from_recommendation.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/ban_track_from_recommendation.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/base_model.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/bookmark_podcast_episode.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/create_playlist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/delete_playlist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/enums.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_album.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_artist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_artist_mix.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_audiobook.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_audiobook_chapter.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_banned_artists.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_banned_tracks.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_charts.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_favorite_albums.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_favorite_artists.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_favorite_audiobooks.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_favorite_playlists.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_favorite_podcasts.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_favorite_tracks.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow_batch.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow_config_tracks.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow_configs.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow_tuner.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_infinite_track_mix.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_latest_podcast_episodes.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_livestream.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_made_for_me.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_me.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_music_together_affinity.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_music_together_group.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_music_together_groups.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_personal_tracks.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_playlist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_podcast.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_podcast_episode.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_podcast_episode_bookmarks.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_podcast_episodes_by_ids.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_recently_played.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_recommendations.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_similar_artists.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_similar_tracks.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_smart_tracklist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_track.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_track_mix.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_tracks_by_ids.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_user_charts.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_user_playlists.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/input_types.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/mark_as_not_played_podcast_episode.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/mark_as_played_podcast_episode.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/music_together_create_group.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/music_together_generate_group_name.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/music_together_join_group.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/music_together_leave_group.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/music_together_refresh_suggested_tracklist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/music_together_update_group_settings.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/remove_album_from_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/remove_artist_from_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/remove_audiobook_from_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/remove_playlist_from_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/remove_podcast_from_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/remove_track_from_favorite.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/remove_tracks_from_playlist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/search.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/search_flows.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/set_flow_discovery_tuner.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/unban_artist_from_recommendation.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/unban_track_from_recommendation.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/unbookmark_podcast_episode.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/update_playlist.py +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/py.typed +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/dependency_links.txt +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/not-zip-safe +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/top_level.txt +0 -0
- {deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deezer-python-gql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: Async typed Python client for Deezer's Pipe GraphQL API.
|
|
5
5
|
Author-email: Julian Daberkow <jdaberkow@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -16,13 +16,13 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
16
16
|
Requires-Dist: yarl>=1.22.0
|
|
17
17
|
Provides-Extra: test
|
|
18
18
|
Requires-Dist: codespell==2.4.3; extra == "test"
|
|
19
|
-
Requires-Dist: mypy==2.
|
|
20
|
-
Requires-Dist: pre-commit==4.
|
|
19
|
+
Requires-Dist: mypy==2.4.0; extra == "test"
|
|
20
|
+
Requires-Dist: pre-commit==4.7.0; extra == "test"
|
|
21
21
|
Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
|
|
22
22
|
Requires-Dist: pytest==9.1.1; extra == "test"
|
|
23
|
-
Requires-Dist: pytest-asyncio==1.4.
|
|
23
|
+
Requires-Dist: pytest-asyncio==1.4.1; extra == "test"
|
|
24
24
|
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
25
|
-
Requires-Dist: ruff==0.
|
|
25
|
+
Requires-Dist: ruff==0.17.2; extra == "test"
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
|
|
28
28
|
Dynamic: license-file
|
|
@@ -136,6 +136,7 @@ asyncio.run(main())
|
|
|
136
136
|
| `get_banned_tracks(first, after)` | Tracks banned from recommendations, with ban date |
|
|
137
137
|
| `get_banned_artists(first, after)` | Artists banned from recommendations, with ban date |
|
|
138
138
|
| `get_flow_tuner()` | Flow tuner: `DEFAULT` or `DISCOVERY` |
|
|
139
|
+
| `get_family()` | Deezer Family members, with `isLoggableAs` for usable profiles |
|
|
139
140
|
|
|
140
141
|
### Music Together (Collaborative Playlists)
|
|
141
142
|
|
|
@@ -190,6 +191,7 @@ Keep a track or artist out of the user's recommendations (Flow, mixes).
|
|
|
190
191
|
| `delete_playlist(playlist_id)` | Delete a playlist |
|
|
191
192
|
| `add_tracks_to_playlist(playlist_id, track_ids)` | Add tracks to a playlist |
|
|
192
193
|
| `remove_tracks_from_playlist(playlist_id, ...)` | Remove tracks from a playlist |
|
|
194
|
+
| `move_track_in_playlist(playlist_id, ...)` | Move a track behind another one or to the top |
|
|
193
195
|
|
|
194
196
|
### Podcast Episode Management
|
|
195
197
|
|
|
@@ -286,6 +288,21 @@ If the ARL is rejected (bad or expired), the client raises
|
|
|
286
288
|
`GraphQLClientAuthError` — catch it to prompt for a new ARL instead of retrying.
|
|
287
289
|
Transient server errors raise `GraphQLClientHttpError` instead.
|
|
288
290
|
|
|
291
|
+
### Deezer Family profiles
|
|
292
|
+
|
|
293
|
+
Profiles of a Deezer Family have no ARL of their own, they share the login of the
|
|
294
|
+
Family admin. Pass `account_id` to act as one of them:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
family = (await client.get_family()).family
|
|
298
|
+
profile = next(m for m in family.linked if m.permissions.is_loggable_as)
|
|
299
|
+
profile_client = DeezerGQLClient(arl="ADMIN_ARL", account_id=profile.id)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Members with their own login (independent accounts) are not loggable, they need their
|
|
303
|
+
own ARL. If Deezer does not sign in as the profile, the client raises
|
|
304
|
+
`GraphQLClientAuthError`.
|
|
305
|
+
|
|
289
306
|
## License
|
|
290
307
|
|
|
291
308
|
Apache-2.0
|
|
@@ -107,6 +107,7 @@ asyncio.run(main())
|
|
|
107
107
|
| `get_banned_tracks(first, after)` | Tracks banned from recommendations, with ban date |
|
|
108
108
|
| `get_banned_artists(first, after)` | Artists banned from recommendations, with ban date |
|
|
109
109
|
| `get_flow_tuner()` | Flow tuner: `DEFAULT` or `DISCOVERY` |
|
|
110
|
+
| `get_family()` | Deezer Family members, with `isLoggableAs` for usable profiles |
|
|
110
111
|
|
|
111
112
|
### Music Together (Collaborative Playlists)
|
|
112
113
|
|
|
@@ -161,6 +162,7 @@ Keep a track or artist out of the user's recommendations (Flow, mixes).
|
|
|
161
162
|
| `delete_playlist(playlist_id)` | Delete a playlist |
|
|
162
163
|
| `add_tracks_to_playlist(playlist_id, track_ids)` | Add tracks to a playlist |
|
|
163
164
|
| `remove_tracks_from_playlist(playlist_id, ...)` | Remove tracks from a playlist |
|
|
165
|
+
| `move_track_in_playlist(playlist_id, ...)` | Move a track behind another one or to the top |
|
|
164
166
|
|
|
165
167
|
### Podcast Episode Management
|
|
166
168
|
|
|
@@ -257,6 +259,21 @@ If the ARL is rejected (bad or expired), the client raises
|
|
|
257
259
|
`GraphQLClientAuthError` — catch it to prompt for a new ARL instead of retrying.
|
|
258
260
|
Transient server errors raise `GraphQLClientHttpError` instead.
|
|
259
261
|
|
|
262
|
+
### Deezer Family profiles
|
|
263
|
+
|
|
264
|
+
Profiles of a Deezer Family have no ARL of their own, they share the login of the
|
|
265
|
+
Family admin. Pass `account_id` to act as one of them:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
family = (await client.get_family()).family
|
|
269
|
+
profile = next(m for m in family.linked if m.permissions.is_loggable_as)
|
|
270
|
+
profile_client = DeezerGQLClient(arl="ADMIN_ARL", account_id=profile.id)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Members with their own login (independent accounts) are not loggable, they need their
|
|
274
|
+
own ARL. If Deezer does not sign in as the profile, the client raises
|
|
275
|
+
`GraphQLClientAuthError`.
|
|
276
|
+
|
|
260
277
|
## License
|
|
261
278
|
|
|
262
279
|
Apache-2.0
|
|
@@ -123,6 +123,9 @@ class DeezerBaseClient:
|
|
|
123
123
|
:param url: GraphQL endpoint URL (defaults to Pipe API).
|
|
124
124
|
:param session: Optional pre-configured aiohttp.ClientSession.
|
|
125
125
|
If provided, the caller is responsible for closing it.
|
|
126
|
+
:param account_id: Optional Deezer Family profile to act as. A profile has no
|
|
127
|
+
ARL of its own; it signs in with the ARL of the Family admin. Only profiles
|
|
128
|
+
that ``get_family()`` marks as ``isLoggableAs`` can be used.
|
|
126
129
|
"""
|
|
127
130
|
|
|
128
131
|
PIPE_URL = "https://pipe.deezer.com/api"
|
|
@@ -136,9 +139,11 @@ class DeezerBaseClient:
|
|
|
136
139
|
arl: str,
|
|
137
140
|
url: str = PIPE_URL,
|
|
138
141
|
session: ClientSession | None = None,
|
|
142
|
+
account_id: str | None = None,
|
|
139
143
|
) -> None:
|
|
140
144
|
self.url = url
|
|
141
145
|
self._arl = arl
|
|
146
|
+
self._account_id = account_id
|
|
142
147
|
self._session = session
|
|
143
148
|
self._owns_session = session is None
|
|
144
149
|
# An external session may be shared by clients for different Deezer accounts.
|
|
@@ -347,12 +352,18 @@ class DeezerBaseClient:
|
|
|
347
352
|
|
|
348
353
|
logger.debug("JWT expired or missing, refreshing from ARL")
|
|
349
354
|
params = {"jo": "p", "rto": "c", "i": "c"}
|
|
355
|
+
body = None
|
|
356
|
+
if self._account_id:
|
|
357
|
+
# A Family profile signs in with the admin's ARL and its own id
|
|
358
|
+
params["i"] = "p"
|
|
359
|
+
body = {"arl": self._arl, "account_id": self._account_id}
|
|
350
360
|
|
|
351
361
|
session = self._get_session()
|
|
352
362
|
async with session.post(
|
|
353
363
|
self.AUTH_URL,
|
|
354
364
|
params=params,
|
|
355
365
|
cookies=self._request_cookies(self.AUTH_URL, include_arl=True),
|
|
366
|
+
json=body,
|
|
356
367
|
timeout=ClientTimeout(total=10),
|
|
357
368
|
) as resp:
|
|
358
369
|
self._store_cookies(resp)
|
|
@@ -383,6 +394,10 @@ class DeezerBaseClient:
|
|
|
383
394
|
msg = "Unexpected response from auth.deezer.com (invalid or missing JWT)"
|
|
384
395
|
raise GraphQLClientAuthError(msg) from exc
|
|
385
396
|
|
|
397
|
+
if self._account_id and str(payload.get("userId")) != self._account_id:
|
|
398
|
+
msg = f"auth.deezer.com did not sign in as Family profile {self._account_id}"
|
|
399
|
+
raise GraphQLClientAuthError(msg)
|
|
400
|
+
|
|
386
401
|
self._jwt = jwt
|
|
387
402
|
self._jwt_expires_at = expires_at
|
|
388
403
|
logger.debug("JWT acquired, expires at %s", self._jwt_expires_at)
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/__init__.py
RENAMED
|
@@ -86,6 +86,8 @@ from .fragments import (
|
|
|
86
86
|
AudiobookFieldsContributorsEdges,
|
|
87
87
|
AudiobookFieldsContributorsEdgesNodeArtist,
|
|
88
88
|
AudiobookFieldsCover,
|
|
89
|
+
FamilyProfileFields,
|
|
90
|
+
FamilyProfileFieldsPermissions,
|
|
89
91
|
LivestreamFields,
|
|
90
92
|
LivestreamFieldsCover,
|
|
91
93
|
LivestreamFieldsMedia,
|
|
@@ -205,6 +207,13 @@ from .get_charts import (
|
|
|
205
207
|
GetChartsChartsCountryTracksEdgesNode,
|
|
206
208
|
GetChartsChartsCountryTracksPageInfo,
|
|
207
209
|
)
|
|
210
|
+
from .get_family import (
|
|
211
|
+
GetFamily,
|
|
212
|
+
GetFamilyMe,
|
|
213
|
+
GetFamilyMeFamily,
|
|
214
|
+
GetFamilyMeFamilyLinked,
|
|
215
|
+
GetFamilyMeFamilyMain,
|
|
216
|
+
)
|
|
208
217
|
from .get_favorite_albums import (
|
|
209
218
|
GetFavoriteAlbums,
|
|
210
219
|
GetFavoriteAlbumsMe,
|
|
@@ -552,6 +561,10 @@ from .mark_as_played_podcast_episode import (
|
|
|
552
561
|
MarkAsPlayedPodcastEpisodeMarkAsPlayedPodcastEpisode,
|
|
553
562
|
MarkAsPlayedPodcastEpisodeMarkAsPlayedPodcastEpisodeBookmark,
|
|
554
563
|
)
|
|
564
|
+
from .move_track_in_playlist import (
|
|
565
|
+
MoveTrackInPlaylist,
|
|
566
|
+
MoveTrackInPlaylistMoveTrackInPlaylist,
|
|
567
|
+
)
|
|
555
568
|
from .music_together_create_group import (
|
|
556
569
|
MusicTogetherCreateGroup,
|
|
557
570
|
MusicTogetherCreateGroupMusicTogetherCreateGroupMusicTogetherCreateGroupError,
|
|
@@ -759,6 +772,8 @@ __all__ = [
|
|
|
759
772
|
"DeletePlaylistDeletePlaylist",
|
|
760
773
|
"DiscoveryTuner",
|
|
761
774
|
"DiscoveryTunerInput",
|
|
775
|
+
"FamilyProfileFields",
|
|
776
|
+
"FamilyProfileFieldsPermissions",
|
|
762
777
|
"GetAlbum",
|
|
763
778
|
"GetAlbumAlbum",
|
|
764
779
|
"GetAlbumAlbumFallback",
|
|
@@ -837,6 +852,11 @@ __all__ = [
|
|
|
837
852
|
"GetChartsChartsCountryTracksEdges",
|
|
838
853
|
"GetChartsChartsCountryTracksEdgesNode",
|
|
839
854
|
"GetChartsChartsCountryTracksPageInfo",
|
|
855
|
+
"GetFamily",
|
|
856
|
+
"GetFamilyMe",
|
|
857
|
+
"GetFamilyMeFamily",
|
|
858
|
+
"GetFamilyMeFamilyLinked",
|
|
859
|
+
"GetFamilyMeFamilyMain",
|
|
840
860
|
"GetFavoriteAlbums",
|
|
841
861
|
"GetFavoriteAlbumsMe",
|
|
842
862
|
"GetFavoriteAlbumsMeUserFavorites",
|
|
@@ -1127,6 +1147,8 @@ __all__ = [
|
|
|
1127
1147
|
"MarkAsPlayedPodcastEpisode",
|
|
1128
1148
|
"MarkAsPlayedPodcastEpisodeMarkAsPlayedPodcastEpisode",
|
|
1129
1149
|
"MarkAsPlayedPodcastEpisodeMarkAsPlayedPodcastEpisodeBookmark",
|
|
1150
|
+
"MoveTrackInPlaylist",
|
|
1151
|
+
"MoveTrackInPlaylistMoveTrackInPlaylist",
|
|
1130
1152
|
"MusicTogetherCreateGroup",
|
|
1131
1153
|
"MusicTogetherCreateGroupMusicTogetherCreateGroupMusicTogetherCreateGroupError",
|
|
1132
1154
|
"MusicTogetherCreateGroupMusicTogetherCreateGroupMusicTogetherCreateGroupOutput",
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/base_client.py
RENAMED
|
@@ -125,6 +125,9 @@ class DeezerBaseClient:
|
|
|
125
125
|
:param url: GraphQL endpoint URL (defaults to Pipe API).
|
|
126
126
|
:param session: Optional pre-configured aiohttp.ClientSession.
|
|
127
127
|
If provided, the caller is responsible for closing it.
|
|
128
|
+
:param account_id: Optional Deezer Family profile to act as. A profile has no
|
|
129
|
+
ARL of its own; it signs in with the ARL of the Family admin. Only profiles
|
|
130
|
+
that ``get_family()`` marks as ``isLoggableAs`` can be used.
|
|
128
131
|
"""
|
|
129
132
|
|
|
130
133
|
PIPE_URL = "https://pipe.deezer.com/api"
|
|
@@ -138,9 +141,11 @@ class DeezerBaseClient:
|
|
|
138
141
|
arl: str,
|
|
139
142
|
url: str = PIPE_URL,
|
|
140
143
|
session: ClientSession | None = None,
|
|
144
|
+
account_id: str | None = None,
|
|
141
145
|
) -> None:
|
|
142
146
|
self.url = url
|
|
143
147
|
self._arl = arl
|
|
148
|
+
self._account_id = account_id
|
|
144
149
|
self._session = session
|
|
145
150
|
self._owns_session = session is None
|
|
146
151
|
# An external session may be shared by clients for different Deezer accounts.
|
|
@@ -349,12 +354,18 @@ class DeezerBaseClient:
|
|
|
349
354
|
|
|
350
355
|
logger.debug("JWT expired or missing, refreshing from ARL")
|
|
351
356
|
params = {"jo": "p", "rto": "c", "i": "c"}
|
|
357
|
+
body = None
|
|
358
|
+
if self._account_id:
|
|
359
|
+
# A Family profile signs in with the admin's ARL and its own id
|
|
360
|
+
params["i"] = "p"
|
|
361
|
+
body = {"arl": self._arl, "account_id": self._account_id}
|
|
352
362
|
|
|
353
363
|
session = self._get_session()
|
|
354
364
|
async with session.post(
|
|
355
365
|
self.AUTH_URL,
|
|
356
366
|
params=params,
|
|
357
367
|
cookies=self._request_cookies(self.AUTH_URL, include_arl=True),
|
|
368
|
+
json=body,
|
|
358
369
|
timeout=ClientTimeout(total=10),
|
|
359
370
|
) as resp:
|
|
360
371
|
self._store_cookies(resp)
|
|
@@ -385,6 +396,10 @@ class DeezerBaseClient:
|
|
|
385
396
|
msg = "Unexpected response from auth.deezer.com (invalid or missing JWT)"
|
|
386
397
|
raise GraphQLClientAuthError(msg) from exc
|
|
387
398
|
|
|
399
|
+
if self._account_id and str(payload.get("userId")) != self._account_id:
|
|
400
|
+
msg = f"auth.deezer.com did not sign in as Family profile {self._account_id}"
|
|
401
|
+
raise GraphQLClientAuthError(msg)
|
|
402
|
+
|
|
388
403
|
self._jwt = jwt
|
|
389
404
|
self._jwt_expires_at = expires_at
|
|
390
405
|
logger.debug("JWT acquired, expires at %s", self._jwt_expires_at)
|
|
@@ -65,6 +65,7 @@ from .get_audiobook_chapter import (
|
|
|
65
65
|
from .get_banned_artists import GetBannedArtists, GetBannedArtistsMe
|
|
66
66
|
from .get_banned_tracks import GetBannedTracks, GetBannedTracksMe
|
|
67
67
|
from .get_charts import GetCharts, GetChartsCharts
|
|
68
|
+
from .get_family import GetFamily, GetFamilyMe
|
|
68
69
|
from .get_favorite_albums import GetFavoriteAlbums, GetFavoriteAlbumsMe
|
|
69
70
|
from .get_favorite_artists import GetFavoriteArtists, GetFavoriteArtistsMe
|
|
70
71
|
from .get_favorite_audiobooks import GetFavoriteAudiobooks, GetFavoriteAudiobooksMe
|
|
@@ -126,6 +127,10 @@ from .mark_as_played_podcast_episode import (
|
|
|
126
127
|
MarkAsPlayedPodcastEpisode,
|
|
127
128
|
MarkAsPlayedPodcastEpisodeMarkAsPlayedPodcastEpisode,
|
|
128
129
|
)
|
|
130
|
+
from .move_track_in_playlist import (
|
|
131
|
+
MoveTrackInPlaylist,
|
|
132
|
+
MoveTrackInPlaylistMoveTrackInPlaylist,
|
|
133
|
+
)
|
|
129
134
|
from .music_together_create_group import (
|
|
130
135
|
MusicTogetherCreateGroup,
|
|
131
136
|
MusicTogetherCreateGroupMusicTogetherCreateGroupMusicTogetherCreateGroupError,
|
|
@@ -1475,6 +1480,38 @@ class DeezerGQLClient(DeezerBaseClient):
|
|
|
1475
1480
|
data = self.get_data(response)
|
|
1476
1481
|
return GetCharts.model_validate(data).charts
|
|
1477
1482
|
|
|
1483
|
+
async def get_family(self, **kwargs: Any) -> Optional[GetFamilyMe]:
|
|
1484
|
+
query = gql("""
|
|
1485
|
+
query GetFamily {
|
|
1486
|
+
me {
|
|
1487
|
+
id
|
|
1488
|
+
family {
|
|
1489
|
+
main {
|
|
1490
|
+
...FamilyProfileFields
|
|
1491
|
+
}
|
|
1492
|
+
linked {
|
|
1493
|
+
...FamilyProfileFields
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
fragment FamilyProfileFields on FamilyUser {
|
|
1500
|
+
id
|
|
1501
|
+
name
|
|
1502
|
+
caption
|
|
1503
|
+
permissions {
|
|
1504
|
+
isLoggableAs
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
""")
|
|
1508
|
+
variables: dict[str, object] = {}
|
|
1509
|
+
response = await self.execute(
|
|
1510
|
+
query=query, operation_name="GetFamily", variables=variables, **kwargs
|
|
1511
|
+
)
|
|
1512
|
+
data = self.get_data(response)
|
|
1513
|
+
return GetFamily.model_validate(data).me
|
|
1514
|
+
|
|
1478
1515
|
async def get_favorite_albums(
|
|
1479
1516
|
self,
|
|
1480
1517
|
first: Union[Optional[int], UnsetType] = UNSET,
|
|
@@ -4402,6 +4439,36 @@ class DeezerGQLClient(DeezerBaseClient):
|
|
|
4402
4439
|
data = self.get_data(response)
|
|
4403
4440
|
return RemoveTracksFromPlaylist.model_validate(data).remove_tracks_from_playlist
|
|
4404
4441
|
|
|
4442
|
+
async def move_track_in_playlist(
|
|
4443
|
+
self,
|
|
4444
|
+
playlist_id: str,
|
|
4445
|
+
track_id: str,
|
|
4446
|
+
after_track_id: Union[Optional[str], UnsetType] = UNSET,
|
|
4447
|
+
**kwargs: Any,
|
|
4448
|
+
) -> MoveTrackInPlaylistMoveTrackInPlaylist:
|
|
4449
|
+
query = gql("""
|
|
4450
|
+
mutation MoveTrackInPlaylist($playlistId: String!, $trackId: String!, $afterTrackId: String) {
|
|
4451
|
+
moveTrackInPlaylist(
|
|
4452
|
+
input: {playlistId: $playlistId, trackId: $trackId, afterTrackId: $afterTrackId}
|
|
4453
|
+
) {
|
|
4454
|
+
moveStatus
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
""")
|
|
4458
|
+
variables: dict[str, object] = {
|
|
4459
|
+
"playlistId": playlist_id,
|
|
4460
|
+
"trackId": track_id,
|
|
4461
|
+
"afterTrackId": after_track_id,
|
|
4462
|
+
}
|
|
4463
|
+
response = await self.execute(
|
|
4464
|
+
query=query,
|
|
4465
|
+
operation_name="MoveTrackInPlaylist",
|
|
4466
|
+
variables=variables,
|
|
4467
|
+
**kwargs,
|
|
4468
|
+
)
|
|
4469
|
+
data = self.get_data(response)
|
|
4470
|
+
return MoveTrackInPlaylist.model_validate(data).move_track_in_playlist
|
|
4471
|
+
|
|
4405
4472
|
async def ban_track_from_recommendation(
|
|
4406
4473
|
self, track_id: str, **kwargs: Any
|
|
4407
4474
|
) -> BanTrackFromRecommendationBanTrackFromRecommendation:
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/fragments.py
RENAMED
|
@@ -132,6 +132,17 @@ class AudiobookFieldsContributorsEdgesNodeArtist(BaseModel):
|
|
|
132
132
|
name: str
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
class FamilyProfileFields(BaseModel):
|
|
136
|
+
id: str
|
|
137
|
+
name: str
|
|
138
|
+
caption: str
|
|
139
|
+
permissions: "FamilyProfileFieldsPermissions"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class FamilyProfileFieldsPermissions(BaseModel):
|
|
143
|
+
is_loggable_as: bool = Field(alias="isLoggableAs")
|
|
144
|
+
|
|
145
|
+
|
|
135
146
|
class LivestreamFields(BaseModel):
|
|
136
147
|
id: str
|
|
137
148
|
name: str
|
|
@@ -292,6 +303,7 @@ AlbumFields.model_rebuild()
|
|
|
292
303
|
ArtistFields.model_rebuild()
|
|
293
304
|
AudiobookChapterFields.model_rebuild()
|
|
294
305
|
AudiobookFields.model_rebuild()
|
|
306
|
+
FamilyProfileFields.model_rebuild()
|
|
295
307
|
LivestreamFields.model_rebuild()
|
|
296
308
|
PageInfoFields.model_rebuild()
|
|
297
309
|
PlaylistFields.model_rebuild()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Generated by ariadne-codegen
|
|
2
|
+
# Source: queries
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from .base_model import BaseModel
|
|
7
|
+
from .fragments import FamilyProfileFields
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GetFamily(BaseModel):
|
|
11
|
+
me: Optional["GetFamilyMe"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GetFamilyMe(BaseModel):
|
|
15
|
+
id: str
|
|
16
|
+
family: Optional["GetFamilyMeFamily"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GetFamilyMeFamily(BaseModel):
|
|
20
|
+
main: "GetFamilyMeFamilyMain"
|
|
21
|
+
linked: list["GetFamilyMeFamilyLinked"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GetFamilyMeFamilyMain(FamilyProfileFields):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GetFamilyMeFamilyLinked(FamilyProfileFields):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
GetFamily.model_rebuild()
|
|
33
|
+
GetFamilyMe.model_rebuild()
|
|
34
|
+
GetFamilyMeFamily.model_rebuild()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Generated by ariadne-codegen
|
|
2
|
+
# Source: queries
|
|
3
|
+
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .base_model import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MoveTrackInPlaylist(BaseModel):
|
|
12
|
+
move_track_in_playlist: "MoveTrackInPlaylistMoveTrackInPlaylist" = Field(
|
|
13
|
+
alias="moveTrackInPlaylist"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MoveTrackInPlaylistMoveTrackInPlaylist(BaseModel):
|
|
18
|
+
move_status: Optional[bool] = Field(alias="moveStatus")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
MoveTrackInPlaylist.model_rebuild()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deezer-python-gql
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: Async typed Python client for Deezer's Pipe GraphQL API.
|
|
5
5
|
Author-email: Julian Daberkow <jdaberkow@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -16,13 +16,13 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
16
16
|
Requires-Dist: yarl>=1.22.0
|
|
17
17
|
Provides-Extra: test
|
|
18
18
|
Requires-Dist: codespell==2.4.3; extra == "test"
|
|
19
|
-
Requires-Dist: mypy==2.
|
|
20
|
-
Requires-Dist: pre-commit==4.
|
|
19
|
+
Requires-Dist: mypy==2.4.0; extra == "test"
|
|
20
|
+
Requires-Dist: pre-commit==4.7.0; extra == "test"
|
|
21
21
|
Requires-Dist: pre-commit-hooks==6.0.0; extra == "test"
|
|
22
22
|
Requires-Dist: pytest==9.1.1; extra == "test"
|
|
23
|
-
Requires-Dist: pytest-asyncio==1.4.
|
|
23
|
+
Requires-Dist: pytest-asyncio==1.4.1; extra == "test"
|
|
24
24
|
Requires-Dist: pytest-cov==7.1.0; extra == "test"
|
|
25
|
-
Requires-Dist: ruff==0.
|
|
25
|
+
Requires-Dist: ruff==0.17.2; extra == "test"
|
|
26
26
|
Provides-Extra: dev
|
|
27
27
|
Requires-Dist: ariadne-codegen[subscriptions]; extra == "dev"
|
|
28
28
|
Dynamic: license-file
|
|
@@ -136,6 +136,7 @@ asyncio.run(main())
|
|
|
136
136
|
| `get_banned_tracks(first, after)` | Tracks banned from recommendations, with ban date |
|
|
137
137
|
| `get_banned_artists(first, after)` | Artists banned from recommendations, with ban date |
|
|
138
138
|
| `get_flow_tuner()` | Flow tuner: `DEFAULT` or `DISCOVERY` |
|
|
139
|
+
| `get_family()` | Deezer Family members, with `isLoggableAs` for usable profiles |
|
|
139
140
|
|
|
140
141
|
### Music Together (Collaborative Playlists)
|
|
141
142
|
|
|
@@ -190,6 +191,7 @@ Keep a track or artist out of the user's recommendations (Flow, mixes).
|
|
|
190
191
|
| `delete_playlist(playlist_id)` | Delete a playlist |
|
|
191
192
|
| `add_tracks_to_playlist(playlist_id, track_ids)` | Add tracks to a playlist |
|
|
192
193
|
| `remove_tracks_from_playlist(playlist_id, ...)` | Remove tracks from a playlist |
|
|
194
|
+
| `move_track_in_playlist(playlist_id, ...)` | Move a track behind another one or to the top |
|
|
193
195
|
|
|
194
196
|
### Podcast Episode Management
|
|
195
197
|
|
|
@@ -286,6 +288,21 @@ If the ARL is rejected (bad or expired), the client raises
|
|
|
286
288
|
`GraphQLClientAuthError` — catch it to prompt for a new ARL instead of retrying.
|
|
287
289
|
Transient server errors raise `GraphQLClientHttpError` instead.
|
|
288
290
|
|
|
291
|
+
### Deezer Family profiles
|
|
292
|
+
|
|
293
|
+
Profiles of a Deezer Family have no ARL of their own, they share the login of the
|
|
294
|
+
Family admin. Pass `account_id` to act as one of them:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
family = (await client.get_family()).family
|
|
298
|
+
profile = next(m for m in family.linked if m.permissions.is_loggable_as)
|
|
299
|
+
profile_client = DeezerGQLClient(arl="ADMIN_ARL", account_id=profile.id)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Members with their own login (independent accounts) are not loggable, they need their
|
|
303
|
+
own ARL. If Deezer does not sign in as the profile, the client raises
|
|
304
|
+
`GraphQLClientAuthError`.
|
|
305
|
+
|
|
289
306
|
## License
|
|
290
307
|
|
|
291
308
|
Apache-2.0
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/SOURCES.txt
RENAMED
|
@@ -37,6 +37,7 @@ deezer_python_gql/generated/get_audiobook_chapter.py
|
|
|
37
37
|
deezer_python_gql/generated/get_banned_artists.py
|
|
38
38
|
deezer_python_gql/generated/get_banned_tracks.py
|
|
39
39
|
deezer_python_gql/generated/get_charts.py
|
|
40
|
+
deezer_python_gql/generated/get_family.py
|
|
40
41
|
deezer_python_gql/generated/get_favorite_albums.py
|
|
41
42
|
deezer_python_gql/generated/get_favorite_artists.py
|
|
42
43
|
deezer_python_gql/generated/get_favorite_audiobooks.py
|
|
@@ -75,6 +76,7 @@ deezer_python_gql/generated/get_user_playlists.py
|
|
|
75
76
|
deezer_python_gql/generated/input_types.py
|
|
76
77
|
deezer_python_gql/generated/mark_as_not_played_podcast_episode.py
|
|
77
78
|
deezer_python_gql/generated/mark_as_played_podcast_episode.py
|
|
79
|
+
deezer_python_gql/generated/move_track_in_playlist.py
|
|
78
80
|
deezer_python_gql/generated/music_together_create_group.py
|
|
79
81
|
deezer_python_gql/generated/music_together_generate_group_name.py
|
|
80
82
|
deezer_python_gql/generated/music_together_join_group.py
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/requires.txt
RENAMED
|
@@ -7,10 +7,10 @@ ariadne-codegen[subscriptions]
|
|
|
7
7
|
|
|
8
8
|
[test]
|
|
9
9
|
codespell==2.4.3
|
|
10
|
-
mypy==2.
|
|
11
|
-
pre-commit==4.
|
|
10
|
+
mypy==2.4.0
|
|
11
|
+
pre-commit==4.7.0
|
|
12
12
|
pre-commit-hooks==6.0.0
|
|
13
13
|
pytest==9.1.1
|
|
14
|
-
pytest-asyncio==1.4.
|
|
14
|
+
pytest-asyncio==1.4.1
|
|
15
15
|
pytest-cov==7.1.0
|
|
16
|
-
ruff==0.
|
|
16
|
+
ruff==0.17.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deezer-python-gql"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.21.0"
|
|
4
4
|
description = "Async typed Python client for Deezer's Pipe GraphQL API."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -24,13 +24,13 @@ text = "Apache-2.0"
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
25
|
test = [
|
|
26
26
|
"codespell==2.4.3",
|
|
27
|
-
"mypy==2.
|
|
28
|
-
"pre-commit==4.
|
|
27
|
+
"mypy==2.4.0",
|
|
28
|
+
"pre-commit==4.7.0",
|
|
29
29
|
"pre-commit-hooks==6.0.0",
|
|
30
30
|
"pytest==9.1.1",
|
|
31
|
-
"pytest-asyncio==1.4.
|
|
31
|
+
"pytest-asyncio==1.4.1",
|
|
32
32
|
"pytest-cov==7.1.0",
|
|
33
|
-
"ruff==0.
|
|
33
|
+
"ruff==0.17.2",
|
|
34
34
|
]
|
|
35
35
|
dev = [
|
|
36
36
|
"ariadne-codegen[subscriptions]",
|
|
@@ -179,3 +179,8 @@ warn_unused_ignores = true
|
|
|
179
179
|
[tool.pytest.ini_options]
|
|
180
180
|
addopts = "--cov deezer_python_gql"
|
|
181
181
|
asyncio_mode = "auto"
|
|
182
|
+
|
|
183
|
+
[tool.uv]
|
|
184
|
+
override-dependencies = [
|
|
185
|
+
"ruff==0.17.2",
|
|
186
|
+
]
|
|
@@ -57,6 +57,7 @@ from deezer_python_gql.generated.get_audiobook_chapter import GetAudiobookChapte
|
|
|
57
57
|
from deezer_python_gql.generated.get_banned_artists import GetBannedArtists
|
|
58
58
|
from deezer_python_gql.generated.get_banned_tracks import GetBannedTracks
|
|
59
59
|
from deezer_python_gql.generated.get_charts import GetCharts
|
|
60
|
+
from deezer_python_gql.generated.get_family import GetFamily
|
|
60
61
|
from deezer_python_gql.generated.get_favorite_albums import GetFavoriteAlbums
|
|
61
62
|
from deezer_python_gql.generated.get_favorite_artists import GetFavoriteArtists
|
|
62
63
|
from deezer_python_gql.generated.get_favorite_audiobooks import GetFavoriteAudiobooks
|
|
@@ -104,6 +105,7 @@ from deezer_python_gql.generated.mark_as_not_played_podcast_episode import (
|
|
|
104
105
|
from deezer_python_gql.generated.mark_as_played_podcast_episode import (
|
|
105
106
|
MarkAsPlayedPodcastEpisode,
|
|
106
107
|
)
|
|
108
|
+
from deezer_python_gql.generated.move_track_in_playlist import MoveTrackInPlaylist
|
|
107
109
|
from deezer_python_gql.generated.music_together_create_group import (
|
|
108
110
|
MusicTogetherCreateGroup,
|
|
109
111
|
)
|
|
@@ -147,11 +149,14 @@ def _load_fixture(name: str) -> dict[str, Any]:
|
|
|
147
149
|
return result
|
|
148
150
|
|
|
149
151
|
|
|
150
|
-
def _make_jwt(
|
|
152
|
+
def _make_jwt(
|
|
153
|
+
exp: float | None = None, subject: str | None = None, user_id: str | None = None
|
|
154
|
+
) -> str:
|
|
151
155
|
"""
|
|
152
156
|
Build a fake JWT with a configurable expiration timestamp.
|
|
153
157
|
|
|
154
158
|
:param exp: Unix timestamp for JWT expiry. Defaults to 6 min from now.
|
|
159
|
+
:param user_id: Optional ``userId`` claim, which Deezer sets to the account id.
|
|
155
160
|
"""
|
|
156
161
|
if exp is None:
|
|
157
162
|
exp = time.time() + 360 # 6 min, matching Deezer's real TTL
|
|
@@ -159,6 +164,8 @@ def _make_jwt(exp: float | None = None, subject: str | None = None) -> str:
|
|
|
159
164
|
payload_data: dict[str, float | str] = {"exp": exp}
|
|
160
165
|
if subject is not None:
|
|
161
166
|
payload_data["sub"] = subject
|
|
167
|
+
if user_id is not None:
|
|
168
|
+
payload_data["userId"] = user_id
|
|
162
169
|
payload = base64.urlsafe_b64encode(json.dumps(payload_data).encode()).rstrip(b"=").decode()
|
|
163
170
|
return f"{header}.{payload}.fake_signature"
|
|
164
171
|
|
|
@@ -334,6 +341,7 @@ def test_client_has_generated_methods() -> None:
|
|
|
334
341
|
"delete_playlist",
|
|
335
342
|
"add_tracks_to_playlist",
|
|
336
343
|
"remove_tracks_from_playlist",
|
|
344
|
+
"move_track_in_playlist",
|
|
337
345
|
"get_podcast",
|
|
338
346
|
"get_podcast_episode",
|
|
339
347
|
"get_podcast_episodes_by_ids",
|
|
@@ -525,6 +533,39 @@ async def test_auth_sends_arl_cookie_to_correct_domain() -> None:
|
|
|
525
533
|
assert auth_call.kwargs["cookies"] == {"arl": "my_secret_arl"}
|
|
526
534
|
|
|
527
535
|
|
|
536
|
+
@pytest.mark.asyncio
|
|
537
|
+
async def test_auth_family_profile_signs_in_with_account_id() -> None:
|
|
538
|
+
"""Verify a Family profile sends the admin's ARL and its id in the payload."""
|
|
539
|
+
client = DeezerBaseClient(arl="admin_arl", account_id="456")
|
|
540
|
+
|
|
541
|
+
with patch("deezer_python_gql.base_client.ClientSession") as mock_cls:
|
|
542
|
+
mock_instance = MagicMock()
|
|
543
|
+
mock_instance.post = MagicMock(return_value=_mock_auth_cm(_make_jwt(user_id="456")))
|
|
544
|
+
mock_instance.close = AsyncMock()
|
|
545
|
+
mock_cls.return_value = mock_instance
|
|
546
|
+
|
|
547
|
+
await client._ensure_jwt() # noqa: SLF001
|
|
548
|
+
|
|
549
|
+
auth_call = mock_instance.post.call_args
|
|
550
|
+
assert auth_call.kwargs["params"]["i"] == "p"
|
|
551
|
+
assert auth_call.kwargs["json"] == {"arl": "admin_arl", "account_id": "456"}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@pytest.mark.asyncio
|
|
555
|
+
async def test_auth_family_profile_rejects_a_token_for_another_account() -> None:
|
|
556
|
+
"""Verify a token for another account is never used for a Family profile."""
|
|
557
|
+
client = DeezerBaseClient(arl="admin_arl", account_id="456")
|
|
558
|
+
|
|
559
|
+
with patch("deezer_python_gql.base_client.ClientSession") as mock_cls:
|
|
560
|
+
mock_instance = MagicMock()
|
|
561
|
+
mock_instance.post = MagicMock(return_value=_mock_auth_cm(_make_jwt(user_id="123")))
|
|
562
|
+
mock_instance.close = AsyncMock()
|
|
563
|
+
mock_cls.return_value = mock_instance
|
|
564
|
+
|
|
565
|
+
with pytest.raises(GraphQLClientAuthError, match="Family profile 456"):
|
|
566
|
+
await client._ensure_jwt() # noqa: SLF001
|
|
567
|
+
|
|
568
|
+
|
|
528
569
|
@pytest.mark.parametrize(("first_arl", "second_arl"), [("arl-a", "arl-b"), ("arl-b", "arl-a")])
|
|
529
570
|
@pytest.mark.asyncio
|
|
530
571
|
async def test_shared_session_keeps_account_cookies_isolated(
|
|
@@ -1484,6 +1525,13 @@ def test_smoke_remove_tracks_from_playlist() -> None:
|
|
|
1484
1525
|
assert result.remove_tracks_from_playlist.removed_track_ids == ["100000001", "100000002"]
|
|
1485
1526
|
|
|
1486
1527
|
|
|
1528
|
+
def test_smoke_move_track_in_playlist() -> None:
|
|
1529
|
+
"""Verify MoveTrackInPlaylist fixture parses with move status."""
|
|
1530
|
+
data = _load_fixture("move_track_in_playlist.json")
|
|
1531
|
+
result = MoveTrackInPlaylist.model_validate(data)
|
|
1532
|
+
assert result.move_track_in_playlist.move_status is True
|
|
1533
|
+
|
|
1534
|
+
|
|
1487
1535
|
def test_smoke_get_livestream() -> None:
|
|
1488
1536
|
"""Verify GetLivestream fixture parses with fields and media URLs."""
|
|
1489
1537
|
data = _load_fixture("get_livestream.json")
|
|
@@ -1969,3 +2017,16 @@ def test_smoke_get_infinite_track_mix() -> None:
|
|
|
1969
2017
|
data = _load_fixture("get_infinite_track_mix.json")
|
|
1970
2018
|
mix = GetInfiniteTrackMix.model_validate(data).raw_infinite_track_mix
|
|
1971
2019
|
assert [t.id for t in mix.tracks] == ["3135554", "3155977", "10284909"]
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
def test_smoke_get_family() -> None:
|
|
2023
|
+
"""Verify GetFamily fixture parses the admin, a profile and an independent account."""
|
|
2024
|
+
data = _load_fixture("get_family.json")
|
|
2025
|
+
me = GetFamily.model_validate(data).me
|
|
2026
|
+
assert me is not None
|
|
2027
|
+
assert me.family is not None
|
|
2028
|
+
assert me.family.main.id == me.id
|
|
2029
|
+
assert [(m.id, m.permissions.is_loggable_as) for m in me.family.linked] == [
|
|
2030
|
+
("2002", True),
|
|
2031
|
+
("3003", False),
|
|
2032
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/base_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/create_playlist.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/delete_playlist.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_album.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_artist.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_artist_mix.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_audiobook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_charts.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow_batch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_flow_tuner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_livestream.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_made_for_me.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_playlist.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_podcast.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_track.py
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_track_mix.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/get_user_charts.py
RENAMED
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/input_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/search_flows.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql/generated/update_playlist.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/not-zip-safe
RENAMED
|
File without changes
|
{deezer_python_gql-0.19.0 → deezer_python_gql-0.21.0}/deezer_python_gql.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|