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 +1 -1
- deezer/_mixin.py +90 -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.4.0.dist-info}/METADATA +4 -3
- deezer_python-7.4.0.dist-info/RECORD +42 -0
- {deezer_python-7.2.0.dist-info → deezer_python-7.4.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.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {deezer_python-7.2.0.dist-info → deezer_python-7.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
4
|
+
from urllib.parse import parse_qs, urlparse
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from deezer.asyncio.client import AsyncClient
|
|
8
|
+
from deezer.asyncio.resources.resource import AsyncResource
|
|
9
|
+
|
|
10
|
+
ResourceType = TypeVar("ResourceType")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncPaginatedList(Generic[ResourceType]):
|
|
14
|
+
"""
|
|
15
|
+
Async paginated response from the API, supporting async iteration.
|
|
16
|
+
|
|
17
|
+
Instances should be created via the :meth:`create` classmethod,
|
|
18
|
+
which fetches the first page of results eagerly.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
client: AsyncClient,
|
|
24
|
+
base_path: str,
|
|
25
|
+
parent: AsyncResource | None = None,
|
|
26
|
+
params: dict | None = None,
|
|
27
|
+
):
|
|
28
|
+
self.__elements: list[ResourceType] = []
|
|
29
|
+
self.__client = client
|
|
30
|
+
self.__base_path = base_path
|
|
31
|
+
self.__base_params = params or {}
|
|
32
|
+
self.__next_path: str | None = base_path
|
|
33
|
+
self.__next_params = params or {}
|
|
34
|
+
self.__parent = parent
|
|
35
|
+
self.__total: int | None = None
|
|
36
|
+
self.__iter_index = 0
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
async def create(
|
|
40
|
+
cls,
|
|
41
|
+
client: AsyncClient,
|
|
42
|
+
base_path: str,
|
|
43
|
+
parent: AsyncResource | None = None,
|
|
44
|
+
params: dict | None = None,
|
|
45
|
+
) -> AsyncPaginatedList[ResourceType]:
|
|
46
|
+
"""Create an AsyncPaginatedList and fetch the first page."""
|
|
47
|
+
instance = cls(client=client, base_path=base_path, parent=parent, params=params)
|
|
48
|
+
await instance._grow()
|
|
49
|
+
return instance
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
"""Convenient representation giving a preview of the content."""
|
|
53
|
+
repr_size = 5
|
|
54
|
+
data: list[ResourceType | str] = list(self.__elements[: repr_size + 1])
|
|
55
|
+
if len(data) > repr_size:
|
|
56
|
+
data[-1] = "..."
|
|
57
|
+
return f"<{self.__class__.__name__} {data!r}>"
|
|
58
|
+
|
|
59
|
+
def __aiter__(self):
|
|
60
|
+
"""Return the async iterator."""
|
|
61
|
+
return self._async_iter()
|
|
62
|
+
|
|
63
|
+
async def _async_iter(self):
|
|
64
|
+
"""Iterate over elements, fetching new pages as needed."""
|
|
65
|
+
yield_index = 0
|
|
66
|
+
while True:
|
|
67
|
+
if yield_index < len(self.__elements):
|
|
68
|
+
yield self.__elements[yield_index]
|
|
69
|
+
yield_index += 1
|
|
70
|
+
elif self._could_grow():
|
|
71
|
+
await self._grow()
|
|
72
|
+
else:
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
async def __anext__(self) -> ResourceType:
|
|
76
|
+
"""Get the next item from the list."""
|
|
77
|
+
if self.__iter_index < len(self.__elements):
|
|
78
|
+
item = self.__elements[self.__iter_index]
|
|
79
|
+
self.__iter_index += 1
|
|
80
|
+
return item
|
|
81
|
+
if self._could_grow():
|
|
82
|
+
new_elements = await self._grow()
|
|
83
|
+
if new_elements and self.__iter_index < len(self.__elements):
|
|
84
|
+
item = self.__elements[self.__iter_index]
|
|
85
|
+
self.__iter_index += 1
|
|
86
|
+
return item
|
|
87
|
+
raise StopAsyncIteration
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def total(self) -> int:
|
|
91
|
+
"""The total number of items in the list, mirroring what Deezer returns."""
|
|
92
|
+
assert self.__total is not None # noqa S101
|
|
93
|
+
return self.__total
|
|
94
|
+
|
|
95
|
+
def __len__(self) -> int:
|
|
96
|
+
"""Get the total number of items across all pages."""
|
|
97
|
+
return self.total
|
|
98
|
+
|
|
99
|
+
async def get(self, index: int) -> ResourceType:
|
|
100
|
+
"""Get an item by index."""
|
|
101
|
+
if index < 0:
|
|
102
|
+
raise IndexError(f"Negative indexing is not supported: {index}")
|
|
103
|
+
await self._fetch_to_index(index)
|
|
104
|
+
if index >= len(self.__elements):
|
|
105
|
+
raise IndexError(f"list index out of range: {index}")
|
|
106
|
+
return self.__elements[index]
|
|
107
|
+
|
|
108
|
+
async def collect(self) -> list[ResourceType]:
|
|
109
|
+
"""Fetch all pages and return the full list of items."""
|
|
110
|
+
while self._could_grow():
|
|
111
|
+
await self._grow()
|
|
112
|
+
return list(self.__elements)
|
|
113
|
+
|
|
114
|
+
def _could_grow(self) -> bool:
|
|
115
|
+
return self.__next_path is not None
|
|
116
|
+
|
|
117
|
+
async def _grow(self) -> list[ResourceType]:
|
|
118
|
+
new_elements = await self._fetch_next_page()
|
|
119
|
+
self.__elements.extend(new_elements)
|
|
120
|
+
return new_elements
|
|
121
|
+
|
|
122
|
+
async def _fetch_next_page(self) -> list[ResourceType]:
|
|
123
|
+
assert self.__next_path is not None # noqa S101
|
|
124
|
+
response_payload = await self.__client.request(
|
|
125
|
+
"GET",
|
|
126
|
+
self.__next_path,
|
|
127
|
+
parent=self.__parent,
|
|
128
|
+
paginate_list=True,
|
|
129
|
+
params=self.__next_params,
|
|
130
|
+
)
|
|
131
|
+
self.__next_path = None
|
|
132
|
+
self.__total = response_payload.get("total")
|
|
133
|
+
next_url = response_payload.get("next", None)
|
|
134
|
+
if next_url:
|
|
135
|
+
url_bits = urlparse(next_url)
|
|
136
|
+
self.__next_path = url_bits.path.lstrip("/")
|
|
137
|
+
self.__next_params = parse_qs(url_bits.query)
|
|
138
|
+
return response_payload["data"]
|
|
139
|
+
|
|
140
|
+
async def _fetch_to_index(self, index: int):
|
|
141
|
+
while len(self.__elements) <= index and self._could_grow():
|
|
142
|
+
await self._grow()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .album import AsyncAlbum
|
|
4
|
+
from .artist import AsyncArtist
|
|
5
|
+
from .chart import AsyncChart
|
|
6
|
+
from .editorial import AsyncEditorial
|
|
7
|
+
from .episode import AsyncEpisode
|
|
8
|
+
from .genre import AsyncGenre
|
|
9
|
+
from .playlist import AsyncPlaylist
|
|
10
|
+
from .podcast import AsyncPodcast
|
|
11
|
+
from .radio import AsyncRadio
|
|
12
|
+
from .resource import AsyncResource
|
|
13
|
+
from .track import AsyncTrack
|
|
14
|
+
from .user import AsyncUser
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"AsyncAlbum",
|
|
18
|
+
"AsyncArtist",
|
|
19
|
+
"AsyncChart",
|
|
20
|
+
"AsyncEditorial",
|
|
21
|
+
"AsyncEpisode",
|
|
22
|
+
"AsyncGenre",
|
|
23
|
+
"AsyncPlaylist",
|
|
24
|
+
"AsyncPodcast",
|
|
25
|
+
"AsyncRadio",
|
|
26
|
+
"AsyncResource",
|
|
27
|
+
"AsyncTrack",
|
|
28
|
+
"AsyncUser",
|
|
29
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from deezer.dates import parse_date
|
|
7
|
+
|
|
8
|
+
from .artist import AsyncArtist
|
|
9
|
+
from .resource import AsyncResource
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncAlbum(AsyncResource):
|
|
16
|
+
"""
|
|
17
|
+
Async album resource.
|
|
18
|
+
|
|
19
|
+
Mirrors the sync Album but with async methods for
|
|
20
|
+
traversing relations.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
id: int
|
|
24
|
+
title: str
|
|
25
|
+
upc: str
|
|
26
|
+
link: str
|
|
27
|
+
share: str
|
|
28
|
+
cover: str
|
|
29
|
+
cover_small: str
|
|
30
|
+
cover_medium: str
|
|
31
|
+
cover_big: str
|
|
32
|
+
cover_xl: str
|
|
33
|
+
md5_image: str
|
|
34
|
+
|
|
35
|
+
genre_id: int
|
|
36
|
+
label: str
|
|
37
|
+
nb_tracks: int
|
|
38
|
+
duration: int
|
|
39
|
+
fans: int
|
|
40
|
+
release_date: dt.date
|
|
41
|
+
record_type: str
|
|
42
|
+
available: bool
|
|
43
|
+
|
|
44
|
+
tracklist: str
|
|
45
|
+
explicit_lyrics: bool
|
|
46
|
+
|
|
47
|
+
explicit_content_lyrics: int
|
|
48
|
+
explicit_content_cover: int
|
|
49
|
+
contributors: list[AsyncArtist]
|
|
50
|
+
|
|
51
|
+
artist: AsyncArtist
|
|
52
|
+
|
|
53
|
+
_parse_release_date = staticmethod(parse_date)
|
|
54
|
+
|
|
55
|
+
def _parse_contributors(self, raw_value):
|
|
56
|
+
return [AsyncArtist(client=self.client, json=val) for val in raw_value]
|
|
57
|
+
|
|
58
|
+
async def get_artist(self) -> AsyncArtist:
|
|
59
|
+
"""
|
|
60
|
+
Get the artist of the Album.
|
|
61
|
+
|
|
62
|
+
:returns: an :class:`AsyncArtist` object
|
|
63
|
+
"""
|
|
64
|
+
return await self.client.request("GET", f"artist/{self.artist.id}")
|
|
65
|
+
|
|
66
|
+
async def get_tracks(self, **kwargs) -> AsyncPaginatedList:
|
|
67
|
+
"""
|
|
68
|
+
Get a list of album's tracks.
|
|
69
|
+
|
|
70
|
+
:returns: an :class:`AsyncPaginatedList` of track resources.
|
|
71
|
+
"""
|
|
72
|
+
return await self.get_paginated_list("tracks", **kwargs)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .resource import AsyncResource
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
9
|
+
|
|
10
|
+
from .album import AsyncAlbum
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncArtist(AsyncResource):
|
|
14
|
+
"""To work with async Deezer artist objects."""
|
|
15
|
+
|
|
16
|
+
id: int
|
|
17
|
+
name: str
|
|
18
|
+
link: str
|
|
19
|
+
share: str
|
|
20
|
+
picture: str
|
|
21
|
+
picture_small: str
|
|
22
|
+
picture_medium: str
|
|
23
|
+
picture_big: str
|
|
24
|
+
picture_xl: str
|
|
25
|
+
nb_album: int
|
|
26
|
+
nb_fan: int
|
|
27
|
+
radio: bool
|
|
28
|
+
tracklist: str
|
|
29
|
+
|
|
30
|
+
async def get_top(self, **kwargs) -> AsyncPaginatedList:
|
|
31
|
+
"""Get the top tracks of an artist."""
|
|
32
|
+
return await self.get_paginated_list("top", **kwargs)
|
|
33
|
+
|
|
34
|
+
async def get_related(self, **kwargs) -> AsyncPaginatedList[AsyncArtist]:
|
|
35
|
+
"""Get a list of related artists."""
|
|
36
|
+
return await self.get_paginated_list("related", **kwargs)
|
|
37
|
+
|
|
38
|
+
async def get_radio(self, **kwargs) -> list:
|
|
39
|
+
"""Get a list of tracks."""
|
|
40
|
+
return await self.get_relation("radio", fwd_parent=False, **kwargs)
|
|
41
|
+
|
|
42
|
+
async def get_albums(self, **kwargs) -> AsyncPaginatedList[AsyncAlbum]:
|
|
43
|
+
"""Get a list of artist's albums."""
|
|
44
|
+
return await self.get_paginated_list("albums", **kwargs)
|
|
45
|
+
|
|
46
|
+
async def get_playlists(self, **kwargs) -> AsyncPaginatedList:
|
|
47
|
+
"""Get a list of artist's playlists."""
|
|
48
|
+
return await self.get_paginated_list("playlists", **kwargs)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .resource import AsyncResource
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncChart(AsyncResource):
|
|
12
|
+
"""Async chart resource."""
|
|
13
|
+
|
|
14
|
+
type = "chart"
|
|
15
|
+
|
|
16
|
+
id: int
|
|
17
|
+
|
|
18
|
+
async def get_tracks(self, **kwargs) -> AsyncPaginatedList:
|
|
19
|
+
"""Return the chart for tracks."""
|
|
20
|
+
return await self.get_paginated_list("tracks", **kwargs)
|
|
21
|
+
|
|
22
|
+
async def get_albums(self, **kwargs) -> AsyncPaginatedList:
|
|
23
|
+
"""Return the chart for albums."""
|
|
24
|
+
return await self.get_paginated_list("albums", **kwargs)
|
|
25
|
+
|
|
26
|
+
async def get_artists(self, **kwargs) -> AsyncPaginatedList:
|
|
27
|
+
"""Return the chart for artists."""
|
|
28
|
+
return await self.get_paginated_list("artists", **kwargs)
|
|
29
|
+
|
|
30
|
+
async def get_playlists(self, **kwargs) -> AsyncPaginatedList:
|
|
31
|
+
"""Return the chart for playlists."""
|
|
32
|
+
return await self.get_paginated_list("playlists", **kwargs)
|
|
33
|
+
|
|
34
|
+
async def get_podcasts(self, **kwargs) -> AsyncPaginatedList:
|
|
35
|
+
"""Return the chart for podcasts."""
|
|
36
|
+
return await self.get_paginated_list("podcasts", **kwargs)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .chart import AsyncChart
|
|
6
|
+
from .resource import AsyncResource
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncEditorial(AsyncResource):
|
|
13
|
+
"""Async editorial resource."""
|
|
14
|
+
|
|
15
|
+
id: int
|
|
16
|
+
name: str
|
|
17
|
+
picture: str
|
|
18
|
+
picture_small: str
|
|
19
|
+
picture_medium: str
|
|
20
|
+
picture_big: str
|
|
21
|
+
picture_xl: str
|
|
22
|
+
|
|
23
|
+
async def get_selection(self) -> list:
|
|
24
|
+
"""Get a list of albums selected every week by the Deezer Team."""
|
|
25
|
+
return await self.get_relation("selection")
|
|
26
|
+
|
|
27
|
+
async def get_chart(self):
|
|
28
|
+
"""Get top charts for tracks, albums, artists and playlists."""
|
|
29
|
+
return await self.get_relation("charts", resource_type=AsyncChart)
|
|
30
|
+
|
|
31
|
+
async def get_releases(self, **kwargs) -> AsyncPaginatedList:
|
|
32
|
+
"""Get the new releases per genre for the current country."""
|
|
33
|
+
return await self.get_paginated_list("releases", **kwargs)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from deezer.dates import parse_datetime
|
|
7
|
+
|
|
8
|
+
from .resource import AsyncResource
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .podcast import AsyncPodcast
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncEpisode(AsyncResource):
|
|
15
|
+
"""Async episode resource."""
|
|
16
|
+
|
|
17
|
+
id: int
|
|
18
|
+
title: str
|
|
19
|
+
description: str
|
|
20
|
+
available: bool
|
|
21
|
+
release_date: dt.datetime
|
|
22
|
+
duration: int
|
|
23
|
+
link: str
|
|
24
|
+
share: str
|
|
25
|
+
picture: str
|
|
26
|
+
picture_small: str
|
|
27
|
+
picture_medium: str
|
|
28
|
+
picture_big: str
|
|
29
|
+
picture_xl: str
|
|
30
|
+
podcast: AsyncPodcast
|
|
31
|
+
|
|
32
|
+
_parse_release_date = staticmethod(parse_datetime)
|
|
33
|
+
|
|
34
|
+
async def add_bookmark(self, offset: int) -> bool:
|
|
35
|
+
"""Set a bookmark on the episode."""
|
|
36
|
+
return await self.client.request(
|
|
37
|
+
"POST",
|
|
38
|
+
f"episode/{self.id}/bookmark",
|
|
39
|
+
params={"offset": offset},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def remove_bookmark(self) -> bool:
|
|
43
|
+
"""Remove the bookmark on the episode."""
|
|
44
|
+
return await self.client.request("DELETE", f"episode/{self.id}/bookmark")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .resource import AsyncResource
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncGenre(AsyncResource):
|
|
12
|
+
"""Async genre resource."""
|
|
13
|
+
|
|
14
|
+
id: int
|
|
15
|
+
name: str
|
|
16
|
+
picture: str
|
|
17
|
+
picture_small: str
|
|
18
|
+
picture_medium: str
|
|
19
|
+
picture_big: str
|
|
20
|
+
picture_xl: str
|
|
21
|
+
|
|
22
|
+
async def get_artists(self, **kwargs) -> list:
|
|
23
|
+
"""Get all artists for a genre."""
|
|
24
|
+
return await self.get_relation("artists", **kwargs)
|
|
25
|
+
|
|
26
|
+
async def get_podcasts(self, **kwargs) -> AsyncPaginatedList:
|
|
27
|
+
"""Get all podcasts for a genre."""
|
|
28
|
+
return await self.get_paginated_list("podcasts", **kwargs)
|
|
29
|
+
|
|
30
|
+
async def get_radios(self, **kwargs) -> list:
|
|
31
|
+
"""Get all radios for a genre."""
|
|
32
|
+
return await self.get_relation("radios", **kwargs)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from deezer.utils import gen_ids
|
|
7
|
+
|
|
8
|
+
from .resource import AsyncResource
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncPlaylist(AsyncResource):
|
|
15
|
+
"""Async playlist resource."""
|
|
16
|
+
|
|
17
|
+
id: int
|
|
18
|
+
title: str
|
|
19
|
+
description: str
|
|
20
|
+
duration: int
|
|
21
|
+
public: bool
|
|
22
|
+
is_loved_track: bool
|
|
23
|
+
collaborative: bool
|
|
24
|
+
nb_tracks: int
|
|
25
|
+
unseen_track_count: int
|
|
26
|
+
fans: int
|
|
27
|
+
link: str
|
|
28
|
+
share: str
|
|
29
|
+
picture: str
|
|
30
|
+
picture_small: str
|
|
31
|
+
picture_medium: str
|
|
32
|
+
picture_big: str
|
|
33
|
+
picture_xl: str
|
|
34
|
+
checksum: str
|
|
35
|
+
|
|
36
|
+
async def get_tracks(self, **kwargs) -> AsyncPaginatedList:
|
|
37
|
+
"""Get tracks from a playlist."""
|
|
38
|
+
return await self.get_paginated_list("tracks", **kwargs)
|
|
39
|
+
|
|
40
|
+
async def get_fans(self, **kwargs) -> AsyncPaginatedList:
|
|
41
|
+
"""Get fans from a playlist."""
|
|
42
|
+
return await self.get_paginated_list("fans", **kwargs)
|
|
43
|
+
|
|
44
|
+
async def mark_seen(self) -> bool:
|
|
45
|
+
"""Mark the playlist as seen."""
|
|
46
|
+
return await self.client.request("POST", f"playlist/{self.id}/seen")
|
|
47
|
+
|
|
48
|
+
async def add_tracks(self, tracks: Iterable[int]) -> bool:
|
|
49
|
+
"""Add tracks to a playlist."""
|
|
50
|
+
track_ids_str = ",".join(str(tid) for tid in gen_ids(tracks))
|
|
51
|
+
return await self.client.request("POST", f"playlist/{self.id}/tracks", params={"songs": track_ids_str})
|
|
52
|
+
|
|
53
|
+
async def delete_tracks(self, tracks: Iterable[int]) -> bool:
|
|
54
|
+
"""Delete tracks from a playlist."""
|
|
55
|
+
track_ids_str = ",".join(map(str, gen_ids(tracks)))
|
|
56
|
+
return await self.client.request("DELETE", f"playlist/{self.id}/tracks", params={"songs": track_ids_str})
|
|
57
|
+
|
|
58
|
+
async def reorder_tracks(self, order: Iterable[int]) -> bool:
|
|
59
|
+
"""Reorder the tracks of a playlist."""
|
|
60
|
+
order_track_ids_str = ",".join(map(str, gen_ids(order)))
|
|
61
|
+
return await self.client.request("POST", f"playlist/{self.id}/tracks", params={"order": order_track_ids_str})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from .resource import AsyncResource
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncPodcast(AsyncResource):
|
|
12
|
+
"""Async podcast resource."""
|
|
13
|
+
|
|
14
|
+
id: int
|
|
15
|
+
title: str
|
|
16
|
+
description: str
|
|
17
|
+
available: bool
|
|
18
|
+
fans: int
|
|
19
|
+
link: str
|
|
20
|
+
share: str
|
|
21
|
+
picture: str
|
|
22
|
+
picture_small: str
|
|
23
|
+
picture_medium: str
|
|
24
|
+
picture_big: str
|
|
25
|
+
picture_xl: str
|
|
26
|
+
|
|
27
|
+
async def get_episodes(self, **kwargs) -> AsyncPaginatedList:
|
|
28
|
+
"""Get episodes from a podcast."""
|
|
29
|
+
return await self.get_paginated_list("episodes", **kwargs)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .resource import AsyncResource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AsyncRadio(AsyncResource):
|
|
7
|
+
"""Async radio resource."""
|
|
8
|
+
|
|
9
|
+
id: int
|
|
10
|
+
title: str
|
|
11
|
+
description: str
|
|
12
|
+
share: str
|
|
13
|
+
picture: str
|
|
14
|
+
picture_small: str
|
|
15
|
+
picture_medium: str
|
|
16
|
+
picture_big: str
|
|
17
|
+
picture_xl: str
|
|
18
|
+
tracklist: str
|
|
19
|
+
md5_image: str
|
|
20
|
+
|
|
21
|
+
async def get_tracks(self) -> list:
|
|
22
|
+
"""Get first 40 tracks in the radio."""
|
|
23
|
+
return await self.get_relation("tracks")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from deezer.asyncio.pagination import AsyncPaginatedList
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncResource:
|
|
10
|
+
"""
|
|
11
|
+
Base class for async resources.
|
|
12
|
+
|
|
13
|
+
Mirrors the sync Resource but with async methods for
|
|
14
|
+
traversing relations via the async client.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
id: int
|
|
18
|
+
type: str
|
|
19
|
+
|
|
20
|
+
_fields: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
def __init__(self, client, json):
|
|
23
|
+
self.client = client
|
|
24
|
+
for field_name in json.keys():
|
|
25
|
+
parse_func = getattr(self, f"_parse_{field_name}", None)
|
|
26
|
+
if callable(parse_func):
|
|
27
|
+
json[field_name] = parse_func(json[field_name])
|
|
28
|
+
self._fields = tuple(json.keys())
|
|
29
|
+
for key in json:
|
|
30
|
+
setattr(self, key, json[key])
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
"""Convenient representation giving a preview of the item."""
|
|
34
|
+
name = getattr(self, "name", None)
|
|
35
|
+
title = getattr(self, "title", None)
|
|
36
|
+
id_ = getattr(self, "id", None)
|
|
37
|
+
return f"<{self.__class__.__name__}: {name or title or id_}>"
|
|
38
|
+
|
|
39
|
+
def as_dict(self) -> dict[str, Any]:
|
|
40
|
+
"""Convert resource to dictionary."""
|
|
41
|
+
result = {}
|
|
42
|
+
for key in self._fields:
|
|
43
|
+
value = getattr(self, key)
|
|
44
|
+
if isinstance(value, list):
|
|
45
|
+
value = [i.as_dict() if isinstance(i, AsyncResource) else i for i in value]
|
|
46
|
+
elif isinstance(value, AsyncResource):
|
|
47
|
+
value = value.as_dict()
|
|
48
|
+
elif isinstance(value, dt.datetime):
|
|
49
|
+
value = f"{value:%Y-%m-%d %H:%M:%S}"
|
|
50
|
+
elif isinstance(value, dt.date):
|
|
51
|
+
value = value.isoformat()
|
|
52
|
+
result[key] = value
|
|
53
|
+
return result
|
|
54
|
+
|
|
55
|
+
async def get_relation(
|
|
56
|
+
self,
|
|
57
|
+
relation: str,
|
|
58
|
+
resource_type: type[AsyncResource] | None = None,
|
|
59
|
+
params: dict | None = None,
|
|
60
|
+
fwd_parent: bool = True,
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Generic async method to load the relation from any resource.
|
|
64
|
+
|
|
65
|
+
Query the client with the object's known parameters
|
|
66
|
+
and try to retrieve the provided relation type.
|
|
67
|
+
"""
|
|
68
|
+
return await self.client.request(
|
|
69
|
+
"GET",
|
|
70
|
+
f"{self.type}/{self.id}/{relation}",
|
|
71
|
+
parent=self if fwd_parent else None,
|
|
72
|
+
resource_type=resource_type,
|
|
73
|
+
params=params,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def post_relation(self, relation: str, params: dict):
|
|
77
|
+
"""Generic async method to make a POST request to a relation."""
|
|
78
|
+
return await self.client.request(
|
|
79
|
+
"POST",
|
|
80
|
+
f"{self.type}/{self.id}/{relation}",
|
|
81
|
+
params=params,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def delete_relation(self, relation: str, params: dict | None = None):
|
|
85
|
+
"""Generic async method to make a DELETE request to a relation."""
|
|
86
|
+
return await self.client.request(
|
|
87
|
+
"DELETE",
|
|
88
|
+
f"{self.type}/{self.id}/{relation}",
|
|
89
|
+
params=params,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def get_paginated_list(
|
|
93
|
+
self,
|
|
94
|
+
relation: str,
|
|
95
|
+
params: dict | None = None,
|
|
96
|
+
) -> AsyncPaginatedList:
|
|
97
|
+
"""Build the pagination object based on the relation."""
|
|
98
|
+
return await AsyncPaginatedList.create(
|
|
99
|
+
client=self.client,
|
|
100
|
+
base_path=f"{self.type}/{self.id}/{relation}",
|
|
101
|
+
parent=self,
|
|
102
|
+
params=params,
|
|
103
|
+
)
|