Files
NovaSonic/src/ui/metadata.js
T
HoneyandClaude Sonnet 4.6 0663b1c923 refactor: split ui/ui.js into 5 focused modules
settings.js  — settings modal, romaji overrides, sidebar, theme, nav mode
modals.js    — context menu, playlist-add modal, confirm dialog, edit-playlist modal
metadata.js  — song info/edit modal, MusicBrainz/iTunes autofill, saveMetadata, deleteSong
notify.js    — notification panel, bell badge, _addNotif, _notifNavigate
downloads.js — download modal, Spotify download, jobs panel, startArtistSortFix
ui.js        — barrel re-export; all external imports unchanged

player.js left unsplit: shared private state (_nativePlaying, _playGen, _queueGen, etc.)
spans every section — splitting would require exporting internal state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 14:00:38 +02:00

309 lines
15 KiB
JavaScript

import { S } from '../state.js';
import { esc, formatDur, toast } from '../utils.js';
import { api, coverUrl, fetchRomajiIndex, romajiLookup, getRomajiOverrides } from '../api.js';
// Circular — safe (only used inside function bodies)
import { loadPlaylists, loadPlaylistDetail } from '../views/views.js';
import { showConfirm } from './modals.js';
// ─── SONG INFO MODAL ───
function _fmtArtists(str, sep) {
if (!str) return '';
return str.split(/\s*[•·;,]\s*/).map(s => s.trim()).filter(Boolean).join(sep);
}
function _renderInfoModal(s) {
const cv = coverUrl(s.coverArt, 200);
document.getElementById('info-cover').innerHTML = cv
? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">`
: '<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:32px">🎵</div>';
document.getElementById('info-top-title').textContent = s.title || '—';
document.getElementById('info-top-sub').textContent = (s.artist||'—') + (s.album ? ' · '+s.album : '');
const fields = [
['Titel', s.title],
['Künstler', s.artist],
['Album-Künstler', s.albumArtist],
['Album', s.album],
['Jahr', s.year],
['Track #', s.track],
['Disc #', s.discNumber],
['Genre', s.genre],
['Dauer', formatDur(s.duration)],
['Bitrate', s.bitRate ? s.bitRate+' kbps' : null],
['Format', s.suffix?.toUpperCase()],
['Dateigröße', s.size ? Math.round(s.size/1024/1024*10)/10+' MB' : null],
['Pfad', s.path],
['Plays', s.playCount],
].filter(([,v]) => v!=null && v!=='' && v!==undefined);
document.getElementById('info-rows').innerHTML = fields.map(([l,v]) =>
`<div class="info-row"><span class="info-label">${l}</span><span class="info-val">${esc(String(v))}</span></div>`
).join('');
}
export async function showSongInfo(s) {
S.infoSong = s;
S._infoArtData = null;
document.getElementById('info-modal').classList.add('open');
_leaveEditMode();
_renderInfoModal(s);
try {
const r = await api('getSong', `&id=${s.id}`);
const full = r.song;
if (full) {
if (!full.albumArtist && full.albumId) {
try {
const ar = await api('getAlbum', `&id=${full.albumId}`);
if (ar.album?.artist) full.albumArtist = ar.album.artist;
} catch {}
}
S.infoSong = full;
_renderInfoModal(full);
if (document.getElementById('info-edit-mode').style.display !== 'none') {
document.getElementById('ie-title').value = full.title || '';
document.getElementById('ie-artist').value = _fmtArtists(full.artist, '; ') || '';
document.getElementById('ie-albumartist').value = _fmtArtists(full.albumArtist, ', ') || '';
document.getElementById('ie-album').value = full.album || '';
document.getElementById('ie-genre').value = full.genre || '';
document.getElementById('ie-year').value = full.year || '';
document.getElementById('ie-track').value = full.track || '';
document.getElementById('ie-disc').value = full.discNumber || '';
document.getElementById('ie-totaltracks').value = full.albumSongCount || '';
}
}
} catch {}
}
export function closeInfoModal() {
document.getElementById('info-modal').classList.remove('open');
_leaveEditMode();
S.infoSong = null;
S._infoArtData = null;
}
function _leaveEditMode() {
document.getElementById('info-view-mode').style.display = '';
document.getElementById('info-edit-mode').style.display = 'none';
document.getElementById('info-edit-toggle').classList.remove('active');
document.getElementById('info-edit-status').textContent = '';
}
export function toggleInfoEdit() {
const editEl = document.getElementById('info-edit-mode');
const viewEl = document.getElementById('info-view-mode');
const btn = document.getElementById('info-edit-toggle');
const isEdit = editEl.style.display !== 'none';
if (isEdit) { _leaveEditMode(); return; }
const s = S.infoSong;
if (!s) return;
editEl.style.display = '';
viewEl.style.display = 'none';
btn.classList.add('active');
document.getElementById('ie-title').value = s.title || '';
document.getElementById('ie-artist').value = _fmtArtists(s.artist, '; ') || '';
document.getElementById('ie-albumartist').value = _fmtArtists(s.albumArtist, ', ') || '';
document.getElementById('ie-album').value = s.album || '';
document.getElementById('ie-genre').value = s.genre || '';
document.getElementById('ie-year').value = s.year || '';
document.getElementById('ie-track').value = s.track || '';
document.getElementById('ie-disc').value = s.discNumber || '';
document.getElementById('ie-totaltracks').value = s.albumSongCount || '';
document.getElementById('ie-titlesort').value = '';
document.getElementById('ie-artistsort').value = '';
document.getElementById('ie-albumartistsort').value = '';
document.getElementById('ie-albumsort').value = '';
const _sId = s.id;
fetchRomajiIndex().catch(() => null).then(() => {
const rm = romajiLookup(_sId);
if (rm) {
const _jp = /[ぁ-んァ-ン一-龯]/;
if (rm.titleRomaji && !_jp.test(rm.titleRomaji)) document.getElementById('ie-titlesort').value = rm.titleRomaji;
if (rm.artistRomaji && !_jp.test(rm.artistRomaji)) {
document.getElementById('ie-artistsort').value = rm.artistRomaji;
document.getElementById('ie-albumartistsort').value = rm.artistRomaji;
}
if (rm.albumRomaji && !_jp.test(rm.albumRomaji)) document.getElementById('ie-albumsort').value = rm.albumRomaji;
}
if (!S.cfg.backendUrl || !_sId) return null;
const p = new URLSearchParams({ ndUrl: S.cfg.url, user: S.cfg.user, pass: S.cfg.pass, songId: _sId });
return fetch(`${S.cfg.backendUrl}/api/song-sorttags?${p}`).then(r => r.ok ? r.json() : null).catch(() => null);
}).then(t => {
if (t) {
const _jp = /[ぁ-んァ-ン一-龯]/;
if (t.titleSort && !_jp.test(t.titleSort)) document.getElementById('ie-titlesort').value = t.titleSort;
if (t.artistSort && !_jp.test(t.artistSort)) document.getElementById('ie-artistsort').value = t.artistSort;
if (t.albumArtistSort && !_jp.test(t.albumArtistSort)) document.getElementById('ie-albumartistsort').value = t.albumArtistSort;
if (t.albumSort && !_jp.test(t.albumSort)) document.getElementById('ie-albumsort').value = t.albumSort;
}
const ov = getRomajiOverrides();
if (s.artist && ov[s.artist]) { document.getElementById('ie-artistsort').value = ov[s.artist]; document.getElementById('ie-albumartistsort').value = ov[s.artist]; }
if (s.albumArtist && ov[s.albumArtist]) document.getElementById('ie-albumartistsort').value = ov[s.albumArtist];
if (s.title && ov[s.title]) document.getElementById('ie-titlesort').value = ov[s.title];
if (s.album && ov[s.album]) document.getElementById('ie-albumsort').value = ov[s.album];
}).catch(() => {});
const cv = coverUrl(s.coverArt, 200);
const prev = document.getElementById('info-edit-art-preview');
prev.innerHTML = cv
? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">`
: '<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px">🎵</div>';
S._infoArtData = null;
}
export function onInfoArtFile(input) {
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
const dataUrl = e.target.result;
const base64 = dataUrl.split(',')[1];
S._infoArtData = { base64, mime: file.type };
document.getElementById('info-edit-art-preview').innerHTML =
`<img src="${dataUrl}" style="width:100%;height:100%;object-fit:cover">`;
};
reader.readAsDataURL(file);
}
export async function autofillMusicBrainz() {
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
const s = S.infoSong;
if (!s) return;
const artist = document.getElementById('ie-artist').value || s.artist || '';
const title = document.getElementById('ie-title').value || s.title || '';
const status = document.getElementById('info-edit-status');
status.textContent = '🔍 MusicBrainz…';
try {
const r = await fetch(`${S.cfg.backendUrl}/api/musicbrainz/lookup?artist=${encodeURIComponent(artist)}&title=${encodeURIComponent(title)}`);
const d = await r.json();
if (!d.found) { status.textContent = '❌ Kein Treffer auf MusicBrainz'; return; }
if (d.title) document.getElementById('ie-title').value = d.title;
if (d.artist) document.getElementById('ie-artist').value = d.artist;
if (d.album) document.getElementById('ie-album').value = d.album;
if (d.genre) document.getElementById('ie-genre').value = d.genre;
if (d.year) document.getElementById('ie-year').value = d.year;
if (d.trackNum) document.getElementById('ie-track').value = d.trackNum;
if (d.totalTracks) document.getElementById('ie-totaltracks').value = d.totalTracks;
if (d.coverUrl) {
status.textContent = '✅ Metadaten gefunden — Cover wird geladen…';
S._infoArtData = { url: d.coverUrl };
document.getElementById('info-edit-art-preview').innerHTML =
`<img src="${d.coverUrl}" style="width:100%;height:100%;object-fit:cover" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\' style=\\'width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px\\'>🎵</div>'">`;
status.textContent = '✅ Metadaten + Cover von MusicBrainz';
} else {
status.textContent = '✅ Metadaten von MusicBrainz (kein Cover)';
}
} catch (e) {
status.textContent = '❌ Fehler: ' + e.message;
}
}
export async function autofillItunes() {
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
const s = S.infoSong;
if (!s) return;
const artist = document.getElementById('ie-artist').value || s.artist || '';
const title = document.getElementById('ie-title').value || s.title || '';
const status = document.getElementById('info-edit-status');
status.textContent = '🔍 Apple Music…';
try {
const r = await fetch(`${S.cfg.backendUrl}/api/itunes/lookup?artist=${encodeURIComponent(artist)}&title=${encodeURIComponent(title)}`);
const d = await r.json();
if (!d.found) { status.textContent = '❌ Kein Treffer auf iTunes'; return; }
if (d.album) document.getElementById('ie-album').value = d.album;
if (d.genre) document.getElementById('ie-genre').value = d.genre;
if (d.year) document.getElementById('ie-year').value = d.year;
if (d.trackNum) document.getElementById('ie-track').value = d.trackNum;
if (d.totalTracks) document.getElementById('ie-totaltracks').value = d.totalTracks;
if (d.coverUrl) {
S._infoArtData = { url: d.coverUrl };
document.getElementById('info-edit-art-preview').innerHTML =
`<img src="${d.coverUrl}" style="width:100%;height:100%;object-fit:cover" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\' style=\\'width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px\\'>🎵</div>'">`;
status.textContent = '✅ Metadaten + Cover von Apple Music';
} else {
status.textContent = '✅ Metadaten von Apple Music (kein Cover)';
}
} catch (e) {
status.textContent = '❌ Fehler: ' + e.message;
}
}
export async function saveMetadata() {
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
const s = S.infoSong;
if (!s?.id) return;
const status = document.getElementById('info-edit-status');
status.textContent = '💾 Speichern…';
const body = {
ndUrl: S.cfg.url,
user: S.cfg.user,
pass: S.cfg.pass,
songId: s.id,
title: document.getElementById('ie-title').value || undefined,
artist: document.getElementById('ie-artist').value || undefined,
albumArtist: document.getElementById('ie-albumartist').value || undefined,
album: document.getElementById('ie-album').value || undefined,
genre: document.getElementById('ie-genre').value || undefined,
year: parseInt(document.getElementById('ie-year').value) || undefined,
trackNum: parseInt(document.getElementById('ie-track').value) || undefined,
totalTracks: parseInt(document.getElementById('ie-totaltracks').value) || undefined,
discNum: parseInt(document.getElementById('ie-disc').value) || undefined,
titleSort: document.getElementById('ie-titlesort').value || undefined,
artistSort: document.getElementById('ie-artistsort').value || undefined,
albumArtistSort:document.getElementById('ie-albumartistsort').value || undefined,
albumSort: document.getElementById('ie-albumsort').value || undefined,
};
if (S._infoArtData?.base64) {
body.artworkBase64 = S._infoArtData.base64;
body.artworkMime = S._infoArtData.mime || 'image/jpeg';
} else if (S._infoArtData?.url) {
body.artworkUrl = S._infoArtData.url;
}
try {
const r = await fetch(`${S.cfg.backendUrl}/api/edit-metadata`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const d = await r.json();
if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
status.textContent = '✅ Gespeichert! Navidrome scannt…';
toast('✅ Metadaten gespeichert');
setTimeout(() => { _leaveEditMode(); closeInfoModal(); }, 1500);
} catch (e) {
status.textContent = '❌ Fehler: ' + e.message;
}
}
export async function deleteSong() {
if (!S.cfg.backendUrl) { toast('⚠ Kein Backend konfiguriert'); return; }
const s = S.infoSong;
if (!s?.id) return;
const ok = await showConfirm(
'Song löschen',
`"${s.title}" wird unwiderruflich von der Festplatte gelöscht. Leere Album- und Künstlerordner werden ebenfalls entfernt.`,
'Löschen'
);
if (!ok) return;
const status = document.getElementById('info-edit-status');
status.textContent = '🗑 Löschen…';
try {
const r = await fetch(`${S.cfg.backendUrl}/api/delete-song`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ndUrl: S.cfg.url, user: S.cfg.user, pass: S.cfg.pass, songId: s.id }),
});
const d = await r.json();
if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
toast(`🗑 "${s.title}" gelöscht`);
closeInfoModal();
setTimeout(() => {
loadPlaylists();
if (document.getElementById('page-playlist-detail')?.classList.contains('active') && S._currentPlaylist)
loadPlaylistDetail(S._currentPlaylist);
}, 3000);
} catch (e) {
status.textContent = '❌ Fehler: ' + e.message;
}
}