Add a soundtrack by picking or dropping a folder (#50)
Build / test (pull_request) Successful in 2m3s
Build / build (pull_request) Skipped

"Add soundtrack…" in the statistics toolbar, or a folder dropped anywhere on
the window, opens a popup that lists the folder's songs and uploads them to
the server, which adds the soundtrack at once (Sansan/MusicServer#33).

- The name comes from the folder and can be changed; characters the server
  refuses are replaced.
- Only MP3s are uploaded, as the server takes no other; other song files
  are listed greyed out as left out. Hidden files are skipped.
- Disc folders are flattened with the disc's number first ("1-01 Title.mp3"),
  as the server wants one folder. With one disc folder, names stay as
  they are.
- Songs are sent one at a time with a progress bar, reading one file at a
  time. A failed song is sent again twice, then the upload is cancelled
  and the server's reason shown. Closing the popup cancels it too.
- Not offered while the server syncs, offline, or without MP3s.
- Afterwards search, the inspiration list and the statistics reload.

The API types come from the server's spec (Upload, UploadFile,
UploadRequest).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Opus 5.5 committed 2026-10-15 21:43:59 +02:00
1 parent f39865fffc
commit c86dacb890
25 files changed
+3306 -4314

No files matched your search

+10
View File
@@ -43,6 +43,7 @@ var queue_id: String = ""
var _responses: Dictionary = {}
var _successes_left: Dictionary = {}
var _failures_left: Dictionary = {}
var _held_paths: Dictionary = {}
var _previous_transport: HttpTransport
var _previous_queue_ids: Dictionary[String, String]
@@ -89,6 +90,11 @@ func respond_bytes(path: String, body: PackedByteArray, response_code: int = 200
_successes_left.erase(path)
## Answers the next [param failures] requests to [param path] with a 503, then as usual.
func fail_first(path: String, failures: int) -> void:
_failures_left[path] = failures
## Lets [param successes] more requests to [param path] succeed, then answers 503 until
## a new response is registered for the path.
func fail_after(path: String, successes: int) -> void:
@@ -156,6 +162,10 @@ func _answer(path: String, on_complete: Callable, follow_redirects: bool = true)
var headers: PackedStringArray = []
if !queue_id.is_empty() && path != "/token":
headers.append("X-Queue-Id: " + queue_id)
if _failures_left.get(path, 0) > 0:
_failures_left[path] -= 1
on_complete.call(503, PackedByteArray(), headers)
return
if _successes_left.has(path):
if _successes_left[path] == 0:
on_complete.call(503, PackedByteArray(), headers)
+35
View File
@@ -72,6 +72,25 @@ func _init(main: Control) -> void:
_main = main
## A folder like a ripped two-disc soundtrack, to upload: MP3s in disc folders, and a
## FLAC that is left out.
static func _upload_folder() -> String:
var folder: String = Settings.config_dir().path_join("upload/Chrono Trigger")
var mp3: PackedByteArray = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
var files: Array[String] = []
for disc: int in [1, 2]:
for track: int in 4:
files.append("Disc %d/%02d %s.mp3" % [disc, track + 1, JAPANESE_SONGS[track]])
files.append("Disc 2/05 Bonus.flac")
for relative: String in files:
var path: String = folder.path_join(relative)
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
file.store_buffer(mp3)
file.close()
return folder
## Returns a fake server that answers every request the main window makes on the tour.
## Call [method FakeHttpTransport.install] before the main window is created.
static func make_server() -> FakeHttpTransport:
@@ -343,6 +362,22 @@ func run(on_state: Callable) -> void:
await on_state.call("popup_SoundtrackStats_cover", stats_popup)
stats_popup.stop_editing()
stats_popup.close()
await _settle()
var upload_popup: UploadPopup = _node("UploadPopup")
upload_popup.show_folder(_upload_folder())
await _settle()
await on_state.call("popup_UploadWindow", upload_popup)
upload_popup._set_uploading(true)
upload_popup.upload_progress.max_value = upload_popup.folder.songs.size()
upload_popup.upload_progress.value = 3
upload_popup.upload_progress.visible = true
upload_popup._show_status("Uploading 4 of %d: %s" % [upload_popup.folder.songs.size(),
upload_popup.folder.songs[3].name])
await _settle()
await on_state.call("popup_UploadWindow_uploading", upload_popup)
upload_popup._set_uploading(false)
upload_popup.close()
_main.show_tab(&"quiz")
await _settle()