177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
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
|