umbenennen und löschen hinzugefügt
This commit is contained in:
@@ -64,6 +64,77 @@ def safe_filename(filename):
|
||||
return filename
|
||||
|
||||
|
||||
def clean_new_filename(filename):
|
||||
"""Validate and normalize a user-supplied MP3 filename."""
|
||||
if not isinstance(filename, str):
|
||||
return None
|
||||
filename = filename.strip()
|
||||
if not filename:
|
||||
return None
|
||||
if Path(filename).name != filename:
|
||||
return None
|
||||
if filename.lower().endswith(".mp3"):
|
||||
filename = filename[:-4]
|
||||
if not filename or filename in {".", ".."}:
|
||||
return None
|
||||
if any(ch in filename for ch in '/\\\x00'):
|
||||
return None
|
||||
return filename + ".mp3"
|
||||
|
||||
|
||||
def directory_for_kind(kind):
|
||||
if kind == "recording":
|
||||
return RECORDINGS_DIR
|
||||
if kind == "finished":
|
||||
return FINISHED_DIR
|
||||
return None
|
||||
|
||||
|
||||
def path_for_kind(kind, filename):
|
||||
filename = safe_filename(filename)
|
||||
directory = directory_for_kind(kind)
|
||||
if filename is None or directory is None:
|
||||
return None
|
||||
return directory / filename
|
||||
|
||||
|
||||
def rename_file(kind, old_filename, new_filename):
|
||||
old_path = path_for_kind(kind, old_filename)
|
||||
new_filename = clean_new_filename(new_filename)
|
||||
if old_path is None or new_filename is None or not old_path.is_file():
|
||||
raise ValueError("Ungültiger Dateiname")
|
||||
|
||||
new_path = old_path.with_name(new_filename)
|
||||
if new_path.exists():
|
||||
raise ValueError("Eine Datei mit diesem Namen existiert bereits")
|
||||
|
||||
old_waveform = waveform_path(old_path.name) if kind == "recording" else None
|
||||
new_waveform = waveform_path(new_path.name) if kind == "recording" else None
|
||||
|
||||
old_path.rename(new_path)
|
||||
if old_waveform and old_waveform.is_file():
|
||||
try:
|
||||
old_waveform.rename(new_waveform)
|
||||
except OSError:
|
||||
# The audio file was renamed successfully; an old waveform cache
|
||||
# is harmless and will be regenerated under the new name.
|
||||
pass
|
||||
return new_path
|
||||
|
||||
|
||||
def delete_file(kind, filename):
|
||||
path = path_for_kind(kind, filename)
|
||||
if path is None or not path.is_file():
|
||||
raise ValueError("Datei nicht gefunden")
|
||||
|
||||
path.unlink()
|
||||
if kind == "recording":
|
||||
try:
|
||||
waveform_path(path.name).unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def source_path(filename):
|
||||
filename = safe_filename(filename)
|
||||
if filename is None:
|
||||
@@ -221,6 +292,9 @@ h1 { margin:0 0 6px; font-size:28px; }
|
||||
.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.danger { background:#4a2020; }
|
||||
.button.danger:hover { background:#6a2b2b; }
|
||||
.actions { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
button.primary { background:#1769aa; }
|
||||
button.danger { background:#8b0000; }
|
||||
button:disabled { opacity:.45; cursor:not-allowed; }
|
||||
@@ -289,8 +363,9 @@ button:disabled { opacity:.45; cursor:not-allowed; }
|
||||
<div class="actions">
|
||||
<button class="primary" id="trimButton" onclick="trim()">✂ Fertige Aufnahme erstellen</button>
|
||||
<a class="button" href="{{ audio_url }}" download>Original herunterladen</a>
|
||||
<button class="button danger" onclick="deleteOriginal()">🗑 Originalaufnahme löschen</button>
|
||||
</div>
|
||||
<div class="info">Die Originalaufnahme bleibt unverändert. Aus dem gewählten Bereich wird eine fertige Aufnahme im separaten Ausgabeordner erstellt.</div>
|
||||
<div class="info">Beim Erstellen einer fertigen Aufnahme bleibt das Original unverändert. Das Original kann hier bewusst gelöscht werden; bereits erstellte fertige Aufnahmen bleiben dabei erhalten.</div>
|
||||
<div id="message" class="message"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -384,6 +459,23 @@ function setEndAtCurrent() {
|
||||
function msg(text, ok) {
|
||||
const el=document.getElementById('message'); el.textContent=text; el.className='message '+(ok?'ok':'error');
|
||||
}
|
||||
async function deleteOriginal() {
|
||||
if (!confirm('Originalaufnahme wirklich löschen?\n\n' + filename + '\n\nBereits erstellte fertige Aufnahmen bleiben erhalten.')) return;
|
||||
|
||||
audio.pause();
|
||||
const response = await fetch('/api/file', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({action: 'delete', kind: 'recording', filename})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Löschen fehlgeschlagen.');
|
||||
return;
|
||||
}
|
||||
location.href = '/';
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -449,7 +541,11 @@ th { color:#aaa; font-weight:normal; }
|
||||
<td>{{ item.datetime }}</td>
|
||||
<td>{{ item.size }}</td>
|
||||
<td>{{ item.duration }}</td>
|
||||
<td><a class="button" href="{{ url_for('edit_recording', filename=item.filename) }}" onclick="showLoading()">✂ Bearbeiten</a></td>
|
||||
<td class="actions">
|
||||
<a class="button" href="{{ url_for('edit_recording', filename=item.filename) }}" onclick="showLoading()">✂ Bearbeiten</a>
|
||||
<button class="button" onclick="renameFile('recording', {{ item.filename|tojson }})">✎ Umbenennen</button>
|
||||
<button class="button danger" onclick="deleteFile('recording', {{ item.filename|tojson }})">🗑 Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -469,7 +565,11 @@ th { color:#aaa; font-weight:normal; }
|
||||
<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>
|
||||
<td class="actions">
|
||||
<a class="button" href="{{ url_for('finished_file', filename=item.filename) }}">▶ Abspielen / Download</a>
|
||||
<button class="button" onclick="renameFile('finished', {{ item.filename|tojson }})">✎ Umbenennen</button>
|
||||
<button class="button danger" onclick="deleteFile('finished', {{ item.filename|tojson }})">🗑 Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -479,6 +579,43 @@ th { color:#aaa; font-weight:normal; }
|
||||
</div>
|
||||
<script>
|
||||
function showLoading() { document.getElementById('loadingOverlay').classList.add('active'); }
|
||||
|
||||
async function renameFile(kind, filename) {
|
||||
const current = filename.replace(/\\.mp3$/i, '');
|
||||
const entered = prompt('Neuer Dateiname:', current);
|
||||
if (entered === null) return;
|
||||
|
||||
const newName = entered.trim();
|
||||
if (!newName || newName === current) return;
|
||||
|
||||
const response = await fetch('/api/file', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({action: 'rename', kind, filename, new_filename: newName})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Umbenennen fehlgeschlagen.');
|
||||
return;
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function deleteFile(kind, filename) {
|
||||
if (!confirm('Datei wirklich löschen?\n\n' + filename)) return;
|
||||
|
||||
const response = await fetch('/api/file', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({action: 'delete', kind, filename})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Löschen fehlgeschlagen.');
|
||||
return;
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -583,6 +720,31 @@ def finished_file(filename):
|
||||
return send_from_directory(FINISHED_DIR, path.name, mimetype="audio/mpeg", as_attachment=False)
|
||||
|
||||
|
||||
@app.route("/api/file", methods=["POST"])
|
||||
def api_file():
|
||||
data = request.get_json(silent=True) or {}
|
||||
action = data.get("action")
|
||||
kind = data.get("kind")
|
||||
filename = data.get("filename")
|
||||
|
||||
if kind not in {"recording", "finished"}:
|
||||
return jsonify({"success": False, "error": "Ungültiger Dateityp"}), 400
|
||||
|
||||
try:
|
||||
if action == "rename":
|
||||
path = rename_file(kind, filename, data.get("new_filename"))
|
||||
return jsonify({"success": True, "filename": path.name})
|
||||
if action == "delete":
|
||||
delete_file(kind, filename)
|
||||
return jsonify({"success": True})
|
||||
return jsonify({"success": False, "error": "Ungültige Aktion"}), 400
|
||||
except ValueError as exc:
|
||||
return jsonify({"success": False, "error": str(exc)}), 400
|
||||
except OSError as exc:
|
||||
print(f"[file] operation failed for {filename}: {exc}", flush=True)
|
||||
return jsonify({"success": False, "error": "Dateioperation fehlgeschlagen"}), 500
|
||||
|
||||
|
||||
@app.route("/api/edit", methods=["POST"])
|
||||
def api_edit():
|
||||
data = request.get_json(silent=True) or {}
|
||||
|
||||
Reference in New Issue
Block a user