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:
HoneyandClaude Sonnet 4.6 committed 2026-05-17 21:15:07 +02:00
1 parent 369a5e82b3
commit aa239f8e6a
4 files changed
+176 -6

No files matched your search

+12 -2
View File
@@ -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;
}