python-mpd2 unreliably returns either a single value or a list of values for commands like currentsong, which is super fun if you're trying to write type stubs for it that statically describe its behaviour. Whee. Anyway, I ended up changing my internal song model to always use lists for tags like artist and genre which are likely to have multiple values. There's some finagling involved in massaging python-mpd2's output into lists every time. However it's much nicer to work with an object that always has a list of artists, even if it's a list of one or zero artists, rather than an object that can have a single artist, a list of multiple artists, or a null. So it's worth it. The MPNowPlayingInfoCenter in MacOS only works with single string values for these tags, not lists, so we have to join the artists and such into a single string for its consumption. I'm using commas for the separator at the moment, but I may make this a config option later on if there's interest.
66 lines
1.8 KiB
Python
66 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TypedDict
|
|
|
|
from yarl import URL
|
|
|
|
from ..cache import Cache, make_cache
|
|
from ..tools.asyncio import run_background_task
|
|
from ..tools.types import un_maybe_plural
|
|
from .types import CurrentSongResponse, MpdStateHandler
|
|
|
|
CACHE_TTL = 60 * 60 # seconds = 1 hour
|
|
|
|
|
|
class ArtCacheEntry(TypedDict):
|
|
data: bytes | None
|
|
|
|
|
|
def calc_album_key(song: CurrentSongResponse) -> str:
|
|
artist = sorted(
|
|
un_maybe_plural(song.get("albumartist", song.get("artist", "Unknown Artist")))
|
|
)
|
|
album = sorted(un_maybe_plural(song.get("album", "Unknown Album")))
|
|
return ":".join(";".join(t).replace(":", "-") for t in (artist, album))
|
|
|
|
|
|
def calc_track_key(song: CurrentSongResponse) -> str:
|
|
return song["file"]
|
|
|
|
|
|
MEMORY = URL("memory://")
|
|
|
|
|
|
class MpdArtworkCache:
|
|
mpd: MpdStateHandler
|
|
album_cache: Cache[ArtCacheEntry]
|
|
track_cache: Cache[ArtCacheEntry]
|
|
|
|
def __init__(self, mpd: MpdStateHandler, cache_url: URL = MEMORY):
|
|
self.mpd = mpd
|
|
self.album_cache = make_cache(cache_url, "album")
|
|
self.track_cache = make_cache(cache_url, "track")
|
|
|
|
async def get_cached_artwork(self, song: CurrentSongResponse) -> bytes | None:
|
|
art = await self.track_cache.get(calc_track_key(song))
|
|
if art:
|
|
return art["data"]
|
|
|
|
# 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["data"]
|
|
|
|
return None
|
|
|
|
async def cache_artwork(self, song: CurrentSongResponse) -> None:
|
|
art = ArtCacheEntry(data=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(calc_track_key(song), art, ttl=CACHE_TTL)
|
|
await self.mpd.refresh()
|