Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
28748df3c1 | |||
c2f67c4781 | |||
413df0979d | |||
7dfd3f85e4 | |||
b41339a8c5 | |||
b9039b2ad4 |
9 changed files with 53 additions and 67 deletions
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[
|
||||||
|
|
|
@ -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"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -2,32 +2,42 @@ 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
|
||||||
@staticmethod
|
def initByReferencingFile_(file: str) -> NSImage: ...
|
||||||
def initByReferencingFile_(file: str) -> NSImage: ...
|
@staticmethod
|
||||||
|
def initWithData_(data: bytes) -> NSImage: ...
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def initWithData_(data: bytes) -> NSImage: ...
|
def initWithSize_(size: CGSize) -> NSImage: ...
|
||||||
|
def size(self) -> CGSize: ...
|
||||||
@staticmethod
|
def lockFocus(self) -> None: ...
|
||||||
def initWithSize_(size: CGSize) -> NSImage: ...
|
def unlockFocus(self) -> None: ...
|
||||||
|
def drawInRect_fromRect_operation_fraction_(
|
||||||
def size(self) -> CGSize: ...
|
self,
|
||||||
|
inRect: NSRect,
|
||||||
def lockFocus(self) -> None: ...
|
fromRect: NSRect,
|
||||||
def unlockFocus(self) -> None: ...
|
operation: NSCompositingOperation,
|
||||||
|
fraction: float,
|
||||||
def drawInRect_fromRect_operation_fraction_(self, inRect: NSRect, fromRect: NSRect, operation: NSCompositingOperation, fraction: float) -> None: ...
|
) -> None: ...
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue