Compare commits

..

No commits in common. "49a75f811874df852786c430d989b73a987f7d3c" and "28748df3c16bd7361be6b334840191cbeebbb8ff" have entirely different histories.

6 changed files with 22 additions and 79 deletions

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copied from https://github.com/alberthier/corefoundationasyncio/blob/5061b9b7daa8bcd40d54d58432d84dcc0a339ca6/corefoundationasyncio.py
# This module is copied, rather than simply installed, because the PyPI version of this module depends on *all* of PyObjC even though it only needs pyobjc-framework-Cocoa.
# There's an open PR to fix this: https://github.com/alberthier/corefoundationasyncio/pull/3
# There's an open PR to fix this: httpsV//github.com/alberthier/corefoundationasyncio/pull/3
import asyncio
@ -10,8 +10,8 @@ import threading
from CoreFoundation import (
CFRunLoopGetCurrent,
CFRunLoopTimerCreate, CFRunLoopAddTimer, CFRunLoopRemoveTimer, CFRunLoopTimerInvalidate, CFAbsoluteTimeGetCurrent,
CFFileDescriptorCreate, CFFileDescriptorIsValid, CFFileDescriptorEnableCallBacks, CFFileDescriptorDisableCallBacks, CFFileDescriptorInvalidate,
CFRunLoopTimerCreate, CFRunLoopAddTimer, CFRunLoopRemoveTimer, CFAbsoluteTimeGetCurrent,
CFFileDescriptorCreate, CFFileDescriptorIsValid, CFFileDescriptorEnableCallBacks, CFFileDescriptorDisableCallBacks,
CFFileDescriptorCreateRunLoopSource, CFRunLoopAddSource, CFRunLoopRemoveSource,
kCFAllocatorDefault, kCFRunLoopDefaultMode, kCFRunLoopCommonModes,
kCFFileDescriptorReadCallBack, kCFFileDescriptorWriteCallBack
@ -106,15 +106,8 @@ class CoreFoundationEventLoop(asyncio.SelectorEventLoop):
if handle.cancelled():
return
def ontimeout(cf_timer, info):
try:
if not handle.cancelled():
handle._run()
finally:
# Explicitly invalidate/remove one-shot timers. Relying on
# implicit cleanup can leak CoreFoundation timer objects under
# long-running loads.
CFRunLoopRemoveTimer(self._runloop, cf_timer, kCFRunLoopCommonModes)
CFRunLoopTimerInvalidate(cf_timer)
when = handle.when() if is_timer else self.time()
cf_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, when, 0, 0, 0, ontimeout, None)
CFRunLoopAddTimer(self._runloop, cf_timer, kCFRunLoopCommonModes)
@ -123,10 +116,7 @@ class CoreFoundationEventLoop(asyncio.SelectorEventLoop):
handle._scheduled = True
def _timer_handle_cancelled(self, handle):
if handle.cf_runloop_timer is not None:
CFRunLoopRemoveTimer(self._runloop, handle.cf_runloop_timer, kCFRunLoopCommonModes)
CFRunLoopTimerInvalidate(handle.cf_runloop_timer)
handle.cf_runloop_timer = None
def time(self):
return CFAbsoluteTimeGetCurrent()
@ -171,7 +161,6 @@ class CoreFoundationEventLoop(asyncio.SelectorEventLoop):
CFFileDescriptorDisableCallBacks(entry.cf_fd, event)
else:
CFRunLoopRemoveSource(self._runloop, entry.cf_source, kCFRunLoopDefaultMode)
CFFileDescriptorInvalidate(entry.cf_fd)
return True
def _add_reader(self, fd, callback, *args):

View file

