Compare commits

...

6 commits
v1.5.1 ... main

9 changed files with 53 additions and 67 deletions

View file

@ -5,9 +5,6 @@
"kind": { "kind": {
"const": "cocoa", "const": "cocoa",
"default": "cocoa", "default": "cocoa",
"enum": [
"cocoa"
],
"title": "Kind", "title": "Kind",
"type": "string" "type": "string"
} }
@ -52,28 +49,14 @@
"WebsocketsReceiverConfig": { "WebsocketsReceiverConfig": {
"properties": { "properties": {
"host": { "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.", "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.",
"title": "Host" "format": "hostname",
"title": "Host",
"type": "string"
}, },
"kind": { "kind": {
"const": "websockets", "const": "websockets",
"default": "websockets", "default": "websockets",
"enum": [
"websockets"
],
"title": "Kind", "title": "Kind",
"type": "string" "type": "string"
}, },

View file

@ -104,7 +104,7 @@
"Queue": { "Queue": {
"properties": { "properties": {
"current": { "current": {
"description": "The zero-based index of the current song in MPD's queue.", "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.",
"title": "Current", "title": "Current",
"type": "integer" "type": "integer"
}, },
@ -136,9 +136,6 @@
}, },
{ {
"const": "oneshot", "const": "oneshot",
"enum": [
"oneshot"
],
"type": "string" "type": "string"
} }
], ],
@ -172,9 +169,6 @@
}, },
{ {
"const": "oneshot", "const": "oneshot",
"enum": [
"oneshot"
],
"type": "string" "type": "string"
} }
], ],
@ -328,9 +322,6 @@
"state": { "state": {
"const": "stop", "const": "stop",
"default": "stop", "default": "stop",
"enum": [
"stop"
],
"title": "State", "title": "State",
"type": "string" "type": "string"
} }

View file

@ -35,7 +35,7 @@ class WebsocketsReceiverConfig(BaseReceiverConfig):
#: The hostname you'd like your WebSockets server to listen on. In most #: The hostname you'd like your WebSockets server to listen on. In most
#: cases the default behaviour, which binds to all network interfaces, will #: cases the default behaviour, which binds to all network interfaces, will
#: be fine. #: be fine.
host: Optional[Host | tuple[Host, ...]] = None host: Optional[Host] = None
ReceiverConfig = Annotated[ ReceiverConfig = Annotated[

View file

@ -9,8 +9,8 @@ from .to_song import to_song
def to_queue(mpd: MpdState) -> Queue: def to_queue(mpd: MpdState) -> Queue:
return Queue( return Queue(
current=int(mpd.current["pos"]), current=option_fmap(int, mpd.current.get("pos")),
next=int(mpd.status["nextsong"]), next=int(mpd.status.get("nextsong", 0)),
length=int(mpd.status["playlistlength"]), length=int(mpd.status["playlistlength"]),
) )

View file

@ -3,8 +3,10 @@ from dataclasses import dataclass
@dataclass(slots=True) @dataclass(slots=True)
class Queue: class Queue:
#: The zero-based index of the current song in MPD's queue. #: The zero-based index of the current song in MPD's queue. If MPD is
current: int #: currently stopped, then there is no current song in the queue, indicated
#: by None.
current: int | None
#: The index of the next song to be played, taking into account random and #: The index of the next song to be played, taking into account random and
#: repeat playback settings. #: repeat playback settings.
next: int next: int

View file

@ -37,4 +37,4 @@ def ns_image_to_media_item_artwork(img: NSImage) -> MPMediaItemArtwork:
) )
MPD_LOGO = logo_to_ns_image() MPD_LOGO = ns_image_to_media_item_artwork(logo_to_ns_image())

View file

