Avoid repeated artwork miss fetches and refresh churn

Lowers CPU usage:
- Only call refresh() when real artwork is found
- Treating NoArtwork as a valid cached result (art is not None check).
- Adding in-flight dedupe (pending_tracks) so the same track doesn't spawn parallel fetches.
This commit is contained in:
Götz 2026-02-28 19:14:40 -05:00
parent 9b910cd991
commit fa82f45ef9

View file

@ -30,19 +30,28 @@ class MpdArtworkCache:
mpd: MpdStateHandler mpd: MpdStateHandler
album_cache: Cache[Artwork] album_cache: Cache[Artwork]
track_cache: Cache[Artwork] track_cache: Cache[Artwork]
pending_tracks: set[str]
def __init__(self, mpd: MpdStateHandler, cache_url: URL = MEMORY): def __init__(self, mpd: MpdStateHandler, cache_url: URL = MEMORY):
self.mpd = mpd self.mpd = mpd
self.album_cache = make_cache(ArtworkSchema, cache_url, "album") self.album_cache = make_cache(ArtworkSchema, cache_url, "album")
self.track_cache = make_cache(ArtworkSchema, cache_url, "track") self.track_cache = make_cache(ArtworkSchema, cache_url, "track")
self.pending_tracks = set()
async def get_cached_artwork(self, song: CurrentSongResponse) -> bytes | None: async def get_cached_artwork(self, song: CurrentSongResponse) -> bytes | None:
art = await self.track_cache.get(calc_track_key(song)) track_key = calc_track_key(song)
if art: art = await self.track_cache.get(track_key)
return art.data 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.
if art:
return art.data
return None
# If we don't have track artwork cached, go find some. # If we don't have track artwork cached, go find some.
run_background_task(self.cache_artwork(song)) 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. # 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)) art = await self.album_cache.get(calc_album_key(song))
@ -52,10 +61,16 @@ class MpdArtworkCache:
return None return None
async def cache_artwork(self, song: CurrentSongResponse) -> None: async def cache_artwork(self, song: CurrentSongResponse) -> None:
art = to_artwork(await self.mpd.get_art(song["file"])) track_key = calc_track_key(song)
try: try:
await self.album_cache.add(calc_album_key(song), art, ttl=CACHE_TTL) art = to_artwork(await self.mpd.get_art(song["file"]))
except ValueError: try:
pass await self.album_cache.add(calc_album_key(song), art, ttl=CACHE_TTL)
await self.track_cache.set(calc_track_key(song), art, ttl=CACHE_TTL) except ValueError:
await self.mpd.refresh() pass
await self.track_cache.set(track_key, art, ttl=CACHE_TTL)
# Refresh receivers only when we discovered actual artwork.
if art:
await self.mpd.refresh()
finally:
self.pending_tracks.discard(track_key)