Compare commits

..

No commits in common. "main" and "v1.5.1" have entirely different histories.
main ... v1.5.1

9 changed files with 67 additions and 53 deletions

View file

@ -5,6 +5,9 @@
"kind": { "kind": {
"const": "cocoa", "const": "cocoa",
"default": "cocoa", "default": "cocoa",
"enum": [
"cocoa"
],
"title": "Kind", "title": "Kind",
"type": "string" "type": "string"
} }
@ -49,14 +52,28 @@
"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.",
"format": "hostname", "title": "Host"
"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. 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", "title": "Current",
"type": "integer" "type": "integer"
}, },
@ -136,6 +136,9 @@
}, },
{ {
"const": "oneshot", "const": "oneshot",
"enum": [
"oneshot"
],
"type": "string" "type": "string"
} }
], ],
@ -169,6 +172,9 @@
}, },
{ {
"const": "oneshot", "const": "oneshot",
"enum": [
"oneshot"
],
"type": "string" "type": "string"
} }
], ],
@ -322,6 +328,9 @@
"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] = None host: Optional[Host | tuple[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=option_fmap(int, mpd.current.get("pos")), current=int(mpd.current["pos"]),
next=int(mpd.status.get("nextsong", 0)), next=int(mpd.status["nextsong"]),
length=int(mpd.status["playlistlength"]), length=int(mpd.status["playlistlength"]),
) )

View file

@ -3,10 +3,8 @@ 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. If MPD is #: The zero-based index of the current song in MPD's queue.
#: currently stopped, then there is no current song in the queue, indicated current: int
#: 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 = ns_image_to_media_item_artwork(logo_to_ns_image()) MPD_LOGO = 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 AppKit import NSApplication, NSApplicationActivationPolicyAccessory from corefoundationasyncio import CoreFoundationEventLoop
from MediaPlayer import ( from MediaPlayer import (
MPChangePlaybackPositionCommandEvent, MPChangePlaybackPositionCommandEvent,
MPMusicPlaybackStatePlaying, MPMusicPlaybackStatePlaying,
@ -12,8 +12,6 @@ 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
@ -43,9 +41,6 @@ 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.asyncio.server import Server, ServerConnection, serve from websockets.server import WebSocketServerProtocol, serve
from yarl import URL from yarl import URL
from ...config.model import WebsocketsReceiverConfig from ...config.model import WebsocketsReceiverConfig
@ -24,11 +24,12 @@ def default(value: object) -> object:
class WebsocketsReceiver(Receiver): class WebsocketsReceiver(Receiver):
config: WebsocketsReceiverConfig config: WebsocketsReceiverConfig
player: Player player: Player
server: Server connections: set[WebSocketServerProtocol]
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:
@ -36,14 +37,18 @@ class WebsocketsReceiver(Receiver):
async def start(self, player: Player) -> None: async def start(self, player: Player) -> None:
self.player = player self.player = player
self.server = await serve( 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: ServerConnection) -> None: async def handle(self, conn: WebSocketServerProtocol) -> None:
self.connections.add(conn)
await conn.send(self.last_status) 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: 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.server.connections, self.last_status) broadcast(self.connections, self.last_status)

View file

@ -2,42 +2,32 @@ 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
NSCompositingOperation = Literal[0, 1] NSCompositingOperation = Literal[0, 1]
class NSRect: class NSRect:
pass pass
def NSMakeRect(x: float, y: float, w: float, h: float) -> NSRect: ... 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
def initByReferencingFile_(file: str) -> NSImage: ... @staticmethod
@staticmethod def initByReferencingFile_(file: str) -> NSImage: ...
def initWithData_(data: bytes) -> NSImage: ...
@staticmethod @staticmethod
def initWithSize_(size: CGSize) -> NSImage: ... def initWithData_(data: bytes) -> NSImage: ...
def size(self) -> CGSize: ...
def lockFocus(self) -> None: ... @staticmethod
def unlockFocus(self) -> None: ... def initWithSize_(size: CGSize) -> NSImage: ...
def drawInRect_fromRect_operation_fraction_(
self, def size(self) -> CGSize: ...
inRect: NSRect,
fromRect: NSRect, def lockFocus(self) -> None: ...
operation: NSCompositingOperation, def unlockFocus(self) -> None: ...
fraction: float,
) -> None: ... def drawInRect_fromRect_operation_fraction_(self, inRect: NSRect, fromRect: NSRect, operation: NSCompositingOperation, fraction: float) -> None: ...