diff --git a/schemata/config-v1.json b/schemata/config-v1.json index bf5b358..68eff9f 100644 --- a/schemata/config-v1.json +++ b/schemata/config-v1.json @@ -5,6 +5,9 @@ "kind": { "const": "cocoa", "default": "cocoa", + "enum": [ + "cocoa" + ], "title": "Kind", "type": "string" } @@ -49,14 +52,28 @@ "WebsocketsReceiverConfig": { "properties": { "host": { + "anyOf": [ + { + "format": "hostname", + "type": "string" + }, + { + "items": { + "format": "hostname", + "type": "string" + }, + "type": "array" + } + ], "description": "The hostname you'd like your WebSockets server to listen on. In most cases the default behaviour, which binds to all network interfaces, will be fine.", - "format": "hostname", - "title": "Host", - "type": "string" + "title": "Host" }, "kind": { "const": "websockets", "default": "websockets", + "enum": [ + "websockets" + ], "title": "Kind", "type": "string" }, diff --git a/schemata/playback-v1.json b/schemata/playback-v1.json index 7c97aa4..58b7244 100644 --- a/schemata/playback-v1.json +++ b/schemata/playback-v1.json @@ -104,7 +104,7 @@ "Queue": { "properties": { "current": { - "description": "The zero-based index of the current song in MPD's queue. If MPD is currently stopped, then there is no current song in the queue, indicated by None.", + "description": "The zero-based index of the current song in MPD's queue.", "title": "Current", "type": "integer" }, @@ -136,6 +136,9 @@ }, { "const": "oneshot", + "enum": [ + "oneshot" + ], "type": "string" } ], @@ -169,6 +172,9 @@ }, { "const": "oneshot", + "enum": [ + "oneshot" + ], "type": "string" } ], @@ -322,6 +328,9 @@ "state": { "const": "stop", "default": "stop", + "enum": [ + "stop" + ], "title": "State", "type": "string" } diff --git a/src/mpd_now_playable/config/model.py b/src/mpd_now_playable/config/model.py index a112042..0fde2d1 100644 --- a/src/mpd_now_playable/config/model.py +++ b/src/mpd_now_playable/config/model.py @@ -35,7 +35,7 @@ class WebsocketsReceiverConfig(BaseReceiverConfig): #: The hostname you'd like your WebSockets server to listen on. In most #: cases the default behaviour, which binds to all network interfaces, will #: be fine. - host: Optional[Host] = None + host: Optional[Host | tuple[Host, ...]] = None ReceiverConfig = Annotated[ diff --git a/src/mpd_now_playable/mpd/convert/to_playback.py b/src/mpd_now_playable/mpd/convert/to_playback.py index 77adfa2..29b2223 100644 --- a/src/mpd_now_playable/mpd/convert/to_playback.py +++ b/src/mpd_now_playable/mpd/convert/to_playback.py @@ -9,8 +9,8 @@ from .to_song import to_song def to_queue(mpd: MpdState) -> Queue: return Queue( - current=option_fmap(int, mpd.current.get("pos")), - next=int(mpd.status.get("nextsong", 0)), + current=int(mpd.current["pos"]), + next=int(mpd.status["nextsong"]), length=int(mpd.status["playlistlength"]), ) diff --git a/src/mpd_now_playable/playback/queue.py b/src/mpd_now_playable/playback/queue.py index df5e76c..7c93741 100644 --- a/src/mpd_now_playable/playback/queue.py +++ b/src/mpd_now_playable/playback/queue.py @@ -3,10 +3,8 @@ from dataclasses import dataclass @dataclass(slots=True) class Queue: - #: The zero-based index of the current song in MPD's queue. If MPD is - #: currently stopped, then there is no current song in the queue, indicated - #: by None. - current: int | None + #: The zero-based index of the current song in MPD's queue. + current: int #: The index of the next song to be played, taking into account random and #: repeat playback settings. next: int diff --git a/src/mpd_now_playable/receivers/cocoa/convert/to_nsimage.py b/src/mpd_now_playable/receivers/cocoa/convert/to_nsimage.py index e8544ed..213914d 100644 --- a/src/mpd_now_playable/receivers/cocoa/convert/to_nsimage.py +++ b/src/mpd_now_playable/receivers/cocoa/convert/to_nsimage.py @@ -37,4 +37,4 @@ def ns_image_to_media_item_artwork(img: NSImage) -> MPMediaItemArtwork: ) -MPD_LOGO = ns_image_to_media_item_artwork(logo_to_ns_image()) +MPD_LOGO = logo_to_ns_image() diff --git a/src/mpd_now_playable/receivers/cocoa/now_playing.py b/src/mpd_now_playable/receivers/cocoa/now_playing.py index 83ba472..602006a 100644 --- a/src/mpd_now_playable/receivers/cocoa/now_playing.py +++ b/src/mpd_now_playable/receivers/cocoa/now_playing.py @@ -1,7 +1,7 @@ from collections.abc import Callable, Coroutine from typing import Literal -from AppKit import NSApplication, NSApplicationActivationPolicyAccessory +from corefoundationasyncio import CoreFoundationEventLoop from MediaPlayer import ( MPChangePlaybackPositionCommandEvent, MPMusicPlaybackStatePlaying, @@ -12,8 +12,6 @@ from MediaPlayer import ( MPRemoteCommandHandlerStatusSuccess, ) -from corefoundationasyncio import CoreFoundationEventLoop - from ...config.model import CocoaReceiverConfig from ...playback import Playback from ...playback.state import PlaybackState @@ -43,9 +41,6 @@ class CocoaNowPlayingReceiver(Receiver): pass async def start(self, player: Player) -> None: - NSApplication.sharedApplication().setActivationPolicy_( - NSApplicationActivationPolicyAccessory - ) self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter() self.info_center = MPNowPlayingInfoCenter.defaultCenter() diff --git a/src/mpd_now_playable/receivers/websockets/receiver.py b/src/mpd_now_playable/receivers/websockets/receiver.py index d218f35..a125b6b 100644 --- a/src/mpd_now_playable/receivers/websockets/receiver.py +++ b/src/mpd_now_playable/receivers/websockets/receiver.py @@ -2,7 +2,7 @@ from pathlib import Path import ormsgpack from websockets import broadcast -from websockets.asyncio.server import Server, ServerConnection, serve +from websockets.server import WebSocketServerProtocol, serve from yarl import URL from ...config.model import WebsocketsReceiverConfig @@ -24,11 +24,12 @@ def default(value: object) -> object: class WebsocketsReceiver(Receiver): config: WebsocketsReceiverConfig player: Player - server: Server + connections: set[WebSocketServerProtocol] last_status: bytes = MSGPACK_NULL def __init__(self, config: WebsocketsReceiverConfig): self.config = config + self.connections = set() @classmethod def loop_factory(cls) -> DefaultLoopFactory: @@ -36,14 +37,18 @@ class WebsocketsReceiver(Receiver): async def start(self, player: Player) -> None: self.player = player - self.server = await serve( + await serve( self.handle, host=self.config.host, port=self.config.port, reuse_port=True ) - async def handle(self, conn: ServerConnection) -> None: + async def handle(self, conn: WebSocketServerProtocol) -> None: + self.connections.add(conn) await conn.send(self.last_status) - await conn.wait_closed() + try: + await conn.wait_closed() + finally: + self.connections.remove(conn) async def update(self, playback: Playback) -> None: self.last_status = ormsgpack.packb(playback, default=default) - broadcast(self.server.connections, self.last_status) + broadcast(self.connections, self.last_status) diff --git a/stubs/AppKit/__init__.pyi b/stubs/AppKit/__init__.pyi index 50b8fcc..30512c2 100644 --- a/stubs/AppKit/__init__.pyi +++ b/stubs/AppKit/__init__.pyi @@ -2,42 +2,32 @@ from typing import Final, Literal from Foundation import CGSize -NSApplicationActivationPolicyRegular: Final = 0 -NSApplicationActivationPolicyAccessory: Final = 1 -NSApplicationActivationPolicyProhibited: Final = 2 -NSApplicationActivationPolicy = Literal[0, 1, 2] - -class NSApplication: - @staticmethod - def sharedApplication() -> NSApplication: ... - def setActivationPolicy_(self, policy: NSApplicationActivationPolicy) -> bool: ... - # There are many other operations available but we only actually use copy, so we don't need all of them here. NSCompositingOperationClear: Final = 0 NSCompositingOperationCopy: Final = 1 NSCompositingOperation = Literal[0, 1] class NSRect: - pass + pass def NSMakeRect(x: float, y: float, w: float, h: float) -> NSRect: ... class NSImage: - @staticmethod - def alloc() -> type[NSImage]: ... - @staticmethod - def initByReferencingFile_(file: str) -> NSImage: ... - @staticmethod - def initWithData_(data: bytes) -> NSImage: ... - @staticmethod - def initWithSize_(size: CGSize) -> NSImage: ... - def size(self) -> CGSize: ... - def lockFocus(self) -> None: ... - def unlockFocus(self) -> None: ... - def drawInRect_fromRect_operation_fraction_( - self, - inRect: NSRect, - fromRect: NSRect, - operation: NSCompositingOperation, - fraction: float, - ) -> None: ... + @staticmethod + def alloc() -> type[NSImage]: ... + + @staticmethod + def initByReferencingFile_(file: str) -> NSImage: ... + + @staticmethod + def initWithData_(data: bytes) -> NSImage: ... + + @staticmethod + def initWithSize_(size: CGSize) -> NSImage: ... + + def size(self) -> CGSize: ... + + def lockFocus(self) -> None: ... + def unlockFocus(self) -> None: ... + + def drawInRect_fromRect_operation_fraction_(self, inRect: NSRect, fromRect: NSRect, operation: NSCompositingOperation, fraction: float) -> None: ...