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,119 @@
|
||||
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
|
||||
Reference in new issue
Block a user