Support persistent track IDs (64-bit ints)
This commit is contained in:
parent
28da4da69f
commit
04a976f6f3
4 changed files with 46 additions and 4 deletions
177
src/mpd_now_playable/cocoa/now_playing.py
Normal file
177
src/mpd_now_playable/cocoa/now_playing.py
Normal file
|
@ -0,0 +1,177 @@
|
|||
from collections.abc import Callable, Coroutine
|
||||
from pathlib import Path
|
||||
|
||||
from AppKit import NSCompositingOperationCopy, NSImage, NSMakeRect
|
||||
from Foundation import CGSize, NSMutableDictionary
|
||||
from MediaPlayer import (
|
||||
MPMediaItemArtwork,
|
||||
MPMediaItemPropertyAlbumTitle,
|
||||
MPMediaItemPropertyAlbumTrackNumber,
|
||||
MPMediaItemPropertyArtist,
|
||||
MPMediaItemPropertyArtwork,
|
||||
MPMediaItemPropertyComposer,
|
||||
MPMediaItemPropertyDiscNumber,
|
||||
MPMediaItemPropertyGenre,
|
||||
MPMediaItemPropertyPersistentID,
|
||||
MPMediaItemPropertyPlaybackDuration,
|
||||
MPMediaItemPropertyTitle,
|
||||
MPMusicPlaybackState,
|
||||
MPMusicPlaybackStatePaused,
|
||||
MPMusicPlaybackStatePlaying,
|
||||
MPMusicPlaybackStateStopped,
|
||||
MPNowPlayingInfoCenter,
|
||||
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 .persistent_id import song_to_persistent_id
|
||||
|
||||
|
||||
def logo_to_ns_image() -> NSImage:
|
||||
return NSImage.alloc().initByReferencingFile_(
|
||||
str(Path(__file__).parent / "mpd/logo.svg")
|
||||
)
|
||||
|
||||
|
||||
def data_to_ns_image(data: bytes) -> NSImage:
|
||||
return NSImage.alloc().initWithData_(data)
|
||||
|
||||
|
||||
def ns_image_to_media_item_artwork(img: NSImage) -> MPMediaItemArtwork:
|
||||
def resize(size: CGSize) -> NSImage:
|
||||
new = NSImage.alloc().initWithSize_(size)
|
||||
new.lockFocus()
|
||||
img.drawInRect_fromRect_operation_fraction_(
|
||||
NSMakeRect(0, 0, size.width, size.height),
|
||||
NSMakeRect(0, 0, img.size().width, img.size().height),
|
||||
NSCompositingOperationCopy,
|
||||
1.0,
|
||||
)
|
||||
new.unlockFocus()
|
||||
return new
|
||||
|
||||
return MPMediaItemArtwork.alloc().initWithBoundsSize_requestHandler_(
|
||||
img.size(), resize
|
||||
)
|
||||
|
||||
|
||||
def playback_state_to_cocoa(state: PlaybackState) -> MPMusicPlaybackState:
|
||||
mapping: dict[PlaybackState, MPMusicPlaybackState] = {
|
||||
PlaybackState.play: MPMusicPlaybackStatePlaying,
|
||||
PlaybackState.pause: MPMusicPlaybackStatePaused,
|
||||
PlaybackState.stop: MPMusicPlaybackStateStopped,
|
||||
}
|
||||
return mapping[state]
|
||||
|
||||
|
||||
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)
|
||||
)
|
||||
return nowplaying_info
|
||||
|
||||
|
||||
def nothing_to_media_item() -> NSMutableDictionary:
|
||||
nowplaying_info = NSMutableDictionary.dictionary()
|
||||
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeNone
|
||||
nowplaying_info[MPMediaItemPropertyArtwork] = MPD_LOGO
|
||||
nowplaying_info[MPMediaItemPropertyTitle] = "MPD (stopped)"
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackRate] = 0.0
|
||||
|
||||
return nowplaying_info
|
||||
|
||||
|
||||
MPD_LOGO = ns_image_to_media_item_artwork(logo_to_ns_image())
|
||||
|
||||
|
||||
class CocoaNowPlaying:
|
||||
def __init__(self, player: Player):
|
||||
self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter()
|
||||
self.info_center = MPNowPlayingInfoCenter.defaultCenter()
|
||||
|
||||
cmds = (
|
||||
(self.cmd_center.togglePlayPauseCommand(), player.on_play_pause),
|
||||
(self.cmd_center.playCommand(), player.on_play),
|
||||
(self.cmd_center.pauseCommand(), player.on_pause),
|
||||
(self.cmd_center.stopCommand(), player.on_stop),
|
||||
(self.cmd_center.nextTrackCommand(), player.on_next),
|
||||
(self.cmd_center.previousTrackCommand(), player.on_prev),
|
||||
)
|
||||
|
||||
for cmd, handler in cmds:
|
||||
cmd.setEnabled_(True)
|
||||
cmd.removeTarget_(None)
|
||||
cmd.addTargetWithHandler_(self._create_handler(handler))
|
||||
|
||||
unsupported_cmds = (
|
||||
self.cmd_center.changePlaybackRateCommand(),
|
||||
self.cmd_center.seekBackwardCommand(),
|
||||
self.cmd_center.skipBackwardCommand(),
|
||||
self.cmd_center.seekForwardCommand(),
|
||||
self.cmd_center.skipForwardCommand(),
|
||||
self.cmd_center.changePlaybackPositionCommand(),
|
||||
)
|
||||
for cmd in unsupported_cmds:
|
||||
cmd.setEnabled_(False)
|
||||
|
||||
# If MPD is paused when this bridge starts up, we actually want the now
|
||||
# playing info center to see a playing -> paused transition, so we can
|
||||
# unpause with remote commands.
|
||||
self.info_center.setPlaybackState_(MPMusicPlaybackStatePlaying)
|
||||
|
||||
def update(self, song: Song | None) -> None:
|
||||
if song:
|
||||
self.info_center.setNowPlayingInfo_(song_to_media_item(song))
|
||||
self.info_center.setPlaybackState_(playback_state_to_cocoa(song.state))
|
||||
else:
|
||||
self.info_center.setNowPlayingInfo_(nothing_to_media_item())
|
||||
self.info_center.setPlaybackState_(MPMusicPlaybackStateStopped)
|
||||
|
||||
def _create_handler(
|
||||
self, player: Callable[[], Coroutine[None, None, PlaybackState | None]]
|
||||
) -> Callable[[MPRemoteCommandEvent], MPRemoteCommandHandlerStatus]:
|
||||
async def invoke_music_player() -> None:
|
||||
result = await player()
|
||||
if result:
|
||||
self.info_center.setPlaybackState_(playback_state_to_cocoa(result))
|
||||
|
||||
def handler(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus:
|
||||
run_background_task(invoke_music_player())
|
||||
return 0
|
||||
|
||||
return handler
|
Loading…
Add table
Add a link
Reference in a new issue