@ -1,7 +1,7 @@
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from typing import Literal from typing import Literal
from corefoundationasyncio import CoreFoundationEventLoop from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
from MediaPlayer import ( from MediaPlayer import (
MPChangePlaybackPositionCommandEvent, MPChangePlaybackPositionCommandEvent,
MPMusicPlaybackStatePlaying, MPMusicPlaybackStatePlaying,
@ -12,6 +12,8 @@ from MediaPlayer import (
MPRemoteCommandHandlerStatusSuccess, MPRemoteCommandHandlerStatusSuccess,
) )
from corefoundationasyncio import CoreFoundationEventLoop
from ...config.model import CocoaReceiverConfig from ...config.model import CocoaReceiverConfig
from ...playback import Playback from ...playback import Playback
from ...playback.state import PlaybackState from ...playback.state import PlaybackState
@ -41,6 +43,9 @@ class CocoaNowPlayingReceiver(Receiver):
pass pass
async def start(self, player: Player) -> None: async def start(self, player: Player) -> None:
NSApplication.sharedApplication().setActivationPolicy_(
NSApplicationActivationPolicyAccessory
)
self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter() self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter()
self.info_center = MPNowPlayingInfoCenter.defaultCenter() self.info_center = MPNowPlayingInfoCenter.defaultCenter()

View file

@ -2,7 +2,7 @@ from pathlib import Path
import ormsgpack import ormsgpack
from websockets import broadcast from websockets import broadcast
from websockets.server import WebSocketServerProtocol, serve from websockets.asyncio.server import Server, ServerConnection, serve
from yarl import URL from yarl import URL
from ...config.model import WebsocketsReceiverConfig from ...config.model import WebsocketsReceiverConfig
@ -24,12 +24,11 @@ def default(value: object) -> object:
class WebsocketsReceiver(Receiver): class WebsocketsReceiver(Receiver):
config: WebsocketsReceiverConfig config: WebsocketsReceiverConfig
player: Player player: Player
connections: set[WebSocketServerProtocol] server: Server
last_status: bytes = MSGPACK_NULL last_status: bytes = MSGPACK_NULL
def __init__(self, config: WebsocketsReceiverConfig): def __init__(self, config: WebsocketsReceiverConfig):
self.config = config self.config = config
self.connections = set()
@classmethod @classmethod
def loop_factory(cls) -> DefaultLoopFactory: def loop_factory(cls) -> DefaultLoopFactory:
@ -37,18 +36,14 @@ class WebsocketsReceiver(Receiver):
async def start(self, player: Player) -> None: async def start(self, player: Player) -> None:
self.player = player self.player = player
await serve( self.server = await serve(
self.handle, host=self.config.host, port=self.config.port, reuse_port=True self.handle, host=self.config.host, port=self.config.port, reuse_port=True
) )
async def handle(self, conn: WebSocketServerProtocol) -> None: async def handle(self, conn: ServerConnection) -> None:
self.connections.add(conn)
await conn.send(self.last_status) await conn.send(self.last_status)
try:
await conn.wait_closed() await conn.wait_closed()
finally:
self.connections.remove(conn)
async def update(self, playback: Playback) -> None: async def update(self, playback: Playback) -> None:
self.last_status = ormsgpack.packb(playback, default=default) self.last_status = ormsgpack.packb(playback, default=default)
broadcast(self.connections, self.last_status) broadcast(self.server.connections, self.last_status)

View file

@ -2,6 +2,16 @@ from typing import Final, Literal
from Foundation import CGSize 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. # There are many other operations available but we only actually use copy, so we don't need all of them here.
NSCompositingOperationClear: Final = 0 NSCompositingOperationClear: Final = 0
NSCompositingOperationCopy: Final = 1 NSCompositingOperationCopy: Final = 1
@ -15,19 +25,19 @@ def NSMakeRect(x: float, y: float, w: float, h: float) -> NSRect: ...
class NSImage: class NSImage:
@staticmethod @staticmethod
def alloc() -> type[NSImage]: ... def alloc() -> type[NSImage]: ...
@staticmethod @staticmethod
def initByReferencingFile_(file: str) -> NSImage: ... def initByReferencingFile_(file: str) -> NSImage: ...
@staticmethod @staticmethod
def initWithData_(data: bytes) -> NSImage: ... def initWithData_(data: bytes) -> NSImage: ...
@staticmethod @staticmethod
def initWithSize_(size: CGSize) -> NSImage: ... def initWithSize_(size: CGSize) -> NSImage: ...
def size(self) -> CGSize: ... def size(self) -> CGSize: ...
def lockFocus(self) -> None: ... def lockFocus(self) -> None: ...
def unlockFocus(self) -> None: ... def unlockFocus(self) -> None: ...
def drawInRect_fromRect_operation_fraction_(
def drawInRect_fromRect_operation_fraction_(self, inRect: NSRect, fromRect: NSRect, operation: NSCompositingOperation, fraction: float) -> None: ... self,
inRect: NSRect,
fromRect: NSRect,
operation: NSCompositingOperation,
fraction: float,
) -> None: ...