A new row, "Downloaded pictures and songs", shows how much space the cached character pictures, covers and downloaded songs take, with a Clear cache button. Clearing deletes them, but keeps the songs the current quiz uses, the settings, config.ini and the saved quiz. The characters are loaded again at once; covers come back when they are shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.2 KiB
GDScript
150 lines
5.2 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.
|
|
##
|
|
## It also remembers each soundtrack's cover time from the soundtrack lists it is given,
|
|
## so [method fetch_cover_of] finds a cover by the soundtrack alone, e.g. in the quiz.
|
|
|
|
## Emitted when the cover time of a soundtrack is new or changed, e.g. once the soundtrack
|
|
## list has loaded or a cover was changed.
|
|
signal cover_times_changed
|
|
|
|
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
|
|
# The server's cover time of each soundtrack seen: uuid -> int, 0 for none
|
|
var _times: Dictionary = {}
|
|
|
|
|
|
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 every cached cover. They are downloaded again when next asked for.
|
|
func clear() -> void:
|
|
_files.clear()
|
|
|
|
|
|
## 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:
|
|
set_cover_times(covers)
|
|
var kept: Dictionary = {}
|
|
for uuid: String in covers:
|
|
if int(covers[uuid]) > 0:
|
|
kept[uuid] = true
|
|
_files.keep_only(kept)
|
|
|
|
|
|
## Remembers the cover times in [param covers] (UUID -> cover time, 0 for no cover) and
|
|
## emits [signal cover_times_changed] when any is new or changed.
|
|
func set_cover_times(covers: Dictionary) -> void:
|
|
var changed: bool = false
|
|
for uuid: String in covers:
|
|
var modified: int = int(covers[uuid])
|
|
if _times.get(uuid, -1) != modified:
|
|
_times[uuid] = modified
|
|
changed = true
|
|
if changed:
|
|
cover_times_changed.emit()
|
|
|
|
|
|
## The cover time of the soundtrack [param uuid], 0 when it has no cover or none is known.
|
|
func cover_time(uuid: String) -> int:
|
|
return _times.get(uuid, 0)
|
|
|
|
|
|
## Like [method fetch_cover], with the cover time remembered for the soundtrack.
|
|
func fetch_cover_of(uuid: String, on_done: Callable) -> void:
|
|
fetch_cover(uuid, cover_time(uuid), on_done)
|
|
|
|
|
|
## Asks the server for the cover time of the soundtrack [param uuid] again, e.g. after
|
|
## its cover was changed, then calls [param on_done] with the new time, 0 for no cover.
|
|
## A removed cover is deleted from disk too. When the server doesn't answer, it is called
|
|
## with the time it had.
|
|
func refresh(uuid: String, on_done: Callable) -> void:
|
|
var with_soundtrack: Callable = func with_soundtrack(
|
|
soundtrack: SoundtrackWithSongList) -> void:
|
|
if soundtrack.cover_modified <= 0:
|
|
_files.remove(uuid)
|
|
set_cover_times({uuid: soundtrack.cover_modified})
|
|
on_done.call(soundtrack.cover_modified)
|
|
# Without an answer the cover is left as it was
|
|
MusicServerClient.fetch_soundtrack(uuid, with_soundtrack,
|
|
func() -> void: on_done.call(cover_time(uuid)))
|
|
|
|
|
|
## 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()
|