Initial commit of source - working, but needs stubs for Cocoa

This commit is contained in:
Danielle McLean 2023-11-27 15:49:33 +11:00
parent 1e7fd270eb
commit a673f2ef90
Signed by: 00dani
GPG key ID: 52C059C3B22A753E
12 changed files with 1339 additions and 0 deletions

View file

View file

@ -0,0 +1,19 @@
import asyncio
from collections.abc import Coroutine
from contextvars import Context
from typing import Optional
__all__ = ("run_background_task",)
background_tasks: set[asyncio.Task[None]] = set()
def run_background_task(
coro: Coroutine[None, None, None],
*,
name: Optional[str] = None,
context: Optional[Context] = None,
) -> None:
task = asyncio.create_task(coro, name=name, context=context)
background_tasks.add(task)
task.add_done_callback(background_tasks.discard)

View file

@ -0,0 +1,30 @@
import asyncio
from os import environ
from corefoundationasyncio import CoreFoundationEventLoop
from .cocoa import CocoaNowPlaying
from .mpd.listener import MpdStateListener
async def listen() -> None:
listener = MpdStateListener()
now_playing = CocoaNowPlaying(listener)
await listener.start(
hostname=environ.get("MPD_HOSTNAME", "localhost"),
port=int(environ.get("MPD_PORT", "6600")),
password=environ.get("MPD_PASSWORD"),
)
await listener.loop(now_playing)
def make_loop() -> CoreFoundationEventLoop:
return CoreFoundationEventLoop(console_app=True)
def main() -> None:
asyncio.run(listen(), loop_factory=make_loop, debug=True)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,152 @@
from collections.abc import Callable, Coroutine
from pathlib import Path
from AppKit import NSCompositingOperationCopy, NSImage, NSMakeRect
from Foundation import CGSize, NSMutableDictionary
from MediaPlayer import (
MPMediaItemArtwork,
MPMediaItemPropertyAlbumTitle,
MPMediaItemPropertyArtist,
MPMediaItemPropertyArtwork,
MPMediaItemPropertyPlaybackDuration,
MPMediaItemPropertyTitle,
MPMusicPlaybackState,
MPMusicPlaybackStatePaused,
MPMusicPlaybackStatePlaying,
MPMusicPlaybackStateStopped,
MPNowPlayingInfoCenter,
MPNowPlayingInfoMediaTypeAudio,
MPNowPlayingInfoMediaTypeNone,
MPNowPlayingInfoPropertyElapsedPlaybackTime,
MPNowPlayingInfoPropertyMediaType,
MPRemoteCommandCenter,
MPRemoteCommandEvent,
MPRemoteCommandHandlerStatus,
)
from .async_tools import run_background_task
from .player import Player
from .song import PlaybackState, Song
def logo_to_ns_image() -> NSImage:
return NSImage.alloc().initByReferencingFile_(
str(Path(__file__).parent / "mpd/logo.svg")
)
def data_to_ns_image(data: bytes) -> NSImage:
return NSImage.alloc().initWithData_(data)
def ns_image_to_media_item_artwork(img: NSImage) -> MPMediaItemArtwork:
def resize(size: CGSize) -> NSImage:
new = NSImage.alloc().initWithSize_(size)
new.lockFocus()
img.drawInRect_fromRect_operation_fraction_(
NSMakeRect(0, 0, size.width, size.height),
NSMakeRect(0, 0, img.size().width, img.size().height),
NSCompositingOperationCopy,
1.0,
)
new.unlockFocus()
return new
return MPMediaItemArtwork.alloc().initWithBoundsSize_requestHandler_(
img.size(), resize
)
def playback_state_to_cocoa(state: PlaybackState) -> MPMusicPlaybackState:
return {
PlaybackState.play: MPMusicPlaybackStatePlaying,
PlaybackState.pause: MPMusicPlaybackStatePaused,
PlaybackState.stop: MPMusicPlaybackStateStopped,
}[state]
def song_to_media_item(song: Song) -> NSMutableDictionary:
nowplaying_info = nothing_to_media_item()
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeAudio
nowplaying_info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = song.elapsed
nowplaying_info[MPMediaItemPropertyTitle] = song.title
nowplaying_info[MPMediaItemPropertyArtist] = song.artist
nowplaying_info[MPMediaItemPropertyAlbumTitle] = song.album
nowplaying_info[MPMediaItemPropertyPlaybackDuration] = song.duration
if song.art:
nowplaying_info[MPMediaItemPropertyArtwork] = ns_image_to_media_item_artwork(
data_to_ns_image(song.art)
)
return nowplaying_info
def nothing_to_media_item() -> NSMutableDictionary:
nowplaying_info = NSMutableDictionary.dictionary()
nowplaying_info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaTypeNone
nowplaying_info[MPMediaItemPropertyArtwork] = MPD_LOGO
nowplaying_info[MPMediaItemPropertyTitle] = "MPD (stopped)"
return nowplaying_info
MPD_LOGO = ns_image_to_media_item_artwork(logo_to_ns_image())
class CocoaNowPlaying:
def __init__(self, player: Player):
self.cmd_center = MPRemoteCommandCenter.sharedCommandCenter()
self.info_center = MPNowPlayingInfoCenter.defaultCenter()
cmds = (
(self.cmd_center.togglePlayPauseCommand(), player.on_play_pause),
(self.cmd_center.playCommand(), player.on_play),
(self.cmd_center.pauseCommand(), player.on_pause),
(self.cmd_center.stopCommand(), player.on_stop),
(self.cmd_center.nextTrackCommand(), player.on_next),
(self.cmd_center.previousTrackCommand(), player.on_prev),
)
for cmd, handler in cmds:
cmd.setEnabled_(True)
cmd.removeTarget_(None)
cmd.addTargetWithHandler_(self._create_handler(handler))
unsupported_cmds = (
self.cmd_center.changePlaybackRateCommand(),
self.cmd_center.seekBackwardCommand(),
self.cmd_center.skipBackwardCommand(),
self.cmd_center.seekForwardCommand(),
self.cmd_center.skipForwardCommand(),
self.cmd_center.changePlaybackPositionCommand(),
)
for cmd in unsupported_cmds:
cmd.setEnabled_(False)
# If MPD is paused when this bridge starts up, we actually want the now
# playing info center to see a playing -> paused transition, so we can
# unpause with remote commands.
self.info_center.setPlaybackState_(MPMusicPlaybackStatePlaying)
def update(self, song: Song | None) -> None:
if song:
self.info_center.setNowPlayingInfo_(song_to_media_item(song))
self.info_center.setPlaybackState_(playback_state_to_cocoa(song.state))
else:
self.info_center.setNowPlayingInfo_(nothing_to_media_item())
self.info_center.setPlaybackState_(MPMusicPlaybackStateStopped)
def _create_handler(
self, player: Callable[[], Coroutine[None, None, PlaybackState | None]]
) -> Callable[[MPRemoteCommandEvent], None]:
async def invoke_music_player() -> None:
result = await player()
if result:
self.info_center.setPlaybackState_(playback_state_to_cocoa(result))
def handler(event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus:
run_background_task(invoke_music_player())
return 0
return handler

View file

View file

@ -0,0 +1,49 @@
from aiocache import Cache
from ..async_tools import run_background_task
from .types import CurrentSongResponse, MpdStateHandler
CACHE_TTL = 60 * 10 # ten minutes
def calc_album_key(song: CurrentSongResponse) -> str:
return f'{song["albumartist"]}:-:-:{song["album"]}'
def calc_track_key(song: CurrentSongResponse) -> str:
return song["file"]
class MpdArtworkCache:
mpd: MpdStateHandler
album_cache: 'Cache[bytes | None]'
track_cache: 'Cache[bytes | None]'
def __init__(self, mpd: MpdStateHandler):
self.mpd = mpd
self.album_cache = Cache()
self.track_cache = Cache()
async def get_cached_artwork(self, song: CurrentSongResponse) -> bytes | None:
art = await self.track_cache.get(calc_track_key(song))
if art:
return art
# If we don't have track artwork cached, go find some.
run_background_task(self.cache_artwork(song))
# Even if we don't have cached track art, we can try looking for cached album art.
art = await self.album_cache.get(calc_album_key(song))
if art:
return art
return None
async def cache_artwork(self, song: CurrentSongResponse) -> None:
art = await self.mpd.readpicture(song["file"])
try:
await self.album_cache.add(calc_album_key(song), art, ttl=CACHE_TTL)
except ValueError:
pass
await self.track_cache.set(calc_track_key(song), art, ttl=CACHE_TTL)
await self.mpd.refresh()

View file

@ -0,0 +1,117 @@
import asyncio
from mpd.asyncio import MPDClient
from mpd.base import CommandError
from ..player import Player
from ..song import PlaybackState, Song, SongListener
from .artwork_cache import MpdArtworkCache
from .types import CurrentSongResponse, StatusResponse
def mpd_current_to_song(
status: StatusResponse, current: CurrentSongResponse, art: bytes | None
) -> Song:
return Song(
state=PlaybackState(status["state"]),
title=current["title"],
artist=current["artist"],
album=current["album"],
album_artist=current["albumartist"],
duration=float(status["duration"]),
elapsed=float(status["elapsed"]),
art=art,
)
class MpdStateListener(Player):
client: MPDClient
listener: SongListener
art_cache: MpdArtworkCache
idle_count = 0
def __init__(self) -> None:
self.client = MPDClient()
self.art_cache = MpdArtworkCache(self)
async def start(
self, hostname: str = "localhost", port: int = 6600, password: str | None = None
) -> None:
print(f"Connecting to MPD server {hostname}:{port}...")
await self.client.connect(hostname, port)
if password is not None:
print("Authorising to MPD with your password...")
await self.client.password(password)
print(f"Connected to MPD v{self.client.mpd_version}")
async def refresh(self) -> None:
await self.update_listener(self.listener)
async def loop(self, listener: SongListener) -> None:
self.listener = listener
# notify our listener of the initial state MPD is in when this script loads up.
await self.update_listener(listener)
# then wait for stuff to change in MPD. :)
async for _ in self.client.idle():
self.idle_count += 1
await self.update_listener(listener)
async def update_listener(self, listener: SongListener) -> None:
# If any async calls in here take long enough that we got another MPD idle event, we want to bail out of this older update.
starting_idle_count = self.idle_count
status, current = await asyncio.gather(
self.client.status(), self.client.currentsong()
)
if starting_idle_count != self.idle_count:
return
if status["state"] == "stop":
print("Nothing playing")
listener.update(None)
return
art = await self.art_cache.get_cached_artwork(current)
if starting_idle_count != self.idle_count:
return
song = mpd_current_to_song(status, current, art)
print(song)
listener.update(song)
async def readpicture(self, file: str) -> bytes | None:
try:
readpic = await self.client.readpicture(file)
return readpic["binary"]
except CommandError:
return None
async def on_play_pause(self) -> PlaybackState:
# python-mpd2 has direct support for toggling the play/pause state by
# calling MPDClient.pause(None), but it doesn't tell you the final
# state, and we also want to support playing from stopped, so we need
# to handle this ourselves.
status = await self.client.status()
return await {
"play": self.on_pause,
"pause": self.on_play,
"stop": self.on_play,
}[status["state"]]()
async def on_play(self) -> PlaybackState:
await self.client.play()
return PlaybackState.play
async def on_pause(self) -> PlaybackState:
await self.client.pause(1)
return PlaybackState.pause
async def on_stop(self) -> PlaybackState:
await self.client.stop()
return PlaybackState.stop
async def on_next(self) -> None:
await self.client.next()
async def on_prev(self) -> None:
await self.client.previous()

View file

@ -0,0 +1,858 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Sodipodi ("http://www.sodipodi.com/") -->
<svg
version="1.0"
x="0"
y="0"
width="128"
height="128"
id="svg1"
sodipodi:version="0.32"
sodipodi:docname="mpd_logo.svg"
inkscape:version="1.3.1 (91b66b0, 2023-11-16)"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-test5.png"
inkscape:export-xdpi="76.799988"
inkscape:export-ydpi="76.799988"
viewBox="0 0 128 128"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="base"
inkscape:zoom="2.6884788"
inkscape:cx="11.344705"
inkscape:cy="145.4354"
inkscape:window-width="1680"
inkscape:window-height="997"
inkscape:window-x="0"
inkscape:window-y="25"
showguides="true"
inkscape:guide-bbox="true"
showgrid="false"
inkscape:window-maximized="0"
inkscape:current-layer="svg1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
showborder="true" />
<defs
id="defs3">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 105.90123 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="160.00001 : 105.90123 : 1"
inkscape:persp3d-origin="80 : 79.234563 : 1"
id="perspective118" />
<linearGradient
id="linearGradient919">
<stop
style="stop-color:#000000;stop-opacity:0.86092716;"
offset="0.0000000"
id="stop920" />
<stop
style="stop-color:#ffffff;stop-opacity:0.0000000;"
offset="1.0000000"
id="stop921" />
</linearGradient>
<linearGradient
id="linearGradient1068">
<stop
offset="0.0000000"
style="stop-color:#d2d2d2;stop-opacity:1.0000000;"
id="stop1070" />
<stop
offset="1.0000000"
style="stop-color:#ffffff;stop-opacity:1.0000000;"
id="stop1069" />
</linearGradient>
<linearGradient
id="linearGradient1065">
<stop
offset="0.0000000"
style="stop-color:#ffffff;stop-opacity:1.0000000;"
id="stop1067" />
<stop
offset="1.0000000"
style="stop-color:#c2bfbf;stop-opacity:0.99607843;"
id="stop1066" />
</linearGradient>
<linearGradient
id="linearGradient1060">
<stop
offset="0.0000000"
style="stop-color:#878787;stop-opacity:1.0000000;"
id="stop1063" />
<stop
offset="1.0000000"
style="stop-color:#000000;stop-opacity:0.99607843;"
id="stop1061" />
</linearGradient>
<linearGradient
id="linearGradient645">
<stop
style="stop-color:#aca597;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop646" />
<stop
style="stop-color:#ffffff;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop647" />
</linearGradient>
<linearGradient
id="linearGradient593">
<stop
style="stop-color:#478acf;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop594" />
<stop
style="stop-color:#65c6f7;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop595" />
</linearGradient>
<linearGradient
id="linearGradient574">
<stop
style="stop-color:#85ad92;stop-opacity:1.0000;"
offset="0"
id="stop575" />
<stop
style="stop-color:#559db2;stop-opacity:0.7725;"
offset="1"
id="stop576" />
</linearGradient>
<linearGradient
id="linearGradient570">
<stop
style="stop-color:#999999;stop-opacity:0.7176;"
offset="0"
id="stop571" />
<stop
style="stop-color:#ffffff;stop-opacity:0.3725;"
offset="1"
id="stop572" />
</linearGradient>
<linearGradient
id="linearGradient573"
xlink:href="#linearGradient1068"
x1="40.458553"
y1="389.65582"
x2="36.063946"
y2="357.28375"
gradientTransform="matrix(2.3025192,0,0,0.29683004,-0.91913426,-1.5117091)"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient1213"
xlink:href="#linearGradient1068"
x1="123.71407"
y1="141.41566"
x2="98.353867"
y2="113.41083"
gradientTransform="matrix(0.91680324,0,0,0.74547827,-0.91913426,-1.5117091)"
gradientUnits="userSpaceOnUse" />
<radialGradient
id="radialGradient581"
xlink:href="#linearGradient919"
cx="0.095785439"
cy="0.16814159"
r="1.5409589"
fx="0.095785439"
fy="0.16814159" />
<linearGradient
id="linearGradient580"
xlink:href="#linearGradient1068"
x1="132.0352"
y1="135.68469"
x2="119.62381"
y2="111.07157"
gradientTransform="matrix(0.90170536,0,0,0.75796032,-0.91913426,-1.5117091)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient1060"
id="linearGradient901"
x1="0.93491787"
y1="0.92044502"
x2="-0.052546836"
y2="0.20347559" />
<linearGradient
xlink:href="#linearGradient593"
id="linearGradient902" />
<linearGradient
xlink:href="#linearGradient1068"
id="linearGradient916"
x1="0.14831461"
y1="-1.6875"
x2="0.43370786"
y2="1.8125" />
<defs
id="defs890">
<linearGradient
id="linearGradient922"
x1="0"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
spreadMethod="pad"
xlink:href="#linearGradient1065" />
<linearGradient
id="linearGradient908"
x1="0"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
spreadMethod="pad"
xlink:href="#linearGradient1060" />
<linearGradient
id="linearGradient894"
x1="0"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
spreadMethod="pad"
xlink:href="#linearGradient1068" />
<linearGradient
xlink:href="#linearGradient894"
id="linearGradient897"
x1="0.5955056"
y1="-0.33587787"
x2="0.61348313"
y2="1.1908396" />
<linearGradient
xlink:href="#linearGradient894"
id="linearGradient898"
x1="0.96449792"
y1="1.0278323"
x2="0.46738392"
y2="0.21800731" />
<linearGradient
xlink:href="#linearGradient908"
id="linearGradient907"
x1="0.57078654"
y1="2.3770492"
x2="0.33258426"
y2="0.49180329" />
<linearGradient
xlink:href="#linearGradient922"
id="linearGradient921"
x1="0.47058824"
y1="0.15384616"
x2="0.46547315"
y2="0.98380566" />
<linearGradient
xlink:href="#linearGradient922"
id="linearGradient948" />
<defs
id="defs987">
<linearGradient
id="linearGradient855"
x1="0"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
spreadMethod="pad"
xlink:href="#linearGradient908" />
<linearGradient
id="linearGradient1188"
x1="0"
y1="0.5"
x2="1"
y2="0.5"
gradientUnits="objectBoundingBox"
spreadMethod="pad"
xlink:href="#linearGradient922" />
<linearGradient
id="linearGradient831">
<stop
style="stop-color:#94897f;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop832" />
<stop
style="stop-color:#fff5fe;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop833" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient1188"
id="linearGradient834"
x1="0.87550199"
y1="0.34817815"
x2="-0.29317269"
y2="0.93522269"
gradientUnits="objectBoundingBox"
spreadMethod="pad" />
<radialGradient
xlink:href="#linearGradient1188"
id="radialGradient835"
r="0.55628061"
fy="0.28125"
fx="0.59090906"
cy="0.28125"
cx="0.59090906"
spreadMethod="pad" />
<linearGradient
xlink:href="#linearGradient1188"
id="linearGradient893"
x1="0.12793733"
y1="0.76923078"
x2="0.49608356"
y2="0.70850199" />
<linearGradient
xlink:href="#linearGradient855"
id="linearGradient625"
x1="0.035955057"
y1="1.0276498"
x2="0.053932585"
y2="-0.359447" />
<linearGradient
xlink:href="#linearGradient1188"
id="linearGradient627"
x1="1.2826855"
y1="0.12550607"
x2="-0.15547703"
y2="0.96356273" />
<radialGradient
xlink:href="#linearGradient1188"
id="radialGradient628"
r="1.5982224"
fy="0.4866707"
fx="0.36789617"
cy="0.4866707"
cx="0.36789617"
gradientTransform="scale(0.877379,1.139758)" />
<linearGradient
xlink:href="#linearGradient1188"
id="linearGradient628"
x1="0.76923078"
y1="0.14979757"
x2="0.41909814"
y2="0.73279351" />
</defs>
<sodipodi:namedview
id="namedview898"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.4732669"
inkscape:cx="50.051177"
inkscape:cy="18.096983"
inkscape:window-width="1022"
inkscape:window-height="670"
showguides="false"
snaptoguides="false"
showgrid="false"
snaptogrid="false"
inkscape:window-x="0"
inkscape:window-y="25">
<sodipodi:guide
orientation="vertical"
position="28.705556"
id="guide879"
inkscape:locked="false" />
<sodipodi:guide
orientation="horizontal"
position="30.130655"
id="guide880"
inkscape:locked="false" />
</sodipodi:namedview>
</defs>
<sodipodi:namedview
id="namedview1003"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.3368738"
inkscape:cx="24.541029"
inkscape:cy="14.368596"
inkscape:window-width="640"
inkscape:window-height="499"
showguides="true"
snaptoguides="true"
inkscape:window-x="138"
inkscape:window-y="169" />
<linearGradient
xlink:href="#linearGradient1060"
id="linearGradient1304"
x1="-0.20218579"
y1="0.21681416"
x2="0.67759562"
y2="0.57522124" />
<linearGradient
xlink:href="#linearGradient1065"
id="linearGradient1322"
x1="0.32404181"
y1="0.77876109"
x2="0.24041812"
y2="0.26548672" />
<defs
id="defs989">
<linearGradient
id="linearGradient850">
<stop
style="stop-color:#eed680;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop852" />
<stop
style="stop-color:#dfb546;stop-opacity:1.0000000;"
offset="0.68035328"
id="stop858" />
<stop
style="stop-color:#d8a429;stop-opacity:1.0000000;"
offset="0.77277374"
id="stop859" />
<stop
style="stop-color:#d1940c;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop857" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient850"
id="linearGradient569"
x1="0.11875"
y1="0.12612613"
x2="0.59375"
y2="0.66066068"
spreadMethod="pad" />
<linearGradient
id="linearGradient839">
<stop
style="stop-color:#46a046;stop-opacity:1.0000000;"
offset="0.0000000"
id="stop840" />
<stop
style="stop-color:#df421e;stop-opacity:1.0000000;"
offset="0.39364964"
id="stop841" />
<stop
style="stop-color:#ada7c8;stop-opacity:1.0000000;"
offset="0.72036445"
id="stop842" />
<stop
style="stop-color:#eed680;stop-opacity:1.0000000;"
offset="1.0000000"
id="stop843" />
</linearGradient>
<linearGradient
xlink:href="#linearGradient839"
id="linearGradient836"
x1="1.3267924e-17"
y1="0.5"
x2="1"
y2="0.5" />
<defs
id="defs604">
<linearGradient
id="linearGradient622">
<stop
style="stop-color:#f8e29d;stop-opacity:0.4471;"
offset="0"
id="stop623" />
<stop
style="stop-color:#272d2d;stop-opacity:0.4784;"
offset="1"
id="stop624" />
</linearGradient>
<linearGradient
id="linearGradient617">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop618" />
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="1"
id="stop619" />
</linearGradient>
<linearGradient
id="linearGradient613">
<stop
style="stop-color:#ffffff;stop-opacity:0.6235;"
offset="0"
id="stop614" />
<stop
style="stop-color:#5d6567;stop-opacity:1;"
offset="1"
id="stop615" />
</linearGradient>
<linearGradient
id="linearGradient607">
<stop
style="stop-color:#d7d5d5;stop-opacity:1;"
offset="0"
id="stop608" />
<stop
style="stop-color:#000000;stop-opacity:0.4039;"
offset="1"
id="stop609" />
</linearGradient>
<radialGradient
xlink:href="#linearGradient607"
id="radialGradient610"
cx="1.4287461"
cy="0.75323397"
r="0.85534656"
fx="1.4287461"
fy="0.75323397"
gradientTransform="matrix(1,2.268336e-6,-1.975559e-5,1,5.713033e-8,3.856326e-8)" />
<linearGradient
xlink:href="#linearGradient617"
id="linearGradient612"
x1="7.7024956"
y1="-2.0263922"
x2="62.759903"
y2="56.137772"
gradientTransform="scale(0.9953779,1.0046436)"
gradientUnits="userSpaceOnUse" />
<radialGradient
xlink:href="#linearGradient613"
id="radialGradient616"
cx="58.70882"
cy="53.831562"
r="43.551846"
fx="58.70882"
fy="53.831562"
gradientTransform="scale(0.99517298,1.0048504)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient617"
id="linearGradient621" />
<linearGradient
xlink:href="#linearGradient617"
id="linearGradient626"
x1="72.060211"
y1="55.161442"
x2="32.409"
y2="12.126946"
gradientTransform="matrix(0.995134,-1.068631e-5,-1.31398e-7,1.00489,0,0)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient607"
id="linearGradient687"
x1="67.707405"
y1="49.314793"
x2="-10.031048"
y2="4.6068792"
gradientTransform="scale(0.99522839,1.0047945)"
gradientUnits="userSpaceOnUse" />
<linearGradient
xlink:href="#linearGradient617"
id="linearGradient742"
x1="-7.4378386"
y1="25.923714"
x2="18.009745"
y2="10.089797"
gradientTransform="scale(0.889853,1.123781)" />
</defs>
<sodipodi:namedview
id="namedview889"
showguides="true"
snaptoguides="true"
inkscape:zoom="7.5625000"
inkscape:cx="24.000000"
inkscape:cy="24.000000"
inkscape:window-width="640"
inkscape:window-height="496"
inkscape:window-x="0"
inkscape:window-y="26" />
</defs>
<sodipodi:namedview
id="namedview1023"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.5521067"
inkscape:cx="66.459318"
inkscape:cy="62.629296"
inkscape:window-width="1150"
inkscape:window-height="752"
showgrid="true"
snaptogrid="true"
inkscape:window-x="0"
inkscape:window-y="29">
<inkscape:grid
id="GridFromPre046Settings"
type="xygrid"
originx="0px"
originy="0px"
spacingx="1.0000000mm"
spacingy="1.0000000mm"
color="#0000ff"
empcolor="#0000ff"
opacity="0.2"
empopacity="0.4"
empspacing="5"
units="px"
visible="true" />
</sodipodi:namedview>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1068"
id="linearGradient2924"
x1="41.673889"
y1="320.40921"
x2="36.082947"
y2="279.22458"
gradientTransform="matrix(2.3376099,0,0,0.29237422,-0.91913426,-1.5117091)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1068"
id="linearGradient2926"
x1="134.95444"
y1="108.16693"
x2="102.05431"
y2="71.835884"
gradientTransform="matrix(0.91680324,0,0,0.74547824,-0.91913426,-1.5117091)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1068"
id="linearGradient2928"
x1="145.32188"
y1="101.97199"
x2="129.22044"
y2="70.041069"
gradientTransform="matrix(0.90170536,0,0,0.75796032,-0.91913426,-1.5117091)"
gradientUnits="userSpaceOnUse" />
</defs>
<rect
style="fill-opacity:0.471545;fill-rule:evenodd;stroke-width:3pt"
id="rect918"
width="48.72493"
height="42.16835"
ry="0.74231374"
x="67.536102"
y="66.474693"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.7811048" />
<rect
style="fill-opacity:0.471545;fill-rule:evenodd;stroke-width:3pt"
id="rect1006"
width="63.211483"
height="54.705563"
ry="0.74231374"
x="64.47226"
y="30.558294"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.7811048" />
<rect
style="fill:#000000;fill-opacity:0.470588;fill-rule:evenodd"
id="rect1005"
width="57.843418"
height="9.0050545"
ry="0.62889248"
x="65.398254"
y="82.153206"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.66175652" />
<rect
style="fill:url(#linearGradient2924);fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.826714"
id="rect1007"
width="54.910637"
height="6.1445785"
ry="0.42912331"
x="64.622299"
y="82.282539"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.44939452" />
<rect
width="57.905403"
height="47.084496"
ry="1.7822117"
x="63.784973"
y="32.456562"
style="font-size:12px;fill:url(#linearGradient2926);fill-rule:evenodd;stroke:#000000;stroke-width:0"
id="rect1009"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="1.6336281" />
<rect
style="fill:#000000;fill-opacity:0.470588;fill-rule:evenodd"
id="rect971"
width="44.58709"
height="6.9413123"
ry="0.62889248"
x="68.249886"
y="106.24529"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.66175652" />
<rect
width="64.637024"
height="54.068516"
ry="1.4120796"
x="59.853096"
y="28.740753"
style="font-size:12px;fill:url(#linearGradient2928);fill-rule:evenodd;stroke:#000000;stroke-width:1.65869"
id="rect1008"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="1.3970968" />
<rect
width="51.129478"
height="39.964478"
ry="0.5422883"
x="66.358932"
y="34.89621"
style="font-size:12px;fill:#00b4ed;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.790543;stroke-linejoin:round"
id="rect976"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.5422883" />
<metadata
id="metadata982">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<rect
width="44.634872"
height="36.293858"
ry="1.7822117"
x="67.006332"
y="67.937927"
style="font-size:12px;fill:url(#linearGradient1213);fill-rule:evenodd;stroke:#000000;stroke-width:0"
id="rect575"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="1.633628" />
<ellipse
style="font-size:12px;fill:#444040;fill-opacity:0.470588;fill-rule:evenodd"
id="path672"
transform="matrix(1.4221482,0,-0.30247168,1.9834766,9.6201687,-10.428817)"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big2.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
cx="44.843102"
cy="57.85183"
rx="23.629898"
ry="3.2222576" />
<ellipse
style="font-size:12px;fill:#4e4d4b;fill-rule:evenodd;stroke:#000000;stroke-width:2.3002;stroke-opacity:0.9565"
id="path625"
transform="matrix(-1.0172416,-0.47376693,-0.5523759,1.3286212,116.84611,57.272851)"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big2.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
cx="38.924049"
cy="27.531645"
rx="19.367088"
ry="19.556963" />
<ellipse
style="font-size:12px;fill:url(#linearGradient612);fill-rule:evenodd;stroke:#000000;stroke-width:2.061;stroke-opacity:0.9565"
id="path605"
transform="matrix(-1.4321234,-0.79696518,-1.1299666,2.2349846,128.07685,29.383033)"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big2.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
cx="38.924049"
cy="27.531645"
rx="19.367088"
ry="19.556963" />
<ellipse
style="font-size:12px;fill:url(#radialGradient616);fill-rule:evenodd;stroke:#000000;stroke-width:0.317;stroke-opacity:0.9783"
id="path606"
transform="matrix(-1.1546358,-0.69851175,-0.95634664,1.9588777,108.06887,31.115628)"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big2.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
cx="38.924049"
cy="27.531645"
rx="19.367088"
ry="19.556963" />
<ellipse
style="font-size:12px;fill-rule:evenodd;stroke-width:1.635"
id="path686"
transform="matrix(-0.39495459,-0.4546194,-0.52881207,0.94219495,73.198184,52.427791)"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big2.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
cx="38.924049"
cy="27.531645"
rx="19.367088"
ry="19.556963" />
<ellipse
style="font-size:12px;fill:url(#linearGradient687);fill-rule:evenodd;stroke:#3f3b3b;stroke-width:0.7738"
id="path611"
transform="matrix(-0.36949013,-0.40957751,-0.49471918,0.84885391,70.248021,52.066881)"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big2.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
cx="38.924049"
cy="27.531645"
rx="19.367088"
ry="19.556963" />
<rect
style="fill:url(#linearGradient573);fill-opacity:0.75;fill-rule:evenodd;stroke:#000000;stroke-width:0.826714"
id="rect934"
width="42.326431"
height="4.7363882"
ry="0.42912331"
x="67.651756"
y="106.34497"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.44939449" />
<rect
width="49.823761"
height="41.677303"
ry="1.4120796"
x="63.975548"
y="65.073692"
style="font-size:12px;fill:url(#linearGradient580);fill-rule:evenodd;stroke:#000000;stroke-width:1.27856;stroke-opacity:1"
id="rect562"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="1.3970969" />
<rect
width="39.411831"
height="30.805574"
ry="0.5422883"
x="68.990387"
y="70.097008"
style="font-size:12px;fill:#003d88;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.614077;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:0.930233"
id="rect975"
inkscape:export-filename="/cowserver/documents/httpd/vhosts/images/mpd-big7.png"
inkscape:export-xdpi="721.66998"
inkscape:export-ydpi="721.66998"
rx="0.5422883" />
</svg>

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -0,0 +1,64 @@
from typing import Protocol, TypedDict
class MpdStateHandler(Protocol):
async def readpicture(self, file: str) -> bytes | None:
...
async def refresh(self) -> None:
...
class StatusResponse(TypedDict):
volume: str
repeat: str
random: str
single: str
consume: str
partition: str
playlist: str
playlistlength: str
mixrampdb: str
state: str
song: str
songid: str
time: str
elapsed: str
bitrate: str
duration: str
audio: str
nextsong: str
nextsongid: str
CurrentSongResponse = TypedDict(
"CurrentSongResponse",
{
"file": str,
"last-modified": str,
"format": str,
"artist": str,
"albumartist": str,
"artistsort": str,
"albumartistsort": str,
"title": str,
"album": str,
"track": str,
"date": str,
"originaldate": str,
"composer": str,
"disc": str,
"label": str,
"musicbrainz_albumid": str,
"musicbrainz_albumartistid": str,
"musicbrainz_releasetrackid": str,
"musicbrainz_artistid": str,
"musicbrainz_trackid": str,
"time": str,
"duration": str,
"pos": str,
"id": str,
},
)
ReadPictureResponse = TypedDict("ReadPictureResponse", {"binary": bytes})

View file

@ -0,0 +1,23 @@
from typing import Protocol
from .song import PlaybackState
class Player(Protocol):
async def on_play_pause(self) -> PlaybackState:
...
async def on_play(self) -> PlaybackState:
...
async def on_pause(self) -> PlaybackState:
...
async def on_stop(self) -> PlaybackState:
...
async def on_next(self) -> None:
...
async def on_prev(self) -> None:
...

View file

View file

@ -0,0 +1,27 @@
from enum import StrEnum
from typing import Protocol
from attrs import define, field
class PlaybackState(StrEnum):
play = "play"
pause = "pause"
stop = "stop"
@define
class Song:
state: PlaybackState
title: str
artist: str
album: str
album_artist: str
duration: float
elapsed: float
art: bytes | None = field(repr=lambda a: "<has art>" if a else "<no art>")
class SongListener(Protocol):
def update(self, song: Song | None) -> None:
...