deezer-python 7.2.0__py3-none-any.whl → 7.3.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 +1 -1
- deezer/_mixin.py +89 -0
- deezer/asyncio/__init__.py +35 -0
- deezer/asyncio/client.py +443 -0
- deezer/asyncio/pagination.py +142 -0
- deezer/asyncio/resources/__init__.py +29 -0
- deezer/asyncio/resources/album.py +72 -0
- deezer/asyncio/resources/artist.py +48 -0
- deezer/asyncio/resources/chart.py +36 -0
- deezer/asyncio/resources/editorial.py +33 -0
- deezer/asyncio/resources/episode.py +44 -0
- deezer/asyncio/resources/genre.py +32 -0
- deezer/asyncio/resources/playlist.py +61 -0
- deezer/asyncio/resources/podcast.py +29 -0
- deezer/asyncio/resources/radio.py +23 -0
- deezer/asyncio/resources/resource.py +103 -0
- deezer/asyncio/resources/track.py +56 -0
- deezer/asyncio/resources/user.py +110 -0
- deezer/auth.py +1 -1
- deezer/client.py +3 -71
- {deezer_python-7.2.0.dist-info → deezer_python-7.3.0.dist-info}/METADATA +1 -1
- deezer_python-7.3.0.dist-info/RECORD +42 -0
- {deezer_python-7.2.0.dist-info → deezer_python-7.3.0.dist-info}/WHEEL +1 -1
- deezer_python-7.2.0.dist-info/RECORD +0 -25
- {deezer_python-7.2.0.dist-info → deezer_python-7.3.0.dist-info}/licenses/LICENSE.txt +0 -0
- {deezer_python-7.2.0.dist-info → deezer_python-7.3.0.dist-info}/top_level.txt +0 -0
deezer/__init__.py
CHANGED
deezer/_mixin.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
if "type" in result and result["type"] in self.objects_types:
|
|
79
|
+
object_class = self.objects_types[result["type"]]
|
|
80
|
+
elif "type" in result or (not resource_type and "id" in result):
|
|
81
|
+
object_class = self._resource_base_class
|
|
82
|
+
elif resource_type:
|
|
83
|
+
object_class = resource_type
|
|
84
|
+
elif item.get("results") is True:
|
|
85
|
+
return True
|
|
86
|
+
else:
|
|
87
|
+
raise DeezerUnknownResource(f"Unable to find resource type for {result!r}")
|
|
88
|
+
assert object_class is not None # noqa S101
|
|
89
|
+
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
|
+
]
|
deezer/asyncio/client.py
ADDED
|
@@ -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)
|