import { S } from '../state.js'; import { esc, toast, fmtCount } from '../utils.js'; import { api, deezerSearch, deezerLastError, deezerLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js'; import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js'; import { playPreview } from '../player/player.js'; import { openDlModal } from '../ui/ui.js'; import { closeNowPlaying } from '../player/nowplaying.js'; // Circular — safe (called inside function bodies) import { navigate } from './navigation.js'; import { getArtistImage } from './home.js'; // ─── ROMAJI → HIRAGANA ─── let _wanakana = null; async function _romajiToHiragana(q) { 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 }); 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 ─── export function onLocalSearch(val) { clearTimeout(S.searchTimer); if(!val.trim()) { navigate('home'); return; } navigate('search'); S.searchTimer = setTimeout(() => doLocalSearch(val), 350); } export async function doLocalSearch(q, el) { if (!el) el = document.getElementById('search-results'); el.innerHTML = '
'; try { const hiragana = await _romajiToHiragana(q); 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 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 = '
🔍

Keine Ergebnisse

'; return; } if(artists.length) { const s = mkSection('Künstler'); const g = document.createElement('div'); g.className = 'scroll-row'; artists.forEach(a => { const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤'); c.classList.add('artist-card'); c.onclick = () => navigate('artist-detail', a); g.appendChild(c); getArtistImage(a.id).then(img => { if(!img) return; const artEl = c.querySelector('.card-art'); if(!artEl) return; artEl.innerHTML = ``; }); }); s.appendChild(g); el.appendChild(s); } if(albums.length) { const s = mkSection('Alben'); const g = document.createElement('div'); g.className = 'scroll-row'; albums.forEach(a => g.appendChild(makeAlbumCard(a))); s.appendChild(g); el.appendChild(s); } if(songs.length) { const sec = mkSection('Titel'); const list = document.createElement('div'); list.className = 'song-list'; songs.forEach((so,i) => list.appendChild(makeSongItem(so, i, songs, { singlePlay: true }))); sec.appendChild(list); el.appendChild(sec); } } catch(e) { el.innerHTML = `

${e.message}

`; } } // ─── HEADER SEARCH ─── export function onHeaderSearch(val) { const overlay = document.getElementById('header-search-overlay'); const clearBtn = document.getElementById('header-search-clear'); if (clearBtn) clearBtn.style.display = val ? 'block' : 'none'; clearTimeout(S._headerSearchTimer); if (!val.trim()) { if (overlay) overlay.style.display = 'none'; return; } if (overlay) { const header = document.getElementById('mobile-header'); if (header) overlay.style.top = header.offsetHeight + 'px'; overlay.style.display = 'block'; } const el = document.getElementById('header-search-results'); S._headerSearchTimer = setTimeout(() => doLocalSearch(val, el), 300); } export function onMobileSearch(val) { onHeaderSearch(val); } // ─── GLOBAL (DEEZER) SEARCH ─── export function onGlobalSearch(val) { clearTimeout(S.globalSearchTimer); const el = document.getElementById('global-search-results'); if(!val.trim()) { el.innerHTML = '
🔍

Gib einen Suchbegriff ein

