fix: iTunes Japanese artist lookup + sort tag editing in info modal
- views.js: skip ASCII word-match when artist name is non-ASCII so Japanese artists (e.g. ヨルシカ) resolve correctly to an iTunes artist ID - index.html + styles.css: collapsible Sort-Tags section in edit modal - ui.js: pre-fill sort fields from romaji index; include in save body - novasonic-server.js: accept manual titleSort/artistSort/albumArtistSort/albumSort overrides; fall back to auto-computed romaji when not provided Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1c0d3eb4cc
commit
5dda1a7101
5 files changed
+54
-22
No files matched your search
@@ -615,7 +615,8 @@ app.get('/api/musicbrainz/lookup', async (req, res) => {
|
||||
// ─── METADATA EDIT ───────────────────────────────────────────────────────────
|
||||
app.post('/api/edit-metadata', async (req, res) => {
|
||||
const { ndUrl, user, pass, songId, title, artist, albumArtist, album, year, trackNum, totalTracks,
|
||||
discNum, discTotal, genre, artworkBase64, artworkMime, artworkUrl: newArtworkUrl } = req.body;
|
||||
discNum, discTotal, genre, artworkBase64, artworkMime, artworkUrl: newArtworkUrl,
|
||||
titleSort, artistSort, albumArtistSort, albumSort } = req.body;
|
||||
if (!ndUrl || !user || !pass || !songId) return res.status(400).json({ error: 'ndUrl, user, pass, songId required' });
|
||||
|
||||
// Normalize artist separators — accept •, ·, ;, , and rejoin canonically
|
||||
@@ -698,12 +699,16 @@ app.post('/api/edit-metadata', async (req, res) => {
|
||||
const resolvedAlbumArtist = normalAlbumArtist || (normalArtist ? normalArtist.split(';')[0].trim() : null);
|
||||
if (resolvedAlbumArtist) ffArgs.push('-metadata', `album_artist=${resolvedAlbumArtist}`);
|
||||
if (album) ffArgs.push('-metadata', `album=${album}`);
|
||||
// Sort tags: romaji versions for apps that use them for sorting / display
|
||||
if (romajiTitle && romajiTitle !== title) ffArgs.push('-metadata', `TITLESORT=${romajiTitle}`);
|
||||
if (romajiArtist && romajiArtist !== normalArtist) ffArgs.push('-metadata', `ARTISTSORT=${romajiArtist}`);
|
||||
const resolvedRomajiAA = romajiAlbumArtist || (romajiArtist ? romajiArtist.split(';')[0].trim() : null);
|
||||
if (resolvedRomajiAA && resolvedRomajiAA !== resolvedAlbumArtist) ffArgs.push('-metadata', `ALBUMARTISTSORT=${resolvedRomajiAA}`);
|
||||
if (romajiAlbum && romajiAlbum !== album) ffArgs.push('-metadata', `ALBUMSORT=${romajiAlbum}`);
|
||||
// Sort tags: manual override from frontend > auto-computed romaji
|
||||
const effectiveTitleSort = titleSort !== undefined ? titleSort : (romajiTitle !== title ? romajiTitle : null);
|
||||
const effectiveArtistSort = artistSort !== undefined ? artistSort : (romajiArtist !== normalArtist ? romajiArtist : null);
|
||||
const resolvedRomajiAA = romajiAlbumArtist || (romajiArtist ? romajiArtist.split(';')[0].trim() : null);
|
||||
const effectiveAlbumArtistSort = albumArtistSort !== undefined ? albumArtistSort : (resolvedRomajiAA !== resolvedAlbumArtist ? resolvedRomajiAA : null);
|
||||
const effectiveAlbumSort = albumSort !== undefined ? albumSort : (romajiAlbum !== album ? romajiAlbum : null);
|
||||
if (effectiveTitleSort) ffArgs.push('-metadata', `TITLESORT=${effectiveTitleSort}`);
|
||||
if (effectiveArtistSort) ffArgs.push('-metadata', `ARTISTSORT=${effectiveArtistSort}`);
|
||||
if (effectiveAlbumArtistSort) ffArgs.push('-metadata', `ALBUMARTISTSORT=${effectiveAlbumArtistSort}`);
|
||||
if (effectiveAlbumSort) ffArgs.push('-metadata', `ALBUMSORT=${effectiveAlbumSort}`);
|
||||
if (year) ffArgs.push('-metadata', `date=${year}`);
|
||||
if (trackNum) ffArgs.push('-metadata', `track=${totalTracks ? `${trackNum}/${totalTracks}` : String(trackNum)}`);
|
||||
if (totalTracks) ffArgs.push('-metadata', `totaltracks=${String(totalTracks)}`);
|
||||
|
||||
@@ -943,6 +943,15 @@
|
||||
<label class="info-edit-label">Tracks ges.<input class="info-edit-input" id="ie-totaltracks" type="number" placeholder="12" min="1"></label>
|
||||
</div>
|
||||
</div>
|
||||
<details class="ie-sort-details">
|
||||
<summary>Sort-Tags (Japanisch)</summary>
|
||||
<div class="info-edit-fields" style="margin-top:8px">
|
||||
<label class="info-edit-label">Titel-Sort<input class="info-edit-input" id="ie-titlesort" type="text" placeholder="Romaji / Sort-Titel"></label>
|
||||
<label class="info-edit-label">Künstler-Sort<input class="info-edit-input" id="ie-artistsort" type="text" placeholder="Romaji / Sort-Künstler"></label>
|
||||
<label class="info-edit-label">Album-Künstler-Sort<input class="info-edit-input" id="ie-albumartistsort" type="text" placeholder="Romaji / Sort-Album-Künstler"></label>
|
||||
<label class="info-edit-label">Album-Sort<input class="info-edit-input" id="ie-albumsort" type="text" placeholder="Romaji / Sort-Album"></label>
|
||||
</div>
|
||||
</details>
|
||||
<div id="info-edit-status" style="font-size:12px;color:var(--text3);padding:0 20px 8px;min-height:18px"></div>
|
||||
<div style="display:flex;gap:8px;padding:0 20px 20px">
|
||||
<button class="info-close" style="flex:1;margin:0" onclick="toggleInfoEdit()">Abbrechen</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { S } from './state.js';
|
||||
import { esc, formatDur, toast } from './utils.js';
|
||||
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert } from './api.js';
|
||||
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup } from './api.js';
|
||||
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
|
||||
// Circular imports — safe (only used inside function bodies)
|
||||
import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt, refreshDisplayMode } from './player.js';
|
||||
@@ -309,6 +309,11 @@ export async function showSongInfo(s) {
|
||||
document.getElementById('ie-track').value = full.track || '';
|
||||
document.getElementById('ie-disc').value = full.discNumber || '';
|
||||
document.getElementById('ie-totaltracks').value = full.albumSongCount || '';
|
||||
const _rm2 = romajiLookup(full.id);
|
||||
document.getElementById('ie-titlesort').value = _rm2?.titleRomaji || '';
|
||||
document.getElementById('ie-artistsort').value = _rm2?.artistRomaji || '';
|
||||
document.getElementById('ie-albumartistsort').value = _rm2?.artistRomaji || '';
|
||||
document.getElementById('ie-albumsort').value = _rm2?.albumRomaji || '';
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
@@ -351,6 +356,13 @@ export function toggleInfoEdit() {
|
||||
document.getElementById('ie-disc').value = s.discNumber || '';
|
||||
document.getElementById('ie-totaltracks').value = s.albumSongCount || '';
|
||||
|
||||
// Sort tags — pre-fill from romaji index when available
|
||||
const _rm = romajiLookup(s.id);
|
||||
document.getElementById('ie-titlesort').value = _rm?.titleRomaji || '';
|
||||
document.getElementById('ie-artistsort').value = _rm?.artistRomaji || '';
|
||||
document.getElementById('ie-albumartistsort').value = _rm?.artistRomaji || '';
|
||||
document.getElementById('ie-albumsort').value = _rm?.albumRomaji || '';
|
||||
|
||||
// Art preview — use existing cover
|
||||
const cv = coverUrl(s.coverArt, 200);
|
||||
const prev = document.getElementById('info-edit-art-preview');
|
||||
@@ -417,19 +429,23 @@ export async function saveMetadata() {
|
||||
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,
|
||||
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;
|
||||
|
||||
+2
-1
@@ -378,8 +378,9 @@ export async function loadArtistDetail(artist) {
|
||||
const _queryWords = _normWords(artist.name);
|
||||
const iArtistResults = iArtistRes.status === 'fulfilled' ? (iArtistRes.value || []) : [];
|
||||
const iArtistMatch = iArtistResults.find(r => {
|
||||
if (_queryWords.size === 0) return true; // non-ASCII name (e.g. Japanese) — trust iTunes ranking
|
||||
const rw = _normWords(r.artistName);
|
||||
return _queryWords.size > 0 && [..._queryWords].every(w => rw.has(w));
|
||||
return [..._queryWords].every(w => rw.has(w));
|
||||
});
|
||||
const itunesArtistId = knownItunes ?? iArtistMatch?.artistId ?? null;
|
||||
|
||||
|
||||
@@ -485,6 +485,7 @@ button:focus{outline:none}
|
||||
.info-edit-art-btn{font-size:11px;color:var(--text2);background:var(--surface2);border:none;border-radius:6px;padding:4px 8px;cursor:pointer;display:flex;align-items:center;gap:4px;font-family:var(--font)}
|
||||
.info-edit-art-btn:hover{color:var(--text)}
|
||||
.info-edit-fields{display:flex;flex-direction:column;gap:8px;padding:0 20px 4px}
|
||||
.ie-sort-details{padding:0 20px 4px;font-size:11px;color:var(--text3)}.ie-sort-details summary{cursor:pointer;font-weight:500;color:var(--text2);padding:4px 0;list-style:none;display:flex;align-items:center;gap:6px;user-select:none}.ie-sort-details summary::before{content:'▶';font-size:9px;transition:transform .15s}.ie-sort-details[open] summary::before{transform:rotate(90deg)}.ie-sort-details .info-edit-fields{padding:0}
|
||||
.info-edit-label{display:flex;flex-direction:column;font-size:11px;color:var(--text3);gap:3px;font-weight:500}
|
||||
.info-edit-hint{font-size:10px;color:var(--text3);font-weight:400;opacity:.7;margin-top:-1px}
|
||||
.info-edit-hint code{background:var(--surface3,var(--surface2));border-radius:3px;padding:0 3px;font-size:10px;font-family:monospace;opacity:.9}
|
||||
|
||||
Reference in new issue
Block a user