From 5dda1a710124c97a0579269cf253a45db9b771e4 Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 25 May 2026 19:49:47 +0200 Subject: [PATCH] fix: iTunes Japanese artist lookup + sort tag editing in info modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/novasonic-server.js | 19 ++++++++++------ index.html | 9 ++++++++ src/ui.js | 44 +++++++++++++++++++++++++------------ src/views.js | 3 ++- styles.css | 1 + 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index e07c8d7..cfe2b5c 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -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)}`); diff --git a/index.html b/index.html index 696195f..bb9b4bc 100644 --- a/index.html +++ b/index.html @@ -943,6 +943,15 @@ +
+ Sort-Tags (Japanisch) +
+ + + + +
+
diff --git a/src/ui.js b/src/ui.js index ddf82a8..54089bd 100644 --- a/src/ui.js +++ b/src/ui.js @@ -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; diff --git a/src/views.js b/src/views.js index 4db3fdf..27b302e 100644 --- a/src/views.js +++ b/src/views.js @@ -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; diff --git a/styles.css b/styles.css index f92a310..95dcae3 100644 --- a/styles.css +++ b/styles.css @@ -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}