Make most song info optional, to support untagged music

This commit is contained in:
Danielle McLean 2023-12-06 10:54:51 +11:00
parent 88a38a1bbd
commit 9ff488d807
Signed by: 00dani
GPG key ID: 52C059C3B22A753E
4 changed files with 105 additions and 55 deletions

View file

@ -1,10 +1,13 @@
import asyncio import asyncio
from pathlib import Path
from uuid import UUID
from mpd.asyncio import MPDClient from mpd.asyncio import MPDClient
from mpd.base import CommandError from mpd.base import CommandError
from ..player import Player from ..player import Player
from ..song import PlaybackState, Song, SongListener from ..song import PlaybackState, Song, SongListener
from ..type_tools import convert_if_exists
from .artwork_cache import MpdArtworkCache from .artwork_cache import MpdArtworkCache
from .types import CurrentSongResponse, StatusResponse from .types import CurrentSongResponse, StatusResponse
@ -14,10 +17,18 @@ def mpd_current_to_song(
) -> Song: ) -> Song:
return Song( return Song(
state=PlaybackState(status["state"]), state=PlaybackState(status["state"]),
title=current["title"], queue_index=int(current["pos"]),
artist=current["artist"], queue_length=int(status["playlistlength"]),
album=current["album"], file=Path(current["file"]),
album_artist=current["albumartist"], 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"]), duration=float(status["duration"]),
elapsed=float(status["elapsed"]), elapsed=float(status["elapsed"]),
art=art, art=art,

View file

@ -1,4 +1,4 @@
from typing import Protocol, TypedDict from typing import Literal, NotRequired, Protocol, TypedDict
class MpdStateHandler(Protocol): 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): class StatusResponse(TypedDict):
volume: str state: Literal["play", "stop", "pause"]
repeat: str
random: str # The total duration and elapsed playback of the current song, measured in seconds. Fractional seconds are allowed.
single: str
consume: str
partition: str
playlist: str
playlistlength: str
mixrampdb: str
state: str
song: str
songid: str
time: str
elapsed: str
bitrate: str
duration: str 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 audio: str
nextsong: str
nextsongid: str
CurrentSongResponse = TypedDict( # All of these are metadata tags read from your music, and are strictly
"CurrentSongResponse", # 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
"file": str, # should work fine with completely untagged music too.
"last-modified": str, class CurrentSongTags(TypedDict, total=False):
"format": str, artist: str
"artist": str, albumartist: str
"albumartist": str, artistsort: str
"artistsort": str, albumartistsort: str
"albumartistsort": str, title: str
"title": str, album: str
"album": str, track: str
"track": str, date: str
"date": str, originaldate: str
"originaldate": str, composer: str
"composer": str, disc: str
"disc": str, label: str
"label": str, genre: str
"musicbrainz_albumid": str, musicbrainz_albumid: str
"musicbrainz_albumartistid": str, musicbrainz_albumartistid: str
"musicbrainz_releasetrackid": str, musicbrainz_releasetrackid: str
"musicbrainz_artistid": str, musicbrainz_artistid: str
"musicbrainz_trackid": str, musicbrainz_trackid: str
"time": str,
"duration": str,
"pos": str, class CurrentSongResponse(CurrentSongTags):
"id": str, # 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}) ReadPictureResponse = TypedDict("ReadPictureResponse", {"binary": bytes})

View file

@ -1,5 +1,7 @@
from enum import StrEnum from enum import StrEnum
from pathlib import Path
from typing import Protocol from typing import Protocol
from uuid import UUID
from attrs import define, field from attrs import define, field
@ -13,10 +15,18 @@ class PlaybackState(StrEnum):
@define @define
class Song: class Song:
state: PlaybackState state: PlaybackState
title: str queue_index: int
artist: str queue_length: int
album: str file: Path
album_artist: str 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 duration: float
elapsed: float elapsed: float
art: bytes | None = field(repr=lambda a: "<has art>" if a else "<no art>") art: bytes | None = field(repr=lambda a: "<has art>" if a else "<no art>")

View file

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