From 739d0f7af64d698bbb3600e37d8ea725c0cb359f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 5 Oct 2026 21:10:42 +0200 Subject: [PATCH] Preload songs only after the song list has loaded At startup and after resetting the playlist, loading the song list and preloading the first song started at the same time. When the list answered last, it cleared the playlist and dropped the preloaded song. Preloading now starts once the list has loaded, and loading the list is retried if it fails. --- MainWindow.gd | 15 ++++--- tests/fakes/fake_http_transport.gd | 22 ++++++++++ tests/fakes/main_window_test_suite.gd | 7 ++++ tests/integration/test_main_window_startup.gd | 41 +++++++++++++++++++ .../test_main_window_startup.gd.uid | 1 + 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 tests/integration/test_main_window_startup.gd create mode 100644 tests/integration/test_main_window_startup.gd.uid diff --git a/MainWindow.gd b/MainWindow.gd index 7c28c98..cf2ce1b 100644 --- a/MainWindow.gd +++ b/MainWindow.gd @@ -186,7 +186,6 @@ func _ready() -> void: get_suggestion_list() fetch_song_list_at_start() - fetch_song() search_view.get_list_of_soundtracks() new_character_select.load_characters() @@ -240,14 +239,12 @@ func show_answer_pressed() -> void: Playlist.song_is_answered() update_song_list() +# 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. func fetch_song_list_at_start() -> void: print("fetch_song_list_at_start") var fetch_song_list_at_start_done: Callable = func fetch_songs_at_start_done(data) -> void: Playlist.clear_song_list() - if data == null: - Playlist.update_last_played_song() - update_song_list() - return if typeof(data) == TYPE_ARRAY: for d: Dictionary in data: var new_song_object: SongObject = SongObject.new() @@ -257,11 +254,14 @@ func fetch_song_list_at_start() -> void: new_song_object.is_answered = true new_song_object.has_played = true Playlist.add_to_playlist(new_song_object) - else: + elif data != null: print("Unexpected data") Playlist.update_last_played_song() update_song_list() - MusicServerClient.fetch_song_list(fetch_song_list_at_start_done) + if !Playlist.is_song_downloading: + fetch_song() + MusicServerClient.fetch_song_list(fetch_song_list_at_start_done, + _retry_later.bind(fetch_song_list_at_start)) func play_next_song() -> void: print("play_next_song ", Playlist.latest_played_song) @@ -360,7 +360,6 @@ func reset_playlist() -> void: var playlist_reset_done: Callable = func playlist_reset_done() -> void: Playlist.reset_playlist() fetch_song_list_at_start() - fetch_song() MusicServerClient.reset_playlist(playlist_reset_done) diff --git a/tests/fakes/fake_http_transport.gd b/tests/fakes/fake_http_transport.gd index 7521215..3cbf698 100644 --- a/tests/fakes/fake_http_transport.gd +++ b/tests/fakes/fake_http_transport.gd @@ -13,6 +13,7 @@ var requested_methods: Array[int] = [] var _responses: Dictionary = {} var _successes_left: Dictionary = {} +var _held_paths: Dictionary = {} var _previous_transport: HttpTransport @@ -49,6 +50,20 @@ func fail_after(path: String, successes: int) -> void: _successes_left[path] = successes +## Holds back responses to [param path] until [method release] is called, like a slow +## server. +func hold(path: String) -> void: + _held_paths[path] = [] + + +## Delivers the responses held back for [param path] and stops holding it. +func release(path: String) -> void: + var waiting: Array = _held_paths.get(path, []) + _held_paths.erase(path) + for pending: Callable in waiting: + pending.call() + + ## Returns how many times [param path] has been requested. func count(path: String) -> int: return requested_paths.count(path) @@ -58,6 +73,13 @@ func request(url: String, method: int, on_complete: Callable) -> void: var path: String = url.trim_prefix(Settings.default_path) requested_paths.append(path) requested_methods.append(method) + if _held_paths.has(path): + _held_paths[path].append(_answer.bind(path, on_complete)) + return + _answer(path, on_complete) + + +func _answer(path: String, on_complete: Callable) -> void: if _successes_left.has(path): if _successes_left[path] == 0: on_complete.call(503, PackedByteArray()) diff --git a/tests/fakes/main_window_test_suite.gd b/tests/fakes/main_window_test_suite.gd index 6bf3490..8ac4fb0 100644 --- a/tests/fakes/main_window_test_suite.gd +++ b/tests/fakes/main_window_test_suite.gd @@ -36,12 +36,19 @@ func before_test() -> void: _server.respond_bytes("/music/reset", PackedByteArray()) _server.respond_bytes("/music/played?song=0", PackedByteArray()) + _before_window_starts() _runner = scene_runner("res://MainWindow.tscn") _window = _runner.scene() # The debug build lowers the winning score on startup; keep it out of reach Settings.winning_score = 20 +## Override to change the fake server before the window starts and sends its first +## requests. +func _before_window_starts() -> void: + pass + + func after_test() -> void: # Let nodes queued for deletion by the window be freed before orphans are counted await await_idle_frame() diff --git a/tests/integration/test_main_window_startup.gd b/tests/integration/test_main_window_startup.gd new file mode 100644 index 0000000..7acffb2 --- /dev/null +++ b/tests/integration/test_main_window_startup.gd @@ -0,0 +1,41 @@ +extends MainWindowTestSuite +## Startup with a slow /music/list response. + + +func _before_window_starts() -> void: + _server.respond_json("/music/list", [ + {"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Corridors of Time"}, + ]) + _next_song_is({"SongNo": 1, "Soundtrack": "Final Fantasy VI", "Song": "Dancing Mad"}) + _server.hold("/music/list") + + +func test_preloaded_song_survives_a_slow_song_list() -> void: + _server.release("/music/list") + + # The song already on the server, then the preloaded one + assert_int(Playlist.song_object_array.size()).is_equal(2) + assert_str(Playlist.song_object_array[1].song_title).is_equal("Dancing Mad") + assert_int(Playlist.get_preloaded_songs()).is_equal(1) + + +func test_next_plays_the_preloaded_song_after_a_slow_song_list() -> void: + _server.release("/music/list") + + await _play_first_song() + + assert_int(Playlist.currently_playing_song).is_equal(1) + + +func test_failed_song_list_is_retried_before_preloading() -> void: + _window.song_fetch_retry_seconds = 0.05 + _server.respond_json("/music/list", [], 503) + _server.release("/music/list") + + assert_int(_server.count("/music/rand")).is_equal(0) + + _server.respond_json("/music/list", []) + await await_millis(200) + + assert_int(_server.count("/music/rand")).is_equal(1) + assert_int(Playlist.get_preloaded_songs()).is_equal(1) diff --git a/tests/integration/test_main_window_startup.gd.uid b/tests/integration/test_main_window_startup.gd.uid new file mode 100644 index 0000000..71b315e --- /dev/null +++ b/tests/integration/test_main_window_startup.gd.uid @@ -0,0 +1 @@ +uid://sfwrqidmuj04