Files
MusicPlayer/popups/upload_popup.gd
T
SansanandClaude Opus 5.5 c86dacb890
Build / test (pull_request) Successful in 2m3s
Build / build (pull_request) Skipped
Add a soundtrack by picking or dropping a folder (#50)
"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>
2026-10-15 21:43:59 +02:00

221 lines
7.1 KiB
GDScript

class_name UploadPopup
extends BasePopup
## Adds a new soundtrack to the server from a folder: picked with Add soundtrack… in the
## statistics, or dropped on the window.
##
## Lists the folder's songs (see [UploadFolder]), lets the name be changed, and uploads
## the songs one at a time with a progress bar. A song that fails is sent again a couple
## of times before the upload is cancelled. The server then adds the soundtrack at once,
## without a sync.
## Emitted when the server has added the soundtrack.
signal soundtrack_added(soundtrack: SoundtrackWithSongList)
## How many times a song that failed is sent again before the upload is given up.
const RETRIES: int = 2
## The folder shown.
var folder: UploadFolder
# The upload on the server, while there is one
var _upload_id: String = ""
# The song being sent, and how often it failed
var _next: int = 0
var _failures: int = 0
var _uploading: bool = false
@onready var name_field: LineEdit = %NameField
@onready var choose_folder_button: Button = %ChooseFolderButton
@onready var folder_label: Label = %FolderLabel
@onready var song_list: VBoxContainer = %SongList
@onready var summary_label: Label = %SummaryLabel
@onready var upload_progress: ProgressBar = %UploadProgress
@onready var status_label: Label = %StatusLabel
@onready var cancel_button: Button = %CancelButton
@onready var upload_button: Button = %UploadButton
@onready var folder_dialog: FileDialog = %FolderDialog
func _ready() -> void:
choose_folder_button.pressed.connect(folder_dialog.popup_centered)
folder_dialog.dir_selected.connect(show_folder)
cancel_button.pressed.connect(close)
upload_button.pressed.connect(upload)
name_field.text_changed.connect(func(_text: String) -> void: _update_buttons())
name_field.text_submitted.connect(func(_text: String) -> void: upload())
## Asks for a folder, then shows it.
func choose_folder() -> void:
folder_dialog.popup_centered()
## Lists the songs in the folder at [param path] and opens the popup.
func show_folder(path: String) -> void:
folder = UploadFolder.scan(path)
name_field.text = folder.suggested_name
folder_label.text = "From " + short_path(folder.path)
folder_label.tooltip_text = folder.path
_fill_song_list()
summary_label.text = summary_text(folder)
upload_progress.visible = false
status_label.visible = false
upload_button.visible = true
cancel_button.text = "Cancel"
if Settings.is_offline():
_show_status("Adding a soundtrack needs the server, not a music folder.")
elif !folder.has_songs():
_show_status("There are no MP3 files in %s." % folder.path.get_file())
_set_uploading(false)
open()
## Starts uploading the songs as a soundtrack with the name in the name field.
func upload() -> void:
if upload_button.disabled || !upload_button.visible:
return
if Settings.currently_syncing:
_show_status("The server is syncing. Try again when it is done.")
return
_set_uploading(true)
upload_progress.max_value = folder.songs.size()
upload_progress.value = 0
upload_progress.visible = true
_show_status("Starting the upload…")
MusicServerClient.start_upload(UploadFolder.clean_name(name_field.text), _on_started,
func() -> void: _stop("Could not start the upload"))
## Stops an upload under way, and closes the popup.
func close() -> void:
if _uploading:
_uploading = false
_cancel_on_server()
super()
## The last two parts of [param path], e.g. "…/Soundtracks/Chrono Trigger", since the
## start of a long path says the least.
static func short_path(path: String) -> String:
var parts: PackedStringArray = path.replace("\\", "/").split("/", false)
if parts.size() <= 2:
return path
return "…/" + "/".join(parts.slice(-2))
## "30 songs, 245.1 MB", and how many files are left out.
static func summary_text(shown: UploadFolder) -> String:
var text: String = "%d %s, %s" % [shown.songs.size(),
"song" if shown.songs.size() == 1 else "songs",
SettingsPopup.size_text(shown.total_size)]
if !shown.skipped.is_empty():
text += ". %d %s left out: only MP3 files can be uploaded." % [shown.skipped.size(),
"file" if shown.skipped.size() == 1 else "files"]
return text
func _on_started(upload_info: Upload) -> void:
if !_uploading:
# Closed while the server was answering
MusicServerClient.cancel_upload(upload_info.id)
return
_upload_id = upload_info.id
_next = 0
_failures = 0
_send_next()
func _send_next() -> void:
if !_uploading:
return
if _next >= folder.songs.size():
_show_status("Adding the soundtrack…")
MusicServerClient.finish_upload(_upload_id, _on_finished,
func() -> void: _stop("Could not add the soundtrack"))
return
var song: Dictionary = folder.songs[_next]
_show_status("Uploading %d of %d: %s" % [_next + 1, folder.songs.size(), song.name])
# One song at a time, so only one is held in memory
var bytes: PackedByteArray = FileAccess.get_file_as_bytes(song.path)
if bytes.is_empty():
MusicServerClient.last_error = "the file could not be read"
_stop("Could not upload " + song.name)
return
MusicServerClient.upload_song(_upload_id, song.name, bytes, _on_song_sent, _on_song_failed)
func _on_song_sent() -> void:
_next += 1
_failures = 0
upload_progress.value = _next
_send_next()
func _on_song_failed() -> void:
_failures += 1
if _failures <= RETRIES && _uploading:
_send_next()
return
_stop("Could not upload " + folder.songs[_next].name)
func _on_finished(soundtrack: SoundtrackWithSongList) -> void:
_upload_id = ""
_uploading = false
upload_progress.value = upload_progress.max_value
_show_status("Added %s with %d %s." % [soundtrack.name, soundtrack.songs.size(),
"song" if soundtrack.songs.size() == 1 else "songs"])
upload_button.visible = false
cancel_button.text = "Close"
_set_uploading(false)
soundtrack_added.emit(soundtrack)
# Gives up, says why with the server's reason, and removes what was uploaded
func _stop(what: String) -> void:
var reason: String = MusicServerClient.last_error
_cancel_on_server()
_uploading = false
upload_progress.visible = false
_set_uploading(false)
_show_status("%s: %s." % [what,
reason if !reason.is_empty() else "the server could not be reached"])
func _cancel_on_server() -> void:
if !_upload_id.is_empty():
MusicServerClient.cancel_upload(_upload_id)
_upload_id = ""
func _set_uploading(uploading: bool) -> void:
_uploading = uploading
name_field.editable = !uploading
choose_folder_button.disabled = uploading
_update_buttons()
func _update_buttons() -> void:
upload_button.disabled = _uploading || folder == null || !folder.has_songs() \
|| UploadFolder.clean_name(name_field.text).is_empty() || Settings.is_offline()
func _fill_song_list() -> void:
Settings.delete_children(song_list)
var row: int = 0
for song: Dictionary in folder.songs:
song_list.add_child(ListRow.create("%s · %s" % [song.name,
SettingsPopup.size_text(song.size)], row, &"TextMedium"))
row += 1
for skipped: String in folder.skipped:
var list_row: ListRow = ListRow.create("%s · not MP3, left out" % skipped, row,
&"TextMedium")
list_row.label.self_modulate.a = 0.5
song_list.add_child(list_row)
row += 1
func _show_status(message: String) -> void:
status_label.text = message
status_label.visible = true