From 3372180a97c932399d0a6a6315283ab4832882db Mon Sep 17 00:00:00 2001 From: Danielle McLean Date: Wed, 6 Dec 2023 13:54:40 +1100 Subject: [PATCH] Enable seeking to specific points in the current song --- src/mpd_now_playable/cocoa/now_playing.py | 19 ++++++++++++- src/mpd_now_playable/mpd/listener.py | 3 +++ src/mpd_now_playable/player.py | 3 +++ stubs/MediaPlayer/__init__.pyi | 24 ++++++++++++++--- stubs/mpd/asyncio.pyi | 33 ++++++++++++----------- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/mpd_now_playable/cocoa/now_playing.py b/src/mpd_now_playable/cocoa/now_playing.py index 9094345..19665f6 100644 --- a/src/mpd_now_playable/cocoa/now_playing.py +++ b/src/mpd_now_playable/cocoa/now_playing.py @@ -4,6 +4,7 @@ from pathlib import Path from AppKit import NSCompositingOperationCopy, NSImage, NSMakeRect from Foundation import CGSize, NSMutableDictionary from MediaPlayer import ( + MPChangePlaybackPositionCommandEvent, MPMediaItemArtwork, MPMediaItemPropertyAlbumTitle, MPMediaItemPropertyAlbumTrackNumber, @@ -31,6 +32,7 @@ from MediaPlayer import ( MPRemoteCommandCenter, MPRemoteCommandEvent, MPRemoteCommandHandlerStatus, + MPRemoteCommandHandlerStatusSuccess, ) from ..async_tools import run_background_task @@ -138,13 +140,17 @@ class CocoaNowPlaying: cmd.removeTarget_(None) cmd.addTargetWithHandler_(self._create_handler(handler)) + seekCmd = self.cmd_center.changePlaybackPositionCommand() + seekCmd.setEnabled_(True) + seekCmd.removeTarget_(None) + seekCmd.addTargetWithHandler_(self._create_seek_handler(player.on_seek)) + 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) @@ -175,3 +181,14 @@ class CocoaNowPlaying: return 0 return handler + + def _create_seek_handler( + self, player: Callable[[float], Coroutine[None, None, None]] + ) -> Callable[[MPChangePlaybackPositionCommandEvent], MPRemoteCommandHandlerStatus]: + def handler( + event: MPChangePlaybackPositionCommandEvent, + ) -> MPRemoteCommandHandlerStatus: + run_background_task(player(event.positionTime())) + return MPRemoteCommandHandlerStatusSuccess + + return handler diff --git a/src/mpd_now_playable/mpd/listener.py b/src/mpd_now_playable/mpd/listener.py index 11f271a..ffd8a61 100644 --- a/src/mpd_now_playable/mpd/listener.py +++ b/src/mpd_now_playable/mpd/listener.py @@ -126,3 +126,6 @@ class MpdStateListener(Player): async def on_prev(self) -> None: await self.client.previous() + + async def on_seek(self, position: float) -> None: + await self.client.seekcur(position) diff --git a/src/mpd_now_playable/player.py b/src/mpd_now_playable/player.py index 4f47e6d..fcadfa4 100644 --- a/src/mpd_now_playable/player.py +++ b/src/mpd_now_playable/player.py @@ -21,3 +21,6 @@ class Player(Protocol): async def on_prev(self) -> None: ... + + async def on_seek(self, position: float) -> None: + ... diff --git a/stubs/MediaPlayer/__init__.pyi b/stubs/MediaPlayer/__init__.pyi index 2e3b7e0..01682fe 100644 --- a/stubs/MediaPlayer/__init__.pyi +++ b/stubs/MediaPlayer/__init__.pyi @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Final, Literal +from typing import Final, Literal, override from AppKit import NSImage from Foundation import CGSize, NSMutableDictionary @@ -52,18 +52,34 @@ class MPNowPlayingInfoCenter: def setNowPlayingInfo_(self, info: NSMutableDictionary) -> None: ... def setPlaybackState_(self, state: MPMusicPlaybackState) -> None: ... -MPRemoteCommandHandlerStatusSuccess: Literal[0] = 0 -MPRemoteCommandHandlerStatusCommandFailed: Literal[200] = 200 +MPRemoteCommandHandlerStatusSuccess: Final = 0 +MPRemoteCommandHandlerStatusCommandFailed: Final = 200 MPRemoteCommandHandlerStatus = Literal[0, 200] class MPRemoteCommandEvent: pass +class MPChangePlaybackPositionCommandEvent(MPRemoteCommandEvent): + def positionTime(self) -> float: + """Return the requested playback position as a number of seconds (fractional seconds are allowed).""" + pass + class MPRemoteCommand: def setEnabled_(self, enabled: bool) -> None: ... def removeTarget_(self, target: object) -> None: ... def addTargetWithHandler_( self, handler: Callable[[MPRemoteCommandEvent], MPRemoteCommandHandlerStatus] + ) -> None: + """Register a callback to handle the commands. Many remote commands don't carry useful information in the event object (play, pause, next track, etc.), so the callback does not necessarily need to care about the event argument.""" + pass + +class MPChangePlaybackPositionCommand(MPRemoteCommand): + @override + def addTargetWithHandler_( + self, + handler: Callable[ + [MPChangePlaybackPositionCommandEvent], MPRemoteCommandHandlerStatus + ], ) -> None: ... class MPRemoteCommandCenter: @@ -80,4 +96,4 @@ class MPRemoteCommandCenter: def skipBackwardCommand(self) -> MPRemoteCommand: ... def seekForwardCommand(self) -> MPRemoteCommand: ... def skipForwardCommand(self) -> MPRemoteCommand: ... - def changePlaybackPositionCommand(self) -> MPRemoteCommand: ... + def changePlaybackPositionCommand(self) -> MPChangePlaybackPositionCommand: ... diff --git a/stubs/mpd/asyncio.pyi b/stubs/mpd/asyncio.pyi index ea29554..221fcf4 100644 --- a/stubs/mpd/asyncio.pyi +++ b/stubs/mpd/asyncio.pyi @@ -5,19 +5,22 @@ from mpd.base import MPDClientBase from mpd_now_playable.mpd import types class MPDClient(MPDClientBase): - mpd_version: str | None + mpd_version: str | None - def __init__(self) -> None: ... - async def connect(self, host: str, port: int = ...) -> None: ... - async def password(self, password: str) -> None: ... - def idle(self, subsystems: Sequence[str] = ...) -> AsyncIterator[Sequence[str]]: ... - - async def status(self) -> types.StatusResponse: ... - async def currentsong(self) -> types.CurrentSongResponse: ... - async def readpicture(self, uri: str) -> types.ReadPictureResponse: ... - - async def play(self) -> None: ... - async def pause(self, pause: Literal[1, 0, None] = None) -> None: ... - async def stop(self) -> None: ... - async def next(self) -> None: ... # noqa: A003 - async def previous(self) -> None: ... + def __init__(self) -> None: ... + async def connect(self, host: str, port: int = ...) -> None: ... + async def password(self, password: str) -> None: ... + def idle(self, subsystems: Sequence[str] = ...) -> AsyncIterator[Sequence[str]]: ... + async def status(self) -> types.StatusResponse: ... + async def currentsong(self) -> types.CurrentSongResponse: ... + async def readpicture(self, uri: str) -> types.ReadPictureResponse: ... + async def play(self) -> None: ... + async def pause(self, pause: Literal[1, 0, None] = None) -> None: + """Pause MPD or toggle its play/pause state. Pass pause=1 to unconditionally pause, pause=0 to unconditionally unpause, or pause=None to toggle.""" + pass + async def stop(self) -> None: ... + async def next(self) -> None: ... # noqa: A003 + async def previous(self) -> None: ... + async def seekcur(self, position: float) -> None: + """Seek to a particular time in the currently playing song, measured in seconds. Fractional seconds are supported.""" + pass