"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>
120 lines
4.8 KiB
GDScript
120 lines
4.8 KiB
GDScript
class_name UploadFolder
|
|
extends RefCounted
|
|
## A folder picked or dropped to upload as a new soundtrack: its MP3s, ready to send, and
|
|
## the other song files, which are left out because the server only takes MP3.
|
|
##
|
|
## The server wants one folder of songs, so songs in subfolders, such as disc folders,
|
|
## are taken too, with the disc's number in front of their names: "Disc 1/01 Title.mp3"
|
|
## is sent as "1-01 Title.mp3". With only one disc folder, its songs keep their names.
|
|
## Hidden files and folders are left out, and names get rid of the characters the server
|
|
## refuses.
|
|
|
|
## The only kind of song the server takes.
|
|
const UPLOAD_EXTENSION: String = "mp3"
|
|
## Other song files, listed as left out so the user knows.
|
|
const OTHER_SONG_EXTENSIONS: Array[String] = [
|
|
"ogg", "wav", "flac", "m4a", "aac", "opus", "wma", "ape", "aif", "aiff",
|
|
]
|
|
## Characters the server refuses in soundtrack and file names.
|
|
const INVALID_NAME_CHARACTERS: String = "/\\:*?\"<>|"
|
|
|
|
## The folder.
|
|
var path: String = ""
|
|
## The soundtrack's name to suggest: the folder's name.
|
|
var suggested_name: String = ""
|
|
## The songs to upload, in order: [code]{"path": String, "name": String, "size": int}[/code],
|
|
## with the name to send each one as.
|
|
var songs: Array[Dictionary] = []
|
|
## The song files left out, as paths in the folder, e.g. [code]"Disc 2/03 Song.flac"[/code].
|
|
var skipped: Array[String] = []
|
|
## The size of the songs to upload, in bytes.
|
|
var total_size: int = 0
|
|
|
|
|
|
## Reads the folder at [param folder].
|
|
static func scan(folder: String) -> UploadFolder:
|
|
var result: UploadFolder = UploadFolder.new()
|
|
result.path = folder.trim_suffix("/").trim_suffix("\\")
|
|
result.suggested_name = clean_name(result.path.get_file())
|
|
result._add_files(result.path, "", "")
|
|
var discs: Array[String] = []
|
|
for disc: String in _visible(DirAccess.get_directories_at(result.path)):
|
|
if _has_songs(result.path.path_join(disc)):
|
|
discs.append(disc)
|
|
for disc: int in discs.size():
|
|
result._add_folder(result.path.path_join(discs[disc]), discs[disc],
|
|
"%d-" % (disc + 1) if discs.size() > 1 else "")
|
|
return result
|
|
|
|
|
|
## [param name] without the characters the server refuses, a leading dot, or a trailing
|
|
## dot or space.
|
|
static func clean_name(name: String) -> String:
|
|
var cleaned: String = ""
|
|
for character: String in name:
|
|
var control: bool = character.unicode_at(0) < 32 || character.unicode_at(0) == 127
|
|
cleaned += "-" if INVALID_NAME_CHARACTERS.contains(character) || control \
|
|
else character
|
|
cleaned = cleaned.strip_edges().lstrip(".")
|
|
while cleaned.ends_with(".") || cleaned.ends_with(" "):
|
|
cleaned = cleaned.left(-1)
|
|
return cleaned
|
|
|
|
|
|
## Whether there is anything to upload.
|
|
func has_songs() -> bool:
|
|
return !songs.is_empty()
|
|
|
|
|
|
# Adds the files of [param folder] and of the folders in it, all with [param prefix]
|
|
func _add_folder(folder: String, relative: String, prefix: String) -> void:
|
|
_add_files(folder, relative, prefix)
|
|
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
|
_add_folder(folder.path_join(inner), relative.path_join(inner), prefix)
|
|
|
|
|
|
func _add_files(folder: String, relative: String, prefix: String) -> void:
|
|
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
|
var extension: String = file_name.get_extension().to_lower()
|
|
if extension == UPLOAD_EXTENSION:
|
|
var file_path: String = folder.path_join(file_name)
|
|
var size: int = FileAccess.get_size(file_path)
|
|
songs.append({"path": file_path, "name": _free_name(prefix + clean_name(file_name)),
|
|
"size": size})
|
|
total_size += size
|
|
elif extension in OTHER_SONG_EXTENSIONS:
|
|
skipped.append(relative.path_join(file_name) if !relative.is_empty() else file_name)
|
|
|
|
|
|
# [param name], or with " (2)" and so on before its extension when a song already has it
|
|
func _free_name(name: String) -> String:
|
|
var taken: Array = songs.map(func(song: Dictionary) -> String: return song.name.to_lower())
|
|
var candidate: String = name
|
|
var number: int = 2
|
|
while taken.has(candidate.to_lower()):
|
|
candidate = "%s (%d).%s" % [name.get_basename(), number, name.get_extension()]
|
|
number += 1
|
|
return candidate
|
|
|
|
|
|
# Whether [param folder] or a folder in it has a song, to upload or to list as left out
|
|
static func _has_songs(folder: String) -> bool:
|
|
for file_name: String in _visible(DirAccess.get_files_at(folder)):
|
|
var extension: String = file_name.get_extension().to_lower()
|
|
if extension == UPLOAD_EXTENSION || extension in OTHER_SONG_EXTENSIONS:
|
|
return true
|
|
for inner: String in _visible(DirAccess.get_directories_at(folder)):
|
|
if _has_songs(folder.path_join(inner)):
|
|
return true
|
|
return false
|
|
|
|
|
|
# The names that are not hidden, in natural order, so "2" comes before "10"
|
|
static func _visible(names: PackedStringArray) -> Array[String]:
|
|
var shown: Array[String] = []
|
|
for name: String in names:
|
|
if !name.begins_with("."):
|
|
shown.append(name)
|
|
shown.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0)
|
|
return shown
|