umbenennen und löschen hinzugefügt

This commit is contained in:
2026-09-29 09:09:08 +02:00
parent 57d8dcdccf
commit cfeb05befa
+165 -3
View File
@@ -64,6 +64,77 @@ def safe_filename(filename):
return 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): def source_path(filename):
filename = safe_filename(filename) filename = safe_filename(filename)
if filename is None: 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; } .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, 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: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.primary { background:#1769aa; }
button.danger { background:#8b0000; } button.danger { background:#8b0000; }
button:disabled { opacity:.45; cursor:not-allowed; } button:disabled { opacity:.45; cursor:not-allowed; }
@@ -289,8 +363,9 @@ button:disabled { opacity:.45; cursor:not-allowed; }
<div class="actions"> <div class="actions">
<button class="primary" id="trimButton" onclick="trim()">✂ Fertige Aufnahme erstellen</button> <button class="primary" id="trimButton" onclick="trim()">✂ Fertige Aufnahme erstellen</button>
<a class="button" href="{{ audio_url }}" download>Original herunterladen</a> <a class="button" href="{{ audio_url }}" download>Original herunterladen</a>
<button class="button danger" onclick="deleteOriginal()">🗑 Originalaufnahme löschen</button>
</div> </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 id="message" class="message"></div>
</div> </div>
</div> </div>
@@ -384,6 +459,23 @@ function setEndAtCurrent() {
function msg(text, ok) { function msg(text, ok) {
const el=document.getElementById('message'); el.textContent=text; el.className='message '+(ok?'ok':'error'); 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() { async function trim() {
if (end-start < 0.5) return msg('Der ausgewählte Bereich ist zu kurz.',false); 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; 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.datetime }}</td>
<td>{{ item.size }}</td> <td>{{ item.size }}</td>
<td>{{ item.duration }}</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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -469,7 +565,11 @@ th { color:#aaa; font-weight:normal; }
<td>{{ item.datetime }}</td> <td>{{ item.datetime }}</td>
<td>{{ item.size }}</td> <td>{{ item.size }}</td>
<td>{{ item.duration }}</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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -479,6 +579,43 @@ th { color:#aaa; font-weight:normal; }
</div> </div>
<script> <script>
function showLoading() { document.getElementById('loadingOverlay').classList.add('active'); } 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> </script>
</body> </body>
</html> </html>
@@ -583,6 +720,31 @@ def finished_file(filename):
return send_from_directory(FINISHED_DIR, path.name, mimetype="audio/mpeg", as_attachment=False) 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"]) @app.route("/api/edit", methods=["POST"])
def api_edit(): def api_edit():
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}