feat: romaji search for Japanese songs via wanakana

When search input is pure ASCII (romaji), converts it to hiragana in
parallel using wanakana and merges both result sets. Deduplicates by ID
so no double entries. Helps find artists/songs tagged with hiragana in
sort fields (e.g. 'inabakumori' → 'いなばくもり').

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-17 21:03:42 +02:00
1 parent 27eb209945
commit a47b2be177
1 file changed
+28 -3
+28 -3
View File
@@ -692,6 +692,24 @@ export async function loadRandom() {
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; } } catch(e) { el.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
} }
// ─── ROMAJI → HIRAGANA (lazy-loaded) ───
let _wanakana = null;
async function _romajiToHiragana(q) {
// Only attempt conversion if query is pure ASCII (likely romaji)
if (!/^[a-z\s'-]+$/i.test(q.trim())) return null;
try {
if (!_wanakana) _wanakana = await import('https://cdn.jsdelivr.net/npm/wanakana@5/esm/wanakana.js');
const h = _wanakana.toHiragana(q.trim(), { passRomaji: false });
// If conversion produced actual hiragana (not just the same ASCII), use it
return /[぀-ゟ]/.test(h) ? h : null;
} catch { return null; }
}
function _mergeUniq(a, b) {
const seen = new Set(a.map(x => x.id));
return [...a, ...(b || []).filter(x => !seen.has(x.id))];
}
// ─── LOCAL SEARCH ─── // ─── LOCAL SEARCH ───
export function onLocalSearch(val) { export function onLocalSearch(val) {
clearTimeout(S.searchTimer); clearTimeout(S.searchTimer);
@@ -704,10 +722,17 @@ export async function doLocalSearch(q) {
const el = document.getElementById('search-results'); const el = document.getElementById('search-results');
el.innerHTML = '<div class="spinner"></div>'; el.innerHTML = '<div class="spinner"></div>';
try { try {
const r = await api('search3', `&query=${encodeURIComponent(q)}&songCount=30&albumCount=10&artistCount=6`); const hiragana = await _romajiToHiragana(q);
const res = r.searchResult3 || {}; const [r, r2] = 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),
]);
const res = r.searchResult3 || {};
const res2 = r2?.searchResult3 || {};
el.innerHTML = ''; el.innerHTML = '';
const artists = res.artist||[], albums = res.album||[], songs = res.song||[]; const artists = _mergeUniq(res.artist||[], res2.artist);
const albums = _mergeUniq(res.album ||[], res2.album);
const songs = _mergeUniq(res.song ||[], res2.song);
if(!artists.length && !albums.length && !songs.length) { if(!artists.length && !albums.length && !songs.length) {
el.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><p>Keine Ergebnisse</p></div>'; return; el.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><p>Keine Ergebnisse</p></div>'; return;
} }