diff --git a/src/mpd_now_playable/mpd/listener.py b/src/mpd_now_playable/mpd/listener.py index 704d9ed..461eaca 100644 --- a/src/mpd_now_playable/mpd/listener.py +++ b/src/mpd_now_playable/mpd/listener.py @@ -1,10 +1,13 @@ import asyncio +from pathlib import Path +from uuid import UUID from mpd.asyncio import MPDClient from mpd.base import CommandError from ..player import Player from ..song import PlaybackState, Song, SongListener +from ..type_tools import convert_if_exists from .artwork_cache import MpdArtworkCache from .types import CurrentSongResponse, StatusResponse @@ -14,10 +17,18 @@ def mpd_current_to_song( ) -> Song: return Song( state=PlaybackState(status["state"]), - title=current["title"], - artist=current["artist"], - album=current["album"], - album_artist=current["albumartist"], + queue_index=int(current["pos"]), + queue_length=int(status["playlistlength"]), + file=Path(current["file"]), + musicbrainz_trackid=convert_if_exists(current.get("musicbrainz_trackid"), UUID), + title=current.get("title"), + artist=current.get("artist"), + album=current.get("album"), + album_artist=current.get("albumartist"), + composer=current.get("composer"), + genre=current.get("genre"), + track=convert_if_exists(current.get("track"), int), + disc=convert_if_exists(current.get("disc"), int), duration=float(status["duration"]), elapsed=float(status["elapsed"]), art=art, diff --git a/src/mpd_now_playable/mpd/types.py b/src/mpd_now_playable/mpd/types.py index 8ede8aa..4fe2d25 100644 --- a/src/mpd_now_playable/mpd/types.py +++ b/src/mpd_now_playable/mpd/types.py @@ -1,4 +1,4 @@ -from typing import Protocol, TypedDict +from typing import Literal, NotRequired, Protocol, TypedDict class MpdStateHandler(Protocol): @@ -9,56 +9,74 @@ class MpdStateHandler(Protocol): ... +BooleanFlag = Literal["0", "1"] + +OneshotFlag = Literal[BooleanFlag, "oneshot"] + + +# This is not the complete status response from MPD, just the parts of it mpd-now-playable uses. class StatusResponse(TypedDict): - volume: str - repeat: str - random: str - single: str - consume: str - partition: str - playlist: str - playlistlength: str - mixrampdb: str - state: str - song: str - songid: str - time: str - elapsed: str - bitrate: str + state: Literal["play", "stop", "pause"] + + # The total duration and elapsed playback of the current song, measured in seconds. Fractional seconds are allowed. duration: str + elapsed: str + + # The volume value ranges from 0-100. It may be omitted from + # the response entirely if MPD has no volume mixer configured. + volume: NotRequired[str] + + # Various toggle-able music playback settings, which can be addressed and modified by Now Playing. + repeat: BooleanFlag + random: BooleanFlag + single: OneshotFlag + consume: OneshotFlag + + # Partitions essentially let one MPD server act as multiple music players. + # For most folks, this will just be "default", but mpd-now-playable will + # eventually support addressing specific partitions. Eventually. + partition: str + + # The total number of items in the play queue, which is called the "playlist" throughout the MPD protocol for legacy reasons. + playlistlength: str + + # The format of decoded audio MPD is producing, expressed as a string in the form "samplerate:bits:channels". audio: str - nextsong: str - nextsongid: str -CurrentSongResponse = TypedDict( - "CurrentSongResponse", - { - "file": str, - "last-modified": str, - "format": str, - "artist": str, - "albumartist": str, - "artistsort": str, - "albumartistsort": str, - "title": str, - "album": str, - "track": str, - "date": str, - "originaldate": str, - "composer": str, - "disc": str, - "label": str, - "musicbrainz_albumid": str, - "musicbrainz_albumartistid": str, - "musicbrainz_releasetrackid": str, - "musicbrainz_artistid": str, - "musicbrainz_trackid": str, - "time": str, - "duration": str, - "pos": str, - "id": str, - }, -) +# All of these are metadata tags read from your music, and are strictly +# optional. mpd-now-playable will work better if your music is properly +# tagged, since then it can pass more information on to Now Playing, but it +# should work fine with completely untagged music too. +class CurrentSongTags(TypedDict, total=False): + artist: str + albumartist: str + artistsort: str + albumartistsort: str + title: str + album: str + track: str + date: str + originaldate: str + composer: str + disc: str + label: str + genre: str + musicbrainz_albumid: str + musicbrainz_albumartistid: str + musicbrainz_releasetrackid: str + musicbrainz_artistid: str + musicbrainz_trackid: str + + +class CurrentSongResponse(CurrentSongTags): + # The name of the music file currently being played, as MPD understands + # it. For locally stored music files, this'll just be a simple file path + # relative to your music directory. + file: str + # The index of the song in the play queue. Will change if you shuffle or + # otherwise reorder the playlist. + pos: str + ReadPictureResponse = TypedDict("ReadPictureResponse", {"binary": bytes}) diff --git a/src/mpd_now_playable/song.py b/src/mpd_now_playable/song.py index 81944c5..fa904f6 100644 --- a/src/mpd_now_playable/song.py +++ b/src/mpd_now_playable/song.py @@ -1,5 +1,7 @@ from enum import StrEnum +from pathlib import Path from typing import Protocol +from uuid import UUID from attrs import define, field @@ -13,10 +15,18 @@ class PlaybackState(StrEnum): @define class Song: state: PlaybackState - title: str - artist: str - album: str - album_artist: str + queue_index: int + queue_length: int + file: Path + musicbrainz_trackid: UUID | None + title: str | None + artist: str | None + composer: str | None + album: str | None + album_artist: str | None + track: int | None + disc: int | None + genre: str | None duration: float elapsed: float art: bytes | None = field(repr=lambda a: "" if a else "") diff --git a/src/mpd_now_playable/type_tools.py b/src/mpd_now_playable/type_tools.py new file mode 100644 index 0000000..7ee4ffe --- /dev/null +++ b/src/mpd_now_playable/type_tools.py @@ -0,0 +1,11 @@ +from typing import Callable, TypeVar + +__all__ = ("convert_if_exists",) + +T = TypeVar("T") + + +def convert_if_exists(value: str | None, converter: Callable[[str], T]) -> T | None: + if value is None: + return None + return converter(value)