Files
MusicPlayer/quiz/song_loader.gd
T
SansanandClaude Opus 5.5 06421bb63e
Build / test (push) Successful in 1m50s
Build / build (push) Successful in 1m14s
Change soundtrack covers from the app and show the cover with the answer
Cover menu in the soundtrack popup ("Change cover"):
- Choose picture uploads a JPEG, PNG or WebP picture (up to 10 MB).
- Pick by ID uses a game's cover on SteamGridDB or an anime's on AniList.
- Remove cover deletes it, so the server searches for a new one.
- The server's reason is shown when it refuses a change.
After a change the new cover time is fetched, and the popup, the
statistics card and the quiz show the new cover.

Quiz: once the answer is shown, the soundtrack's cover shows to the left of
the titles. Cover and titles are centered together, left and right and up
and down, and each title keeps to two lines. Cover times come from the
soundtrack list, and songs keep their soundtrack's UUID, also in saves.

Requests can now send any body with its content type, and every 2xx answer
counts as success. Cover frames are 2:3 like the server's covers. Small
copies of real covers are used by the tests and screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:32:09 +02:00

186 lines
7.0 KiB
GDScript

class_name SongLoader
extends Node
## Downloads songs from the server into the Playlist ahead of time, so the next song
## can start at once. With a music folder (see [method Settings.is_offline]) the songs are
## picked from the folder instead.
##
## How many songs it keeps ready is the cache size in the settings
## ([code]Playlist.number_of_tracks_to_preload[/code]).
## Emitted when the songs already in the server playlist have been loaded into the
## Playlist.
signal song_list_loaded
## Emitted when a new song has been downloaded and added to the Playlist.
signal song_added
## Emitted when the songs of a saved quiz could not be continued, because the server's
## playlist no longer has them, e.g. after a sync. The quiz starts over.
signal saved_songs_dropped
## Seconds to wait before retrying a download step that failed.
var retry_seconds: float = 5.0
## Loads the songs already in the server playlist, then starts preloading new ones.
## Preloading waits for the list, because loading the list clears the playlist.
##
## With [param saved], the songs of a saved quiz are used where they match the server's
## playlist, so who got points and what was answered or downloaded ahead is kept. When
## the server's playlist no longer has them, they are dropped and
## [signal saved_songs_dropped] is emitted.
func load_song_list(saved: QuizSave = null) -> void:
GameLog.debug("load_song_list")
if Settings.is_offline():
_start_folder_playlist(saved)
return
var song_list_fetched: Callable = func song_list_fetched(songs: Array[SongInfo]) -> void:
if Settings.is_offline():
return
Playlist.clear_song_list()
var continued: int = 0
if saved != null && !saved.songs.is_empty():
if saved_songs_match(saved.songs, songs):
continued = saved.songs.size()
_continue_songs(saved)
else:
GameLog.warning("The server's playlist changed, so the saved quiz starts over")
saved_songs_dropped.emit()
for info: SongInfo in songs.slice(continued):
var new_song_object: SongObject = SongObject.new()
new_song_object.song_number = info.song_no
new_song_object.soundtrack_title = info.soundtrack
new_song_object.song_title = info.song
new_song_object.soundtrack_uuid = info.soundtrack_uuid
new_song_object.is_answered = true
new_song_object.has_played = true
Playlist.add_to_playlist(new_song_object)
if continued == 0:
Playlist.update_last_played_song()
# Songs already on the server were played before, so the game starts after them
Playlist.game_start_song = Playlist.song_object_array.size()
song_list_loaded.emit()
# Songs downloaded ahead before a restart already fill some of the cache
fill_cache()
MusicServerClient.fetch_song_list(song_list_fetched,
_retry_later.bind(load_song_list.bind(saved)))
## Whether [param saved_songs] are the first songs of the server's playlist [param songs]:
## the same numbers, soundtracks and titles in the same order.
static func saved_songs_match(saved_songs: Array[SongObject], songs: Array[SongInfo]) -> bool:
if saved_songs.size() > songs.size():
return false
for i: int in saved_songs.size():
if saved_songs[i].song_number != songs[i].song_no \
|| saved_songs[i].soundtrack_title != songs[i].soundtrack \
|| saved_songs[i].song_title != songs[i].song:
return false
return true
# Puts the saved songs in the playlist, where the quiz was
func _continue_songs(saved: QuizSave) -> void:
for song_object: SongObject in saved.songs:
Playlist.add_to_playlist(song_object)
Playlist.currently_playing_song = saved.currently_playing_song
Playlist.latest_played_song = saved.latest_played_song
Playlist.game_start_song = saved.game_start_song
Playlist.first_song_played = saved.first_song_played
## Starts downloading one more song, which keeps downloading until the cache is full.
func fetch_song() -> void:
GameLog.debug("fetch_song")
if Settings.is_offline():
_add_folder_songs()
return
Playlist.is_song_downloading = true
_download_random_song(SongObject.new())
## Starts downloading a song unless one is already downloading.
func fetch_song_if_idle() -> void:
if !Playlist.is_song_downloading:
fetch_song()
## Starts downloading if nothing is downloading and the cache is not full, e.g. after
## the cache size was raised.
func fill_cache() -> void:
if !Playlist.is_song_downloading && Playlist.should_preload_more_songs():
fetch_song()
# Fetching a song takes three requests. If one fails, only that step is retried, so
# the server and the game keep the same songs in their playlists.
func _download_random_song(new_song_object: SongObject) -> void:
var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
GameLog.debug("song_downloaded")
# A music folder was picked while the song was downloading
if Settings.is_offline():
return
Playlist.store_song(new_song_object, body)
_add_song_to_queue(new_song_object)
MusicServerClient.fetch_random_song(Settings.random_mode, song_downloaded,
_retry_later.bind(_download_random_song.bind(new_song_object)))
func _add_song_to_queue(new_song_object: SongObject) -> void:
MusicServerClient.add_to_queue(_fetch_song_info.bind(new_song_object),
_retry_later.bind(_add_song_to_queue.bind(new_song_object)))
func _fetch_song_info(new_song_object: SongObject) -> void:
var song_info_fetched: Callable = func song_info_fetched(info: SongInfo) -> void:
GameLog.debug("song_info_fetched")
if Settings.is_offline():
return
new_song_object.song_number = info.song_no
new_song_object.soundtrack_title = info.soundtrack
new_song_object.song_title = info.song
new_song_object.soundtrack_uuid = info.soundtrack_uuid
GameLog.debug("new_song_object: ", new_song_object.to_string())
Playlist.add_to_playlist(new_song_object)
if Playlist.should_preload_more_songs():
fetch_song()
else:
Playlist.is_song_downloading = false
song_added.emit()
MusicServerClient.fetch_song_info(song_info_fetched,
_retry_later.bind(_fetch_song_info.bind(new_song_object)))
func _retry_later(step: Callable) -> void:
GameLog.warning("Song download failed, retrying in %s seconds" % retry_seconds)
get_tree().create_timer(retry_seconds).timeout.connect(func() -> void:
if !Settings.is_offline():
step.call())
# A music folder has no playlist on a server, so the game starts with an empty one, or
# with the saved quiz's songs
func _start_folder_playlist(saved: QuizSave = null) -> void:
Playlist.clear_song_list()
Playlist.update_last_played_song()
Playlist.game_start_song = 0
if saved != null && !saved.songs.is_empty():
_continue_songs(saved)
Playlist.is_song_downloading = false
song_list_loaded.emit()
fill_cache()
# Picks songs from the music folder until enough are ready. A prepared quiz runs out
# when every song is in the playlist.
func _add_folder_songs() -> void:
Playlist.is_song_downloading = false
while Playlist.should_preload_more_songs():
var song_object: SongObject = Settings.music_folder.next_song()
if song_object == null:
return
song_object.song_number = Playlist.song_object_array.size()
Playlist.add_to_playlist(song_object)
song_added.emit()