diff --git a/schemata/config-v1.json b/schemata/config-v1.json index 68eff9f..a73d6cf 100644 --- a/schemata/config-v1.json +++ b/schemata/config-v1.json @@ -24,12 +24,6 @@ "title": "Host", "type": "string" }, - "music_directory": { - "description": "Your music directory, just as it's set up in your mpd.conf. mpd-now-playable uses this setting to figure out an absolute file:// URL for the current song, which MPNowPlayingInfoCenter will use to display cool stuff like audio waveforms. It'll still work fine without setting this, though.", - "format": "directory-path", - "title": "Music Directory", - "type": "string" - }, "password": { "description": "The password required to connect to your MPD instance, if you need one.", "format": "password", diff --git a/schemata/song-v1.json b/schemata/song-v1.json index 6ecc4ec..a16bda4 100644 --- a/schemata/song-v1.json +++ b/schemata/song-v1.json @@ -194,12 +194,6 @@ "description": "The track number the song has on its album. This is usually one-based, but it's just an arbitrary audio tag so a particular album might start at zero or do something weird with it.", "title": "Track", "type": "integer" - }, - "url": { - "description": "An absolute URL referring to the current song, if available. If the song's a local file and its absolute path can be determined (mpd-now-playable has been configured with your music directory), then this field will contain a file:// URL. If the song's remote, then MPD itself returns an absolute URL in the first place.", - "format": "uri", - "title": "Url", - "type": "string" } }, "required": [ diff --git a/src/mpd_now_playable/tools/schema/fields.py b/src/mpd_now_playable/config/fields.py similarity index 77% rename from src/mpd_now_playable/tools/schema/fields.py rename to src/mpd_now_playable/config/fields.py index f171e28..f73a7f9 100644 --- a/src/mpd_now_playable/tools/schema/fields.py +++ b/src/mpd_now_playable/config/fields.py @@ -1,9 +1,7 @@ -from os.path import expanduser from typing import Annotated, NewType from annotated_types import Ge, Le from pydantic import ( - BeforeValidator, Field, PlainSerializer, PlainValidator, @@ -11,12 +9,9 @@ from pydantic import ( Strict, WithJsonSchema, ) -from pydantic import ( - DirectoryPath as DirectoryType, -) from yarl import URL as Yarl -__all__ = ("DirectoryPath", "Host", "Password", "Port", "Url") +__all__ = ("Host", "Password", "Port", "Url") def from_yarl(url: Yarl) -> str: @@ -29,7 +24,6 @@ def to_yarl(value: object) -> Yarl: raise NotImplementedError(f"Cannot convert {type(object)} to URL") -DirectoryPath = Annotated[DirectoryType, BeforeValidator(expanduser)] Host = NewType( "Host", Annotated[str, Strict(), Field(json_schema_extra={"format": "hostname"})] ) diff --git a/src/mpd_now_playable/config/model.py b/src/mpd_now_playable/config/model.py index 0fde2d1..561d830 100644 --- a/src/mpd_now_playable/config/model.py +++ b/src/mpd_now_playable/config/model.py @@ -4,7 +4,7 @@ from typing import Annotated, Literal, Optional, Protocol from pydantic import Field from ..tools.schema.define import schema -from ..tools.schema.fields import DirectoryPath, Host, Password, Port, Url +from .fields import Host, Password, Port, Url __all__ = ( "Config", @@ -55,12 +55,6 @@ class MpdConfig: #: servers on one machine for some reason, you probably haven't changed this #: from the default port, 6600. port: Port = Port(6600) - #: Your music directory, just as it's set up in your mpd.conf. - #: mpd-now-playable uses this setting to figure out an absolute file:// URL - #: for the current song, which MPNowPlayingInfoCenter will use to display - #: cool stuff like audio waveforms. It'll still work fine without setting - #: this, though. - music_directory: Optional[DirectoryPath] = None @schema("https://cdn.00dani.me/m/schemata/mpd-now-playable/config-v1.json") diff --git a/src/mpd_now_playable/mpd/listener.py b/src/mpd_now_playable/mpd/listener.py index 75852f7..1743bbb 100644 --- a/src/mpd_now_playable/mpd/listener.py +++ b/src/mpd_now_playable/mpd/listener.py @@ -9,55 +9,37 @@ from yarl import URL from ..config.model import MpdConfig from ..player import Player -from ..song import PlaybackState, Song, to_artwork, to_brainz +from ..song import Artwork, PlaybackState, Song, to_artwork, to_brainz from ..song_receiver import Receiver from ..tools.types import option_fmap, un_maybe_plural from .artwork_cache import MpdArtworkCache -from .types import MpdState +from .types import CurrentSongResponse, StatusResponse -def mpd_file_to_uri(config: MpdConfig, file: str) -> URL | None: - url = URL(file) - if url.scheme != "": - # We already got an absolute URL - probably a stream? - so we can just return it. - return url - - if not config.music_directory: - # We have a relative song URI, but we can't make it absolute since no music directory is configured. - return None - - # Prepend the configured music directory, then turn the whole path into a file:// URL. - abs_file = config.music_directory / file - return URL(abs_file.as_uri()) - - -def mpd_state_to_song(config: MpdConfig, mpd: MpdState) -> Song: - file = mpd.current["file"] - url = mpd_file_to_uri(config, file) - +def mpd_current_to_song( + status: StatusResponse, current: CurrentSongResponse, art: Artwork +) -> Song: return Song( - state=PlaybackState(mpd.status["state"]), - queue_index=int(mpd.current["pos"]), - queue_length=int(mpd.status["playlistlength"]), - file=Path(file), - url=url, - title=mpd.current.get("title"), - artist=un_maybe_plural(mpd.current.get("artist")), - album=un_maybe_plural(mpd.current.get("album")), - album_artist=un_maybe_plural(mpd.current.get("albumartist")), - composer=un_maybe_plural(mpd.current.get("composer")), - genre=un_maybe_plural(mpd.current.get("genre")), - track=option_fmap(int, mpd.current.get("track")), - disc=option_fmap(int, mpd.current.get("disc")), - duration=option_fmap(float, mpd.status.get("duration")), - elapsed=float(mpd.status["elapsed"]), - musicbrainz=to_brainz(mpd.current), - art=to_artwork(mpd.art), + state=PlaybackState(status["state"]), + queue_index=int(current["pos"]), + queue_length=int(status["playlistlength"]), + file=Path(current["file"]), + title=current.get("title"), + artist=un_maybe_plural(current.get("artist")), + album=un_maybe_plural(current.get("album")), + album_artist=un_maybe_plural(current.get("albumartist")), + composer=un_maybe_plural(current.get("composer")), + genre=un_maybe_plural(current.get("genre")), + track=option_fmap(int, current.get("track")), + disc=option_fmap(int, current.get("disc")), + duration=float(status["duration"]), + elapsed=float(status["elapsed"]), + musicbrainz=to_brainz(current), + art=art, ) class MpdStateListener(Player): - config: MpdConfig client: MPDClient receivers: Iterable[Receiver] art_cache: MpdArtworkCache @@ -70,7 +52,6 @@ class MpdStateListener(Player): ) async def start(self, conf: MpdConfig) -> None: - self.config = conf print(f"Connecting to MPD server {conf.host}:{conf.port}...") await self.client.connect(conf.host, conf.port) if conf.password is not None: @@ -96,7 +77,6 @@ class MpdStateListener(Player): status, current = await asyncio.gather( self.client.status(), self.client.currentsong() ) - state = MpdState(status, current) if starting_idle_count != self.idle_count: return @@ -110,8 +90,7 @@ class MpdStateListener(Player): if starting_idle_count != self.idle_count: return - state = MpdState(status, current, art) - song = mpd_state_to_song(self.config, state) + song = mpd_current_to_song(status, current, to_artwork(art)) rprint(song) await self.update(song) diff --git a/src/mpd_now_playable/mpd/types.py b/src/mpd_now_playable/mpd/types.py index 8d9db81..4183e5d 100644 --- a/src/mpd_now_playable/mpd/types.py +++ b/src/mpd_now_playable/mpd/types.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from typing import Literal, NotRequired, Protocol, TypedDict from ..song.musicbrainz import MusicBrainzTags @@ -20,11 +19,8 @@ OneshotFlag = Literal[BooleanFlag, "oneshot"] class StatusResponse(TypedDict): state: Literal["play", "stop", "pause"] - # The total duration and elapsed playback of the current song, measured in - # seconds. Fractional seconds are allowed. The duration field may be - # omitted because MPD cannot determine the duration of certain sources, - # such as Internet radio streams. - duration: NotRequired[str] + # 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 @@ -80,10 +76,3 @@ class CurrentSongResponse(CurrentSongTags): ReadPictureResponse = TypedDict("ReadPictureResponse", {"binary": bytes}) - - -@dataclass -class MpdState: - status: StatusResponse - current: CurrentSongResponse - art: bytes | None = None diff --git a/src/mpd_now_playable/receivers/cocoa/now_playing.py b/src/mpd_now_playable/receivers/cocoa/now_playing.py index c67c733..d207b65 100644 --- a/src/mpd_now_playable/receivers/cocoa/now_playing.py +++ b/src/mpd_now_playable/receivers/cocoa/now_playing.py @@ -25,7 +25,6 @@ from MediaPlayer import ( MPNowPlayingInfoCenter, MPNowPlayingInfoMediaTypeAudio, MPNowPlayingInfoMediaTypeNone, - MPNowPlayingInfoPropertyAssetURL, MPNowPlayingInfoPropertyElapsedPlaybackTime, MPNowPlayingInfoPropertyExternalContentIdentifier, MPNowPlayingInfoPropertyMediaType, @@ -107,9 +106,6 @@ def song_to_media_item(song: Song) -> NSMutableDictionary: nowplaying_info[MPMediaItemPropertyComposer] = join_plural_field(song.composer) nowplaying_info[MPMediaItemPropertyPlaybackDuration] = song.duration - if song.url is not None: - nowplaying_info[MPNowPlayingInfoPropertyAssetURL] = song.url.human_repr() - # MPD can't play back music at different rates, so we just want to set it # to 1.0 if the song is playing. (Leave it at 0.0 if the song is paused.) if song.state == PlaybackState.play: diff --git a/src/mpd_now_playable/receivers/websockets/receiver.py b/src/mpd_now_playable/receivers/websockets/receiver.py index 5cbce3d..22c35bb 100644 --- a/src/mpd_now_playable/receivers/websockets/receiver.py +++ b/src/mpd_now_playable/receivers/websockets/receiver.py @@ -3,7 +3,6 @@ from pathlib import Path import ormsgpack from websockets import broadcast from websockets.server import WebSocketServerProtocol, serve -from yarl import URL from ...config.model import WebsocketsReceiverConfig from ...player import Player @@ -16,8 +15,6 @@ MSGPACK_NULL = ormsgpack.packb(None) def default(value: object) -> object: if isinstance(value, Path): return str(value) - if isinstance(value, URL): - return value.human_repr() raise TypeError diff --git a/src/mpd_now_playable/song/song.py b/src/mpd_now_playable/song/song.py index 45c4425..716dff0 100644 --- a/src/mpd_now_playable/song/song.py +++ b/src/mpd_now_playable/song/song.py @@ -3,7 +3,6 @@ from enum import StrEnum from pathlib import Path from ..tools.schema.define import schema -from ..tools.schema.fields import Url from .artwork import Artwork from .musicbrainz import MusicBrainzIds @@ -15,7 +14,7 @@ class PlaybackState(StrEnum): @schema("https://cdn.00dani.me/m/schemata/mpd-now-playable/song-v1.json") -@dataclass(slots=True, kw_only=True) +@dataclass(slots=True) class Song: #: Whether MPD is currently playing, paused, or stopped. Pretty simple. state: PlaybackState @@ -31,13 +30,6 @@ class Song: #: places, so you can safely do the same. file: Path - #: An absolute URL referring to the current song, if available. If the - #: song's a local file and its absolute path can be determined - #: (mpd-now-playable has been configured with your music directory), then - #: this field will contain a file:// URL. If the song's remote, then MPD - #: itself returns an absolute URL in the first place. - url: Url | None = None - #: The song's title, if it's been tagged with one. Currently only one title #: is supported, since it doesn't make a lot of sense to tag a single audio #: file with multiple titles. @@ -69,9 +61,8 @@ class Song: genre: list[str] #: The song's duration as read from its tags, measured in seconds. - #: Fractional seconds are allowed. The duration may be unavailable for some - #: sources, such as internet radio streams. - duration: float | None + #: Fractional seconds are allowed. + duration: float #: How far into the song MPD is, measured in seconds. Fractional seconds #: are allowed. This is usually going to be less than or equal to the diff --git a/src/mpd_now_playable/tools/schema/generate.py b/src/mpd_now_playable/tools/schema/generate.py index a8d71ef..ec1b539 100644 --- a/src/mpd_now_playable/tools/schema/generate.py +++ b/src/mpd_now_playable/tools/schema/generate.py @@ -11,8 +11,8 @@ from .define import ModelWithSchema __all__ = ("write",) -def write(model: ModelWithSchema, mode: JsonSchemaMode = "validation") -> None: - schema = model.schema.json_schema(schema_generator=MyGenerateJsonSchema, mode=mode) +def write(model: ModelWithSchema) -> None: + schema = model.schema.json_schema(schema_generator=MyGenerateJsonSchema) schema["$id"] = model.id.human_repr() schema_file = Path(__file__).parents[4] / "schemata" / model.id.name print(f"Writing this schema to {schema_file}") @@ -46,10 +46,8 @@ class MyGenerateJsonSchema(GenerateJsonSchema): def nullable_schema(self, schema: s.NullableSchema) -> JsonSchemaValue: return self.generate_inner(schema["schema"]) - -if __name__ == "__main__": +if __name__ == '__main__': from ...config.model import Config from ...song import Song - write(Config) - write(Song, mode="serialization") + write(Song) diff --git a/stubs/MediaPlayer/__init__.pyi b/stubs/MediaPlayer/__init__.pyi index 86c1226..01682fe 100644 --- a/stubs/MediaPlayer/__init__.pyi +++ b/stubs/MediaPlayer/__init__.pyi @@ -34,7 +34,6 @@ MPNowPlayingInfoPropertyPlaybackQueueIndex: Final = ( MPNowPlayingInfoPropertyElapsedPlaybackTime: Final = ( "MPNowPlayingInfoPropertyElapsedPlaybackTime" ) -MPNowPlayingInfoPropertyAssetURL: Final = "MPNowPlayingInfoPropertyAssetURL" MPNowPlayingInfoPropertyExternalContentIdentifier: Final = ( "MPNowPlayingInfoPropertyExternalContentIdentifier" )