'; return; } el.innerHTML = '
'; S.globalSearchTimer = setTimeout(() => doGlobalSearch(val), 400); } export async function doGlobalSearch(q) { const el = document.getElementById('global-search-results'); try { const [tracks, albums, artists] = await Promise.all([ deezerSearch(q,'musicTrack',25), deezerSearch(q,'album',10), deezerSearch(q,'musicArtist',6), ]); el.innerHTML = ''; const trackResults = tracks.filter(t => t.wrapperType === 'track'); if(artists.length) { const s = mkSection('Künstler'); const row = document.createElement('div'); row.className = 'scroll-row'; artists.forEach(a => { const sub = a.fanCount ? `${fmtCount(a.fanCount)} Fans` : 'Deezer Künstler'; const c = makeCard(a.artistName, sub, a.artworkUrl100 || null, '🎤'); c.onclick = () => loadDeezerArtistResults(a.artistId, a.artistName, a.fanCount); row.appendChild(c); if (a.artworkUrl100) return; // Deezer artists already carry a photo — no extra lookup deezerSearch(a.artistName,'album',1).then(res => { if(!res[0]) return; const imgUrl = res[0].artworkUrl100?.replace('100x100','300x300'); if(!imgUrl) return; const artEl = c.querySelector('.card-art'); if(!artEl) return; const img = document.createElement('img'); img.src = imgUrl; img.loading = 'lazy'; img.style.cssText = 'width:100%;height:100%;object-fit:cover;transition:transform .4s'; const overlay = artEl.querySelector('.play-overlay'); artEl.innerHTML = ''; artEl.appendChild(img); if(overlay) artEl.appendChild(overlay); img.onerror = () => { artEl.innerHTML = '
🎤
'; if(overlay) artEl.appendChild(overlay); }; c.addEventListener('mouseenter', () => img.style.transform = 'scale(1.05)'); c.addEventListener('mouseleave', () => img.style.transform = 'scale(1)'); }).catch(() => {}); }); s.appendChild(row); el.appendChild(s); } if(albums.length) { const s = mkSection('Alben'); const row = document.createElement('div'); row.className = 'scroll-row'; albums.forEach(a => { const art = a.artworkUrl100?.replace('100x100','300x300'); const c = makeCard(a.collectionName, a.artistName, art, '💿'); c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadDeezerAlbum(a); }; c.querySelector('.dl-btn-overlay')?.remove(); const dlOverlay = document.createElement('button'); dlOverlay.className = 'dl-btn-overlay'; dlOverlay.title = 'Auf Server laden'; dlOverlay.innerHTML = ``; dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,deezerData:a}); }; c.querySelector('.play-overlay').appendChild(dlOverlay); c.onclick = () => loadDeezerAlbum(a); row.appendChild(c); }); s.appendChild(row); el.appendChild(s); } if(trackResults.length) { const s = mkSection('Titel'); const list = document.createElement('div'); list.className = 'song-list'; trackResults.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, trackResults))); s.appendChild(list); el.appendChild(s); } if(!artists.length && !albums.length && !trackResults.length) { const err = deezerLastError(); if (err) { // Distinguish "genuinely no results" from "search backend is down" so the user knows where // the problem is. const detail = err === 'network' ? 'Keine Verbindung zur Deezer-Such-API (Netzwerk/Backend). Läuft dein Backend?' : `Deezer-Suche nicht verfügbar (Fehler ${err}).`; el.innerHTML = `
⚠️

Deezer-Suche gerade nicht verfügbar

${detail}

`; } else { el.innerHTML = '
🔍

Keine Ergebnisse gefunden

'; } } } catch(e) { el.innerHTML = `

Suche fehlgeschlagen: ${e.message}

`; } } export async function loadDeezerAlbum(album) { try { const tracks = await deezerLookupAlbum(album.collectionId); if(!tracks.length) { toast('Keine Tracks gefunden'); return; } const el = document.getElementById('global-search-results'); const s = document.createElement('div'); s.className = 'search-section'; s.innerHTML = `
${esc(album.collectionName)}
${esc(album.artistName)}
Tracks
`; const list = document.createElement('div'); list.className = 'song-list'; tracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, tracks))); s.appendChild(list); el.innerHTML = ''; el.appendChild(s); } catch(e) { toast('Fehler: ' + e.message); } } export function loadDeezerArtistResults(artistId, artistName, fanCount) { navigate('artist-detail', { id: null, name: artistName, deezerArtistId: artistId, deezerFanCount: fanCount }); } // ─── SEARCH HELPERS ─── export function searchSimilarArtist(artistName) { navigate('discover'); document.getElementById('global-search-input').value = artistName; onGlobalSearch(artistName); } export async function searchAndPreview(trackName, artistName) { try { const res = await deezerSearch(trackName+' '+artistName, 'musicTrack', 1); if(res[0]?.previewUrl) playPreview(res[0]); else toast('Kein Preview verfügbar'); } catch { toast('Suche fehlgeschlagen'); } } // ─── NAVIGATION SHORTCUTS ─── export async function s_goArtist(e, name, id) { if(!name) return; if(id) { navigate('artist-detail', {id, name}); return; } try { const r = await api('search3', `&query=${encodeURIComponent(name)}&artistCount=5&albumCount=0&songCount=0`); const artists = r.searchResult3?.artist || []; const match = artists.find(a => a.name.toLowerCase() === name.toLowerCase()) || artists[0]; if(match) navigate('artist-detail', {id: match.id, name: match.name}); else toast('Künstler nicht gefunden'); } catch { toast('Suche fehlgeschlagen'); } } export function s_goAlbum(e, albumId, albumName, artist, artistId) { if(albumId) navigate('album-detail', {id:albumId, name:albumName, artist, artistId:artistId||'', coverArt:''}); } export function playerBarGoAlbum() { const s = S.queue[S.qIdx]; if(!s) return; if(S.npOpen) closeNowPlaying(); if(s.albumId) navigate('album-detail', {id:s.albumId, name:s.album||'', artist:s.artist||'', artistId:s.artistId||'', coverArt:s.coverArt||''}); } export function playerBarGoArtist() { const s = S.queue[S.qIdx]; if(!s || !s.artist) return; if(S.npOpen) closeNowPlaying(); navigate('artist-detail', {id: s.artistId||s.artist, name: s.artist}); }