diff --git a/src/api.js b/src/api.js index 771e354..373e0f7 100644 --- a/src/api.js +++ b/src/api.js @@ -91,7 +91,8 @@ export async function ndAuth() { // Fetched once per session from the backend, cached in memory. let _romajiIndex = null; let _romajiMap = null; // Map -let _artistRomajiMap = null; // Map +let _artistRomajiMap = null; // Map — kuromoji backend +let _artistSortMap = null; // Map — Navidrome sort tag (preferred) let _albumRomajiMap = null; // Map — for album cards without song ID let _romajiIndexFetch = null; // in-flight promise dedup @@ -121,7 +122,16 @@ export async function fetchRomajiIndex() { return _romajiIndexFetch; } -export function invalidateRomajiIndex() { _romajiIndex = null; _romajiMap = null; _artistRomajiMap = null; _albumRomajiMap = null; } +export function invalidateRomajiIndex() { _romajiIndex = null; _romajiMap = null; _artistRomajiMap = null; _artistSortMap = null; _albumRomajiMap = null; } + +// Populate _artistSortMap from Navidrome artist objects (OpenSubsonic sortName field). +// Called whenever getArtists or getArtist data is loaded so dispArtistById can use it. +export function cacheArtistSortNames(artists) { + if (!_artistSortMap) _artistSortMap = new Map(); + for (const a of artists) { + if (a.id && a.sortName && !_isJp(a.sortName)) _artistSortMap.set(String(a.id), a.sortName); + } +} // ─── ROMAJI OVERRIDES ─── // Stored server-side (shared across all clients). Cached in memory per session. @@ -164,6 +174,8 @@ export function dispArtistById(id, name) { if (!S.cfg.showRomaji) return name || ''; const ov = getRomajiOverrides(); if (name && ov[name]) return ov[name]; + const sort = _artistSortMap?.get(String(id)); + if (sort) return sort; return _artistRomajiMap?.get(String(id)) || name || ''; } export function dispAlbumName(name) { diff --git a/src/views/detail.js b/src/views/detail.js index 9833eb1..6bac391 100644 --- a/src/views/detail.js +++ b/src/views/detail.js @@ -1,6 +1,6 @@ import { S } from '../state.js'; import { esc, toast } from '../utils.js'; -import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, fetchRomajiIndex, dispArtistById } from '../api.js'; +import { api, coverUrl, itunesSearch, itunesLookupAlbum, itunesLookupArtistSongs, itunesLookupArtistAlbums, fetchRomajiIndex, dispArtistById, cacheArtistSortNames } from '../api.js'; import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem } from '../ui/components.js'; import { playQueue, dlSongLocal, toggleStar, parseArtists } from '../player/player.js'; import { showCtxMenu, openDlModal } from '../ui/ui.js'; @@ -99,7 +99,9 @@ export async function loadArtistDetail(artist) { // We reveal the hero with the album cover immediately, then patch in the artist photo async. const [albumRes] = await Promise.allSettled([albumP]); - const _firstAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : []; + const _ndArtist = albumRes.status === 'fulfilled' ? albumRes.value?.artist : null; + if (_ndArtist) cacheArtistSortNames([_ndArtist]); + const _firstAlbums = _ndArtist?.album || []; const _bgCover = _firstAlbums[0] ? coverUrl(_firstAlbums[0].coverArt, 512) : null; const _revealHero = () => requestAnimationFrame(() => { @@ -171,7 +173,7 @@ export async function loadArtistDetail(artist) { } // ── Top Songs ── - const ndAlbums = albumRes.status === 'fulfilled' ? (albumRes.value?.artist?.album || []) : []; + const ndAlbums = _ndArtist?.album || []; const topSongs = topRes.status === 'fulfilled' ? (topRes.value?.topSongs?.song || []) : []; const iTracks = iTracksRes.status === 'fulfilled' ? (iTracksRes.value || []).filter(t => t.wrapperType === 'track') : []; const iAlbums = iAlbumsRes.status === 'fulfilled' ? (iAlbumsRes.value || []) : []; diff --git a/src/views/home.js b/src/views/home.js index df5a80a..a63dc7f 100644 --- a/src/views/home.js +++ b/src/views/home.js @@ -1,6 +1,6 @@ import { S } from '../state.js'; import { esc } from '../utils.js'; -import { api, apiCached, apiIsCached, coverUrl, fetchRomajiIndex, dispArtistById } from '../api.js'; +import { api, apiCached, apiIsCached, coverUrl, fetchRomajiIndex, dispArtistById, cacheArtistSortNames } from '../api.js'; import { makeCard, makeAlbumCard, makeSongItem } from '../ui/components.js'; import { playQueue } from '../player/player.js'; // Circular — safe (called inside event handlers) @@ -202,6 +202,7 @@ export async function loadArtists() { try { const r = await apiCached('getArtists'); const all = (r.artists?.index || []).flatMap(i => i.artist || []); + cacheArtistSortNames(all); if (S.cfg.showRomaji) await fetchRomajiIndex().catch(() => {}); g.innerHTML = ''; all.forEach(a => {