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:
HoneyandClaude Sonnet 4.6 committed 2026-05-25 19:49:47 +02:00
1 parent 1c0d3eb4cc
commit 5dda1a7101
5 files changed
+54 -22

No files matched your search

+12 -7
View File
@@ -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)}`);
+9
View File
@@ -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>
+30 -14
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -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}