From 28da4da69f038dd2eb365c276b048edef2579ef3 Mon Sep 17 00:00:00 2001 From: Danielle McLean Date: Wed, 6 Dec 2023 12:02:13 +1100 Subject: [PATCH 1/2] Pass through more simple media item info to MacOS --- src/mpd_now_playable/cocoa.py | 21 +++++++++++++++++++++ stubs/MediaPlayer/__init__.pyi | 1 + 2 files changed, 22 insertions(+) diff --git a/src/mpd_now_playable/cocoa.py b/src/mpd_now_playable/cocoa.py index 8554067..3826581 100644 --- a/src/mpd_now_playable/cocoa.py +++ b/src/mpd_now_playable/cocoa.py @@ -6,8 +6,12 @@ from Foundation import CGSize, NSMutableDictionary from MediaPlayer import ( MPMediaItemArtwork, MPMediaItemPropertyAlbumTitle, + MPMediaItemPropertyAlbumTrackNumber, MPMediaItemPropertyArtist, MPMediaItemPropertyArtwork, + MPMediaItemPropertyComposer, + MPMediaItemPropertyDiscNumber, + MPMediaItemPropertyGenre, MPMediaItemPropertyPlaybackDuration, MPMediaItemPropertyTitle, MPMusicPlaybackState, @@ -18,7 +22,11 @@ from MediaPlayer import ( MPNowPlayingInfoMediaTypeAudio, MPNowPlayingInfoMediaTypeNone, MPNowPlayingInfoPropertyElapsedPlaybackTime, + MPNowPlayingInfoPropertyExternalContentIdentifier, MPNowPlayingInfoPropertyMediaType, + MPNowPlayingInfoPropertyPlaybackQueueCount, + MPNowPlayingInfoPropertyPlaybackQueueIndex, + MPNowPlayingInfoPropertyPlaybackRate, MPRemoteCommandCenter, MPRemoteCommandEvent, MPRemoteCommandHandlerStatus, @@ -70,12 +78,24 @@ 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[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 +108,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/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" ) From 04a976f6f3f9f30e152575d2e40f680c4ae29b63 Mon Sep 17 00:00:00 2001 From: Danielle McLean Date: Wed, 6 Dec 2023 12:27:24 +1100 Subject: [PATCH 2/2] Support persistent track IDs (64-bit ints) --- src/mpd_now_playable/cli.py | 2 +- src/mpd_now_playable/cocoa/__init__.py | 0 .../{cocoa.py => cocoa/now_playing.py} | 9 +++-- src/mpd_now_playable/cocoa/persistent_id.py | 39 +++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/mpd_now_playable/cocoa/__init__.py rename src/mpd_now_playable/{cocoa.py => cocoa/now_playing.py} (95%) create mode 100644 src/mpd_now_playable/cocoa/persistent_id.py 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 95% rename from src/mpd_now_playable/cocoa.py rename to src/mpd_now_playable/cocoa/now_playing.py index 3826581..9094345 100644 --- a/src/mpd_now_playable/cocoa.py +++ b/src/mpd_now_playable/cocoa/now_playing.py @@ -12,6 +12,7 @@ from MediaPlayer import ( MPMediaItemPropertyComposer, MPMediaItemPropertyDiscNumber, MPMediaItemPropertyGenre, + MPMediaItemPropertyPersistentID, MPMediaItemPropertyPlaybackDuration, MPMediaItemPropertyTitle, MPMusicPlaybackState, @@ -32,9 +33,10 @@ from MediaPlayer import ( 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: @@ -81,6 +83,7 @@ def song_to_media_item(song: Song) -> NSMutableDictionary: 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 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)