Everything now uses bog-standard Python dataclasses, with Pydantic providing validation and type conversion through separate classes using its type adapter feature. It's also possible to define your classes using Pydantic's own model type directly, making the type adapter unnecessary, but I didn't want to do things that way because no actual validation is needed when constructing a Song instance for example. Having Pydantic do its thing only on-demand was preferable. I tried a number of validation libraries before settling on Pydantic for this. It's not the fastest option out there (msgspec is I think), but it makes adding support for third-party types like yarl.URL really easy, it generates a nice clean JSON Schema which is easy enough to adjust to my requirements through its GenerateJsonSchema hooks, and raw speed isn't all that important anyway since this is a single-user desktop program that reads its configuration file once on startup. Also, MessagePack is now mandatory if you're caching to an external service. It just didn't make a whole lot sense to explicitly install mpd-now-playable's Redis or Memcached support and then use pickling with them. With all this fussing around done, I'm probably finally ready to actually use that configuration file to configure new features! Yay!
61 lines
1.8 KiB
Python
61 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
from yarl import URL
|
|
|
|
from ..cache import Cache, make_cache
|
|
from ..song import Artwork, ArtworkSchema, to_artwork
|
|
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
|
|
|
|
|
|
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[Artwork]
|
|
track_cache: Cache[Artwork]
|
|
|
|
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")
|
|
|
|
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 = 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(calc_track_key(song), art, ttl=CACHE_TTL)
|
|
await self.mpd.refresh()
|