Initial commit of source - working, but needs stubs for Cocoa
This commit is contained in:
parent
1e7fd270eb
commit
a673f2ef90
12 changed files with 1339 additions and 0 deletions
0
src/mpd_now_playable/__init__.py
Normal file
0
src/mpd_now_playable/__init__.py
Normal file
19
src/mpd_now_playable/async_tools.py
Normal file
19
src/mpd_now_playable/async_tools.py
Normal 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)
|
30
src/mpd_now_playable/cli.py
Normal file
30
src/mpd_now_playable/cli.py
Normal 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()
|
152
src/mpd_now_playable/cocoa.py
Normal file
152
src/mpd_now_playable/cocoa.py
Normal 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
|
0
src/mpd_now_playable/mpd/__init__.py
Normal file
0
src/mpd_now_playable/mpd/__init__.py
Normal file
49
src/mpd_now_playable/mpd/artwork_cache.py
Normal file
49
src/mpd_now_playable/mpd/artwork_cache.py
Normal 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()
|
117
src/mpd_now_playable/mpd/listener.py
Normal file
117
src/mpd_now_playable/mpd/listener.py
Normal 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()
|
858
src/mpd_now_playable/mpd/logo.svg
Normal file
858
src/mpd_now_playable/mpd/logo.svg
Normal 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 |
64
src/mpd_now_playable/mpd/types.py
Normal file
64
src/mpd_now_playable/mpd/types.py
Normal 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})
|
23
src/mpd_now_playable/player.py
Normal file
23
src/mpd_now_playable/player.py
Normal 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:
|
||||||
|
...
|
0
src/mpd_now_playable/py.typed
Normal file
0
src/mpd_now_playable/py.typed
Normal file
27
src/mpd_now_playable/song.py
Normal file
27
src/mpd_now_playable/song.py
Normal 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:
|
||||||
|
...
|
Loading…
Reference in a new issue