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