Compare commits
2 commits
ffc399290f
...
9ff488d807
Author | SHA1 | Date | |
---|---|---|---|
9ff488d807 | |||
88a38a1bbd |
5 changed files with 106 additions and 56 deletions
|
@ -7,7 +7,7 @@ This enables your keyboard's standard media keys to control MPD, as well as more
|
||||||
|
|
||||||
The recommended way to install mpd-now-playable and its dependencies is with [pipx](https://pypa.github.io/pipx/). I'm currently unable to register a PyPI account, so to install you'll currently need to do something like this:
|
The recommended way to install mpd-now-playable and its dependencies is with [pipx](https://pypa.github.io/pipx/). I'm currently unable to register a PyPI account, so to install you'll currently need to do something like this:
|
||||||
```shell
|
```shell
|
||||||
pipx install 'https://git.00dani.me/00dani/mpd-now-playable/archive/main.tar.gz'
|
pipx install 'git+https://git.00dani.me/00dani/mpd-now-playable'
|
||||||
```
|
```
|
||||||
|
|
||||||
However once PyPI opens registration again, this will work too, and it's the approach I definitely recommend:
|
However once PyPI opens registration again, this will work too, and it's the approach I definitely recommend:
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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>")
|
||||||
|
|
11
src/mpd_now_playable/type_tools.py
Normal file
11
src/mpd_now_playable/type_tools.py
Normal 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)
|
Loading…
Reference in a new issue