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()