Smart download filenames: 'Artist - Title - REEL.mp4' + validation

Two improvements:

1. DOWNLOAD FILENAME: instead of 'reel_<job-id>.mp4' (e.g. reel_25e076af7600.mp4),
   downloads now have descriptive names like:
   - 'Lady Gaga - Abracadabra - REEL.mp4'
   - 'Modrijani - S teboj - REEL.mp4'
   - 'Sarah Connor - FICKA - REEL.mp4'

2. PRE-UPLOAD VALIDATION: when filename doesn't follow 'Artist - Title' format,
   browser prompts user for both fields. Without them, upload is blocked.
   This prevents files with names like '12345.mp4' or 'video_final.mp4' from
   being processed without identifying info.

Implementation:
- parse_artist_title() helper handles common formats:
  - 'Artist - Title.mp4' / 'Artist – Title' (em-dash)
  - 'Artist | Title' / 'Artist : Title'
  - Strips noise: '(Official Music Video)', '(Audio)', '(HD)', '[Lyric Video]'
- Client-side parser mirrors backend (validation before upload)
- Backend accepts artist + title form fields (override parsed)
- Job stored with parsed_artist + parsed_title + has_clean_name fields
- YouTube jobs auto-fetch title via yt-dlp --info-only and parse it
- Filename hint to Scribe/Claude uses parsed values (cleaner than raw filename)
- Download endpoint uses build_download_filename() for content-disposition
- Jobs list shows 'Artist — Title' instead of raw filename

Result: downloaded reels are auto-named correctly for Facebook/Instagram
upload, no more renaming files manually.
This commit is contained in:
2026-04-29 14:15:18 +00:00
parent a30137f1f2
commit 3877b822ff
2 changed files with 227 additions and 11 deletions
+84 -7
View File
@@ -443,11 +443,13 @@
const dz = $("#dropzone");
const fileInput = $("#file-input");
let pendingFile = null;
let pendingArtist = null;
let pendingTitle = null;
dz.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", () => {
if (fileInput.files[0]) {
pendingFile = fileInput.files[0];
dz.querySelector("div").textContent = `📹 ${pendingFile.name}`;
handleFileSelected(fileInput.files[0]);
}
});
["dragover", "dragenter"].forEach(ev =>
@@ -456,11 +458,82 @@
dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.remove("drag"); }));
dz.addEventListener("drop", e => {
const f = e.dataTransfer.files[0];
if (f) {
pendingFile = f;
dz.querySelector("div").textContent = `📹 ${f.name}`;
}
if (f) handleFileSelected(f);
});
// Klient-side parser (mora ustrezati backend parse_artist_title)
function parseArtistTitle(filename) {
if (!filename) return [null, null];
let name = filename.replace(/\.[^.]+$/, ""); // remove ext
// Odstrani noise
const noise = [
/\(Official\s+(?:Music\s+)?Video\)/gi,
/\(Officia[lk]\s+Audio\)/gi,
/\(Offizielles\s+(?:Musik)?[Vv]ideo\)/gi,
/\(Lyric[s]?\s+Video\)/gi,
/\(Audio\)/gi,
/\(HD\)|\(HQ\)|\(4K\)/gi,
/\(Live\)|\(Remix\)|\(Remaster(?:ed)?\s*\d{0,4}\)/gi,
/\[Official.*?\]|\[Music.*?\]|\[Audio.*?\]/gi,
/\bofficial\s+video\b|\bofficial\s+audio\b/gi,
/\boriginal\s+(?:video|audio)\b/gi,
/\bMV\b|\b4K\b|\bHD\b|\bHQ\b/g,
];
for (const r of noise) name = name.replace(r, "");
name = name.replace(/\s+/g, " ").trim();
// Probaj separatorje
for (const sep of [" - ", " – ", " — ", " | ", " : "]) {
if (name.includes(sep)) {
const parts = name.split(sep);
if (parts.length >= 2) {
const artist = parts[0].trim().replace(/^[\s\-–—|.:_]+|[\s\-–—|.:_]+$/g, "");
const title = parts.slice(1).join(sep).trim().replace(/^[\s\-–—|.:_]+|[\s\-–—|.:_]+$/g, "");
if (artist && title) return [artist, title];
}
}
}
return [null, null];
}
function handleFileSelected(f) {
const [artist, title] = parseArtistTitle(f.name);
if (!artist || !title) {
// Ni razvidno ime — vprašaj uporabnika
const userArtist = prompt(
`❗ Iz imena datoteke ni razviden izvajalec in naslov.\n\n` +
`Datoteka: "${f.name}"\n\n` +
`Vpiši IZVAJALCA (npr. "Lady Gaga"):`,
""
);
if (!userArtist || !userArtist.trim()) {
alert("⛔ Brez izvajalca ne morem nadaljevati.\n\nPoimenuj datoteko v formatu:\n Izvajalec - Naslov.mp4");
fileInput.value = "";
return;
}
const userTitle = prompt(
`Vpiši NASLOV pesmi (npr. "Abracadabra"):`,
""
);
if (!userTitle || !userTitle.trim()) {
alert("⛔ Brez naslova ne morem nadaljevati.");
fileInput.value = "";
return;
}
pendingArtist = userArtist.trim();
pendingTitle = userTitle.trim();
} else {
pendingArtist = artist;
pendingTitle = title;
}
pendingFile = f;
dz.querySelector("div").innerHTML =
`📹 <b>${pendingArtist} — ${pendingTitle}</b>` +
`<div style="font-size: 11px; color: var(--muted); margin-top: 4px;">${f.name} (${(f.size/1024/1024).toFixed(1)} MB)</div>`;
}
// ─── Settings collector ─────────────────────────
function collectSettings() {
@@ -610,6 +683,8 @@
}
const fd = new FormData();
fd.append("file", pendingFile);
if (pendingArtist) fd.append("artist", pendingArtist);
if (pendingTitle) fd.append("title", pendingTitle);
showLive("Nalaganje datoteke", `${pendingFile.name} (${(pendingFile.size / 1024 / 1024).toFixed(1)} MB)`, 0);
@@ -755,7 +830,9 @@
const title = job.source_type === "youtube"
? (job.youtube_url || "YouTube")
: (job.filename || job.id);
: (job.parsed_artist && job.parsed_title
? `${job.parsed_artist} — ${job.parsed_title}`
: (job.filename || job.id));
const sizeStr = job.output_size_mb ? `${job.output_size_mb} MB` :
job.size_mb ? `${job.size_mb} MB` : "";