Use /api/v1 with token authentication and typed responses

Closes #38, #39 and #40; adds the client side of #41.

- Every request goes to /api/v1. MusicServerClient gets a token with
  Settings.api_password the first time one is needed, reuses it, and on a
  401 gets a new one and sends the request once more. Switching server gets
  a new token too. Callers never see the token.
- Sync is started with POST, as MusicServer 5.0 requires.
- The client has every endpoint in the server's OpenAPI spec, including
  statistics, soundtracks and songs by UUID, version, downloads and tokens.
- tools/generate_api_types.gd generates a typed class per schema in
  server/openapi.json into server/generated/. Song info, statistics and
  versions are passed to callers as these classes. A list the server sends
  as null is an empty list.
- FakeHttpTransport hands out tokens and answers 401 without one, so every
  test goes through the token flow. New tests cover the token handling,
  every endpoint and each generated class.
This commit is contained in:
Sansan committed 2026-10-06 22:45:28 +02:00
1 parent 8b71723a34
commit f7619baf34
44 files changed
+5161 -71

No files matched your search

+13 -16
View File
@@ -22,19 +22,16 @@ var retry_seconds: float = 5.0
## Preloading waits for the list, because loading the list clears the playlist.
func load_song_list() -> void:
GameLog.debug("load_song_list")
var song_list_fetched: Callable = func song_list_fetched(data: Variant) -> void:
var song_list_fetched: Callable = func song_list_fetched(songs: Array[SongInfo]) -> void:
Playlist.clear_song_list()
if typeof(data) == TYPE_ARRAY:
for d: Dictionary in data:
var new_song_object: SongObject = SongObject.new()
new_song_object.song_number = d.SongNo
new_song_object.soundtrack_title = d.Soundtrack
new_song_object.song_title = d.Song
new_song_object.is_answered = true
new_song_object.has_played = true
Playlist.add_to_playlist(new_song_object)
elif data != null:
GameLog.warning("Unexpected data")
for info: SongInfo in songs:
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)
Playlist.update_last_played_song()
song_list_loaded.emit()
fetch_song_if_idle()
@@ -80,11 +77,11 @@ func _add_song_to_queue(new_song_object: SongObject) -> void:
func _fetch_song_info(new_song_object: SongObject) -> void:
var song_info_fetched: Callable = func song_info_fetched(data_received: Dictionary) -> void:
var song_info_fetched: Callable = func song_info_fetched(info: SongInfo) -> void:
GameLog.debug("song_info_fetched")
new_song_object.song_number = data_received.SongNo
new_song_object.soundtrack_title = data_received.Soundtrack
new_song_object.song_title = data_received.Song
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():