Compare commits

..

2 commits

6 changed files with 63 additions and 23 deletions

View file

@ -43,9 +43,7 @@ Make sure this wrapper script is only readable by you, with something like `chmo
## Limitations ## Limitations
Currently mpd-now-playable does not support seeking through the current track, because I didn't personally feel the need for that feature. It explicitly tells MacOS that seeking isn't supported by this media player, but for some reason the Mac UI still lets the user seek anyway, which of course does nothing. mpd-now-playable is currently *very* specific to MacOS. I did my best to keep the generic MPD and extremely Apple parts separate, but it definitely won't work with MPRIS2 or the Windows system media feature.
Also, mpd-now-playable is currently *very* specific to MacOS. I did my best to keep the generic MPD and extremely Apple parts separate, but it definitely won't work with MPRIS2 or the Windows system media feature.
Chances are my MacOS integration code isn't the best, either. This is the first project I've written using PyObjC and it took a lot of fiddling to get working. Chances are my MacOS integration code isn't the best, either. This is the first project I've written using PyObjC and it took a lot of fiddling to get working.

View file

@ -4,6 +4,7 @@ from pathlib import Path
from AppKit import NSCompositingOperationCopy, NSImage, NSMakeRect from AppKit import NSCompositingOperationCopy, NSImage, NSMakeRect
from Foundation import CGSize, NSMutableDictionary from Foundation import CGSize, NSMutableDictionary
from MediaPlayer import ( from MediaPlayer import (
MPChangePlaybackPositionCommandEvent,
MPMediaItemArtwork, MPMediaItemArtwork,
MPMediaItemPropertyAlbumTitle, MPMediaItemPropertyAlbumTitle,
MPMediaItemPropertyAlbumTrackNumber, MPMediaItemPropertyAlbumTrackNumber,
@ -31,6 +32,7 @@ from MediaPlayer import (
MPRemoteCommandCenter, MPRemoteCommandCenter,
MPRemoteCommandEvent, MPRemoteCommandEvent,
MPRemoteCommandHandlerStatus, MPRemoteCommandHandlerStatus,
MPRemoteCommandHandlerStatusSuccess,
) )
from ..async_tools import run_background_task from ..async_tools import run_background_task
@ -138,13 +140,17 @@ class CocoaNowPlaying:
cmd.removeTarget_(None) cmd.removeTarget_(None)
cmd.addTargetWithHandler_(self._create_handler(handler)) 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 = ( unsupported_cmds = (
self.cmd_center.changePlaybackRateCommand(), self.cmd_center.changePlaybackRateCommand(),
self.cmd_center.seekBackwardCommand(), self.cmd_center.seekBackwardCommand(),
self.cmd_center.skipBackwardCommand(), self.cmd_center.skipBackwardCommand(),
self.cmd_center.seekForwardCommand(), self.cmd_center.seekForwardCommand(),
self.cmd_center.skipForwardCommand(), self.cmd_center.skipForwardCommand(),
self.cmd_center.changePlaybackPositionCommand(),
) )
for cmd in unsupported_cmds: for cmd in unsupported_cmds:
cmd.setEnabled_(False) cmd.setEnabled_(False)
@ -175,3 +181,14 @@ class CocoaNowPlaying:
return 0 return 0
return handler 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

View file

@ -126,3 +126,6 @@ class MpdStateListener(Player):
async def on_prev(self) -> None: async def on_prev(self) -> None:
await self.client.previous() await self.client.previous()
async def on_seek(self, position: float) -> None:
await self.client.seekcur(position)

View file

@ -21,3 +21,6 @@ class Player(Protocol):
async def on_prev(self) -> None: async def on_prev(self) -> None:
... ...
async def on_seek(self, position: float) -> None:
...

View file

@ -1,5 +1,5 @@
from collections.abc import Callable from collections.abc import Callable
from typing import Final, Literal from typing import Final, Literal, override
from AppKit import NSImage from AppKit import NSImage
from Foundation import CGSize, NSMutableDictionary from Foundation import CGSize, NSMutableDictionary
@ -52,18 +52,34 @@ class MPNowPlayingInfoCenter:
def setNowPlayingInfo_(self, info: NSMutableDictionary) -> None: ... def setNowPlayingInfo_(self, info: NSMutableDictionary) -> None: ...
def setPlaybackState_(self, state: MPMusicPlaybackState) -> None: ... def setPlaybackState_(self, state: MPMusicPlaybackState) -> None: ...
MPRemoteCommandHandlerStatusSuccess: Literal[0] = 0 MPRemoteCommandHandlerStatusSuccess: Final = 0
MPRemoteCommandHandlerStatusCommandFailed: Literal[200] = 200 MPRemoteCommandHandlerStatusCommandFailed: Final = 200
MPRemoteCommandHandlerStatus = Literal[0, 200] MPRemoteCommandHandlerStatus = Literal[0, 200]
class MPRemoteCommandEvent: class MPRemoteCommandEvent:
pass 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: class MPRemoteCommand:
def setEnabled_(self, enabled: bool) -> None: ... def setEnabled_(self, enabled: bool) -> None: ...
def removeTarget_(self, target: object) -> None: ... def removeTarget_(self, target: object) -> None: ...
def addTargetWithHandler_( def addTargetWithHandler_(
self, handler: Callable[[MPRemoteCommandEvent], MPRemoteCommandHandlerStatus] 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: ... ) -> None: ...
class MPRemoteCommandCenter: class MPRemoteCommandCenter:
@ -80,4 +96,4 @@ class MPRemoteCommandCenter:
def skipBackwardCommand(self) -> MPRemoteCommand: ... def skipBackwardCommand(self) -> MPRemoteCommand: ...
def seekForwardCommand(self) -> MPRemoteCommand: ... def seekForwardCommand(self) -> MPRemoteCommand: ...
def skipForwardCommand(self) -> MPRemoteCommand: ... def skipForwardCommand(self) -> MPRemoteCommand: ...
def changePlaybackPositionCommand(self) -> MPRemoteCommand: ... def changePlaybackPositionCommand(self) -> MPChangePlaybackPositionCommand: ...

View file

@ -5,19 +5,22 @@ from mpd.base import MPDClientBase
from mpd_now_playable.mpd import types from mpd_now_playable.mpd import types
class MPDClient(MPDClientBase): class MPDClient(MPDClientBase):
mpd_version: str | None mpd_version: str | None
def __init__(self) -> None: ... def __init__(self) -> None: ...
async def connect(self, host: str, port: int = ...) -> None: ... async def connect(self, host: str, port: int = ...) -> None: ...
async def password(self, password: str) -> None: ... async def password(self, password: str) -> None: ...
def idle(self, subsystems: Sequence[str] = ...) -> AsyncIterator[Sequence[str]]: ... def idle(self, subsystems: Sequence[str] = ...) -> AsyncIterator[Sequence[str]]: ...
async def status(self) -> types.StatusResponse: ...
async def status(self) -> types.StatusResponse: ... async def currentsong(self) -> types.CurrentSongResponse: ...
async def currentsong(self) -> types.CurrentSongResponse: ... async def readpicture(self, uri: str) -> types.ReadPictureResponse: ...
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 play(self) -> 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."""
async def pause(self, pause: Literal[1, 0, None] = None) -> None: ... pass
async def stop(self) -> None: ... async def stop(self) -> None: ...
async def next(self) -> None: ... # noqa: A003 async def next(self) -> None: ... # noqa: A003
async def previous(self) -> None: ... 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