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.
@@ -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
+ )