Send the play queue ID to the server
Build / test (push) Successful in 45s
Build / build (push) Successful in 1m12s

The server keeps a play queue per client, identified by the X-Queue-Id
header. The client sends the ID it has for the selected server with every
request and keeps the one in each response, which is new after a reset or a
sync. The IDs are saved per server in the [queues] section of settings.cfg,
so the game goes on with the same queue after a restart.

HttpTransport now passes the response headers on to the client.
This commit is contained in:
Sansan committed 2026-10-07 21:23:37 +02:00
1 parent d21b3e2ce0
commit 4a45e8516d
8 files changed
+212 -16

No files matched your search

+2 -1
View File
@@ -14,7 +14,8 @@ static func take(autoloads: Array[Node]) -> AutoloadSnapshot:
for property: Dictionary in autoload.get_property_list():
if property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
var value: Variant = autoload.get(property.name)
values[property.name] = value.duplicate() if value is Array else value
values[property.name] = (value.duplicate()
if value is Array || value is Dictionary else value)
snapshot._values[autoload] = values
return snapshot
+18 -5
View File
@@ -12,6 +12,9 @@ extends HttpTransport
## Like the real server it hands out tokens on [code]POST /token[/code] and answers 401
## to requests without a valid one. Token requests are counted in
## [member token_requests], not in [member requested_paths].
##
## Set [member queue_id] to answer with an [code]X-Queue-Id[/code] header, like the
## server's music endpoints.
## Paths that need no token, like on the real server.
const OPEN_PATHS: Array[String] = ["/token", "/health"]
@@ -26,11 +29,14 @@ var requested_headers: Array[PackedStringArray] = []
var token_requests: int = 0
## The JSON body of the last token request.
var last_token_request: Dictionary = {}
## Sent in an X-Queue-Id header with every answer that is not about a token, unless empty.
var queue_id: String = ""
var _responses: Dictionary = {}
var _successes_left: Dictionary = {}
var _held_paths: Dictionary = {}
var _previous_transport: HttpTransport
var _previous_queue_ids: Dictionary[String, String]
var _valid_tokens: Array[String] = []
@@ -43,6 +49,8 @@ func install() -> FakeHttpTransport:
_previous_transport = MusicServerClient.transport
MusicServerClient.transport = self
MusicServerClient.clear_token()
_previous_queue_ids = Settings.queue_ids.duplicate()
Settings.queue_ids.clear()
return self
@@ -50,6 +58,7 @@ func install() -> FakeHttpTransport:
func uninstall() -> void:
MusicServerClient.transport = _previous_transport
MusicServerClient.clear_token()
Settings.queue_ids = _previous_queue_ids
## Answers requests to [param path] with [param data] encoded as JSON.
@@ -112,7 +121,8 @@ func request(
requested_methods.append(method)
requested_headers.append(headers)
if !OPEN_PATHS.has(path.get_slice("?", 0)) && !_has_valid_token(headers):
on_complete.call(401, JSON.stringify({"error": "Unauthorized"}).to_utf8_buffer())
on_complete.call(401, JSON.stringify({"error": "Unauthorized"}).to_utf8_buffer(),
PackedStringArray())
return
if _held_paths.has(path):
_held_paths[path].append(_answer.bind(path, on_complete))
@@ -121,15 +131,18 @@ func request(
func _answer(path: String, on_complete: Callable) -> void:
var headers: PackedStringArray = []
if !queue_id.is_empty() && path != "/token":
headers.append("X-Queue-Id: " + queue_id)
if _successes_left.has(path):
if _successes_left[path] == 0:
on_complete.call(503, PackedByteArray())
on_complete.call(503, PackedByteArray(), headers)
return
_successes_left[path] -= 1
if _responses.has(path):
on_complete.call(_responses[path].code, _responses[path].body)
on_complete.call(_responses[path].code, _responses[path].body, headers)
else:
on_complete.call(404, PackedByteArray())
on_complete.call(404, PackedByteArray(), headers)
func _answer_token_request(body: String, on_complete: Callable) -> void:
@@ -147,7 +160,7 @@ func _answer_token_request(body: String, on_complete: Callable) -> void:
_valid_tokens.append(token)
on_complete.call(200, JSON.stringify({
"token": token, "expires_at": "2026-10-07T12:00:00Z", "client_type": "godot",
}).to_utf8_buffer())
}).to_utf8_buffer(), PackedStringArray())
func _has_valid_token(headers: PackedStringArray) -> bool:
+1 -1
View File
@@ -32,7 +32,7 @@ func test_request_to_silent_server_times_out_as_failed() -> void:
transport.request("http://127.0.0.1:%d/music/rand" % _server.get_local_port(),
HTTPClient.METHOD_GET,
func(response_code: int, _body: PackedByteArray) -> void:
func(response_code: int, _body: PackedByteArray, _headers: PackedStringArray) -> void:
response_codes.append(response_code))
for i: int in 40:
_accept_connections()
+107 -1
View File
@@ -542,6 +542,111 @@ func _call_every_statistics_endpoint(on_done: Callable, on_failed: Callable) ->
MusicServerClient.fetch_least_played_songs(3, on_done, on_failed)
# Queue IDs
func test_no_queue_id_is_sent_before_the_server_sends_one() -> void:
_server.respond_json("/music/list", [])
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
assert_array(_server.requested_headers[0]) \
.contains_exactly(["Authorization: Bearer fake-token-1"])
func test_queue_id_from_a_response_is_sent_with_the_next_requests() -> void:
_server.respond_json("/music/list", [])
_server.respond_bytes("/music/addQue", PackedByteArray())
_server.queue_id = "queue-1"
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
MusicServerClient.add_to_queue(func() -> void: pass)
assert_str(MusicServerClient.queue_id()).is_equal("queue-1")
assert_array(_server.requested_headers[1]) \
.contains_exactly(["Authorization: Bearer fake-token-1", "X-Queue-Id: queue-1"])
DirAccess.remove_absolute(Settings.saved_settings_path())
func test_new_queue_id_replaces_the_old_one() -> void:
_server.respond_bytes("/music/reset", PackedByteArray())
_server.respond_json("/music/list", [])
_server.queue_id = "queue-1"
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
_server.queue_id = "queue-2"
MusicServerClient.reset_playlist(func() -> void: pass)
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
assert_array(_server.requested_headers[1]).contains(["X-Queue-Id: queue-1"])
assert_array(_server.requested_headers[2]).contains(["X-Queue-Id: queue-2"])
DirAccess.remove_absolute(Settings.saved_settings_path())
func test_queue_id_is_kept_per_server() -> void:
var previous_path: String = Settings.default_path
_server.respond_json("/music/list", [])
_server.queue_id = "queue-1"
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
_server.queue_id = ""
Settings.default_path = "http://another-server:8080"
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
var other_server_id: String = MusicServerClient.queue_id()
Settings.default_path = previous_path
assert_str(other_server_id).is_empty()
assert_array(_server.requested_headers[1]) \
.not_contains(["X-Queue-Id: queue-1"])
assert_str(MusicServerClient.queue_id()).is_equal("queue-1")
DirAccess.remove_absolute(Settings.saved_settings_path())
func test_queue_id_is_saved_for_the_next_start() -> void:
_server.respond_json("/music/list", [])
_server.queue_id = "queue-1"
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
Settings.queue_ids.clear()
assert_str(MusicServerClient.queue_id()).is_empty()
Settings.load_saved_settings()
assert_str(MusicServerClient.queue_id()).is_equal("queue-1")
DirAccess.remove_absolute(Settings.saved_settings_path())
func test_queue_id_header_name_is_case_insensitive() -> void:
var transport: _HeaderTransport = _HeaderTransport.new(["x-queue-id: lower-case"])
var previous_transport: HttpTransport = MusicServerClient.transport
MusicServerClient.transport = transport
MusicServerClient.fetch_song_list(func(_data: Array[SongInfo]) -> void: pass)
MusicServerClient.transport = previous_transport
assert_str(MusicServerClient.queue_id()).is_equal("lower-case")
DirAccess.remove_absolute(Settings.saved_settings_path())
# Answers every request with 200, an empty JSON list and the given headers
class _HeaderTransport:
extends HttpTransport
var _headers: PackedStringArray
func _init(headers: PackedStringArray) -> void:
super(null)
_headers = headers
func request(
_url: String,
_method: int,
on_complete: Callable,
_request_headers: PackedStringArray = PackedStringArray(),
_body: String = "",
) -> void:
on_complete.call(200, JSON.stringify({"token": "t"} if _url.ends_with("/token") else [])
.to_utf8_buffer(), _headers)
class _RecordingTransport:
extends HttpTransport
@@ -560,4 +665,5 @@ class _RecordingTransport:
) -> void:
_urls.append(url)
if url.ends_with("/token"):
on_complete.call(200, JSON.stringify({"token": "t"}).to_utf8_buffer())
on_complete.call(200, JSON.stringify({"token": "t"}).to_utf8_buffer(),
PackedStringArray())
+21
View File
@@ -95,6 +95,27 @@ func test_statistics_settings_are_not_saved() -> void:
assert_bool(file.has_section_key("quiz", "use_low_played_mode")).is_false()
func test_queue_ids_are_saved_and_load_back() -> void:
Settings.set_queue_id("https://music.example.com", "queue-1")
Settings.winning_score = 12
Settings.save_settings()
Settings.queue_ids.clear()
Settings.load_saved_settings()
assert_str(_saved_file().get_value("queues", "https://music.example.com")).is_equal("queue-1")
assert_dict(Settings.queue_ids).is_equal({"https://music.example.com": "queue-1"})
func test_file_without_queues_keeps_the_current_queue_ids() -> void:
Settings.save_settings()
Settings.queue_ids["https://music.example.com"] = "queue-1"
Settings.load_saved_settings()
assert_dict(Settings.queue_ids).is_equal({"https://music.example.com": "queue-1"})
func test_fullscreen_is_saved_when_it_changes() -> void:
Settings.set_fullscreen(true)
assert_bool(_saved_file().get_value("window", "fullscreen")).is_true()