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}