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>
96 lines
3.1 KiB
GDScript
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()
|