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

+22
View File
@@ -0,0 +1,22 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name ApiError
extends RefCounted
## The [code]APIError[/code] schema of the MusicServer API.
## What went wrong
var error: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> ApiError:
var result: ApiError = ApiError.new()
result.error = ApiJson.get_string(data, "error", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"error": error,
}
+1
View File
@@ -0,0 +1 @@
uid://4ak2e86vltp2
+36
View File
@@ -0,0 +1,36 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name ApiJson
extends RefCounted
## Reads fields from parsed JSON for the generated API classes. A field that is
## missing, null or of the wrong type gives the default instead.
static func get_string(data: Dictionary, key: String, default: String = "") -> String:
var value: Variant = data.get(key)
return value if value is String else default
static func get_int(data: Dictionary, key: String, default: int = 0) -> int:
var value: Variant = data.get(key)
return int(value) if value is int || value is float else default
static func get_float(data: Dictionary, key: String, default: float = 0.0) -> float:
var value: Variant = data.get(key)
return float(value) if value is int || value is float else default
static func get_bool(data: Dictionary, key: String, default: bool = false) -> bool:
var value: Variant = data.get(key)
return value if value is bool else default
static func get_array(data: Dictionary, key: String) -> Array:
var value: Variant = data.get(key)
return value if value is Array else []
static func get_dictionary(data: Dictionary, key: String) -> Dictionary:
var value: Variant = data.get(key)
return value if value is Dictionary else {}
+1
View File
@@ -0,0 +1 @@
uid://kpihxs4wsldn
+24
View File
@@ -0,0 +1,24 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name ProgressResponse
extends RefCounted
## The [code]ProgressResponse[/code] schema of the MusicServer API.
var progress: String
var time_spent: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> ProgressResponse:
var result: ProgressResponse = ProgressResponse.new()
result.progress = ApiJson.get_string(data, "progress", "")
result.time_spent = ApiJson.get_string(data, "time_spent", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"progress": progress,
"time_spent": time_spent,
}
@@ -0,0 +1 @@
uid://blka8xmsytbw1
+36
View File
@@ -0,0 +1,36 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name Song
extends RefCounted
## The [code]Song[/code] schema of the MusicServer API.
var file_name: String
var name: String
var soundtrack_name: String
var soundtrack_uuid: String
var times_played: int
var uuid: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> Song:
var result: Song = Song.new()
result.file_name = ApiJson.get_string(data, "file_name", "")
result.name = ApiJson.get_string(data, "name", "")
result.soundtrack_name = ApiJson.get_string(data, "soundtrack_name", "")
result.soundtrack_uuid = ApiJson.get_string(data, "soundtrack_uuid", "")
result.times_played = ApiJson.get_int(data, "times_played", 0)
result.uuid = ApiJson.get_string(data, "uuid", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"file_name": file_name,
"name": name,
"soundtrack_name": soundtrack_name,
"soundtrack_uuid": soundtrack_uuid,
"times_played": times_played,
"uuid": uuid,
}
+1
View File
@@ -0,0 +1 @@
uid://cmsr3kl148gth
+42
View File
@@ -0,0 +1,42 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name SongInfo
extends RefCounted
## The [code]SongInfo[/code] schema of the MusicServer API.
var currently_playing: bool
var song: String
var song_no: int
var song_played: int
var song_uuid: String
var soundtrack: String
var soundtrack_played: int
var soundtrack_uuid: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> SongInfo:
var result: SongInfo = SongInfo.new()
result.currently_playing = ApiJson.get_bool(data, "CurrentlyPlaying", false)
result.song = ApiJson.get_string(data, "Song", "")
result.song_no = ApiJson.get_int(data, "SongNo", 0)
result.song_played = ApiJson.get_int(data, "SongPlayed", 0)
result.song_uuid = ApiJson.get_string(data, "SongUuid", "")
result.soundtrack = ApiJson.get_string(data, "Soundtrack", "")
result.soundtrack_played = ApiJson.get_int(data, "SoundtrackPlayed", 0)
result.soundtrack_uuid = ApiJson.get_string(data, "SoundtrackUuid", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"CurrentlyPlaying": currently_playing,
"Song": song,
"SongNo": song_no,
"SongPlayed": song_played,
"SongUuid": song_uuid,
"Soundtrack": soundtrack,
"SoundtrackPlayed": soundtrack_played,
"SoundtrackUuid": soundtrack_uuid,
}
+1
View File
@@ -0,0 +1 @@
uid://blybmr7a2rm6a
+42
View File
@@ -0,0 +1,42 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name SongInfoForStats
extends RefCounted
## The [code]SongInfoForStats[/code] schema of the MusicServer API.
var file_name: String
var path: String
var song_name: String
var song_uuid: String
var soundtrack_id: int
var soundtrack_name: String
var soundtrack_uuid: String
var times_played: int
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> SongInfoForStats:
var result: SongInfoForStats = SongInfoForStats.new()
result.file_name = ApiJson.get_string(data, "file_name", "")
result.path = ApiJson.get_string(data, "path", "")
result.song_name = ApiJson.get_string(data, "song_name", "")
result.song_uuid = ApiJson.get_string(data, "song_uuid", "")
result.soundtrack_id = ApiJson.get_int(data, "soundtrack_id", 0)
result.soundtrack_name = ApiJson.get_string(data, "soundtrack_name", "")
result.soundtrack_uuid = ApiJson.get_string(data, "soundtrack_uuid", "")
result.times_played = ApiJson.get_int(data, "times_played", 0)
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"file_name": file_name,
"path": path,
"song_name": song_name,
"song_uuid": song_uuid,
"soundtrack_id": soundtrack_id,
"soundtrack_name": soundtrack_name,
"soundtrack_uuid": soundtrack_uuid,
"times_played": times_played,
}
@@ -0,0 +1 @@
uid://chexehl48yklr
+33
View File
@@ -0,0 +1,33 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name Soundtrack
extends RefCounted
## The [code]Soundtrack[/code] schema of the MusicServer API.
var added: String
var last_played: String
var name: String
var times_played: int
var uuid: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> Soundtrack:
var result: Soundtrack = Soundtrack.new()
result.added = ApiJson.get_string(data, "added", "")
result.last_played = ApiJson.get_string(data, "last_played", "")
result.name = ApiJson.get_string(data, "name", "")
result.times_played = ApiJson.get_int(data, "times_played", 0)
result.uuid = ApiJson.get_string(data, "uuid", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"added": added,
"last_played": last_played,
"name": name,
"times_played": times_played,
"uuid": uuid,
}
+1
View File
@@ -0,0 +1 @@
uid://bdbaxmn66tuv7
@@ -0,0 +1,38 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name SoundtrackWithSongList
extends RefCounted
## The [code]SoundtrackWithSongList[/code] schema of the MusicServer API.
var added: String
var last_played: String
var name: String
var songs: Array[Song]
var times_played: int
var uuid: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> SoundtrackWithSongList:
var result: SoundtrackWithSongList = SoundtrackWithSongList.new()
result.added = ApiJson.get_string(data, "added", "")
result.last_played = ApiJson.get_string(data, "last_played", "")
result.name = ApiJson.get_string(data, "name", "")
for item: Variant in ApiJson.get_array(data, "songs"):
if item is Dictionary:
result.songs.append(Song.from_dict(item))
result.times_played = ApiJson.get_int(data, "times_played", 0)
result.uuid = ApiJson.get_string(data, "uuid", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"added": added,
"last_played": last_played,
"name": name,
"songs": songs.map(func(item: Song) -> Dictionary: return item.to_dict()),
"times_played": times_played,
"uuid": uuid,
}
@@ -0,0 +1 @@
uid://cj4rx6m53oarm
+38
View File
@@ -0,0 +1,38 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name SoundtrackWithSongs
extends RefCounted
## The [code]SoundtrackWithSongs[/code] schema of the MusicServer API.
var songs: Array[SongInfoForStats]
var soundtrack_id: int
var soundtrack_last_played: String
var soundtrack_name: String
var soundtrack_played: int
var soundtrack_uuid: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> SoundtrackWithSongs:
var result: SoundtrackWithSongs = SoundtrackWithSongs.new()
for item: Variant in ApiJson.get_array(data, "songs"):
if item is Dictionary:
result.songs.append(SongInfoForStats.from_dict(item))
result.soundtrack_id = ApiJson.get_int(data, "soundtrack_id", 0)
result.soundtrack_last_played = ApiJson.get_string(data, "soundtrack_last_played", "")
result.soundtrack_name = ApiJson.get_string(data, "soundtrack_name", "")
result.soundtrack_played = ApiJson.get_int(data, "soundtrack_played", 0)
result.soundtrack_uuid = ApiJson.get_string(data, "soundtrack_uuid", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"songs": songs.map(func(item: SongInfoForStats) -> Dictionary: return item.to_dict()),
"soundtrack_id": soundtrack_id,
"soundtrack_last_played": soundtrack_last_played,
"soundtrack_name": soundtrack_name,
"soundtrack_played": soundtrack_played,
"soundtrack_uuid": soundtrack_uuid,
}
@@ -0,0 +1 @@
uid://yxxvcbbr5ioi
+39
View File
@@ -0,0 +1,39 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name StatisticsSummary
extends RefCounted
## The [code]StatisticsSummary[/code] schema of the MusicServer API.
var avg_soundtrack_plays: float
var max_soundtrack_plays: int
var min_soundtrack_plays: int
var never_played_soundtracks: int
var played_soundtracks: int
var total_soundtrack_plays: int
var total_soundtracks: int
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> StatisticsSummary:
var result: StatisticsSummary = StatisticsSummary.new()
result.avg_soundtrack_plays = ApiJson.get_float(data, "avg_soundtrack_plays", 0.0)
result.max_soundtrack_plays = ApiJson.get_int(data, "max_soundtrack_plays", 0)
result.min_soundtrack_plays = ApiJson.get_int(data, "min_soundtrack_plays", 0)
result.never_played_soundtracks = ApiJson.get_int(data, "never_played_soundtracks", 0)
result.played_soundtracks = ApiJson.get_int(data, "played_soundtracks", 0)
result.total_soundtrack_plays = ApiJson.get_int(data, "total_soundtrack_plays", 0)
result.total_soundtracks = ApiJson.get_int(data, "total_soundtracks", 0)
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"avg_soundtrack_plays": avg_soundtrack_plays,
"max_soundtrack_plays": max_soundtrack_plays,
"min_soundtrack_plays": min_soundtrack_plays,
"never_played_soundtracks": never_played_soundtracks,
"played_soundtracks": played_soundtracks,
"total_soundtrack_plays": total_soundtrack_plays,
"total_soundtracks": total_soundtracks,
}
@@ -0,0 +1 @@
uid://d2acyameyuhv2
+21
View File
@@ -0,0 +1,21 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name StatusMessage
extends RefCounted
## The [code]StatusMessage[/code] schema of the MusicServer API.
var status: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> StatusMessage:
var result: StatusMessage = StatusMessage.new()
result.status = ApiJson.get_string(data, "status", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"status": status,
}
+1
View File
@@ -0,0 +1 @@
uid://5inwlixefu0m
+41
View File
@@ -0,0 +1,41 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name SyncResponse
extends RefCounted
## The [code]SyncResponse[/code] schema of the MusicServer API.
var catched_errors: Array[String]
var soundtracks_added: Array[String]
var soundtracks_changed_content: Array[String]
var soundtracks_changed_title: Dictionary[String, String]
var soundtracks_re_added: Array[String]
var soundtracks_removed: Array[String]
var total_time: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> SyncResponse:
var result: SyncResponse = SyncResponse.new()
result.catched_errors.assign(ApiJson.get_array(data, "catched_errors"))
result.soundtracks_added.assign(ApiJson.get_array(data, "soundtracks_added"))
result.soundtracks_changed_content.assign(
ApiJson.get_array(data, "soundtracks_changed_content"))
result.soundtracks_changed_title.assign(
ApiJson.get_dictionary(data, "soundtracks_changed_title"))
result.soundtracks_re_added.assign(ApiJson.get_array(data, "soundtracks_re_added"))
result.soundtracks_removed.assign(ApiJson.get_array(data, "soundtracks_removed"))
result.total_time = ApiJson.get_string(data, "total_time", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"catched_errors": catched_errors,
"soundtracks_added": soundtracks_added,
"soundtracks_changed_content": soundtracks_changed_content,
"soundtracks_changed_title": soundtracks_changed_title,
"soundtracks_re_added": soundtracks_re_added,
"soundtracks_removed": soundtracks_removed,
"total_time": total_time,
}
+1
View File
@@ -0,0 +1 @@
uid://d2fcfuusfbhah
+26
View File
@@ -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 TokenRequest
extends RefCounted
## The [code]TokenRequest[/code] schema of the MusicServer API.
## Kind of client, for example web, godot or api
var client_type: String = "web"
## API password, required when the server has one configured
var password: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> TokenRequest:
var result: TokenRequest = TokenRequest.new()
result.client_type = ApiJson.get_string(data, "client_type", "web")
result.password = ApiJson.get_string(data, "password", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"client_type": client_type,
"password": password,
}
+1
View File
@@ -0,0 +1 @@
uid://c83b66bi6vpmp
+28
View File
@@ -0,0 +1,28 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name TokenResponse
extends RefCounted
## The [code]TokenResponse[/code] schema of the MusicServer API.
var client_type: String
var expires_at: String
## Send as Authorization: Bearer <token>
var token: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> TokenResponse:
var result: TokenResponse = TokenResponse.new()
result.client_type = ApiJson.get_string(data, "client_type", "")
result.expires_at = ApiJson.get_string(data, "expires_at", "")
result.token = ApiJson.get_string(data, "token", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"client_type": client_type,
"expires_at": expires_at,
"token": token,
}
+1
View File
@@ -0,0 +1 @@
uid://4a44v85naqw0
+24
View File
@@ -0,0 +1,24 @@
# Generated by tools/generate_api_types.gd from server/openapi.json.
# Do not edit; change the server and generate again.
class_name VersionData
extends RefCounted
## The [code]VersionData[/code] schema of the MusicServer API.
var changelog: Array[String]
var version: String
## Reads the fields from parsed JSON. Missing fields keep their defaults.
static func from_dict(data: Dictionary) -> VersionData:
var result: VersionData = VersionData.new()
result.changelog.assign(ApiJson.get_array(data, "changelog"))
result.version = ApiJson.get_string(data, "version", "")
return result
## Returns the fields under their JSON names, ready for [method JSON.stringify].
func to_dict() -> Dictionary:
return {
"changelog": changelog,
"version": version,
}
+1
View File
@@ -0,0 +1 @@
uid://no7af2fycku5