"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>
221 lines
7.1 KiB
GDScript
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
|