@ -1,5 +1,4 @@
import asyncio
import sys
from collections.abc import Iterable
from rich import print
@ -23,23 +22,7 @@ async def listen(
await listener.loop(receivers)
def print_help() -> None:
print("Usage: mpd-now-playable [OPTIONS]")
print("")
print("Options:")
print(" -h, --help Show this help message and exit.")
print(" -v, --version Show version and exit.")
def main() -> None:
args = set(sys.argv[1:])
if "-h" in args or "--help" in args:
print_help()
return
if "-v" in args or "--version" in args:
print(f"mpd-now-playable v{__version__}")
return
print(f"mpd-now-playable v{__version__}")
config = loadConfig()
print(config)
@ -51,7 +34,7 @@ def main() -> None:
asyncio.run(
listen(config, listener, receivers),
loop_factory=factory.make_loop,
debug=False,
debug=True,
)

View file

@ -30,27 +30,18 @@ class MpdArtworkCache:
mpd: MpdStateHandler
album_cache: Cache[Artwork]
track_cache: Cache[Artwork]
pending_tracks: set[str]
def __init__(self, mpd: MpdStateHandler, cache_url: URL = MEMORY):
self.mpd = mpd
self.album_cache = make_cache(ArtworkSchema, cache_url, "album")
self.track_cache = make_cache(ArtworkSchema, cache_url, "track")
self.pending_tracks = set()
async def get_cached_artwork(self, song: CurrentSongResponse) -> bytes | None:
track_key = calc_track_key(song)
art = await self.track_cache.get(track_key)
if art is not None:
# NoArtwork is a valid cached value too: returning None here avoids
# repeatedly re-querying MPD for files that have no embedded art.
art = await self.track_cache.get(calc_track_key(song))
if art:
return art.data
return None
# If we don't have track artwork cached, go find some.
if track_key not in self.pending_tracks:
self.pending_tracks.add(track_key)
run_background_task(self.cache_artwork(song))
# Even if we don't have cached track art, we can try looking for cached album art.
@ -61,16 +52,10 @@ class MpdArtworkCache:
return None
async def cache_artwork(self, song: CurrentSongResponse) -> None:
track_key = calc_track_key(song)
try:
art = to_artwork(await self.mpd.get_art(song["file"]))
try:
await self.album_cache.add(calc_album_key(song), art, ttl=CACHE_TTL)
except ValueError:
pass
await self.track_cache.set(track_key, art, ttl=CACHE_TTL)
# Refresh receivers only when we discovered actual artwork.
if art:
await self.track_cache.set(calc_track_key(song), art, ttl=CACHE_TTL)
await self.mpd.refresh()
finally:
self.pending_tracks.discard(track_key)

View file

@ -17,13 +17,10 @@ def to_queue(mpd: MpdState) -> Queue:
def to_mixramp(mpd: MpdState) -> MixRamp:
delay = mpd.status.get("mixrampdelay", 0)
db = mpd.status.get("mixrampdb", 0)
if delay == "nan":
delay = 0
if db == "nan":
db = 0
return MixRamp(
db=float(db),
db=float(mpd.status.get("mixrampdb", 0)),
delay=float(delay),
)

View file

@ -3,6 +3,7 @@ from collections.abc import Iterable
from mpd.asyncio import MPDClient
from mpd.base import CommandError
from rich import print as rprint
from yarl import URL
from ..config.model import MpdConfig
@ -17,23 +18,17 @@ from .types import MpdState
class MpdStateListener(Player):
# Subsystems relevant to now-playing metadata and remote controls.
# Listening to all MPD subsystems can cause noisy wakeups (e.g. database
# updates), which drives unnecessary status/currentsong polling.
WATCHED_SUBSYSTEMS = ("player", "mixer", "options", "playlist", "partition")
config: MpdConfig
client: MPDClient
receivers: Iterable[Receiver]
art_cache: MpdArtworkCache
idle_count = 0
last_playback: Playback | None
def __init__(self, cache: URL | None = None) -> None:
self.client = MPDClient()
self.art_cache = (
MpdArtworkCache(self, cache) if cache else MpdArtworkCache(self)
)
self.last_playback = None
async def start(self, conf: MpdConfig) -> None:
self.config = conf
@ -58,12 +53,9 @@ class MpdStateListener(Player):
# Notify our receivers of the initial state MPD is in when this script loads up.
await self.update_receivers()
# And then wait for stuff to change in MPD. :)
async for subsystems in self.client.idle(self.WATCHED_SUBSYSTEMS):
async for subsystems in self.client.idle():
# If no subsystems actually changed, we don't need to update the receivers.
if not subsystems:
# MPD/python-mpd2 can occasionally wake idle() without reporting a
# changed subsystem; avoid a hot loop if that happens repeatedly.
await asyncio.sleep(0.1)
continue
self.idle_count += 1
await self.update_receivers()
@ -87,9 +79,7 @@ class MpdStateListener(Player):
state = MpdState(status, current, art)
pb = to_playback(self.config, state)
if pb == self.last_playback:
return
self.last_playback = pb
rprint(pb)
await self.update(pb)
async def update(self, playback: Playback) -> None:

View file

@ -19,7 +19,6 @@ def playback_to_media_item(playback: Playback) -> NSMutableDictionary:
if song := playback.active_song:
nowplaying_info = song_to_media_item(song)
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueCount] = playback.queue.length
if playback.queue.current is not None:
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueIndex] = playback.queue.current
return nowplaying_info