Files
MusicPlayer/common/file_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

118 lines
3.6 KiB
GDScript

class_name FileCache
extends RefCounted
## Downloaded files kept in a folder next to the settings, with an [code]index.json[/code]
## that records for each file when the server last changed it and when it was
## downloaded:
## [codeblock]
## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}}
## [/codeblock]
## Used by [CharacterCache] and [CoverCache], which decide when a file is fresh.
const INDEX_FILE_NAME: String = "index.json"
## The change time of a file when the server can't tell it.
const UNKNOWN_TIME: int = -1
## The folder's name in [method Settings.config_dir].
var folder_name: String
# What index.json holds: key -> {"modified": int, "downloaded": int}
var _index: Dictionary = {}
func _init(new_folder_name: String) -> void:
folder_name = new_folder_name
## The folder the files are kept in.
func folder() -> String:
return Settings.config_dir().path_join(folder_name)
## Reads [code]index.json[/code]. A missing or broken index starts empty.
func load_index() -> void:
_index = {}
var path: String = folder().path_join(INDEX_FILE_NAME)
if FileAccess.file_exists(path):
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
if parsed is Dictionary:
_index = parsed
## The keys of the files in the index.
func keys() -> Array:
return _index.keys()
## Whether the file for [param key] is in the index and on disk.
func has_file(key: String) -> bool:
return _index.has(key) && FileAccess.file_exists(file_path(key))
## When the server last changed the file for [param key], as recorded when it was
## downloaded, or [constant UNKNOWN_TIME].
func modified(key: String) -> int:
return int(_index.get(key, {}).get("modified", UNKNOWN_TIME))
## How many seconds ago the file for [param key] was downloaded.
func age(key: String) -> float:
return Time.get_unix_time_from_system() - float(_index.get(key, {}).get("downloaded", 0))
## The file for [param key].
func read(key: String) -> PackedByteArray:
return FileAccess.get_file_as_bytes(file_path(key))
## Writes [param bytes] as the file for [param key], last changed on the server at
## [param modified_time], and saves the index. Returns false if it could not be written.
func store(key: String, bytes: PackedByteArray, modified_time: int) -> bool:
DirAccess.make_dir_recursive_absolute(folder())
var file: FileAccess = FileAccess.open(file_path(key), FileAccess.WRITE)
if file == null:
GameLog.warning("Could not cache ", key, " in ", folder_name, ": ",
error_string(FileAccess.get_open_error()))
return false
file.store_buffer(bytes)
file.close()
_index[key] = {
"modified": modified_time,
"downloaded": int(Time.get_unix_time_from_system()),
}
save_index()
return true
## Deletes the file for [param key] and its entry, and saves the index.
func remove(key: String) -> void:
DirAccess.remove_absolute(file_path(key))
if _index.erase(key):
save_index()
## Deletes every file whose key is not in [param kept], e.g. files of things the server
## no longer has.
func keep_only(kept: Dictionary) -> void:
var removed: bool = false
for key: String in _index.keys():
if !kept.has(key):
DirAccess.remove_absolute(file_path(key))
_index.erase(key)
removed = true
if removed:
save_index()
func save_index() -> void:
DirAccess.make_dir_recursive_absolute(folder())
var file: FileAccess = FileAccess.open(folder().path_join(INDEX_FILE_NAME),
FileAccess.WRITE)
if file != null:
file.store_string(JSON.stringify(_index, "\t", true))
## Where the file for [param key] is. Keys come from the server, so they are made safe
## as file names.
func file_path(key: String) -> String:
return folder().path_join(key.validate_filename())