Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ and this project adheres to

### Added

- Add playlist support
- Add fallback track support
- Fetch track details in separated threads (when requested)
- Add the `CONNECTION_POOL_MAXSIZE` configuration setting
- Add the `ALWAYS_FETCH_RELEASE_DATE` configuration setting
- CLI: add the `playlist` command
- CLI: add the `search` command `--playlist/-p` option
- CLI: add track/album release date to outputs for the `search`
and `artist` commands using the `-r/--release` option

Expand Down
77 changes: 77 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ $ onzr --help
│ search Search tracks, artists and/or albums. │
│ artist Get artist popular track ids. │
│ album Get album tracks. │
│ playlist Get playlist tracks. │
│ mix Create a playlist from multiple artists. │
│ add Add one (or more) tracks to the queue. │
│ queue List queue tracks. │
Expand Down Expand Up @@ -420,6 +421,82 @@ onzr search --album "Friday night in San Francisco" --ids --first | \
onzr add -
```

## `playlist`

The `playlist` command lists playlist tracks to check or play them:

```sh
onzr search --playlist "Peaky blinders" --ids --first | \
onzr playlist -
```

And there it is:

```
« Peaky Blinders soundtrack » by Camojada - Deezer Soundtracks Editor
┏━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
┃ # ┃ ID ┃ Track ┃ Album ┃ Artist ┃
┡━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
│ 1 │ 134523602 │ You Want It Darker │ You Want It Darker │ Leonard Cohen │
│ 2 │ 935528 │ I'm a Man │ Burn Your Own │ Black Strobe │
│ │ │ │ Church │ │
│ 3 │ 69000458 │ Red Right Hand │ Let Love In │ Nick Cave & The │
│ │ │ │ │ Bad Seeds │
│ 4 │ 127792993 │ In Flames │ In Flames │ Lungley │
│ 5 │ 70322130 │ Do I Wanna Know? │ AM │ Arctic Monkeys │
│ 6 │ 694053262 │ Blue Veins │ Broken Boy │ The Raconteurs │
│ │ │ │ Soldiers │ │
│ 7 │ 397479472 │ Evil │ Holiday │ Nadine Shah │
│ │ │ │ Destination │ │
│ 8 │ 138540343 │ I Might Be Wrong │ Amnesiac │ Radiohead │
│ 9 │ 950489 │ The Hardest Button │ The Hardest Button │ The White Stripes │
│ │ │ to Button │ to Button │ │
│ 10 │ 106218096 │ Snake Oil │ What Went Down │ Foals │
│ 11 │ 2963445 │ Clap Hands │ Rain Dogs │ Tom Waits │
│ 12 │ 65445466 │ Riders on the │ L.A. Woman │ The Doors │
│ │ │ Storm │ │ │
│ 13 │ 1169910 │ Down By The Water │ To Bring You My │ PJ Harvey │
│ │ │ │ Love │ │
│ 14 │ 70322132 │ R U Mine? │ AM │ Arctic Monkeys │
│ 15 │ 4645351 │ Pull A U │ Pull A U │ The Kills │
│ 16 │ 620493572 │ Black Math │ Elephant │ The White Stripes │
│ 17 │ 138540341 │ You And Whose │ Amnesiac │ Radiohead │
│ │ │ Army? │ │ │
│ 18 │ 5522774 │ What He Wrote │ I Speak Because I │ Laura Marling │
│ │ │ │ Can │ │
│ 19 │ 355649581 │ Breathless │ Lovely Creatures - │ Nick Cave & The │
│ │ │ │ The Best of Nick │ Bad Seeds │
│ │ │ │ Cave and The Bad │ │
│ │ │ │ Seeds (1984-2014) │ │
│ │ │ │ (Deluxe Edition) │ │
│ 20 │ 137013086 │ Bad Habits │ Everything You've │ The Last Shadow │
│ │ │ │ Come To Expect │ Puppets │
│ │ │ │ (Deluxe Edition) │ │
│ 21 │ 116944274 │ Lazarus │ Blackstar │ David Bowie │
│ 22 │ 65690370 │ Further On Up The │ American V: A │ Johnny Cash │
│ │ │ Road (Album │ Hundred Highways │ │
│ │ │ Version) │ │ │
│ 23 │ 694053192 │ Broken Boy Soldier │ Broken Boy │ The Raconteurs │
│ │ │ │ Soldiers │ │
│ 24 │ 795435472 │ You’re Not God │ You’re Not God │ Anna Calvi │
│ │ │ (From ‘Peaky │ (From ‘Peaky │ │
│ │ │ Blinders’ Original │ Blinders’ Original │ │
│ │ │ Soundtrack) │ Soundtrack) │ │
│ 25 │ 4637198 │ Crying Lightning │ Crying Lightning │ Arctic Monkeys │
│ 26 │ 344935301 │ The Longing │ Life Love Flesh │ Imelda May │
│ │ │ │ Blood (Deluxe) │ │
│ 27 │ 2502145 │ This Is Love │ Stories From The │ PJ Harvey │
│ │ │ │ City, Stories From │ │
│ │ │ │ The Sea │ │
│ 28 │ 948066 │ The Hardest Button │ Elephant │ The White Stripes │
│ │ │ To Button │ │ │
│ 29 │ 131590754 │ Abattoir Blues │ Abattoir Blues / │ Nick Cave & The │
│ │ │ │ The Lyre of │ Bad Seeds │
│ │ │ │ Orpheus │ │
│ 30 │ 3129334 │ Pyramid Song │ The Best Of │ Radiohead │
└────┴───────────┴────────────────────┴────────────────────┴────────────────────┘
```

## `mix`

The `mix` command generates playlists using various artists definition. You can
Expand Down
53 changes: 50 additions & 3 deletions src/onzr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
ArtistShort,
Collection,
PlayerControl,
PlaylistShort,
ServerState,
TrackShort,
)
Expand Down Expand Up @@ -146,6 +147,7 @@ def print_collection_table(collection: Collection, title="Collection"):
)
logger.debug(f"{show_artist=} - {show_album=} - {show_track=}")

table.add_column("#", justify="left")
table.add_column("ID", justify="right")
if show_track:
table.add_column("Track", style=theme.title_color.as_hex())
Expand All @@ -172,8 +174,17 @@ def print_collection_table(collection: Collection, title="Collection"):
sorted_collection.extend(albums_without_release_date)
collection = sorted_collection

for item in collection:
table.add_row(*map(str, item.model_dump(exclude_none=True).values()))
if isinstance(sample, PlaylistShort):
table.add_column("Title", style=theme.title_color.as_hex())
table.add_column("Public", style="italic")
table.add_column("# tracks", style="bold")
table.add_column("User", style=theme.secondary_color.as_hex())

for rk, item in enumerate(collection):
table.add_row(
str(rk + 1),
*map(str, item.model_dump(exclude_none=True, exclude={"tracks"}).values()),
)

console.print(table)

Expand Down Expand Up @@ -259,6 +270,9 @@ def search(
track: Annotated[
str, typer.Option("--track", "-t", help="Search by track title.")
] = "",
playlist: Annotated[
str, typer.Option("--playlist", "-p", help="Search by playlist name.")
] = "",
strict: Annotated[
bool, typer.Option("--strict", "-s", help="Only consider strict matches.")
] = False,
Expand All @@ -285,7 +299,7 @@ def search(
if not quiet:
console.print("🔍 start searching…")
try:
results = deezer.search(artist, album, track, strict, release)
results = deezer.search(artist, album, track, playlist, strict, release)
except ValueError as err:
raise typer.Exit(code=ExitCodes.INVALID_ARGUMENTS) from err

Expand Down Expand Up @@ -391,6 +405,39 @@ def album(
print_collection_table(collection, title="Album tracks")


@cli.command()
def playlist(
playlist_id: str,
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Quiet output.")] = False,
ids: Annotated[
bool, typer.Option("--ids", "-i", help="Show only result IDs.")
] = False,
):
"""Get playlist tracks."""
if ids:
quiet = True

if playlist_id == "-":
logger.debug("Reading playlist id from stdin…")
playlist_id = click.get_text_stream("stdin").read().strip()
logger.debug(f"{playlist_id=}")

deezer = get_deezer_client(quiet=quiet)
playlist = deezer.playlist(int(playlist_id))

if playlist.tracks is None:
console.print("This playlist contains no tracks")
raise typer.Exit(code=ExitCodes.INVALID_ARGUMENTS)

if ids:
print_collection_ids(playlist.tracks)
return

print_collection_table(
playlist.tracks, title=f"« {playlist.title} » by {playlist.user or '?'}"
)


@cli.command()
def mix(
artist: list[str],
Expand Down
56 changes: 41 additions & 15 deletions src/onzr/deezer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,38 @@
from Cryptodome.Cipher import Blowfish
from pydantic import HttpUrl

from onzr.models.deezer import (
from .exceptions import DeezerTrackException
from .models.core import (
AlbumShort,
Collection,
PlaylistShort,
StreamQuality,
TrackInfo,
TrackShort,
)
from .models.deezer import (
DeezerAdvancedSearchResponse,
DeezerAlbum,
DeezerAlbumResponse,
DeezerArtist,
DeezerArtistAlbumsResponse,
DeezerArtistRadioResponse,
DeezerArtistResponse,
DeezerArtistTopResponse,
DeezerPlaylist,
DeezerSearchAlbumResponse,
DeezerSearchArtistResponse,
DeezerSearchPlaylistResponse,
DeezerSearchResponse,
DeezerSearchTrackResponse,
DeezerSong,
DeezerTrack,
to_albums,
to_artists,
to_playlists,
to_tracks,
)

from .exceptions import DeezerTrackException
from .models.core import (
AlbumShort,
Collection,
StreamQuality,
TrackInfo,
TrackShort,
)
from .models.deezer import DeezerAlbumResponse

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -99,7 +102,6 @@ def _fast_login(self):
def _collection_details(
self,
collection: Collection,
# ) -> Generator[TrackShort, None, None] | Generator[AlbumShort, None, None]:
) -> Collection:
"""Add detailled informations to collection.

Expand Down Expand Up @@ -163,6 +165,7 @@ def _api(
| type[DeezerAlbumResponse]
| type[DeezerArtist]
| type[DeezerArtistResponse]
| type[DeezerPlaylist]
| type[DeezerSearchResponse]
| type[DeezerTrack]
),
Expand All @@ -174,14 +177,15 @@ def _api(
| DeezerAlbumResponse
| DeezerArtist
| DeezerArtistResponse
| DeezerPlaylist
| DeezerSearchResponse
| DeezerTrack
):
"""An API proxy that validates response using the input model."""
logger.debug(f"Will query {endpoint=} to {model=} with {args=}/{kwargs=}")

response = endpoint(*args, **kwargs)
logger.debug(f"{pformat(response,sort_dicts=True)=}")
logger.debug(pformat(response, sort_dicts=True))

instance = model(**response)
logger.debug(f"{instance=}")
Expand Down Expand Up @@ -242,7 +246,8 @@ def artist(
artist.id,
limit=limit,
),
)
),
artist=artist,
)
)
else:
Expand Down Expand Up @@ -270,16 +275,24 @@ def track(self, track_id: int) -> TrackShort:
DeezerTrack, self._api(DeezerTrack, self.api.get_track, track_id)
).to_short()

def playlist(self, playlist_id: int) -> PlaylistShort:
"""Get playlist tracks."""
return cast(
DeezerPlaylist,
self._api(DeezerPlaylist, self.api.get_playlist, playlist_id),
).to_short()

def search(
self,
artist: str = "",
album: str = "",
track: str = "",
playlist: str = "",
strict: bool = False,
fetch_release_date: bool = False,
) -> Collection:
"""Mixed custom search."""
criteria = list(filter(None, (artist, album, track)))
criteria = list(filter(None, (artist, album, track, playlist)))
results: Collection = []

if len(criteria) == 0:
Expand Down Expand Up @@ -335,6 +348,19 @@ def search(
),
)
)
elif playlist:
results = list(
to_playlists(
cast(
DeezerSearchPlaylistResponse,
self._api(
DeezerSearchPlaylistResponse,
self.api.search_playlist,
playlist,
),
),
)
)

if (self.always_fetch_release_date or fetch_release_date) and not artist:
results = self._collection_details(results)
Expand Down
15 changes: 14 additions & 1 deletion src/onzr/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,20 @@ class TrackShort(BaseModel):
release_date: Optional[date] = None


Collection: TypeAlias = List[ArtistShort] | List[AlbumShort] | List[TrackShort]
class PlaylistShort(BaseModel):
"""A small model to represent a playlist."""

id: int
title: str
public: bool
nb_tracks: int
user: Optional[str] = None
tracks: Optional[List[TrackShort]] = None


Collection: TypeAlias = (
List[ArtistShort] | List[AlbumShort] | List[TrackShort] | List[PlaylistShort]
)


class TrackInfo(BaseModel):
Expand Down
Loading