feat: romaji index for Japanese kanji song search
Backend: - Add /api/romaji-index endpoint using kuromoji morphological analyzer - Fetches all Navidrome songs, converts kanji → hiragana readings → romaji - Only indexes songs with Japanese characters; caches for 20 min - /api/romaji-index DELETE clears cache (force rebuild) Frontend: - fetchRomajiIndex() in api.js: lazy-fetches and caches the index - doLocalSearch: when query is romaji (ASCII), queries romaji index in parallel with Navidrome search3 and merges+deduplicates results Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
369a5e82b3
commit
aa239f8e6a
4 files changed
+176
-6
No files matched your search
+19
@@ -39,6 +39,25 @@ export async function ndAuth() {
|
||||
return S._ndToken;
|
||||
}
|
||||
|
||||
// ─── ROMAJI INDEX ───
|
||||
// Fetched once per session from the backend, cached in memory.
|
||||
let _romajiIndex = null;
|
||||
let _romajiIndexFetch = null; // in-flight promise dedup
|
||||
|
||||
export async function fetchRomajiIndex() {
|
||||
if (!S.cfg.backendUrl || !S.cfg.url || !S.cfg.user || !S.cfg.pass) return null;
|
||||
if (_romajiIndex) return _romajiIndex;
|
||||
if (_romajiIndexFetch) return _romajiIndexFetch;
|
||||
_romajiIndexFetch = fetch(
|
||||
`${S.cfg.backendUrl}/api/romaji-index?ndUrl=${encodeURIComponent(S.cfg.url)}&user=${encodeURIComponent(S.cfg.user)}&pass=${encodeURIComponent(S.cfg.pass)}`
|
||||
).then(r => r.ok ? r.json() : null)
|
||||
.then(idx => { _romajiIndex = idx; _romajiIndexFetch = null; return idx; })
|
||||
.catch(() => { _romajiIndexFetch = null; return null; });
|
||||
return _romajiIndexFetch;
|
||||
}
|
||||
|
||||
export function invalidateRomajiIndex() { _romajiIndex = null; }
|
||||
|
||||
// ─── ITUNES ───
|
||||
// Route through backend proxy to avoid CORS; fall back to direct if no backend configured.
|
||||
function _itunesBase() {
|
||||
|
||||
+12
-2
@@ -6,6 +6,7 @@ import { playQueue, dlSongLocal, playPreview } from './player.js';
|
||||
import { showCtxMenu, openEditPlModal, openDlModal, closeSidebar, switchNavMode } from './ui.js';
|
||||
import { closeNowPlaying, closeNpMenu } from './nowplaying.js';
|
||||
import { makeCard, makeAlbumCard, makeSongItem, makeGlobalTrackItem, mkSection } from './components.js';
|
||||
import { fetchRomajiIndex } from './api.js';
|
||||
import { loadSpotifyImporter } from './spotify.js';
|
||||
|
||||
// ─── NAVIGATION HISTORY ───
|
||||
@@ -723,16 +724,25 @@ export async function doLocalSearch(q) {
|
||||
el.innerHTML = '<div class="spinner"></div>';
|
||||
try {
|
||||
const hiragana = await _romajiToHiragana(q);
|
||||
const [r, r2] = await Promise.all([
|
||||
const isRomaji = /^[a-z\s'-]+$/i.test(q.trim());
|
||||
const [r, r2, romajiIdx] = await Promise.all([
|
||||
api('search3', `&query=${encodeURIComponent(q)}&songCount=30&albumCount=10&artistCount=6`),
|
||||
hiragana ? api('search3', `&query=${encodeURIComponent(hiragana)}&songCount=20&albumCount=6&artistCount=4`) : Promise.resolve(null),
|
||||
isRomaji ? fetchRomajiIndex() : Promise.resolve(null),
|
||||
]);
|
||||
const res = r.searchResult3 || {};
|
||||
const res2 = r2?.searchResult3 || {};
|
||||
el.innerHTML = '';
|
||||
const artists = _mergeUniq(res.artist||[], res2.artist);
|
||||
const albums = _mergeUniq(res.album ||[], res2.album);
|
||||
const songs = _mergeUniq(res.song ||[], res2.song);
|
||||
|
||||
// Merge Navidrome results with romaji index matches
|
||||
const qLow = q.trim().toLowerCase();
|
||||
const romajiHits = romajiIdx
|
||||
? romajiIdx.filter(e => e.r.includes(qLow))
|
||||
: [];
|
||||
const songs = _mergeUniq(_mergeUniq(res.song||[], res2.song), romajiHits);
|
||||
|
||||
if(!artists.length && !albums.length && !songs.length) {
|
||||
el.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><p>Keine Ergebnisse</p></div>'; return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user