Cache character pictures and songs on disk
Character pictures are kept in characters/ next to the settings, with an index.json of when the server last changed each one and when it was downloaded. Only new or changed pictures are downloaded, using the server's new /character/details. Older servers fall back to the name list, and pictures are downloaded again after 7 days. Downloaded songs are written to songs/ and only their paths are kept in memory. The folder is emptied when the game starts and when the playlist resets.
This commit is contained in:
1 parent
62fa63fd8b
commit
240c331ab2
17 files changed
+912
-48
No files matched your search
@@ -0,0 +1,138 @@
|
||||
class_name CharacterCache
|
||||
extends RefCounted
|
||||
## Character pictures kept on disk in the [code]characters[/code] folder next to the
|
||||
## settings, so each one is downloaded only when it changed on the server.
|
||||
##
|
||||
## [code]index.json[/code] in the folder records, for each picture, when the server last
|
||||
## changed it and when it was downloaded:
|
||||
## [codeblock]
|
||||
## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}}
|
||||
## [/codeblock]
|
||||
## A cached picture is used while the server's change time matches the index. Servers that
|
||||
## can't tell when pictures changed answer 404 for [code]/character/details[/code]; then a
|
||||
## cached picture is used until it is [constant MAX_AGE_SECONDS] old.
|
||||
|
||||
const FOLDER_NAME: String = "characters"
|
||||
const INDEX_FILE_NAME: String = "index.json"
|
||||
## How long a picture is used before downloading it again, when the server can't tell
|
||||
## whether it changed.
|
||||
const MAX_AGE_SECONDS: int = 7 * 24 * 60 * 60
|
||||
## The change time of a picture when the server can't tell it.
|
||||
const UNKNOWN_TIME: int = -1
|
||||
|
||||
# What index.json holds: name -> {"modified": int, "downloaded": int}
|
||||
var _index: Dictionary = {}
|
||||
# When the server last changed each picture on the list, or UNKNOWN_TIME
|
||||
var _modified: Dictionary[String, int] = {}
|
||||
|
||||
|
||||
## The folder the pictures are kept in.
|
||||
static func folder() -> String:
|
||||
return Settings.config_dir().path_join(FOLDER_NAME)
|
||||
|
||||
|
||||
## Fetches the names of the characters and calls [param on_done] with them as an
|
||||
## [code]Array[String][/code]. Cached pictures of characters no longer on the server are
|
||||
## deleted.
|
||||
func load_list(on_done: Callable) -> void:
|
||||
_load_index()
|
||||
var details_fetched: Callable = func details_fetched(details: Array) -> void:
|
||||
_modified.clear()
|
||||
for character: CharacterDetails in details:
|
||||
_modified[character.name] = character.modified
|
||||
_list_loaded(on_done)
|
||||
MusicServerClient.fetch_character_details(details_fetched, _load_names_only.bind(on_done))
|
||||
|
||||
|
||||
## Calls [param on_done] with the picture of [param character_name] as an [Image], from
|
||||
## disk when the cached one is still fresh, otherwise from the server.
|
||||
func fetch_image(character_name: String, on_done: Callable) -> void:
|
||||
var path: String = _file_path(character_name)
|
||||
if _is_fresh(character_name) && FileAccess.file_exists(path):
|
||||
var cached: Image = CharacterCard.image_from_bytes(FileAccess.get_file_as_bytes(path))
|
||||
if cached != null:
|
||||
on_done.call(cached)
|
||||
return
|
||||
GameLog.warning("The cached picture of ", character_name, " is broken")
|
||||
var image_fetched: Callable = func image_fetched(bytes: PackedByteArray) -> void:
|
||||
var picture: Image = CharacterCard.image_from_bytes(bytes)
|
||||
if picture == null:
|
||||
GameLog.warning("Could not load the picture of ", character_name)
|
||||
return
|
||||
_store(character_name, bytes)
|
||||
on_done.call(picture)
|
||||
MusicServerClient.fetch_character_image(character_name, image_fetched)
|
||||
|
||||
|
||||
# Servers older than /character/details only list the names
|
||||
func _load_names_only(on_done: Callable) -> void:
|
||||
var list_fetched: Callable = func list_fetched(list: Variant) -> void:
|
||||
_modified.clear()
|
||||
if list is Array:
|
||||
for character_name: Variant in list:
|
||||
_modified[str(character_name)] = UNKNOWN_TIME
|
||||
_list_loaded(on_done)
|
||||
MusicServerClient.fetch_character_list(list_fetched)
|
||||
|
||||
|
||||
func _list_loaded(on_done: Callable) -> void:
|
||||
var removed: bool = false
|
||||
for character_name: String in _index.keys():
|
||||
if !_modified.has(character_name):
|
||||
DirAccess.remove_absolute(_file_path(character_name))
|
||||
_index.erase(character_name)
|
||||
removed = true
|
||||
if removed:
|
||||
_save_index()
|
||||
var names: Array[String] = []
|
||||
names.assign(_modified.keys())
|
||||
on_done.call(names)
|
||||
|
||||
|
||||
func _is_fresh(character_name: String) -> bool:
|
||||
if !_index.has(character_name):
|
||||
return false
|
||||
var entry: Dictionary = _index[character_name]
|
||||
var modified: int = _modified.get(character_name, UNKNOWN_TIME)
|
||||
if modified != UNKNOWN_TIME:
|
||||
return int(entry.get("modified", UNKNOWN_TIME)) == modified
|
||||
var age: float = Time.get_unix_time_from_system() - float(entry.get("downloaded", 0))
|
||||
return age < MAX_AGE_SECONDS
|
||||
|
||||
|
||||
func _store(character_name: String, bytes: PackedByteArray) -> void:
|
||||
DirAccess.make_dir_recursive_absolute(folder())
|
||||
var file: FileAccess = FileAccess.open(_file_path(character_name), FileAccess.WRITE)
|
||||
if file == null:
|
||||
GameLog.warning("Could not cache the picture of ", character_name, ": ",
|
||||
error_string(FileAccess.get_open_error()))
|
||||
return
|
||||
file.store_buffer(bytes)
|
||||
file.close()
|
||||
_index[character_name] = {
|
||||
"modified": _modified.get(character_name, UNKNOWN_TIME),
|
||||
"downloaded": int(Time.get_unix_time_from_system()),
|
||||
}
|
||||
_save_index()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
func _save_index() -> void:
|
||||
var file: FileAccess = FileAccess.open(folder().path_join(INDEX_FILE_NAME),
|
||||
FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_string(JSON.stringify(_index, "\t", true))
|
||||
|
||||
|
||||
# The server's names are plain file names, but they are made safe anyway since they
|
||||
# become paths on this computer
|
||||
func _file_path(character_name: String) -> String:
|
||||
return folder().path_join(character_name.validate_filename())
|
||||
@@ -0,0 +1 @@
|
||||
uid://34xwe7vs2md7
|
||||
+58
-3
@@ -3,6 +3,12 @@ extends Node
|
||||
## played, answered and playing.
|
||||
##
|
||||
## Registered as the [code]Playlist[/code] autoload.
|
||||
##
|
||||
## Downloaded songs are kept on disk in the [code]songs[/code] folder next to the
|
||||
## settings, not in memory. The folder only holds the current playlist: it is emptied when
|
||||
## the game starts and when the playlist is reset.
|
||||
|
||||
const SONGS_FOLDER_NAME: String = "songs"
|
||||
|
||||
var song_object_array: Array[SongObject] = []
|
||||
var latest_played_song: int = 0
|
||||
@@ -15,6 +21,18 @@ var is_song_downloading: bool = false
|
||||
## ended with Reset points.
|
||||
var game_start_song: int = 0
|
||||
|
||||
# How many song files have been written, so each gets a name of its own
|
||||
var _song_files_written: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
clear_song_files()
|
||||
|
||||
|
||||
## The folder downloaded songs are kept in.
|
||||
static func songs_folder() -> String:
|
||||
return Settings.config_dir().path_join(SONGS_FOLDER_NAME)
|
||||
|
||||
|
||||
func get_next_song() -> SongObject:
|
||||
return song_object_array[latest_played_song]
|
||||
@@ -73,6 +91,7 @@ func reset_playlist() -> void:
|
||||
latest_played_song = 0
|
||||
game_start_song = 0
|
||||
clear_song_list()
|
||||
clear_song_files()
|
||||
|
||||
|
||||
func clear_song_list() -> void:
|
||||
@@ -116,12 +135,48 @@ func remove_player_from_list(player_id: String) -> void:
|
||||
song_object.remove_point(player_id)
|
||||
|
||||
|
||||
## The song of [param song_no], read from its file. Returns null if the file is gone, so
|
||||
## the song can be downloaded again.
|
||||
func get_song(song_no: int) -> AudioStream:
|
||||
return song_object_array[song_no].song
|
||||
var song_object: SongObject = song_object_array[song_no]
|
||||
if song_object.song_path.is_empty():
|
||||
return song_object.song
|
||||
if !FileAccess.file_exists(song_object.song_path):
|
||||
return null
|
||||
return AudioStreamMP3.load_from_file(song_object.song_path)
|
||||
|
||||
|
||||
func add_song_to_song_object(song_no: int, sound: AudioStream) -> void:
|
||||
song_object_array[song_no].song = sound
|
||||
func add_song_to_song_object(song_no: int, mp3: PackedByteArray) -> void:
|
||||
store_song(song_object_array[song_no], mp3)
|
||||
|
||||
|
||||
## Keeps a downloaded song in a file in [method songs_folder]. If the file can't be
|
||||
## written, the song is kept in memory instead.
|
||||
func store_song(song_object: SongObject, mp3: PackedByteArray) -> void:
|
||||
DirAccess.make_dir_recursive_absolute(songs_folder())
|
||||
var path: String = songs_folder().path_join("song_%d.mp3" % _song_files_written)
|
||||
_song_files_written += 1
|
||||
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
GameLog.warning("Could not save a song to ", path, ", keeping it in memory: ",
|
||||
error_string(FileAccess.get_open_error()))
|
||||
var sound: AudioStreamMP3 = AudioStreamMP3.new()
|
||||
sound.data = mp3
|
||||
song_object.song = sound
|
||||
song_object.song_path = ""
|
||||
return
|
||||
file.store_buffer(mp3)
|
||||
file.close()
|
||||
song_object.song = null
|
||||
song_object.song_path = path
|
||||
|
||||
|
||||
## Deletes the files of the songs downloaded so far.
|
||||
func clear_song_files() -> void:
|
||||
if !DirAccess.dir_exists_absolute(songs_folder()):
|
||||
return
|
||||
for file_name: String in DirAccess.get_files_at(songs_folder()):
|
||||
DirAccess.remove_absolute(songs_folder().path_join(file_name))
|
||||
|
||||
|
||||
func song_has_played(song_no: int = latest_played_song) -> void:
|
||||
|
||||
@@ -12,6 +12,10 @@ var players_given_point: Array[String] = []
|
||||
var is_answered: bool = false
|
||||
var has_played: bool = false
|
||||
var is_playing: bool = false
|
||||
## The song's file in [method Playlist.songs_folder]. Read the song with
|
||||
## [method Playlist.get_song].
|
||||
var song_path: String = ""
|
||||
## The song itself, only when it could not be saved to a file.
|
||||
var song: AudioStream
|
||||
|
||||
|
||||
|
||||
@@ -16,17 +16,15 @@ func _ready() -> void:
|
||||
mouse_exited.connect(_set_hovered.bind(false))
|
||||
|
||||
|
||||
## Shows [param character_name] and fetches its picture from the server.
|
||||
func set_character_name(character_name: String) -> void:
|
||||
## Shows [param character_name] and its picture. [param fetch_image] is called with the
|
||||
## name and a Callable that takes the picture as an [Image], e.g.
|
||||
## [method CharacterCache.fetch_image].
|
||||
func set_character_name(character_name: String, fetch_image: Callable) -> void:
|
||||
character_name_label.text = display_name(character_name)
|
||||
tooltip_text = character_name_label.text
|
||||
var image_fetched: Callable = func image_fetched(image: PackedByteArray) -> void:
|
||||
var picture: Image = image_from_bytes(image)
|
||||
if picture == null:
|
||||
GameLog.warning("Could not load the picture of ", character_name)
|
||||
return
|
||||
var image_fetched: Callable = func image_fetched(picture: Image) -> void:
|
||||
character_image.texture = ImageTexture.create_from_image(picture)
|
||||
MusicServerClient.fetch_character_image(character_name, image_fetched)
|
||||
fetch_image.call(character_name, image_fetched)
|
||||
|
||||
|
||||
## The name to show for a picture's [param file_name]: without the file extension, and
|
||||
|
||||
@@ -6,6 +6,8 @@ signal character_selected(texture: ImageTexture)
|
||||
|
||||
var can_select_character: bool = false
|
||||
var character: PackedScene = preload("res://popups/CharacterCard.tscn")
|
||||
## Keeps the pictures on disk, so only changed ones are downloaded.
|
||||
var character_cache: CharacterCache = CharacterCache.new()
|
||||
|
||||
@onready var character_panel: Panel = %CharacterPanel
|
||||
@onready var title_label: Label = %TitleLabel
|
||||
@@ -25,17 +27,16 @@ func _on_close_timeout() -> void:
|
||||
|
||||
func load_characters() -> void:
|
||||
GameLog.debug("load_characters")
|
||||
var fetch_character_list: Callable = func fetch_character_list(list: Variant) -> void:
|
||||
GameLog.debug("fetch_character_list")
|
||||
var list_loaded: Callable = func list_loaded(names: Array[String]) -> void:
|
||||
GameLog.debug("list_loaded")
|
||||
Settings.delete_children(character_grid)
|
||||
if typeof(list) == TYPE_ARRAY:
|
||||
for character_name: String in list:
|
||||
GameLog.debug("character_name ", character_name)
|
||||
var character_box: CharacterCard = character.instantiate()
|
||||
character_grid.add_child(character_box)
|
||||
character_box.set_character_name(character_name)
|
||||
character_box.new_character_selected.connect(_on_character_selected)
|
||||
MusicServerClient.fetch_character_list(fetch_character_list)
|
||||
for character_name: String in names:
|
||||
GameLog.debug("character_name ", character_name)
|
||||
var character_box: CharacterCard = character.instantiate()
|
||||
character_grid.add_child(character_box)
|
||||
character_box.set_character_name(character_name, character_cache.fetch_image)
|
||||
character_box.new_character_selected.connect(_on_character_selected)
|
||||
character_cache.load_list(list_loaded)
|
||||
|
||||
|
||||
func show_grid(player_name: String) -> void:
|
||||
|
||||
+1
-3
@@ -161,9 +161,7 @@ func song_object_clicked(song_no: int) -> void:
|
||||
if Playlist.get_song(song_no) == null:
|
||||
GameLog.debug("has no song, need to download")
|
||||
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
|
||||
var sound: AudioStream = AudioStreamMP3.new()
|
||||
sound.data = body
|
||||
Playlist.add_song_to_song_object(song_no, sound)
|
||||
Playlist.add_song_to_song_object(song_no, body)
|
||||
music_player.play_song_object(song_no)
|
||||
MusicServerClient.fetch_song(song_no, song_downloaded)
|
||||
else:
|
||||
|
||||
+1
-3
@@ -65,9 +65,7 @@ func fill_cache() -> void:
|
||||
func _download_random_song(new_song_object: SongObject) -> void:
|
||||
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
|
||||
GameLog.debug("song_downloaded")
|
||||
var sound: AudioStream = AudioStreamMP3.new()
|
||||
sound.data = body
|
||||
new_song_object.song = sound
|
||||
Playlist.store_song(new_song_object, body)
|
||||
_add_song_to_queue(new_song_object)
|
||||
MusicServerClient.fetch_random_song(Settings.use_low_played_mode, song_downloaded,
|
||||
_retry_later.bind(_download_random_song.bind(new_song_object)))
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by tools/generate_api_types.gd from server/openapi.json.
|
||||
# Do not edit; change the server and generate again.
|
||||
class_name CharacterDetails
|
||||
extends RefCounted
|
||||
## The [code]CharacterDetails[/code] schema of the MusicServer API.
|
||||
|
||||
## When the image was last changed, in Unix seconds
|
||||
var modified: int
|
||||
## File name of the character image
|
||||
var name: String
|
||||
|
||||
|
||||
## Reads the fields from parsed JSON. Missing fields keep their defaults.
|
||||
static func from_dict(data: Dictionary) -> CharacterDetails:
|
||||
var result: CharacterDetails = CharacterDetails.new()
|
||||
result.modified = ApiJson.get_int(data, "modified", 0)
|
||||
result.name = ApiJson.get_string(data, "name", "")
|
||||
return result
|
||||
|
||||
|
||||
## Returns the fields under their JSON names, ready for [method JSON.stringify].
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"modified": modified,
|
||||
"name": name,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cu6wgjxv120en
|
||||
@@ -216,6 +216,12 @@ func fetch_character_list(on_done: Callable, on_failed: Callable = Callable()) -
|
||||
get_json("/character/list", on_done, on_failed)
|
||||
|
||||
|
||||
## Fetches every character image with when it was last changed, as an
|
||||
## [code]Array[CharacterDetails][/code]. Servers older than this endpoint answer 404.
|
||||
func fetch_character_details(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_json_list("/character/details", CharacterDetails, on_done, on_failed)
|
||||
|
||||
|
||||
## Downloads a character image as PNG bytes.
|
||||
func fetch_character_image(
|
||||
character_name: String, on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
|
||||
+378
-20
@@ -17,6 +17,25 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CharacterDetails": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"modified": {
|
||||
"description": "When the image was last changed, in Unix seconds",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "File name of the character image",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"modified"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProgressResponse": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -437,7 +456,7 @@
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"description": "API for the MusicServer application.\n\nGet a token with `POST /api/v1/token` and send it as `Authorization: Bearer <token>`.\n\nThe same endpoints without the `/api/v1` prefix (legacy routes) are deprecated, need no token and are not documented here. They will be removed.",
|
||||
"description": "API for the MusicServer application.\n\nGet a token with `POST /api/v1/token` and send it as `Authorization: Bearer <token>`.\n\nEvery client has its own play queue. The music endpoints return its ID in the `X-Queue-Id` header; send it back in the same header to keep using that queue. Without it, or when the queue has ended (after a reset or a sync), a new queue is started.",
|
||||
"title": "MusicServer API",
|
||||
"version": "5.0.0"
|
||||
},
|
||||
@@ -561,6 +580,56 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/character/details": {
|
||||
"get": {
|
||||
"description": "Returns every character image with the time it was last changed, so clients can cache images and download only the changed ones.",
|
||||
"operationId": "list-character-details",
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CharacterDetails"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/APIError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/APIError"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
}
|
||||
],
|
||||
"summary": "List characters with change times",
|
||||
"tags": [
|
||||
"characters"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/character/list": {
|
||||
"get": {
|
||||
"description": "Returns the file names of all character images.",
|
||||
@@ -904,6 +973,15 @@
|
||||
"description": "Jumps to the song at the given queue position (clamped to the queue) and returns it.",
|
||||
"operationId": "get-queue-song",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Position of the song in the queue (SongNo)",
|
||||
"explode": false,
|
||||
@@ -928,7 +1006,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "The song (MP3)"
|
||||
"description": "The song (MP3)",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"content": {
|
||||
@@ -940,7 +1027,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Part of the file, for a Range request"
|
||||
"description": "Part of the file, for a Range request",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1008,9 +1104,29 @@
|
||||
"get": {
|
||||
"description": "Increments the play counters of the current song and its soundtrack.",
|
||||
"operationId": "mark-current-played",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
"description": "OK",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1058,9 +1174,29 @@
|
||||
"get": {
|
||||
"description": "Adds the song returned by the last random song request to the end of the queue.",
|
||||
"operationId": "add-latest-to-queue",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
"description": "OK",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1288,6 +1424,17 @@
|
||||
"get": {
|
||||
"description": "Returns the current song in the queue.",
|
||||
"operationId": "get-song-info",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -1297,7 +1444,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
"description": "OK",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1335,6 +1491,17 @@
|
||||
"get": {
|
||||
"description": "Returns all songs in the queue.",
|
||||
"operationId": "get-played-songs",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -1347,7 +1514,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
"description": "OK",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1385,6 +1561,17 @@
|
||||
"get": {
|
||||
"description": "Moves to the next song in the queue and returns it. Stays on the last song at the end.",
|
||||
"operationId": "get-next-song",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -1396,7 +1583,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "The song (MP3)"
|
||||
"description": "The song (MP3)",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"content": {
|
||||
@@ -1408,7 +1604,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Part of the file, for a Range request"
|
||||
"description": "Part of the file, for a Range request",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1467,6 +1672,15 @@
|
||||
"description": "Increments the play counters of the song at the given queue position. Positions outside the queue are ignored.",
|
||||
"operationId": "mark-song-played",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Position of the song in the queue (SongNo)",
|
||||
"explode": false,
|
||||
@@ -1482,7 +1696,16 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
"description": "OK",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1540,6 +1763,17 @@
|
||||
"get": {
|
||||
"description": "Moves to the previous song in the queue and returns it. Stays on the first song at the start.",
|
||||
"operationId": "get-previous-song",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -1551,7 +1785,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "The song (MP3)"
|
||||
"description": "The song (MP3)",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"content": {
|
||||
@@ -1563,7 +1806,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Part of the file, for a Range request"
|
||||
"description": "Part of the file, for a Range request",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1621,6 +1873,17 @@
|
||||
"get": {
|
||||
"description": "Picks a random song from a random soundtrack. Add it to the queue with /music/addQue.",
|
||||
"operationId": "get-random-song",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -1632,7 +1895,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "The song (MP3)"
|
||||
"description": "The song (MP3)",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"content": {
|
||||
@@ -1644,7 +1916,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Part of the file, for a Range request"
|
||||
"description": "Part of the file, for a Range request",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1702,6 +1983,17 @@
|
||||
"get": {
|
||||
"description": "Picks a random song where every song has the same chance, regardless of soundtrack.",
|
||||
"operationId": "get-random-song-classic",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -1713,7 +2005,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "The song (MP3)"
|
||||
"description": "The song (MP3)",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"content": {
|
||||
@@ -1725,7 +2026,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Part of the file, for a Range request"
|
||||
"description": "Part of the file, for a Range request",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1783,6 +2093,17 @@
|
||||
"get": {
|
||||
"description": "Picks a random song, favouring soundtracks that have been played less than average.",
|
||||
"operationId": "get-random-song-low-chance",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
@@ -1794,7 +2115,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "The song (MP3)"
|
||||
"description": "The song (MP3)",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"206": {
|
||||
"content": {
|
||||
@@ -1806,7 +2136,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Part of the file, for a Range request"
|
||||
"description": "Part of the file, for a Range request",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"description": "ID of the client's play queue. Send it with the following requests.",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
@@ -1862,11 +2201,30 @@
|
||||
},
|
||||
"/api/v1/music/reset": {
|
||||
"get": {
|
||||
"description": "Clears the queue and reloads the soundtrack list.",
|
||||
"description": "Ends the queue, starts a new one and reloads the soundtrack list. The new queue's ID is returned in the X-Queue-Id header; send it with the following requests.",
|
||||
"operationId": "reset-queue",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ID of the client's play queue, from an earlier response. Omit it to start a new queue.",
|
||||
"in": "header",
|
||||
"name": "X-Queue-Id",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK"
|
||||
"description": "OK",
|
||||
"headers": {
|
||||
"X-Queue-Id": {
|
||||
"schema": {
|
||||
"description": "ID of the new queue",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
|
||||
@@ -64,7 +64,9 @@ func test_fetch_song_downloads_queues_and_adds_the_song() -> void:
|
||||
.contains_exactly(["/music/rand", "/music/addQue", "/music/info"])
|
||||
var song: SongObject = Playlist.song_object_array[0]
|
||||
assert_str(song.song_title).is_equal("Corridors of Time")
|
||||
assert_object(song.song).is_not_null()
|
||||
assert_object(song.song).override_failure_message("Songs are kept on disk").is_null()
|
||||
assert_str(song.song_path.get_base_dir()).is_equal(Playlist.songs_folder())
|
||||
assert_object(Playlist.get_song(0)).is_not_null()
|
||||
assert_bool(Playlist.is_song_downloading).is_false()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
extends GdUnitTestSuite
|
||||
## Character pictures cached on disk: downloaded once, then only again when they changed
|
||||
## on the server, or when they are old and the server can't tell.
|
||||
|
||||
var _server: FakeHttpTransport
|
||||
var _config_dir_before: String
|
||||
var _png: PackedByteArray
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_server = FakeHttpTransport.new().install()
|
||||
_config_dir_before = OS.get_environment(Settings.CONFIG_DIR_ENV)
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, create_temp_dir("character_cache"))
|
||||
if DirAccess.dir_exists_absolute(CharacterCache.folder()):
|
||||
for file_name: String in DirAccess.get_files_at(CharacterCache.folder()):
|
||||
DirAccess.remove_absolute(CharacterCache.folder().path_join(file_name))
|
||||
var image: Image = Image.create(4, 4, false, Image.FORMAT_RGB8)
|
||||
image.fill(Color.ORANGE)
|
||||
_png = image.save_png_to_buffer()
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, _config_dir_before)
|
||||
_server.uninstall()
|
||||
|
||||
|
||||
func _serve(characters: Dictionary) -> void:
|
||||
var details: Array = []
|
||||
for character_name: String in characters:
|
||||
details.append({"name": character_name, "modified": characters[character_name]})
|
||||
_server.respond_bytes(_image_path(character_name), _png)
|
||||
_server.respond_json("/character/details", details)
|
||||
|
||||
|
||||
# Serves the names only, like a server older than /character/details
|
||||
func _serve_names_only(names: Array[String]) -> void:
|
||||
_server.respond_json("/character/list", names)
|
||||
for character_name: String in names:
|
||||
_server.respond_bytes(_image_path(character_name), _png)
|
||||
|
||||
|
||||
func _image_path(character_name: String) -> String:
|
||||
return "/character?name=" + character_name.uri_encode()
|
||||
|
||||
|
||||
# Loads the list and every picture with a new cache, like starting the game
|
||||
func _load_all() -> Array[Image]:
|
||||
var cache: CharacterCache = CharacterCache.new()
|
||||
var images: Array[Image] = []
|
||||
cache.load_list(func(names: Array[String]) -> void:
|
||||
for character_name: String in names:
|
||||
cache.fetch_image(character_name, func(image: Image) -> void: images.append(image)))
|
||||
return images
|
||||
|
||||
|
||||
func _index() -> Dictionary:
|
||||
return JSON.parse_string(FileAccess.get_file_as_string(
|
||||
CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME)))
|
||||
|
||||
|
||||
func _set_index(index: Dictionary) -> void:
|
||||
FileAccess.open(CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME),
|
||||
FileAccess.WRITE).store_string(JSON.stringify(index))
|
||||
|
||||
|
||||
func test_first_load_downloads_and_keeps_every_picture() -> void:
|
||||
_serve({"Crono.png": 100, "Marle.png": 200})
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Crono.png"))).is_true()
|
||||
assert_int(int(_index()["Marle.png"]["modified"])).is_equal(200)
|
||||
|
||||
|
||||
func test_unchanged_pictures_come_from_disk() -> void:
|
||||
_serve({"Crono.png": 100, "Marle.png": 200})
|
||||
_load_all()
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Marle.png"))).is_equal(1)
|
||||
|
||||
|
||||
func test_only_changed_pictures_are_downloaded_again() -> void:
|
||||
_serve({"Crono.png": 100, "Marle.png": 200})
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100, "Marle.png": 300})
|
||||
_load_all()
|
||||
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Marle.png"))).is_equal(2)
|
||||
assert_int(int(_index()["Marle.png"]["modified"])).is_equal(300)
|
||||
|
||||
|
||||
func test_new_character_is_downloaded() -> void:
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100, "Lucca.png": 100})
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Lucca.png"))).is_equal(1)
|
||||
|
||||
|
||||
func test_characters_removed_on_the_server_are_deleted() -> void:
|
||||
_serve({"Crono.png": 100, "Magus.png": 100})
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
|
||||
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Magus.png"))) \
|
||||
.is_false()
|
||||
assert_bool(_index().has("Magus.png")).is_false()
|
||||
|
||||
|
||||
func test_broken_cached_picture_is_downloaded_again() -> void:
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
FileAccess.open(CharacterCache.folder().path_join("Crono.png"), FileAccess.WRITE) \
|
||||
.store_string("not a picture")
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
||||
|
||||
|
||||
func test_older_server_lists_names_and_young_pictures_come_from_disk() -> void:
|
||||
_serve_names_only(["Crono.png"])
|
||||
_load_all()
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(1)
|
||||
assert_int(_server.count("/character/details")).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(int(_index()["Crono.png"]["modified"])).is_equal(CharacterCache.UNKNOWN_TIME)
|
||||
|
||||
|
||||
func test_older_server_pictures_are_downloaded_again_when_old() -> void:
|
||||
_serve_names_only(["Crono.png", "Marle.png"])
|
||||
_load_all()
|
||||
var index: Dictionary = _index()
|
||||
var too_old: int = int(Time.get_unix_time_from_system()) - CharacterCache.MAX_AGE_SECONDS - 1
|
||||
index["Marle.png"]["downloaded"] = too_old
|
||||
_set_index(index)
|
||||
|
||||
_load_all()
|
||||
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Marle.png"))).is_equal(2)
|
||||
|
||||
|
||||
func test_picture_cached_without_a_time_is_downloaded_once_the_server_has_times() -> void:
|
||||
_serve_names_only(["Crono.png"])
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
_load_all()
|
||||
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
||||
|
||||
|
||||
func test_picture_that_cannot_be_read_is_not_cached() -> void:
|
||||
_server.respond_json("/character/details", [{"name": "Crono.png", "modified": 100}])
|
||||
_server.respond_bytes(_image_path("Crono.png"), "not a picture".to_utf8_buffer())
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_array(images).is_empty()
|
||||
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Crono.png"))).is_false()
|
||||
|
||||
|
||||
func test_broken_index_starts_over() -> void:
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
FileAccess.open(CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME),
|
||||
FileAccess.WRITE).store_string("{broken")
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb74d4gbslmcf
|
||||
@@ -0,0 +1,85 @@
|
||||
extends GdUnitTestSuite
|
||||
## Downloaded songs are kept in files in the songs folder, not in memory.
|
||||
|
||||
var _snapshot: AutoloadSnapshot
|
||||
var _config_dir_before: String
|
||||
var _mp3: PackedByteArray
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_snapshot = AutoloadSnapshot.take([Playlist])
|
||||
_config_dir_before = OS.get_environment(Settings.CONFIG_DIR_ENV)
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, create_temp_dir("song_files"))
|
||||
Playlist.clear_song_files()
|
||||
Playlist.song_object_array.clear()
|
||||
_mp3 = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
Playlist.clear_song_files()
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, _config_dir_before)
|
||||
_snapshot.restore()
|
||||
|
||||
|
||||
func _add_song() -> SongObject:
|
||||
var song_object: SongObject = SongObject.new()
|
||||
Playlist.store_song(song_object, _mp3)
|
||||
Playlist.add_to_playlist(song_object)
|
||||
return song_object
|
||||
|
||||
|
||||
func test_stored_song_is_a_file_and_plays_back() -> void:
|
||||
var song_object: SongObject = _add_song()
|
||||
|
||||
assert_object(song_object.song).is_null()
|
||||
assert_bool(FileAccess.file_exists(song_object.song_path)).is_true()
|
||||
var stream: AudioStream = Playlist.get_song(0)
|
||||
assert_object(stream).is_instanceof(AudioStreamMP3)
|
||||
assert_float(stream.get_length()).is_greater(0.0)
|
||||
|
||||
|
||||
func test_every_song_gets_a_file_of_its_own() -> void:
|
||||
var first: SongObject = _add_song()
|
||||
var second: SongObject = _add_song()
|
||||
|
||||
assert_str(first.song_path).is_not_equal(second.song_path)
|
||||
assert_array(DirAccess.get_files_at(Playlist.songs_folder())).has_size(2)
|
||||
|
||||
|
||||
func test_downloading_a_song_again_replaces_its_file() -> void:
|
||||
var song_object: SongObject = _add_song()
|
||||
var old_path: String = song_object.song_path
|
||||
|
||||
Playlist.add_song_to_song_object(0, _mp3)
|
||||
|
||||
assert_str(song_object.song_path).is_not_equal(old_path)
|
||||
assert_object(Playlist.get_song(0)).is_not_null()
|
||||
|
||||
|
||||
func test_resetting_the_playlist_deletes_the_files() -> void:
|
||||
_add_song()
|
||||
_add_song()
|
||||
|
||||
Playlist.reset_playlist()
|
||||
|
||||
assert_array(DirAccess.get_files_at(Playlist.songs_folder())).is_empty()
|
||||
|
||||
|
||||
func test_missing_file_gives_no_song_so_it_is_downloaded_again() -> void:
|
||||
var song_object: SongObject = _add_song()
|
||||
|
||||
DirAccess.remove_absolute(song_object.song_path)
|
||||
|
||||
assert_object(Playlist.get_song(0)).is_null()
|
||||
|
||||
|
||||
func test_song_stays_in_memory_when_it_cannot_be_saved() -> void:
|
||||
# A file where the folder should be, so the folder can't be made
|
||||
var blocker: String = create_temp_dir("song_files_blocked").path_join("not_a_folder")
|
||||
FileAccess.open(blocker, FileAccess.WRITE).store_string("x")
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, blocker)
|
||||
|
||||
var song_object: SongObject = _add_song()
|
||||
|
||||
assert_str(song_object.song_path).is_empty()
|
||||
assert_object(Playlist.get_song(0)).is_instanceof(AudioStreamMP3)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0x7lfm0d5kmc
|
||||
Reference in new issue
Block a user