editor.py hinzugefügt
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from flask import Flask, abort, jsonify, render_template_string, request, send_from_directory, url_for
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
RECORDINGS_DIR = Path(os.environ.get("RECORDINGS_DIR", "/recordings"))
|
||||
FINISHED_DIR = Path(os.environ.get("FINISHED_DIR", "/finished"))
|
||||
WAVEFORMS_DIR = Path(os.environ.get("WAVEFORMS_DIR", "/waveforms"))
|
||||
WEB_PORT = int(os.environ.get("WEB_PORT", "8080"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def format_datetime(timestamp):
|
||||
return datetime.fromtimestamp(timestamp, ZoneInfo("Europe/Berlin")).strftime(
|
||||
"%d.%m.%Y %H:%M:%S"
|
||||
)
|
||||
|
||||
|
||||
def format_size(size):
|
||||
if size < 1024:
|
||||
return f"{size} B"
|
||||
if size < 1024 ** 2:
|
||||
return f"{size / 1024:.1f} KB"
|
||||
if size < 1024 ** 3:
|
||||
return f"{size / 1024 ** 2:.1f} MB"
|
||||
return f"{size / 1024 ** 3:.2f} GB"
|
||||
|
||||
|
||||
def format_duration(seconds):
|
||||
if seconds is None:
|
||||
return "–"
|
||||
seconds = int(seconds)
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
seconds = seconds % 60
|
||||
if hours:
|
||||
return f"{hours}:{minutes:02d}:{seconds:02d}"
|
||||
return f"{minutes}:{seconds:02d}"
|
||||
|
||||
|
||||
def safe_filename(filename):
|
||||
if not filename or Path(filename).name != filename:
|
||||
return None
|
||||
if not filename.lower().endswith(".mp3"):
|
||||
return None
|
||||
return filename
|
||||
|
||||
|
||||
def source_path(filename):
|
||||
filename = safe_filename(filename)
|
||||
if filename is None:
|
||||
return None
|
||||
path = RECORDINGS_DIR / filename
|
||||
return path if path.is_file() else None
|
||||
|
||||
|
||||
def finished_path(filename):
|
||||
filename = safe_filename(filename)
|
||||
if filename is None:
|
||||
return None
|
||||
return FINISHED_DIR / filename
|
||||
|
||||
|
||||
def get_duration(path):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffprobe", "-v", "error",
|
||||
"-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||
str(path),
|
||||
],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
return float(result.stdout.strip())
|
||||
except (OSError, ValueError, subprocess.SubprocessError):
|
||||
return None
|
||||
|
||||
|
||||
def waveform_path(filename):
|
||||
return WAVEFORMS_DIR / f"{filename}.png"
|
||||
|
||||
|
||||
def create_waveform(filename):
|
||||
source = source_path(filename)
|
||||
if source is None:
|
||||
return None
|
||||
|
||||
output = waveform_path(filename)
|
||||
WAVEFORMS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
if output.is_file() and output.stat().st_mtime >= source.stat().st_mtime:
|
||||
return output
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
temp = output.with_suffix(".tmp.png")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
|
||||
"-i", str(source),
|
||||
"-filter_complex",
|
||||
"aformat=channel_layouts=mono,showwavespic=s=2400x240:split_channels=0",
|
||||
"-frames:v", "1",
|
||||
str(temp),
|
||||
],
|
||||
capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
if result.returncode != 0 or not temp.is_file():
|
||||
print(f"[waveform] failed for {filename}: {result.stderr.strip()}", flush=True)
|
||||
return None
|
||||
temp.replace(output)
|
||||
return output
|
||||
except Exception as exc:
|
||||
print(f"[waveform] failed for {filename}: {exc}", flush=True)
|
||||
try:
|
||||
temp.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def create_finished(filename, start, end):
|
||||
source = source_path(filename)
|
||||
if source is None:
|
||||
raise ValueError("Aufnahme nicht gefunden")
|
||||
|
||||
try:
|
||||
start = float(start)
|
||||
end = float(end)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError("Ungültige Zeitangabe")
|
||||
|
||||
duration = get_duration(source)
|
||||
if duration is None:
|
||||
raise ValueError("Aufnahmedauer konnte nicht ermittelt werden")
|
||||
|
||||
if start < 0 or end > duration or end <= start:
|
||||
raise ValueError("Ungültiger Auswahlbereich")
|
||||
|
||||
if end - start < 0.5:
|
||||
raise ValueError("Der ausgewählte Bereich ist zu kurz")
|
||||
|
||||
FINISHED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
destination = finished_path(filename)
|
||||
temp = FINISHED_DIR / f".{filename}.tmp.mp3"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "warning", "-y",
|
||||
"-i", str(source),
|
||||
"-ss", f"{start:.3f}",
|
||||
"-t", f"{end - start:.3f}",
|
||||
"-c:a", "libmp3lame",
|
||||
"-b:a", "128k",
|
||||
str(temp),
|
||||
],
|
||||
capture_output=True, text=True, timeout=1800,
|
||||
)
|
||||
|
||||
if result.returncode != 0 or not temp.is_file():
|
||||
try:
|
||||
temp.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
print(f"[edit] ffmpeg failed for {filename}: {result.stderr.strip()}", flush=True)
|
||||
raise RuntimeError("Schneiden fehlgeschlagen")
|
||||
|
||||
temp.replace(destination)
|
||||
return destination, get_duration(destination)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTML
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
EDITOR_HTML = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Event Audio – Aufnahme bearbeiten</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; padding:24px; background:#111; color:#eee; font-family:Arial,Helvetica,sans-serif; }
|
||||
.container { max-width:1400px; margin:0 auto; }
|
||||
h1 { margin:0 0 6px; font-size:28px; }
|
||||
.subtitle { color:#aaa; margin-bottom:22px; }
|
||||
.editor { background:#181818; border:1px solid #333; border-radius:10px; padding:20px; }
|
||||
.wave-wrap { position:relative; width:100%; height:240px; background:#0b0b0b; border-radius:6px; overflow:hidden; user-select:none; touch-action:none; }
|
||||
.wave-wrap img { width:100%; height:100%; object-fit:fill; display:block; opacity:.9; }
|
||||
.selection { position:absolute; top:0; bottom:0; background:rgba(80,150,255,.16); border-left:2px solid #fff; border-right:2px solid #fff; pointer-events:none; }
|
||||
.handle { position:absolute; top:0; bottom:0; width:18px; margin-left:-9px; cursor:ew-resize; z-index:3; touch-action:none; }
|
||||
.handle::after { content:""; position:absolute; left:7px; top:0; bottom:0; width:4px; background:#fff; border-radius:2px; box-shadow:0 0 5px #000; }
|
||||
.time-row { display:flex; justify-content:space-between; margin-top:8px; color:#aaa; font-size:14px; }
|
||||
.controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:18px; }
|
||||
button, a.button { border:0; border-radius:6px; padding:11px 16px; background:#333; color:#fff; cursor:pointer; text-decoration:none; font-size:15px; }
|
||||
button:hover, a.button:hover { background:#444; }
|
||||
button.primary { background:#1769aa; }
|
||||
button.danger { background:#8b0000; }
|
||||
button:disabled { opacity:.45; cursor:not-allowed; }
|
||||
.position { margin-left:auto; color:#ccc; font-variant-numeric:tabular-nums; }
|
||||
.values { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-top:20px; }
|
||||
.value { background:#222; border-radius:6px; padding:12px; }
|
||||
.value span { display:block; color:#999; font-size:13px; margin-bottom:5px; }
|
||||
.value strong { font-size:20px; font-variant-numeric:tabular-nums; }
|
||||
.actions { display:flex; gap:10px; flex-wrap:wrap; margin-top:20px; }
|
||||
.info { margin-top:16px; color:#999; font-size:14px; }
|
||||
.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; }
|
||||
.back { display:inline-block; margin-bottom:18px; color:#bbb; text-decoration:none; }
|
||||
@media(max-width:700px) {
|
||||
body { padding:12px; }
|
||||
.editor { padding:12px; }
|
||||
.wave-wrap { height:180px; }
|
||||
.values { grid-template-columns:1fr; }
|
||||
.position { width:100%; margin-left:0; }
|
||||
button, a.button { flex:1 1 auto; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="/">← Zurück zu den Aufnahmen</a>
|
||||
<h1>Aufnahme bearbeiten</h1>
|
||||
<div class="subtitle">{{ filename }} · Originaldauer {{ duration }}</div>
|
||||
|
||||
<div class="editor">
|
||||
<div class="wave-wrap" id="wave">
|
||||
<img src="{{ waveform_url }}" alt="Wellenform">
|
||||
<div class="selection" id="selection"></div>
|
||||
<div class="handle" id="startHandle" title="Anfang verschieben"></div>
|
||||
<div class="handle" id="endHandle" title="Ende verschieben"></div>
|
||||
</div>
|
||||
<div class="time-row">
|
||||
<span>00:00:00</span>
|
||||
<span>{{ duration }}</span>
|
||||
</div>
|
||||
|
||||
<audio id="audio" controls preload="metadata" style="width:100%;margin-top:18px">
|
||||
<source src="{{ audio_url }}" type="audio/mpeg">
|
||||
</audio>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="jump(-10)">⏮ 10 s</button>
|
||||
<button onclick="jump(10)">10 s ⏭</button>
|
||||
<button onclick="playSelection()">▶ Auswahl abspielen</button>
|
||||
<button onclick="playFromStart()">▶ Ab Auswahlstart</button>
|
||||
<span class="position" id="position">00:00:00</span>
|
||||
</div>
|
||||
|
||||
<div class="values">
|
||||
<div class="value"><span>Anfang</span><strong id="startValue">00:00:00</strong></div>
|
||||
<div class="value"><span>Ende</span><strong id="endValue">00:00:00</strong></div>
|
||||
<div class="value"><span>Neue Dauer</span><strong id="keepValue">00:00:00</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" id="trimButton" onclick="trim()">✂ Fertige Aufnahme erstellen</button>
|
||||
<a class="button" href="{{ audio_url }}" download>Original herunterladen</a>
|
||||
</div>
|
||||
<div class="info">Die erste Version der Aufnahme wird als Original auf dem Server behalten. Weitere Schnitte sind möglich und können jederzeit auf das Original zurückgesetzt werden.</div>
|
||||
<div id="message" class="message"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const duration = {{ duration_seconds|tojson }};
|
||||
const filename = {{ filename|tojson }};
|
||||
let start = 0;
|
||||
let end = duration;
|
||||
let dragging = null;
|
||||
const wave = document.getElementById('wave');
|
||||
const startHandle = document.getElementById('startHandle');
|
||||
const endHandle = document.getElementById('endHandle');
|
||||
const selection = document.getElementById('selection');
|
||||
const audio = document.getElementById('audio');
|
||||
|
||||
function fmt(sec) {
|
||||
sec = Math.max(0, Math.round(sec));
|
||||
const h = Math.floor(sec / 3600);
|
||||
const m = Math.floor((sec % 3600) / 60);
|
||||
const s = sec % 60;
|
||||
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
|
||||
}
|
||||
function xToTime(clientX) {
|
||||
const r = wave.getBoundingClientRect();
|
||||
return Math.max(0, Math.min(duration, ((clientX-r.left)/r.width)*duration));
|
||||
}
|
||||
function render() {
|
||||
const a = (start/duration)*100;
|
||||
const b = (end/duration)*100;
|
||||
startHandle.style.left = a+'%';
|
||||
endHandle.style.left = b+'%';
|
||||
selection.style.left = a+'%';
|
||||
selection.style.width = (b-a)+'%';
|
||||
document.getElementById('startValue').textContent = fmt(start);
|
||||
document.getElementById('endValue').textContent = fmt(end);
|
||||
document.getElementById('keepValue').textContent = fmt(end-start);
|
||||
}
|
||||
function beginDrag(which, e) {
|
||||
e.preventDefault();
|
||||
dragging = which;
|
||||
document.body.style.cursor='ew-resize';
|
||||
}
|
||||
startHandle.addEventListener('pointerdown', e => beginDrag('start',e));
|
||||
endHandle.addEventListener('pointerdown', e => beginDrag('end',e));
|
||||
document.addEventListener('pointermove', e => {
|
||||
if (!dragging) return;
|
||||
const t = xToTime(e.clientX);
|
||||
if (dragging === 'start') start = Math.min(t, end-0.5);
|
||||
else end = Math.max(t, start+0.5);
|
||||
render();
|
||||
});
|
||||
document.addEventListener('pointerup', () => { dragging=null; document.body.style.cursor=''; });
|
||||
wave.addEventListener('pointerdown', e => {
|
||||
if (e.target === startHandle || e.target === endHandle) return;
|
||||
const t=xToTime(e.clientX);
|
||||
audio.currentTime = Math.max(0, Math.min(duration,t));
|
||||
});
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
document.getElementById('position').textContent=fmt(audio.currentTime);
|
||||
if (audio.currentTime >= end && !audio.paused) audio.pause();
|
||||
});
|
||||
function jump(sec) { audio.currentTime=Math.max(0,Math.min(duration,audio.currentTime+sec)); }
|
||||
function playFromStart() { audio.currentTime=start; audio.play(); }
|
||||
function playSelection() { audio.currentTime=start; audio.play(); }
|
||||
function msg(text, ok) {
|
||||
const el=document.getElementById('message'); el.textContent=text; el.className='message '+(ok?'ok':'error');
|
||||
}
|
||||
async function trim() {
|
||||
if (end-start < 0.5) return msg('Der ausgewählte Bereich ist zu kurz.',false);
|
||||
if (!confirm(`Aufnahme auf ${fmt(start)} bis ${fmt(end)} kürzen?`)) return;
|
||||
const btn=document.getElementById('trimButton'); btn.disabled=true; audio.pause();
|
||||
msg('Aufnahme wird geschnitten …',true);
|
||||
try {
|
||||
const r=await fetch('/api/edit', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({filename,start,end})});
|
||||
const d=await r.json();
|
||||
if(!r.ok || !d.success) throw new Error(d.error || 'Fehler beim Schneiden');
|
||||
msg('Fertige Aufnahme erfolgreich erstellt.',true);
|
||||
setTimeout(()=>location.reload(),700);
|
||||
} catch(e) { msg(e.message,false); btn.disabled=false; }
|
||||
}
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
INDEX_HTML = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Event Audio – Editor</title>
|
||||
<style>
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; padding:24px; background:#111; color:#eee; font-family:Arial,Helvetica,sans-serif; }
|
||||
.container { max-width:1400px; margin:0 auto; }
|
||||
h1 { margin:0 0 6px; }
|
||||
.subtitle { color:#aaa; margin-bottom:22px; }
|
||||
.section { background:#181818; border:1px solid #333; border-radius:10px; padding:18px; margin-bottom:18px; }
|
||||
table { width:100%; border-collapse:collapse; }
|
||||
th,td { padding:10px 8px; border-bottom:1px solid #333; text-align:left; }
|
||||
th { color:#aaa; font-weight:normal; }
|
||||
.button { display:inline-block; border:0; border-radius:6px; padding:9px 13px; background:#333; color:#fff; cursor:pointer; text-decoration:none; }
|
||||
.button:hover { background:#444; }
|
||||
.empty { color:#999; padding:15px 0; }
|
||||
@media(max-width:800px) { body { padding:12px; } table,thead,tbody,tr,th,td { display:block; } thead { display:none; } tr { padding:10px 0; border-bottom:1px solid #333; } td { border:0; padding:5px 0; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Event Audio – Editor</h1>
|
||||
<div class="subtitle">Fertige Aufnahmen aus {{ recordings_dir }}</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Aufnahmen bearbeiten</h2>
|
||||
{% if recordings %}
|
||||
<table>
|
||||
<thead><tr><th>Aufnahme</th><th>Datum</th><th>Größe</th><th>Dauer</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in recordings %}
|
||||
<tr>
|
||||
<td>{{ item.filename }}</td>
|
||||
<td>{{ item.datetime }}</td>
|
||||
<td>{{ item.size }}</td>
|
||||
<td>{{ item.duration }}</td>
|
||||
<td><a class="button" href="{{ url_for('edit_recording', filename=item.filename) }}">✂ Bearbeiten</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}<div class="empty">Keine fertigen Aufnahmen vorhanden.</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Bearbeitete Aufnahmen</h2>
|
||||
{% if finished %}
|
||||
<table>
|
||||
<thead><tr><th>Datei</th><th>Datum</th><th>Größe</th><th>Dauer</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in finished %}
|
||||
<tr>
|
||||
<td>{{ item.filename }}</td>
|
||||
<td>{{ item.datetime }}</td>
|
||||
<td>{{ item.size }}</td>
|
||||
<td>{{ item.duration }}</td>
|
||||
<td><a class="button" href="{{ url_for('finished_file', filename=item.filename) }}">▶ Abspielen / Download</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}<div class="empty">Noch keine bearbeiteten Aufnahmen vorhanden.</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Routes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
RECORDINGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
FINISHED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
recordings = []
|
||||
for path in RECORDINGS_DIR.glob("*.mp3"):
|
||||
try:
|
||||
stat = path.stat()
|
||||
recordings.append({
|
||||
"filename": path.name,
|
||||
"datetime": format_datetime(stat.st_mtime),
|
||||
"size": format_size(stat.st_size),
|
||||
"duration": format_duration(get_duration(path)),
|
||||
"mtime": stat.st_mtime,
|
||||
})
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
finished = []
|
||||
for path in FINISHED_DIR.glob("*.mp3"):
|
||||
try:
|
||||
stat = path.stat()
|
||||
finished.append({
|
||||
"filename": path.name,
|
||||
"datetime": format_datetime(stat.st_mtime),
|
||||
"size": format_size(stat.st_size),
|
||||
"duration": format_duration(get_duration(path)),
|
||||
"mtime": stat.st_mtime,
|
||||
})
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
recordings.sort(key=lambda x: x["mtime"], reverse=True)
|
||||
finished.sort(key=lambda x: x["mtime"], reverse=True)
|
||||
|
||||
return render_template_string(
|
||||
INDEX_HTML,
|
||||
recordings=recordings,
|
||||
finished=finished,
|
||||
recordings_dir=str(RECORDINGS_DIR),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/edit/<path:filename>")
|
||||
def edit_recording(filename):
|
||||
path = source_path(filename)
|
||||
if path is None:
|
||||
abort(404)
|
||||
|
||||
duration = get_duration(path)
|
||||
if duration is None:
|
||||
abort(500, description="Aufnahmedauer konnte nicht ermittelt werden")
|
||||
|
||||
waveform = create_waveform(filename)
|
||||
if waveform is None:
|
||||
abort(500, description="Wellenform konnte nicht erzeugt werden")
|
||||
|
||||
return render_template_string(
|
||||
EDITOR_HTML,
|
||||
filename=filename,
|
||||
duration=format_duration(duration),
|
||||
duration_seconds=duration,
|
||||
waveform_url=url_for("waveform", filename=filename),
|
||||
audio_url=url_for("recording", filename=filename),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/waveforms/<path:filename>")
|
||||
def waveform(filename):
|
||||
if source_path(filename) is None:
|
||||
abort(404)
|
||||
output = create_waveform(filename)
|
||||
if output is None:
|
||||
abort(404)
|
||||
return send_from_directory(WAVEFORMS_DIR, output.name, mimetype="image/png")
|
||||
|
||||
|
||||
@app.route("/recordings/<path:filename>")
|
||||
def recording(filename):
|
||||
path = source_path(filename)
|
||||
if path is None:
|
||||
abort(404)
|
||||
return send_from_directory(RECORDINGS_DIR, path.name, mimetype="audio/mpeg")
|
||||
|
||||
|
||||
@app.route("/finished/<path:filename>")
|
||||
def finished_file(filename):
|
||||
path = finished_path(filename)
|
||||
if path is None or not path.is_file():
|
||||
abort(404)
|
||||
return send_from_directory(FINISHED_DIR, path.name, mimetype="audio/mpeg", as_attachment=False)
|
||||
|
||||
|
||||
@app.route("/api/edit", methods=["POST"])
|
||||
def api_edit():
|
||||
data = request.get_json(silent=True) or {}
|
||||
filename = data.get("filename")
|
||||
|
||||
try:
|
||||
destination, duration = create_finished(filename, data.get("start"), data.get("end"))
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"filename": destination.name,
|
||||
"duration": duration,
|
||||
"duration_formatted": format_duration(duration),
|
||||
})
|
||||
except ValueError as exc:
|
||||
return jsonify({"success": False, "error": str(exc)}), 400
|
||||
except Exception as exc:
|
||||
print(f"[edit] failed for {filename}: {exc}", flush=True)
|
||||
return jsonify({"success": False, "error": "Schneiden fehlgeschlagen"}), 500
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
RECORDINGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
FINISHED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
app.run(host="0.0.0.0", port=WEB_PORT)
|
||||
Reference in New Issue
Block a user