Skip to content

Commit be4e7c7

Browse files
committed
✨(project) add playlist support
We are now able to explore and play Deezer playlists!
1 parent 597d990 commit be4e7c7

File tree

4 files changed

+133
-15
lines changed

4 files changed

+133
-15
lines changed

src/onzr/cli.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
ArtistShort,
4343
Collection,
4444
PlayerControl,
45+
PlaylistShort,
4546
ServerState,
4647
TrackShort,
4748
)
@@ -172,8 +173,16 @@ def print_collection_table(collection: Collection, title="Collection"):
172173
sorted_collection.extend(albums_without_release_date)
173174
collection = sorted_collection
174175

176+
if isinstance(sample, PlaylistShort):
177+
table.add_column("Title", style=theme.title_color.as_hex())
178+
table.add_column("Public", style="italic")
179+
table.add_column("# tracks", style="bold")
180+
table.add_column("User", style=theme.secondary_color.as_hex())
181+
175182
for item in collection:
176-
table.add_row(*map(str, item.model_dump(exclude_none=True).values()))
183+
table.add_row(
184+
*map(str, item.model_dump(exclude_none=True, exclude={"tracks"}).values())
185+
)
177186

178187
console.print(table)
179188

@@ -259,6 +268,9 @@ def search(
259268
track: Annotated[
260269
str, typer.Option("--track", "-t", help="Search by track title.")
261270
] = "",
271+
playlist: Annotated[
272+
str, typer.Option("--playlist", "-p", help="Search by playlist name.")
273+
] = "",
262274
strict: Annotated[
263275
bool, typer.Option("--strict", "-s", help="Only consider strict matches.")
264276
] = False,
@@ -285,7 +297,7 @@ def search(
285297
if not quiet:
286298
console.print("🔍 start searching…")
287299
try:
288-
results = deezer.search(artist, album, track, strict, release)
300+
results = deezer.search(artist, album, track, playlist, strict, release)
289301
except ValueError as err:
290302
raise typer.Exit(code=ExitCodes.INVALID_ARGUMENTS) from err
291303

@@ -391,6 +403,37 @@ def album(
391403
print_collection_table(collection, title="Album tracks")
392404

393405

406+
@cli.command()
407+
def playlist(
408+
playlist_id: str,
409+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Quiet output.")] = False,
410+
ids: Annotated[
411+
bool, typer.Option("--ids", "-i", help="Show only result IDs.")
412+
] = False,
413+
):
414+
"""Get playlist tracks."""
415+
if ids:
416+
quiet = True
417+
418+
if playlist_id == "-":
419+
logger.debug("Reading playlist id from stdin…")
420+
playlist_id = click.get_text_stream("stdin").read().strip()
421+
logger.debug(f"{playlist_id=}")
422+
423+
deezer = get_deezer_client(quiet=quiet)
424+
playlist = deezer.playlist(int(playlist_id))
425+
426+
if playlist.tracks is None:
427+
console.print("This playlist contains no tracks.")
428+
raise typer.Exit(code=ExitCodes.INVALID_ARGUMENTS)
429+
430+
if ids:
431+
print_collection_ids(playlist.tracks)
432+
return
433+
434+
print_collection_table(playlist.tracks, title=f"{playlist.title}")
435+
436+
394437
@cli.command()
395438
def mix(
396439
artist: list[str],

src/onzr/deezer.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
DeezerArtistTopResponse,
2626
DeezerSearchAlbumResponse,
2727
DeezerSearchArtistResponse,
28+
DeezerSearchPlaylistResponse,
2829
DeezerSearchResponse,
2930
DeezerSearchTrackResponse,
3031
DeezerSong,
3132
DeezerTrack,
3233
to_albums,
3334
to_artists,
35+
to_playlists,
3436
to_tracks,
3537
)
3638

@@ -99,7 +101,6 @@ def _fast_login(self):
99101
def _collection_details(
100102
self,
101103
collection: Collection,
102-
# ) -> Generator[TrackShort, None, None] | Generator[AlbumShort, None, None]:
103104
) -> Collection:
104105
"""Add detailled informations to collection.
105106
@@ -270,11 +271,25 @@ def track(self, track_id: int) -> TrackShort:
270271
DeezerTrack, self._api(DeezerTrack, self.api.get_track, track_id)
271272
).to_short()
272273

274+
def playlist(self, playlist_id: int) -> PlaylistShort:
275+
"""Get playlist tracks."""
276+
response = self.api.get_playlist(playlist_id)
277+
logger.debug(f"{response=}")
278+
return PlaylistShort(
279+
id=response["id"],
280+
title=response["title"],
281+
public=response["public"],
282+
nb_tracks=response["nb_tracks"],
283+
user=response["creator"]["name"],
284+
tracks=list(self._to_tracks(response["tracks"]["data"])),
285+
)
286+
273287
def search(
274288
self,
275289
artist: str = "",
276290
album: str = "",
277291
track: str = "",
292+
playlist: str = "",
278293
strict: bool = False,
279294
fetch_release_date: bool = False,
280295
) -> Collection:
@@ -335,6 +350,19 @@ def search(
335350
),
336351
)
337352
)
353+
elif playlist:
354+
results = list(
355+
to_playlists(
356+
cast(
357+
DeezerSearchPlaylistResponse,
358+
self._api(
359+
DeezerSearchPlaylistResponse,
360+
self.api.search_playlist,
361+
playlist,
362+
),
363+
),
364+
)
365+
)
338366

339367
if (self.always_fetch_release_date or fetch_release_date) and not artist:
340368
results = self._collection_details(results)

src/onzr/models/core.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,20 @@ class TrackShort(BaseModel):
9393
release_date: Optional[date] = None
9494

9595

96-
Collection: TypeAlias = List[ArtistShort] | List[AlbumShort] | List[TrackShort]
96+
class PlaylistShort(BaseModel):
97+
"""A small model to represent a playlist."""
98+
99+
id: int
100+
title: str
101+
public: bool
102+
nb_tracks: int
103+
user: str
104+
tracks: Optional[List[TrackShort]] = None
105+
106+
107+
Collection: TypeAlias = (
108+
List[ArtistShort] | List[AlbumShort] | List[TrackShort] | List[PlaylistShort]
109+
)
97110

98111

99112
class TrackInfo(BaseModel):

src/onzr/models/deezer.py

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
from annotated_types import Ge, Gt
88
from pydantic import BaseModel, PlainSerializer, PositiveInt
99

10-
from .core import AlbumShort, ArtistShort, StreamQuality, TrackInfo, TrackShort
10+
from .core import (
11+
AlbumShort,
12+
ArtistShort,
13+
PlaylistShort,
14+
StreamQuality,
15+
TrackInfo,
16+
TrackShort,
17+
)
1118

1219
logger = logging.getLogger(__name__)
1320

@@ -79,6 +86,32 @@ def to_short(self) -> TrackShort:
7986
)
8087

8188

89+
class DeezerUser(BaseDeezerModel):
90+
"""Deezer API user."""
91+
92+
name: str
93+
94+
95+
class DeezerPlaylist(BaseDeezerModel):
96+
"""Deezer API playlist."""
97+
98+
id: PositiveInt
99+
title: str
100+
public: bool
101+
nb_tracks: Annotated[int, Ge(0)]
102+
user: DeezerUser
103+
104+
def to_short(self) -> PlaylistShort:
105+
"""Get PlaylistShort."""
106+
return PlaylistShort(
107+
id=self.id,
108+
title=self.title,
109+
public=self.public,
110+
nb_tracks=self.nb_tracks,
111+
user=self.user.name,
112+
)
113+
114+
82115
class DeezerAlbumResponse(BaseDeezerAPIResponse):
83116
"""Deezer album response."""
84117

@@ -113,6 +146,7 @@ def get_tracks(self) -> Generator[TrackShort, None, None]:
113146
DeezerAdvancedSearchResponse = DeezerAPIResponseCollection[DeezerTrack]
114147
DeezerSearchAlbumResponse = DeezerAPIResponseCollection[DeezerAlbum]
115148
DeezerSearchArtistResponse = DeezerAPIResponseCollection[DeezerArtist]
149+
DeezerSearchPlaylistResponse = DeezerAPIResponseCollection[DeezerPlaylist]
116150
DeezerSearchTrackResponse = DeezerAPIResponseCollection[DeezerTrack]
117151
DeezerSearchResponse: TypeAlias = (
118152
DeezerAdvancedSearchResponse
@@ -133,12 +167,7 @@ def to_tracks(
133167
) -> Generator[TrackShort, None, None]:
134168
"""Convert deezer API response tracks collection to short tracks."""
135169
for track in collection.data:
136-
yield TrackShort(
137-
id=track.id,
138-
title=track.title,
139-
album=track.album.title,
140-
artist=track.artist.name,
141-
)
170+
yield track.to_short()
142171

143172

144173
def to_albums(
@@ -162,10 +191,15 @@ def to_artists(
162191
) -> Generator[ArtistShort, None, None]:
163192
"""Get artists collection iterator."""
164193
for artist in collection.data:
165-
yield ArtistShort(
166-
id=artist.id,
167-
name=artist.name,
168-
)
194+
yield artist.to_short()
195+
196+
197+
def to_playlists(
198+
collection: DeezerSearchPlaylistResponse,
199+
) -> Generator[PlaylistShort, None, None]:
200+
"""Get playlists collection iterator."""
201+
for playlist in collection.data:
202+
yield playlist.to_short()
169203

170204

171205
# Deezer API Gateway models

0 commit comments

Comments
 (0)