Files
MusicPlayer/quiz/song_loader.gd
T
SansanandClaude Opus 5.5 a735c515bf
Build / test (push) Successful in 1m40s
Build / build (push) Successful in 1m28s
Add Short songs and a choice of how random songs are picked (#30, #51)
Short songs (#30): a quiz settings switch and a Seconds per song row (30 by
default, 10-120 in steps of 5), saved with the quiz settings. Only part of
each quiz song plays, from a random point 10-60% into it, picked once per song
so restart and repeat play the same part. The progress bar, time and seeking
stay within the part, a hidden beginning counts inside it, and its end counts
as the song ending (repeat, stop or go to the next song as set).

Random songs (#51): the low-played switch in Settings is now a Normal / Low
played / Classic choice, stored as Settings.random_mode and not saved.
fetch_random_song(mode) picks /music/rand, /music/rand/low or
/music/rand/classic.

Closes #30
Closes #51

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:21:20 +02:00

184 lines
6.9 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.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
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()