Add Docker stack, Reverb live progress, and duplicate upload detection.
Ship FrankenPHP Compose services with Reverb WebSockets for real-time transcription status, skip re-uploading identical audio via content hash, and format disk usage without requiring intl.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Upload dropzone: discard duplicate files in the selection (and known server fingerprints)
|
||||
* before submitting the form.
|
||||
*/
|
||||
|
||||
export function uploadDropzone({ existingFingerprints = [] } = {}) {
|
||||
const acceptExt = ['.mp3', '.wav', '.ogg', '.oga', '.flac', '.m4a', '.mp4', '.aac', '.webm', '.wma', '.aiff', '.aif'];
|
||||
const known = new Set(existingFingerprints);
|
||||
|
||||
return {
|
||||
files: [],
|
||||
dragging: false,
|
||||
uploading: false,
|
||||
error: null,
|
||||
notice: null,
|
||||
|
||||
get uploadLabel() {
|
||||
if (this.uploading) {
|
||||
return 'Uploading…';
|
||||
}
|
||||
|
||||
if (this.files.length <= 1) {
|
||||
return 'Upload';
|
||||
}
|
||||
|
||||
return 'Upload ' + this.files.length + ' files';
|
||||
},
|
||||
|
||||
onBrowse(event) {
|
||||
this.addFiles(Array.from(event.target.files || []));
|
||||
},
|
||||
|
||||
onDrop(event) {
|
||||
this.dragging = false;
|
||||
this.addFiles(Array.from(event.dataTransfer?.files || []));
|
||||
},
|
||||
|
||||
addFiles(incoming) {
|
||||
this.error = null;
|
||||
this.notice = null;
|
||||
|
||||
const accepted = [];
|
||||
let skippedUnsupported = 0;
|
||||
let skippedTooLarge = 0;
|
||||
let skippedDuplicates = 0;
|
||||
|
||||
for (const file of incoming) {
|
||||
if (! this.isAccepted(file)) {
|
||||
skippedUnsupported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
skippedTooLarge++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fingerprint = this.fingerprint(file);
|
||||
|
||||
if (known.has(fingerprint) || this.files.some((existing) => this.fingerprint(existing) === fingerprint)) {
|
||||
skippedDuplicates++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (accepted.some((existing) => this.fingerprint(existing) === fingerprint)) {
|
||||
skippedDuplicates++;
|
||||
continue;
|
||||
}
|
||||
|
||||
accepted.push(file);
|
||||
}
|
||||
|
||||
this.files = [...this.files, ...accepted];
|
||||
|
||||
if (this.files.length > 50) {
|
||||
this.error = 'You can upload at most 50 files at once.';
|
||||
this.files = this.files.slice(0, 50);
|
||||
}
|
||||
|
||||
if (skippedUnsupported > 0) {
|
||||
this.error = 'Skipped unsupported file type. Use common audio formats only.';
|
||||
} else if (skippedTooLarge > 0) {
|
||||
this.error = 'Skipped a file larger than 100 MB.';
|
||||
}
|
||||
|
||||
if (skippedDuplicates > 0) {
|
||||
this.notice = skippedDuplicates === 1
|
||||
? 'Skipped 1 duplicate file.'
|
||||
: `Skipped ${skippedDuplicates} duplicate files.`;
|
||||
}
|
||||
|
||||
this.syncInput();
|
||||
},
|
||||
|
||||
isAccepted(file) {
|
||||
const name = (file.name || '').toLowerCase();
|
||||
|
||||
if (acceptExt.some((ext) => name.endsWith(ext))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (file.type || '').startsWith('audio/');
|
||||
},
|
||||
|
||||
fingerprint(file) {
|
||||
return `${String(file.name || '').toLowerCase()}:${Number(file.size) || 0}`;
|
||||
},
|
||||
|
||||
fileListKey(file, index = 0) {
|
||||
return `${this.fingerprint(file)}:${index}`;
|
||||
},
|
||||
|
||||
removeFile(index) {
|
||||
this.files.splice(index, 1);
|
||||
this.syncInput();
|
||||
},
|
||||
|
||||
clearFiles() {
|
||||
this.files = [];
|
||||
this.notice = null;
|
||||
this.syncInput();
|
||||
},
|
||||
|
||||
syncInput() {
|
||||
const input = this.$refs.fileInput;
|
||||
|
||||
if (! input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transfer = new DataTransfer();
|
||||
this.files.forEach((file) => transfer.items.add(file));
|
||||
input.files = transfer.files;
|
||||
},
|
||||
|
||||
ensureFilesSelected(event) {
|
||||
if (this.files.length === 0) {
|
||||
event.preventDefault();
|
||||
this.error = 'Drop or choose at least one audio file.';
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
this.error = null;
|
||||
},
|
||||
|
||||
formatSize(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
if (bytes < 1024 * 1024) {
|
||||
return (bytes / 1024).toFixed(1) + ' KB';
|
||||
}
|
||||
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user