deezer-python 7.2.0__py3-none-any.whl → 7.4.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -15,7 +15,7 @@ from deezer.resources import (
15
15
  User,
16
16
  )
17
17
 
18
- __version__ = "7.2.0"
18
+ __version__ = "7.4.0"
19
19
  __all__ = [
20
20
  "Album",
21
21
  "Artist",
deezer/_mixin.py ADDED
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from deezer.exceptions import DeezerUnknownResource
6
+ from deezer.resources import (
7
+ Album,
8
+ Artist,
9
+ Chart,
10
+ Editorial,
11
+ Episode,
12
+ Genre,
13
+ Playlist,
14
+ Podcast,
15
+ Radio,
16
+ Resource,
17
+ Track,
18
+ User,
19
+ )
20
+
21
+
22
+ class DeezerMixin:
23
+ """Mixin providing shared logic for both sync and async Deezer clients."""
24
+
25
+ _resource_base_class: ClassVar[type[Resource]] = Resource
26
+
27
+ objects_types: ClassVar[dict[str, type[Resource] | None]] = {
28
+ "album": Album,
29
+ "artist": Artist,
30
+ "chart": Chart,
31
+ "editorial": Editorial,
32
+ "episode": Episode,
33
+ "genre": Genre,
34
+ "playlist": Playlist,
35
+ "podcast": Podcast,
36
+ "radio": Radio,
37
+ "search": None,
38
+ "track": Track,
39
+ "user": User,
40
+ }
41
+
42
+ def _process_json(
43
+ self,
44
+ item: dict[str, Any],
45
+ parent: Any = None,
46
+ resource_type: type[Any] | None = None,
47
+ resource_id: int | None = None,
48
+ paginate_list=False,
49
+ ):
50
+ """
51
+ Recursively convert dictionary to :class:`~deezer.Resource` object.
52
+
53
+ :param item: the JSON response as dict.
54
+ :param parent: A reference to the parent resource, to avoid fetching again.
55
+ :param resource_type: The resource class to use as top level.
56
+ :param resource_id: The resource id to use as top level.
57
+ :param paginate_list: Whether to wrap list into a pagination object.
58
+ :returns: instance of :class:`~deezer.Resource`
59
+ """
60
+ if "data" in item:
61
+ parsed_data = [self._process_json(i, parent, paginate_list=False) for i in item["data"]]
62
+ if not paginate_list:
63
+ return parsed_data
64
+ item["data"] = parsed_data
65
+ return item
66
+
67
+ result = {}
68
+ for key, value in item.items():
69
+ if isinstance(value, dict) and ("type" in value or "data" in value):
70
+ value = self._process_json(value, parent)
71
+ result[key] = value
72
+ if parent is not None:
73
+ result[parent.type] = parent
74
+
75
+ if "id" not in result and resource_id is not None:
76
+ result["id"] = resource_id
77
+
78
+ resource_type_name = result.get("type")
79
+ if isinstance(resource_type_name, str) and resource_type_name in self.objects_types:
80
+ object_class = self.objects_types[resource_type_name]
81
+ elif "type" in result or (not resource_type and "id" in result):
82
+ object_class = self._resource_base_class
83
+ elif resource_type:
84
+ object_class = resource_type
85
+ elif item.get("results") is True:
86
+ return True
87
+ else:
88
+ raise DeezerUnknownResource(f"Unable to find resource type for {result!r}")
89
+ assert object_class is not None # noqa S101
90
+ return object_class(self, result)
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from .client import AsyncClient
4
+ from .pagination import AsyncPaginatedList
5
+ from .resources import (
6
+ AsyncAlbum,
7
+ AsyncArtist,
8
+ AsyncChart,
9
+ AsyncEditorial,
10
+ AsyncEpisode,
11
+ AsyncGenre,
12
+ AsyncPlaylist,
13
+ AsyncPodcast,
14
+ AsyncRadio,
15
+ AsyncResource,
16
+ AsyncTrack,
17
+ AsyncUser,
18
+ )
19
+
20
+ __all__ = [
21
+ "AsyncAlbum",
22
+ "AsyncArtist",
23
+ "AsyncChart",
24
+ "AsyncClient",
25
+ "AsyncEditorial",
26
+ "AsyncEpisode",
27
+ "AsyncGenre",
28
+ "AsyncPaginatedList",
29
+ "AsyncPlaylist",
30
+ "AsyncPodcast",
31
+ "AsyncRadio",
32
+ "AsyncResource",
33
+ "AsyncTrack",
34
+ "AsyncUser",
35
+ ]
@@ -0,0 +1,443 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ import httpx
6
+ from httpx._types import HeaderTypes
7
+
8
+ from deezer._mixin import DeezerMixin
9
+ from deezer.auth import DeezerQueryAuth
10
+ from deezer.exceptions import (
11
+ DeezerErrorResponse,
12
+ DeezerHTTPError,
13
+ )
14
+ from deezer.resources import Resource
15
+
16
+ from .pagination import AsyncPaginatedList
17
+ from .resources import (
18
+ AsyncAlbum,
19
+ AsyncArtist,
20
+ AsyncChart,
21
+ AsyncEditorial,
22
+ AsyncEpisode,
23
+ AsyncGenre,
24
+ AsyncPlaylist,
25
+ AsyncPodcast,
26
+ AsyncRadio,
27
+ AsyncResource,
28
+ AsyncTrack,
29
+ AsyncUser,
30
+ )
31
+
32
+
33
+ class AsyncClient(DeezerMixin, httpx.AsyncClient):
34
+ """
35
+ An async client to retrieve some basic infos about Deezer resources.
36
+
37
+ Create a client instance with the given options. Options should
38
+ be passed in to the constructor as kwargs.
39
+
40
+ >>> from deezer.asyncio import AsyncClient
41
+ >>> client = AsyncClient()
42
+
43
+ This client provides several async methods to retrieve the content of
44
+ Deezer objects, based on their json structure.
45
+
46
+ Headers can be forced by using the ``headers`` kwarg.
47
+ For example, use ``Accept-Language`` header to force the output language.
48
+
49
+ >>> from deezer.asyncio import AsyncClient
50
+ >>> client = AsyncClient(headers={'Accept-Language': 'fr'})
51
+
52
+ :param access_token: user access token.
53
+ :param headers: a dictionary of headers to be used.
54
+ """
55
+
56
+ _resource_base_class: ClassVar[type[AsyncResource]] = AsyncResource
57
+
58
+ objects_types: ClassVar[dict[str, type[AsyncResource] | None]] = {
59
+ "album": AsyncAlbum,
60
+ "artist": AsyncArtist,
61
+ "chart": AsyncChart,
62
+ "editorial": AsyncEditorial,
63
+ "episode": AsyncEpisode,
64
+ "genre": AsyncGenre,
65
+ "playlist": AsyncPlaylist,
66
+ "podcast": AsyncPodcast,
67
+ "radio": AsyncRadio,
68
+ "track": AsyncTrack,
69
+ "user": AsyncUser,
70
+ }
71
+
72
+ def __init__(
73
+ self,
74
+ access_token: str | None = None,
75
+ headers: HeaderTypes | None = None,
76
+ ):
77
+ if access_token:
78
+ deezer_auth = DeezerQueryAuth(access_token=access_token)
79
+ else:
80
+ deezer_auth = None
81
+ super().__init__(
82
+ base_url="https://api.deezer.com",
83
+ auth=deezer_auth,
84
+ headers=headers,
85
+ )
86
+
87
+ async def request( # ty: ignore[invalid-method-override]
88
+ self,
89
+ method: str,
90
+ path: str,
91
+ parent: AsyncResource | Resource | None = None,
92
+ resource_type: type[AsyncResource] | type[Resource] | None = None,
93
+ resource_id: int | None = None,
94
+ paginate_list=False,
95
+ **kwargs,
96
+ ):
97
+ """
98
+ Make a request to the API and parse the response.
99
+
100
+ :param method: HTTP verb to use: GET, POST, DELETE, ...
101
+ :param path: The path to make the API call to (e.g. 'artist/1234').
102
+ :param parent: A reference to the parent resource, to avoid fetching again.
103
+ :param resource_type: The resource class to use as top level.
104
+ :param resource_id: The resource id to use as top level.
105
+ :param paginate_list: Whether to wrap list into a pagination object.
106
+ """
107
+ response = await super().request(
108
+ method,
109
+ path,
110
+ **kwargs,
111
+ )
112
+ try:
113
+ response.raise_for_status()
114
+ except httpx.HTTPStatusError as exc:
115
+ raise DeezerHTTPError.from_http_error(exc) from exc
116
+ json_data = response.json()
117
+ if not isinstance(json_data, dict):
118
+ return json_data
119
+ if json_data.get("error"):
120
+ raise DeezerErrorResponse(json_data)
121
+ return self._process_json(
122
+ json_data,
123
+ parent=parent,
124
+ resource_type=resource_type,
125
+ resource_id=resource_id,
126
+ paginate_list=paginate_list,
127
+ )
128
+
129
+ async def _get_paginated_list(self, path: str, params: dict | None = None):
130
+ return await AsyncPaginatedList.create(client=self, base_path=path, params=params)
131
+
132
+ async def get_artist(self, artist_id: int) -> AsyncArtist:
133
+ """
134
+ Get the artist with the given ID.
135
+
136
+ :returns: an :class:`~deezer.asyncio.AsyncArtist` object
137
+ """
138
+ return await self.request("GET", f"artist/{artist_id}")
139
+
140
+ async def get_album(self, album_id: int) -> AsyncAlbum:
141
+ """
142
+ Get the album with the given ID.
143
+
144
+ :returns: an :class:`~deezer.asyncio.AsyncAlbum` object
145
+ """
146
+ return await self.request("GET", f"album/{album_id}")
147
+
148
+ async def get_track(self, track_id: int) -> AsyncTrack:
149
+ """
150
+ Get the track with the given ID.
151
+
152
+ :returns: an :class:`~deezer.asyncio.AsyncTrack` object
153
+ """
154
+ return await self.request("GET", f"track/{track_id}")
155
+
156
+ async def get_genre(self, genre_id: int) -> AsyncGenre:
157
+ """
158
+ Get the genre with the given ID.
159
+
160
+ :returns: an :class:`~deezer.asyncio.AsyncGenre` object
161
+ """
162
+ return await self.request("GET", f"genre/{genre_id}")
163
+
164
+ async def get_radio(self, radio_id: int) -> AsyncRadio:
165
+ """
166
+ Get the radio with the given ID.
167
+
168
+ :returns: an :class:`~deezer.asyncio.AsyncRadio` object
169
+ """
170
+ return await self.request("GET", f"radio/{radio_id}")
171
+
172
+ async def get_podcast(self, podcast_id: int) -> AsyncPodcast:
173
+ """
174
+ Get the podcast with the given ID.
175
+
176
+ :returns: an :class:`~deezer.asyncio.AsyncPodcast` object
177
+ """
178
+ return await self.request("GET", f"podcast/{podcast_id}")
179
+
180
+ async def get_episode(self, episode_id: int) -> AsyncEpisode:
181
+ """
182
+ Get the episode with the given ID.
183
+
184
+ :returns: an :class:`~deezer.asyncio.AsyncEpisode` object
185
+ """
186
+ return await self.request("GET", f"episode/{episode_id}")
187
+
188
+ async def get_editorial(self, editorial_id: int) -> AsyncEditorial:
189
+ """
190
+ Get the editorial with the given ID.
191
+
192
+ :returns: an :class:`~deezer.asyncio.AsyncEditorial` object
193
+ """
194
+ return await self.request("GET", f"editorial/{editorial_id}")
195
+
196
+ async def get_playlist(self, playlist_id: int) -> AsyncPlaylist:
197
+ """
198
+ Get the playlist with the given ID.
199
+
200
+ :returns: an :class:`~deezer.asyncio.AsyncPlaylist` object
201
+ """
202
+ return await self.request("GET", f"playlist/{playlist_id}")
203
+
204
+ async def get_user(self, user_id: int | None = None) -> AsyncUser:
205
+ """
206
+ Get the user with the given ID.
207
+
208
+ :returns: an :class:`~deezer.asyncio.AsyncUser` object
209
+ """
210
+ user_id_str = str(user_id) if user_id else "me"
211
+ return await self.request("GET", f"user/{user_id_str}")
212
+
213
+ async def get_chart(self, genre_id: int = 0) -> AsyncChart:
214
+ """
215
+ Get overall charts for the given genre ID.
216
+
217
+ :returns: an :class:`~deezer.asyncio.AsyncChart` object
218
+ """
219
+ return await self.request("GET", f"chart/{genre_id}", resource_type=AsyncChart, resource_id=genre_id)
220
+
221
+ async def list_editorials(self) -> AsyncPaginatedList:
222
+ """
223
+ List editorials.
224
+
225
+ :returns: an :class:`AsyncPaginatedList` of :class:`AsyncEditorial` objects.
226
+ """
227
+ return await self._get_paginated_list("editorial")
228
+
229
+ async def list_genres(self) -> list[AsyncGenre]:
230
+ """
231
+ List musical genres.
232
+
233
+ :returns: a list of :class:`AsyncGenre` instances
234
+ """
235
+ return await self.request("GET", "genre")
236
+
237
+ async def list_radios(self) -> list[AsyncRadio]:
238
+ """
239
+ List radios.
240
+
241
+ :returns: a list of :class:`AsyncRadio` instances
242
+ """
243
+ return await self.request("GET", "radio")
244
+
245
+ async def get_radios_top(self) -> AsyncPaginatedList:
246
+ """
247
+ Get the top radios.
248
+
249
+ :returns: an :class:`AsyncPaginatedList` of :class:`AsyncRadio` objects.
250
+ """
251
+ return await self._get_paginated_list("radio/top")
252
+
253
+ async def get_user_recommended_tracks(self, **kwargs) -> AsyncPaginatedList:
254
+ """Get user's recommended tracks."""
255
+ return await AsyncPaginatedList.create(client=self, base_path="user/me/recommendations/tracks", **kwargs)
256
+
257
+ async def get_user_recommended_albums(self, **kwargs) -> AsyncPaginatedList:
258
+ """Get user's recommended albums."""
259
+ return await AsyncPaginatedList.create(client=self, base_path="user/me/recommendations/albums", **kwargs)
260
+
261
+ async def get_user_recommended_artists(self, **kwargs) -> AsyncPaginatedList:
262
+ """Get user's recommended artists."""
263
+ return await AsyncPaginatedList.create(client=self, base_path="user/me/recommendations/artists", **kwargs)
264
+
265
+ async def get_user_recommended_playlists(self, **kwargs) -> AsyncPaginatedList:
266
+ """Get user's recommended playlists."""
267
+ return await AsyncPaginatedList.create(client=self, base_path="user/me/recommendations/playlists", **kwargs)
268
+
269
+ async def get_user_flow(self, **kwargs) -> AsyncPaginatedList:
270
+ """Get user's flow."""
271
+ return await AsyncPaginatedList.create(client=self, base_path="user/me/flow", **kwargs)
272
+
273
+ async def get_user_albums(self, user_id: int | None = None) -> AsyncPaginatedList:
274
+ """Get the favourites albums for the given user_id or current user."""
275
+ user_id_str = str(user_id) if user_id else "me"
276
+ return await self._get_paginated_list(f"user/{user_id_str}/albums")
277
+
278
+ async def add_user_album(self, album_id: int) -> bool:
279
+ """Add an album to the user's library."""
280
+ return await self.request("POST", "user/me/albums", params={"album_id": album_id})
281
+
282
+ async def remove_user_album(self, album_id: int) -> bool:
283
+ """Remove an album from the user's library."""
284
+ return await self.request("DELETE", "user/me/albums", params={"album_id": album_id})
285
+
286
+ async def get_user_artists(self, user_id: int | None = None) -> AsyncPaginatedList:
287
+ """Get the favourites artists for the given user_id or current user."""
288
+ user_id_str = str(user_id) if user_id else "me"
289
+ return await self._get_paginated_list(f"user/{user_id_str}/artists")
290
+
291
+ async def add_user_artist(self, artist_id: int) -> bool:
292
+ """Add an artist to the user's library."""
293
+ return await self.request("POST", "user/me/artists", params={"artist_id": artist_id})
294
+
295
+ async def remove_user_artist(self, artist_id: int) -> bool:
296
+ """Remove an artist from the user's library."""
297
+ return await self.request("DELETE", "user/me/artists", params={"artist_id": artist_id})
298
+
299
+ async def get_user_followers(self, user_id: int | None = None) -> AsyncPaginatedList:
300
+ """Get the followers for the given user_id or current user."""
301
+ user_id_str = str(user_id) if user_id else "me"
302
+ return await self._get_paginated_list(f"user/{user_id_str}/followers")
303
+
304
+ async def get_user_followings(self, user_id: int | None = None) -> AsyncPaginatedList:
305
+ """Get the followings for the given user_id or current user."""
306
+ user_id_str = str(user_id) if user_id else "me"
307
+ return await self._get_paginated_list(f"user/{user_id_str}/followings")
308
+
309
+ async def add_user_following(self, user_id: int) -> bool:
310
+ """Follow the given user ID as the currently authenticated user."""
311
+ return await self.request("POST", "user/me/followings", params={"user_id": user_id})
312
+
313
+ async def remove_user_following(self, user_id: int) -> bool:
314
+ """Stop following the given user ID as the currently authenticated user."""
315
+ return await self.request("DELETE", "user/me/followings", params={"user_id": user_id})
316
+
317
+ async def get_user_history(self) -> AsyncPaginatedList:
318
+ """Get the recently played tracks for the current user."""
319
+ return await self._get_paginated_list("user/me/history")
320
+
321
+ async def get_user_tracks(self, user_id: int | None = None) -> AsyncPaginatedList:
322
+ """Get the favourites tracks for the given user_id or current user."""
323
+ user_id_str = str(user_id) if user_id else "me"
324
+ return await self._get_paginated_list(f"user/{user_id_str}/tracks")
325
+
326
+ async def add_user_track(self, track_id: int) -> bool:
327
+ """Add a track to the user's library."""
328
+ return await self.request("POST", "user/me/tracks", params={"track_id": track_id})
329
+
330
+ async def remove_user_track(self, track_id: int) -> bool:
331
+ """Remove a track from the user's library."""
332
+ return await self.request("DELETE", "user/me/tracks", params={"track_id": track_id})
333
+
334
+ async def add_user_playlist(self, playlist_id: int) -> bool:
335
+ """Add a playlist to the user's library."""
336
+ return await self.request("POST", "user/me/playlists", params={"playlist_id": playlist_id})
337
+
338
+ async def remove_user_playlist(self, playlist_id: int) -> bool:
339
+ """Remove a playlist from the user's library."""
340
+ return await self.request("DELETE", "user/me/playlists", params={"playlist_id": playlist_id})
341
+
342
+ async def create_playlist(self, playlist_name: str) -> int:
343
+ """
344
+ Create a playlist on the user's account.
345
+
346
+ :returns: the ID of the playlist that was created
347
+ """
348
+ result = await self.request("POST", "user/me/playlists", params={"title": playlist_name})
349
+ return result.id
350
+
351
+ async def delete_playlist(self, playlist_id: int) -> bool:
352
+ """Delete a playlist from the user's account."""
353
+ return await self.request("DELETE", f"playlist/{playlist_id}")
354
+
355
+ async def _search(
356
+ self,
357
+ path: str,
358
+ query: str = "",
359
+ strict: bool | None = None,
360
+ ordering: str | None = None,
361
+ **advanced_params: str | int | None,
362
+ ):
363
+ optional_params = {}
364
+ if strict is True:
365
+ optional_params["strict"] = "on"
366
+ if ordering:
367
+ optional_params["ordering"] = ordering
368
+ query_parts = []
369
+ if query:
370
+ query_parts.append(query)
371
+ query_parts.extend(
372
+ f'{param_name}:"{param_value}"' for param_name, param_value in advanced_params.items() if param_value
373
+ )
374
+
375
+ return await self._get_paginated_list(
376
+ path=f"search/{path}" if path else "search",
377
+ params={
378
+ "q": " ".join(query_parts),
379
+ **optional_params,
380
+ },
381
+ )
382
+
383
+ async def search(
384
+ self,
385
+ query: str = "",
386
+ strict: bool | None = None,
387
+ ordering: str | None = None,
388
+ artist: str | None = None,
389
+ album: str | None = None,
390
+ track: str | None = None,
391
+ label: str | None = None,
392
+ dur_min: int | None = None,
393
+ dur_max: int | None = None,
394
+ bpm_min: int | None = None,
395
+ bpm_max: int | None = None,
396
+ ) -> AsyncPaginatedList:
397
+ """
398
+ Search tracks.
399
+
400
+ Advanced search is available by either formatting the query yourself or
401
+ by using the dedicated keywords arguments.
402
+ """
403
+ return await self._search(
404
+ "",
405
+ query=query,
406
+ strict=strict,
407
+ ordering=ordering,
408
+ artist=artist,
409
+ album=album,
410
+ track=track,
411
+ label=label,
412
+ dur_min=dur_min,
413
+ dur_max=dur_max,
414
+ bpm_min=bpm_min,
415
+ bpm_max=bpm_max,
416
+ )
417
+
418
+ async def search_albums(
419
+ self,
420
+ query: str = "",
421
+ strict: bool | None = None,
422
+ ordering: str | None = None,
423
+ ) -> AsyncPaginatedList:
424
+ """Search albums matching the given query."""
425
+ return await self._search(path="album", query=query, strict=strict, ordering=ordering)
426
+
427
+ async def search_artists(
428
+ self,
429
+ query: str = "",
430
+ strict: bool | None = None,
431
+ ordering: str | None = None,
432
+ ) -> AsyncPaginatedList:
433
+ """Search artists matching the given query."""
434
+ return await self._search(path="artist", query=query, strict=strict, ordering=ordering)
435
+
436
+ async def search_playlists(
437
+ self,
438
+ query: str = "",
439
+ strict: bool | None = None,
440
+ ordering: str | None = None,
441
+ ) -> AsyncPaginatedList:
442
+ """Search playlists matching the given query."""
443
+ return await self._search(path="playlist", query=query, strict=strict, ordering=ordering)