diff --git a/editor.py b/editor.py index a6c6323..4fdd795 100644 --- a/editor.py +++ b/editor.py @@ -165,7 +165,74 @@ def write_metadata(path, metadata): raise ValueError("Speichern der ID3-Tags hat zu lange gedauert") except OSError as exc: temp.unlink(missing_ok=True) - raise ValueError(f"ID3-Tags konnten nicht gespeichert werden: {exc}") + raise ValueError(f"Speichern der ID3-Tags fehlgeschlagen: {exc}") + + +def update_finished_file(old_filename, new_filename, metadata): + """Update metadata and optionally rename a finished file in one operation.""" + old_path = finished_path(old_filename) + new_filename = clean_new_filename(new_filename) + if old_path is None or not old_path.is_file(): + raise ValueError("Datei nicht gefunden") + if new_filename is None: + raise ValueError("Ungültiger Dateiname") + + new_path = FINISHED_DIR / new_filename + if new_path != old_path and new_path.exists(): + raise ValueError("Eine Datei mit diesem Namen existiert bereits") + + date = str(metadata.get("date", "")).strip() + if not date: + raise ValueError("Zeitpunkt fehlt") + try: + dt = datetime.fromisoformat(date) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=ZoneInfo("Europe/Berlin")) + date = dt.isoformat() + except ValueError: + raise ValueError("Ungültiger Zeitpunkt") + + values = { + "date": date, + "artist": str(metadata.get("artist", "")).strip(), + "title": str(metadata.get("title", "")).strip(), + "album": str(metadata.get("album", "")).strip(), + } + + # Build the complete new MP3 first. The original remains untouched until + # FFmpeg has successfully written the new file. + temp = FINISHED_DIR / f".{new_path.name}.update.tmp.mp3" + temp.unlink(missing_ok=True) + cmd = [ + "ffmpeg", "-hide_banner", "-loglevel", "error", "-y", + "-i", str(old_path), + "-map", "0", + "-c", "copy", + "-map_metadata", "-1", + ] + for key, value in values.items(): + cmd += ["-metadata", f"{key}={value}"] + cmd.append(str(temp)) + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + if result.returncode != 0 or not temp.is_file() or temp.stat().st_size == 0: + raise ValueError("ID3-Tags konnten nicht gespeichert werden") + + temp.replace(new_path) + if new_path != old_path: + old_path.unlink() + + return new_path + except subprocess.TimeoutExpired: + temp.unlink(missing_ok=True) + raise ValueError("Speichern der Datei hat zu lange gedauert") + except OSError as exc: + temp.unlink(missing_ok=True) + raise ValueError(f"Datei konnte nicht gespeichert werden: {exc}") + except ValueError: + temp.unlink(missing_ok=True) + raise def delete_file(kind, filename): @@ -1013,20 +1080,15 @@ async function saveFinishedMeta() { album: document.getElementById('finishedAlbum').value.trim() }; - let targetFilename = filename; - if (newFilename.replace(/\.mp3$/i, '') !== filename.replace(/\.mp3$/i, '')) { - const renameResponse = await fetch('/api/file', { - method: 'POST', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({action:'rename', kind:'finished', filename, new_filename:newFilename}) - }); - const renameResult = await renameResponse.json(); - if (!renameResponse.ok || !renameResult.success) throw new Error(renameResult.error || `HTTP ${renameResponse.status}`); - targetFilename = renameResult.filename; - } - const response = await fetch('/api/file', { method: 'POST', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({action:'update_metadata', kind:'finished', filename:targetFilename, metadata}) + body: JSON.stringify({ + action: 'update_finished', + kind: 'finished', + filename, + new_filename: newFilename, + metadata + }) }); const result = await response.json(); if (!response.ok || !result.success) throw new Error(result.error || `HTTP ${response.status}`); @@ -1204,6 +1266,13 @@ def api_file(): if action == "rename": path = rename_file(kind, filename, data.get("new_filename")) return jsonify({"success": True, "filename": path.name}) + if action == "update_finished": + if kind != "finished": + return jsonify({"success": False, "error": "Metadaten können nur bei bearbeiteten Aufnahmen geändert werden"}), 400 + metadata = data.get("metadata") or {} + path = update_finished_file(filename, data.get("new_filename"), metadata) + return jsonify({"success": True, "filename": path.name}) + if action == "update_metadata": if kind != "finished": return jsonify({"success": False, "error": "Metadaten können nur bei bearbeiteten Aufnahmen geändert werden"}), 400