diff --git a/src/mpd_now_playable/cli.py b/src/mpd_now_playable/cli.py index aa6a09a..2b822d3 100644 --- a/src/mpd_now_playable/cli.py +++ b/src/mpd_now_playable/cli.py @@ -3,7 +3,7 @@ from os import environ from corefoundationasyncio import CoreFoundationEventLoop -from .cocoa import CocoaNowPlaying +from .cocoa.now_playing import CocoaNowPlaying from .mpd.listener import MpdStateListener diff --git a/src/mpd_now_playable/cocoa/__init__.py b/src/mpd_now_playable/cocoa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mpd_now_playable/cocoa.py b/src/mpd_now_playable/cocoa/now_playing.py similarity index 78% rename from src/mpd_now_playable/cocoa.py rename to src/mpd_now_playable/cocoa/now_playing.py index 8554067..9094345 100644 --- a/src/mpd_now_playable/cocoa.py +++ b/src/mpd_now_playable/cocoa/now_playing.py @@ -6,8 +6,13 @@ from Foundation import CGSize, NSMutableDictionary from MediaPlayer import ( MPMediaItemArtwork, MPMediaItemPropertyAlbumTitle, + MPMediaItemPropertyAlbumTrackNumber, MPMediaItemPropertyArtist, MPMediaItemPropertyArtwork, + MPMediaItemPropertyComposer, + MPMediaItemPropertyDiscNumber, + MPMediaItemPropertyGenre, + MPMediaItemPropertyPersistentID, MPMediaItemPropertyPlaybackDuration, MPMediaItemPropertyTitle, MPMusicPlaybackState, @@ -18,15 +23,20 @@ from MediaPlayer import ( MPNowPlayingInfoMediaTypeAudio, MPNowPlayingInfoMediaTypeNone, MPNowPlayingInfoPropertyElapsedPlaybackTime, + MPNowPlayingInfoPropertyExternalContentIdentifier, MPNowPlayingInfoPropertyMediaType, + MPNowPlayingInfoPropertyPlaybackQueueCount, + MPNowPlayingInfoPropertyPlaybackQueueIndex, + MPNowPlayingInfoPropertyPlaybackRate, MPRemoteCommandCenter, MPRemoteCommandEvent, MPRemoteCommandHandlerStatus, ) -from .async_tools import run_background_task -from .player import Player -from .song import PlaybackState, Song +from ..async_tools import run_background_task +from ..player import Player +from ..song import PlaybackState, Song +from .persistent_id import song_to_persistent_id def logo_to_ns_image() -> NSImage: @@ -70,12 +80,25 @@ def song_to_media_item(song: Song) -> NSMutableDictionary: nowplaying_info = nothing_to_media_item() nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeAudio nowplaying_info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = song.elapsed + nowplaying_info[MPNowPlayingInfoPropertyExternalContentIdentifier] = song.file + nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueCount] = song.queue_length + nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueIndex] = song.queue_index + nowplaying_info[MPMediaItemPropertyPersistentID] = song_to_persistent_id(song) nowplaying_info[MPMediaItemPropertyTitle] = song.title nowplaying_info[MPMediaItemPropertyArtist] = song.artist nowplaying_info[MPMediaItemPropertyAlbumTitle] = song.album + nowplaying_info[MPMediaItemPropertyAlbumTrackNumber] = song.track + nowplaying_info[MPMediaItemPropertyDiscNumber] = song.disc + nowplaying_info[MPMediaItemPropertyGenre] = song.genre + nowplaying_info[MPMediaItemPropertyComposer] = song.composer nowplaying_info[MPMediaItemPropertyPlaybackDuration] = song.duration + # 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: + nowplaying_info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 + if song.art: nowplaying_info[MPMediaItemPropertyArtwork] = ns_image_to_media_item_artwork( data_to_ns_image(song.art) @@ -88,6 +111,7 @@ def nothing_to_media_item() -> NSMutableDictionary: nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeNone nowplaying_info[MPMediaItemPropertyArtwork] = MPD_LOGO nowplaying_info[MPMediaItemPropertyTitle] = "MPD (stopped)" + nowplaying_info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 return nowplaying_info diff --git a/src/mpd_now_playable/cocoa/persistent_id.py b/src/mpd_now_playable/cocoa/persistent_id.py new file mode 100644 index 0000000..be88dc8 --- /dev/null +++ b/src/mpd_now_playable/cocoa/persistent_id.py @@ -0,0 +1,39 @@ +from hashlib import blake2b +from pathlib import Path +from typing import Final +from uuid import UUID + +from ..song import Song + +# The maximum size for a BLAKE2b "person" value is sixteen bytes, so we need to be concise. +HASH_PERSON_PREFIX: Final = b"mnp.mac." +TRACKID_HASH_PERSON: Final = HASH_PERSON_PREFIX + b"mb_tid" +FILE_HASH_PERSON: Final = HASH_PERSON_PREFIX + b"f" + +PERSISTENT_ID_BITS: Final = 64 +PERSISTENT_ID_BYTES: Final = PERSISTENT_ID_BITS // 8 + + +def digest_trackid(trackid: UUID) -> bytes: + return blake2b( + trackid.bytes, digest_size=PERSISTENT_ID_BYTES, person=TRACKID_HASH_PERSON + ).digest() + + +def digest_file_uri(file: Path) -> bytes: + return blake2b( + bytes(file), digest_size=PERSISTENT_ID_BYTES, person=FILE_HASH_PERSON + ).digest() + + +# The MPMediaItemPropertyPersistentID is only 64 bits, while a UUID is 128 +# bits and not all tracks will even have their MusicBrainz track ID included. +# To work around this, we compute a BLAKE2 hash from the UUID, or failing +# that from the file URI. BLAKE2 can be customised to different digest sizes, +# making it perfect for this problem. +def song_to_persistent_id(song: Song) -> int: + if song.musicbrainz_trackid: + hashed_id = digest_trackid(song.musicbrainz_trackid) + else: + hashed_id = digest_file_uri(song.file) + return int.from_bytes(hashed_id) diff --git a/stubs/MediaPlayer/__init__.pyi b/stubs/MediaPlayer/__init__.pyi index 4b793c4..2e3b7e0 100644 --- a/stubs/MediaPlayer/__init__.pyi +++ b/stubs/MediaPlayer/__init__.pyi @@ -24,6 +24,7 @@ MPNowPlayingInfoPropertyMediaType: Final = "MPNowPlayingInfoPropertyMediaType" MPNowPlayingInfoMediaTypeAudio: Final = 1 MPNowPlayingInfoMediaTypeNone: Final = 0 +MPNowPlayingInfoPropertyPlaybackRate: Final = "MPNowPlayingInfoPropertyPlaybackRate" MPNowPlayingInfoPropertyPlaybackQueueCount: Final = ( "MPNowPlayingInfoPropertyPlaybackQueueCount" )