Bearbeiten Dialog angepasst
This commit is contained in:
@@ -134,6 +134,40 @@ def rename_file(kind, old_filename, new_filename):
|
|||||||
return new_path
|
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):
|
def delete_file(kind, filename):
|
||||||
path = path_for_kind(kind, filename)
|
path = path_for_kind(kind, filename)
|
||||||
if path is None or not path.is_file():
|
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 { display:inline-block; border:0; border-radius:6px; padding:9px 13px; background:#333; color:#fff; cursor:pointer; text-decoration:none; }
|
||||||
.button:hover { background:#444; }
|
.button:hover { background:#444; }
|
||||||
.empty { color:#999; padding:15px 0; }
|
.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 { 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; }
|
.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; }
|
.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>{{ item.duration }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a class="button" href="{{ url_for('finished_file', filename=item.filename) }}">▶ Abspielen / Download</a>
|
<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>
|
<button class="button danger" onclick='deleteFile("finished", {{ item.filename|tojson }})'>🗑 Löschen</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -920,7 +962,82 @@ th { color:#aaa; font-weight:normal; }
|
|||||||
{% else %}<div class="empty">Noch keine bearbeiteten Aufnahmen vorhanden.</div>{% endif %}
|
{% else %}<div class="empty">Noch keine bearbeiteten Aufnahmen vorhanden.</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<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'); }
|
function showLoading() { document.getElementById('loadingOverlay').classList.add('active'); }
|
||||||
|
|
||||||
async function renameFile(kind, filename) {
|
async function renameFile(kind, filename) {
|
||||||
@@ -999,7 +1116,7 @@ def index():
|
|||||||
tags = read_metadata(path)
|
tags = read_metadata(path)
|
||||||
finished.append({
|
finished.append({
|
||||||
"filename": path.name,
|
"filename": path.name,
|
||||||
"datetime": tags.get("date", "") or format_datetime(stat.st_mtime),
|
"datetime": metadata_datetime(path),
|
||||||
"artist": tags.get("artist", ""),
|
"artist": tags.get("artist", ""),
|
||||||
"title": tags.get("title", ""),
|
"title": tags.get("title", ""),
|
||||||
"album": tags.get("album", ""),
|
"album": tags.get("album", ""),
|
||||||
@@ -1087,6 +1204,30 @@ def api_file():
|
|||||||
if action == "rename":
|
if action == "rename":
|
||||||
path = rename_file(kind, filename, data.get("new_filename"))
|
path = rename_file(kind, filename, data.get("new_filename"))
|
||||||
return jsonify({"success": True, "filename": path.name})
|
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":
|
if action == "delete":
|
||||||
delete_file(kind, filename)
|
delete_file(kind, filename)
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|||||||
Reference in New Issue
Block a user