Compare commits

...

6 commits
v1.5.1 ... main

9 changed files with 53 additions and 67 deletions

View file

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

View file

@ -104,7 +104,7 @@
"Queue": {
"properties": {
"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",
"type": "integer"
},
@ -136,9 +136,6 @@
},
{
"const": "oneshot",
"enum": [
"oneshot"
],
"type": "string"
}
],
@ -172,9 +169,6 @@
},
{
"const": "oneshot",
"enum": [
"oneshot"
],
"type": "string"
}
],
@ -328,9 +322,6 @@
"state": {
"const": "stop",
"default": "stop",
"enum": [
"stop"
],
"title": "State",
"type": "string"
}

View file

@ -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 | tuple[Host, ...]] = None
host: Optional[Host] = None
ReceiverConfig = Annotated[

View file

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

View file

@ -3,8 +3,10 @@ from dataclasses import dataclass
@dataclass(slots=True)
class Queue:
#: The zero-based index of the current song in MPD's queue.
current: int
#: 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 index of the next song to be played, taking into account random and
#: repeat playback settings.
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 typing import Literal
from corefoundationasyncio import CoreFoundationEventLoop
from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
from MediaPlayer import (
MPChangePlaybackPositionCommandEvent,
MPMusicPlaybackStatePlaying,
@ -12,6 +12,8 @@ from MediaPlayer import (
MPRemoteCommandHandlerStatusSuccess,
)
from corefoundationasyncio import CoreFoundationEventLoop
from ...config.model import CocoaReceiverConfig
from ...playback import Playback
from ...playback.state import PlaybackState
@ -41,6 +43,9 @@ 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()

View file

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

View file

@ -2,32 +2,42 @@ 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: ...