diff --git a/editor.py b/editor.py index adbd3b7..df00166 100644 --- a/editor.py +++ b/editor.py @@ -3,6 +3,7 @@ import subprocess import threading import time import uuid +import json from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo @@ -224,7 +225,48 @@ def create_waveform(filename): return None -def create_finished(filename, start, end, progress_callback=None): +def read_metadata(path): + """Read common ID3/FFmpeg metadata from an MP3.""" + try: + result = subprocess.run( + ["ffprobe", "-v", "error", "-show_entries", "format_tags=date,artist,title,album", + "-of", "json", str(path)], + capture_output=True, text=True, timeout=10, + ) + if result.returncode != 0: + return {} + data = json.loads(result.stdout) + tags = data.get("format", {}).get("tags", {}) or {} + return {str(k).lower(): str(v) for k, v in tags.items() if v is not None} + except Exception: + return {} + + +def metadata_datetime(path): + tags = read_metadata(path) + value = tags.get("date", "") + if value: + try: + dt = datetime.fromisoformat(value.replace("Z", "+00:00")) + return dt.astimezone(ZoneInfo("Europe/Berlin")).strftime("%d.%m.%Y %H:%M:%S") + except ValueError: + pass + return format_datetime(path.stat().st_mtime) + + +def metadata_form_value(path): + tags = read_metadata(path) + value = tags.get("date", "") + if value: + try: + dt = datetime.fromisoformat(value.replace("Z", "+00:00")) + return dt.astimezone(ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%dT%H:%M") + except ValueError: + pass + return datetime.fromtimestamp(path.stat().st_mtime, ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%dT%H:%M") + + +def create_finished(filename, start, end, metadata=None, progress_callback=None): source = source_path(filename) if source is None: raise ValueError("Aufnahme nicht gefunden") @@ -249,6 +291,7 @@ def create_finished(filename, start, end, progress_callback=None): destination = finished_path(filename) temp = FINISHED_DIR / f".{filename}.tmp.mp3" selected_duration = end - start + metadata = metadata or {} cmd = [ "ffmpeg", "-hide_banner", "-loglevel", "error", "-y", @@ -262,6 +305,10 @@ def create_finished(filename, start, end, progress_callback=None): "-c:a", "libmp3lame", "-b:a", "128k", "-avoid_negative_ts", "make_zero", + "-metadata", f"date={metadata.get('date', '')}", + "-metadata", f"artist={metadata.get('artist', '')}", + "-metadata", f"title={metadata.get('title', '')}", + "-metadata", f"album={metadata.get('album', '')}", "-progress", "pipe:1", "-nostats", str(temp), @@ -322,7 +369,7 @@ def create_finished(filename, start, end, progress_callback=None): raise -def run_edit_job(job_id, filename, start, end): +def run_edit_job(job_id, filename, start, end, metadata): def update(**values): with JOBS_LOCK: job = JOBS.get(job_id) @@ -332,7 +379,7 @@ def run_edit_job(job_id, filename, start, end): update(status="running", progress=0.0) try: destination, duration = create_finished( - filename, start, end, + filename, start, end, metadata=metadata, progress_callback=lambda value: update(progress=value), ) update(status="done", progress=100.0, filename=destination.name, @@ -391,6 +438,14 @@ button:disabled { opacity:.45; cursor:not-allowed; } .message { margin-top:15px; padding:10px 12px; border-radius:6px; display:none; } .message.ok { display:block; background:#173d24; color:#b8f5c9; } .message.error { display:block; background:#4a1b1b; color:#ffd0d0; } +.meta-dialog { position:fixed; inset:0; background:rgba(0,0,0,.78); display:none; align-items:center; justify-content:center; z-index:900; padding:20px; } +.meta-dialog.active { display:flex; } +.meta-card { width:min(560px,100%); background:#202020; border:1px solid #444; border-radius:10px; padding:22px; box-shadow:0 10px 40px #000; } +.meta-card h2 { margin-top:0; } +.meta-card label { display:block; margin:14px 0; color:#ccc; } +.meta-card input { display:block; width:100%; margin-top:6px; padding:10px; background:#111; color:#eee; border:1px solid #555; border-radius:5px; font-size:16px; } +.meta-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:20px; } +.meta-actions button { margin:0; } .progress-box { display:none; margin-top:16px; } .progress-box.active { display:block; } .progress-track { height:18px; background:#292929; border-radius:9px; overflow:hidden; } @@ -460,7 +515,17 @@ button:disabled { opacity:.45; cursor:not-allowed; } Original herunterladen
| Datei | Datum | Größe | Dauer | ||||
|---|---|---|---|---|---|---|---|
| Zeitpunkt | Redner | Beschreibung | Veranstaltung | Dauer | |||
| {{ item.filename }} | {{ item.datetime }} | -{{ item.size }} | +{{ item.artist or "–" }} | +{{ item.title or "–" }} | +{{ item.album or "–" }} | {{ item.duration }} | ▶ Abspielen / Download @@ -954,18 +998,22 @@ def index(): for path in FINISHED_DIR.glob("*.mp3"): try: stat = path.stat() + tags = read_metadata(path) finished.append({ "filename": path.name, - "datetime": format_datetime(stat.st_mtime), + "datetime": tags.get("date", "") or format_datetime(stat.st_mtime), + "artist": tags.get("artist", ""), + "title": tags.get("title", ""), + "album": tags.get("album", ""), "size": format_size(stat.st_size), "duration": format_duration(get_duration(path)), - "mtime": stat.st_mtime, + "sort_date": tags.get("date", "") or datetime.fromtimestamp(stat.st_mtime, ZoneInfo("Europe/Berlin")).isoformat(), }) except FileNotFoundError: pass recordings.sort(key=lambda x: x["mtime"], reverse=True) - finished.sort(key=lambda x: x["mtime"], reverse=True) + finished.sort(key=lambda x: x["sort_date"], reverse=True) return render_template_string( INDEX_HTML, @@ -994,6 +1042,8 @@ def edit_recording(filename): filename=filename, duration=format_duration(duration), duration_seconds=duration, + metadata=read_metadata(path), + metadata_datetime=metadata_form_value(path), waveform_url=url_for("waveform", filename=filename), audio_url=url_for("recording", filename=filename), ) @@ -1056,6 +1106,7 @@ def api_edit(): filename = data.get("filename") start = data.get("start") end = data.get("end") + metadata = data.get("metadata") or {} source = source_path(filename) if source is None: @@ -1072,7 +1123,22 @@ def api_edit(): with JOBS_LOCK: JOBS[job_id] = {"status":"queued", "progress":0.0, "filename":filename, "created_at":time.time()} - threading.Thread(target=run_edit_job, args=(job_id, filename, start_f, end_f), daemon=True).start() + metadata = { + "date": str(metadata.get("date", "")).strip(), + "artist": str(metadata.get("artist", "")).strip(), + "title": str(metadata.get("title", "")).strip(), + "album": str(metadata.get("album", "")).strip(), + } + if not metadata["date"]: + return jsonify({"success": False, "error": "Zeitpunkt fehlt"}), 400 + try: + dt = datetime.fromisoformat(metadata["date"]) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=ZoneInfo("Europe/Berlin")) + metadata["date"] = dt.isoformat() + except ValueError: + return jsonify({"success": False, "error": "Ungültiger Zeitpunkt"}), 400 + threading.Thread(target=run_edit_job, args=(job_id, filename, start_f, end_f, metadata), daemon=True).start() return jsonify({"success": True, "job_id": job_id}), 202 |