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>
This commit is contained in:
1 parent
f39865fffc
commit
c86dacb890
25 files changed
+3306
-4314
No files matched your search
@@ -0,0 +1,220 @@
|
||||
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
|
||||
Reference in new issue
Block a user