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
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,

View file

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

View file

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