From 04a976f6f3f9f30e152575d2e40f680c4ae29b63 Mon Sep 17 00:00:00 2001 From: Danielle McLean Date: Wed, 6 Dec 2023 12:27:24 +1100 Subject: [PATCH] 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)