Bearbeiten Dialog angepasst
This commit is contained in:
@@ -134,6 +134,40 @@ def rename_file(kind, old_filename, new_filename):
|
||||
return new_path
|
||||
|
||||
|
||||
def write_metadata(path, metadata):
|
||||
"""Rewrite only the MP3 metadata without re-encoding the audio."""
|
||||
temp = path.with_name(f".{path.name}.metadata.mp3")
|
||||
values = {
|
||||
"date": str(metadata.get("date", "")).strip(),
|
||||
"artist": str(metadata.get("artist", "")).strip(),
|
||||
"title": str(metadata.get("title", "")).strip(),
|
||||
"album": str(metadata.get("album", "")).strip(),
|
||||
}
|
||||
cmd = [
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
|
||||
"-i", str(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:
|
||||
temp.unlink(missing_ok=True)
|
||||
raise ValueError("ID3-Tags konnten nicht gespeichert werden")
|
||||
temp.replace(path)
|
||||
except subprocess.TimeoutExpired:
|
||||
temp.unlink(missing_ok=True)
|
||||
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}")
|
||||
|
||||
|
||||
def delete_file(kind, filename):
|
||||
path = path_for_kind(kind, filename)
|
||||
if path is None or not path.is_file():
|
||||
@@ -859,6 +893,14 @@ 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; }
|
||||
.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; }
|
||||
.loading-overlay { position:fixed; inset:0; background:rgba(0,0,0,.78); display:none; align-items:center; justify-content:center; z-index:1000; }
|
||||
.loading-overlay.active { display:flex; }
|
||||
.spinner { width:54px; height:54px; border:5px solid #444; border-top-color:#fff; border-radius:50%; animation:spin .8s linear infinite; }
|
||||
@@ -910,7 +952,7 @@ th { color:#aaa; font-weight:normal; }
|
||||
<td>{{ item.duration }}</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" onclick='editFinishedFile({{ item.filename|tojson }}, {{ item.datetime|tojson }}, {{ item.artist|tojson }}, {{ item.title|tojson }}, {{ item.album|tojson }})'>✎ Bearbeiten</button>
|
||||
<button class="button danger" onclick='deleteFile("finished", {{ item.filename|tojson }})'>🗑 Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -920,7 +962,82 @@ th { color:#aaa; font-weight:normal; }
|
||||
{% else %}<div class="empty">Noch keine bearbeiteten Aufnahmen vorhanden.</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-dialog" id="finishedMetaDialog">
|
||||
<div class="meta-card">
|
||||
<h2>Bearbeitete Aufnahme ändern</h2>
|
||||
<label>Dateiname<input id="finishedFilename" type="text"></label>
|
||||
<label>Zeitpunkt<input id="finishedDate" type="datetime-local" required></label>
|
||||
<label>Redner<input id="finishedArtist" type="text"></label>
|
||||
<label>Beschreibung<input id="finishedTitle" type="text"></label>
|
||||
<label>Veranstaltung<input id="finishedAlbum" type="text"></label>
|
||||
<div class="meta-actions">
|
||||
<button onclick="closeFinishedMetaDialog()">Abbrechen</button>
|
||||
<button class="primary" onclick="saveFinishedMeta()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let finishedEditOriginalFilename = null;
|
||||
|
||||
function toDatetimeLocal(value) {
|
||||
const m = String(value || '').match(/^(\d{2})\.(\d{2})\.(\d{4}) (\d{2}):(\d{2}):(\d{2})$/);
|
||||
return m ? `${m[3]}-${m[2]}-${m[1]}T${m[4]}:${m[5]}` : '';
|
||||
}
|
||||
|
||||
function editFinishedFile(filename, datetime, artist, title, album) {
|
||||
finishedEditOriginalFilename = filename;
|
||||
document.getElementById('finishedFilename').value = filename.replace(/\.mp3$/i, '');
|
||||
document.getElementById('finishedDate').value = toDatetimeLocal(datetime);
|
||||
document.getElementById('finishedArtist').value = artist || '';
|
||||
document.getElementById('finishedTitle').value = title || '';
|
||||
document.getElementById('finishedAlbum').value = album || '';
|
||||
document.getElementById('finishedMetaDialog').classList.add('active');
|
||||
}
|
||||
|
||||
function closeFinishedMetaDialog() {
|
||||
document.getElementById('finishedMetaDialog').classList.remove('active');
|
||||
}
|
||||
|
||||
async function saveFinishedMeta() {
|
||||
const filename = finishedEditOriginalFilename;
|
||||
const newFilename = document.getElementById('finishedFilename').value.trim();
|
||||
const date = document.getElementById('finishedDate').value;
|
||||
if (!newFilename || !date) return alert('Bitte Dateiname und Zeitpunkt angeben.');
|
||||
|
||||
try {
|
||||
const metadata = {
|
||||
date,
|
||||
artist: document.getElementById('finishedArtist').value.trim(),
|
||||
title: document.getElementById('finishedTitle').value.trim(),
|
||||
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})
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok || !result.success) throw new Error(result.error || `HTTP ${response.status}`);
|
||||
closeFinishedMetaDialog();
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
console.error('Ändern fehlgeschlagen:', error);
|
||||
alert('Ändern fehlgeschlagen: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() { document.getElementById('loadingOverlay').classList.add('active'); }
|
||||
|
||||
async function renameFile(kind, filename) {
|
||||
@@ -999,7 +1116,7 @@ def index():
|
||||
tags = read_metadata(path)
|
||||
finished.append({
|
||||
"filename": path.name,
|
||||
"datetime": tags.get("date", "") or format_datetime(stat.st_mtime),
|
||||
"datetime": metadata_datetime(path),
|
||||
"artist": tags.get("artist", ""),
|
||||
"title": tags.get("title", ""),
|
||||
"album": tags.get("album", ""),
|
||||
@@ -1087,6 +1204,30 @@ 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_metadata":
|
||||
if kind != "finished":
|
||||
return jsonify({"success": False, "error": "Metadaten können nur bei bearbeiteten Aufnahmen geändert werden"}), 400
|
||||
path = finished_path(filename)
|
||||
if path is None or not path.is_file():
|
||||
raise ValueError("Datei nicht gefunden")
|
||||
metadata = data.get("metadata") or {}
|
||||
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")
|
||||
write_metadata(path, {
|
||||
"date": date,
|
||||
"artist": str(metadata.get("artist", "")).strip(),
|
||||
"title": str(metadata.get("title", "")).strip(),
|
||||
"album": str(metadata.get("album", "")).strip(),
|
||||
})
|
||||
return jsonify({"success": True})
|
||||
if action == "delete":
|
||||
delete_file(kind, filename)
|
||||
return jsonify({"success": True})
|
||||
|
||||
Reference in New Issue
Block a user