Files
MusicPlayer/common/cover_cache.gd
T
SansanandClaude Opus 5.5 ef6ac8f1fd
Build / test (push) Successful in 1m43s
Build / build (push) Successful in 1m14s
Show soundtrack covers and edit alternative names (#49)
Covers: statistics cards show the soundtrack's cover on the left, and the
statistics popup shows it large. Until it arrives, and without a cover, the
soundtrack's initials stand in. Covers load only for cards on or near the
screen, three at a time, and are cached in covers/ next to the settings
(CoverCache), downloaded again only when cover_modified changes. The index and
file handling are shared with CharacterCache in a new FileCache.

Alternative names: the popup shows them and has an editor (add, remove, save)
that sends PUT /soundtracks/{uuid}/alternative-names. The statistics search and
the Search popup find soundtracks by them; Search shows the matching name after
the real one. Search now loads /soundtracks, falling back to the plain list.

The API types are regenerated from the server's spec, with a SoundtrackCover
theme style in every theme.

Closes #49

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:06:30 +02:00

96 lines
3.1 KiB
GDScript

class_name CoverCache
extends RefCounted
## Soundtrack covers kept on disk in the [code]covers[/code] folder next to the settings,
## by soundtrack UUID, so each one is downloaded only when it changed on the server (its
## [code]cover_modified[/code] time). See [FileCache].
##
## Covers are downloaded a few at a time, in the order they are asked for, so opening the
## statistics doesn't send a request for every soundtrack at once.
const FOLDER_NAME: String = "covers"
## How many covers are downloaded at the same time.
const MAX_DOWNLOADS: int = 3
# The covers on disk and their index
var _files: FileCache = FileCache.new(FOLDER_NAME)
# Covers waiting to be downloaded: [uuid, modified]
var _waiting: Array[Array] = []
# Who is waiting for each cover that is asked for or downloading: uuid -> Array[Callable]
var _callbacks: Dictionary = {}
var _downloading: int = 0
func _init() -> void:
_files.load_index()
## The folder the covers are kept in.
static func folder() -> String:
return Settings.config_dir().path_join(FOLDER_NAME)
## Deletes the covers of soundtracks that are not in [param covers] (UUID -> cover
## time), and those whose soundtrack no longer has a cover.
func keep_only(covers: Dictionary) -> void:
var kept: Dictionary = {}
for uuid: String in covers:
if int(covers[uuid]) > 0:
kept[uuid] = true
_files.keep_only(kept)
## Calls [param on_done] with the cover of the soundtrack [param uuid] as an [Image]:
## from disk when the cached one has the server's time [param modified], otherwise once
## it has been downloaded. Nothing is called when the soundtrack has no cover
## ([param modified] 0) or the download fails.
func fetch_cover(uuid: String, modified: int, on_done: Callable) -> void:
if modified <= 0 || uuid.is_empty():
return
if _files.has_file(uuid) && _files.modified(uuid) == modified:
var cached: Image = CharacterCard.image_from_bytes(_files.read(uuid))
if cached != null:
on_done.call(cached)
return
if _callbacks.has(uuid):
_callbacks[uuid].append(on_done)
return
_callbacks[uuid] = [on_done]
_waiting.append([uuid, modified])
_download_next()
## How many covers are waiting for their turn or downloading.
func busy() -> int:
return _callbacks.size()
func _download_next() -> void:
while _downloading < MAX_DOWNLOADS && !_waiting.is_empty():
var next: Array = _waiting.pop_front()
var uuid: String = next[0]
_downloading += 1
MusicServerClient.fetch_soundtrack_cover(uuid,
_on_downloaded.bind(uuid, next[1]), _on_failed.bind(uuid))
func _on_downloaded(bytes: PackedByteArray, uuid: String, modified: int) -> void:
_downloading -= 1
var cover: Image = CharacterCard.image_from_bytes(bytes)
if cover == null:
GameLog.warning("The cover of soundtrack ", uuid, " could not be read")
else:
_files.store(uuid, bytes, modified)
for on_done: Callable in _callbacks.get(uuid, []):
# A card may be gone by the time its cover arrives
if cover != null && on_done.is_valid():
on_done.call(cover)
_callbacks.erase(uuid)
_download_next()
# A 404 means the server has no cover for it yet
func _on_failed(uuid: String) -> void:
_downloading -= 1
_callbacks.erase(uuid)
_download_next()