import { GreyscaleAPI } from './api.js'; import { SimplePlayer } from './player.js'; import { DownloadManager } from './downloads.js'; import { LyricsManager } from './lyrics.js'; import { debounce, escapeHtml, formatDate, formatDuration, formatYear, getTrackArtists } from './utils.js'; const els = { form: document.getElementById('search-form'), input: document.getElementById('search-input'), button: document.getElementById('search-button'), status: document.getElementById('status-text'), tabs: document.getElementById('search-tabs'), pageContent: document.getElementById('page-content'), backBtn: document.getElementById('back-btn'), audio: document.getElementById('audio-player'), playPauseBtn: document.getElementById('play-pause-btn'), prevBtn: document.getElementById('prev-btn'), nextBtn: document.getElementById('next-btn'), lyricsBtn: document.getElementById('lyrics-btn'), playerCover: document.getElementById('player-cover'), playerTitle: document.getElementById('player-title'), playerArtist: document.getElementById('player-artist'), currentTime: document.getElementById('current-time'), duration: document.getElementById('duration'), progress: document.getElementById('progress'), lyricsPanel: document.getElementById('lyrics-panel'), lyricsCloseBtn: document.getElementById('lyrics-close-btn'), lyricsContent: document.getElementById('lyrics-content'), downloadsList: document.getElementById('downloads-list'), downloadsClearBtn: document.getElementById('downloads-clear-btn'), }; const api = new GreyscaleAPI(); const player = new SimplePlayer({ audio: els.audio, progressEl: els.progress, currentTimeEl: els.currentTime, durationEl: els.duration, }); const downloads = new DownloadManager(api); const lyrics = new LyricsManager(); const state = { query: '', activeTab: 'tracks', tracks: [], albums: [], artists: [], currentLyrics: null, currentLyricsTrackId: null, currentQueue: [], }; function setStatus(text, type = 'neutral') { els.status.textContent = text; els.status.dataset.type = type; } function setSearchLoading(loading) { els.button.disabled = loading; els.input.disabled = loading; els.button.textContent = loading ? 'Searching...' : 'Search'; } function setPlayButtonState() { els.playPauseBtn.textContent = els.audio.paused ? '▶' : '❚❚'; } function updatePlayerMeta(track) { if (!track) { els.playerTitle.textContent = 'No track selected'; els.playerArtist.textContent = '-'; els.playerCover.src = ''; return; } els.playerTitle.textContent = track.title || 'Unknown title'; els.playerArtist.textContent = track.artistName || getTrackArtists(track); const cover = api.getCoverUrl(track.cover, 320); els.playerCover.src = cover || ''; } function normalizeTrack(track) { if (!track) return null; return { ...track, artistName: track.artistName || getTrackArtists(track), cover: track.cover || track.album?.cover || null, }; } function navigate(path) { const finalPath = path.startsWith('/') ? path : `/${path}`; window.location.hash = `#${finalPath}`; } function goBack() { if (window.history.length > 1) { window.history.back(); } else { navigate('/home'); } } function parseRoute() { const hash = window.location.hash || '#/home'; const clean = hash.startsWith('#') ? hash.slice(1) : hash; const route = clean || '/home'; const parts = route.split('/').filter(Boolean); if (parts.length === 0) return { page: 'home', id: '' }; return { page: parts[0], id: decodeURIComponent(parts.slice(1).join('/')) }; } function setActiveTab(tab) { state.activeTab = tab; els.tabs.querySelectorAll('.tab').forEach((btn) => { btn.classList.toggle('active', btn.dataset.tab === tab); }); } function renderTrackList(items, context = {}) { if (!items.length) return '
No tracks found.
'; return ` `; } function renderAlbumGrid(items) { if (!items.length) return '
No albums found.
'; return `
${items .map((album) => { const cover = api.getCoverUrl(album.cover, 320); const artist = album.artist?.name || getTrackArtists(album); return ` `; }) .join('')}
`; } function renderArtistGrid(items) { if (!items.length) return '
No artists found.
'; return `
${items .map((artist) => { const pic = api.getArtistPictureUrl(artist.picture, 320) || api.getCoverUrl(artist.picture, 320); return ` `; }) .join('')}
`; } function renderHome() { const { activeTab, tracks, albums, artists, query } = state; let inner = '
Search to get started.
'; if (query) { if (activeTab === 'tracks') inner = renderTrackList(tracks, { queueKey: 'search:tracks' }); if (activeTab === 'albums') inner = renderAlbumGrid(albums); if (activeTab === 'artists') inner = renderArtistGrid(artists); } els.pageContent.innerHTML = `

${query ? `Results for "${escapeHtml(query)}"` : 'Search Results'}

${inner}
`; } async function renderAlbumPage(id) { els.pageContent.innerHTML = '
Loading album...
'; try { const { album, tracks } = await api.getAlbum(id); const normalizedTracks = tracks.map((t) => normalizeTrack(t)); state.currentQueue = normalizedTracks; els.pageContent.innerHTML = `

${escapeHtml(album.title || 'Unknown album')}

${escapeHtml(formatDate(album.releaseDate) || '')} • ${normalizedTracks.length} tracks

Tracks

${renderTrackList(normalizedTracks, { queueKey: `album:${album.id}` })}
`; state.currentQueue = normalizedTracks; } catch (error) { els.pageContent.innerHTML = `
Album load failed: ${escapeHtml(error.message || 'Unknown error')}
`; } } async function renderArtistPage(id) { els.pageContent.innerHTML = '
Loading artist...
'; try { const artist = await api.getArtist(id); const tracks = (artist.tracks || []).map((t) => normalizeTrack(t)); const albums = artist.albums || []; const eps = artist.eps || []; const pic = api.getArtistPictureUrl(artist.picture, 640) || api.getCoverUrl(artist.picture, 640); els.pageContent.innerHTML = `

${escapeHtml(artist.name || 'Unknown artist')}

Top Tracks

${renderTrackList(tracks, { queueKey: `artist:${artist.id}:tracks` })}

Albums

${renderAlbumGrid(albums)}

Singles & EPs

${renderAlbumGrid(eps)}
`; state.currentQueue = tracks; } catch (error) { els.pageContent.innerHTML = `
Artist load failed: ${escapeHtml(error.message || 'Unknown error')}
`; } } async function renderTrackPage(id) { els.pageContent.innerHTML = '
Loading track...
'; try { const [trackRaw, recsRaw] = await Promise.all([api.getTrackMetadata(id), api.getTrackRecommendations(id)]); const track = normalizeTrack(trackRaw); const recs = (recsRaw || []).map((t) => normalizeTrack(t)); els.pageContent.innerHTML = `

${escapeHtml(track.title || 'Unknown title')}

${formatDuration(track.duration || 0)}${ track.audioQuality ? ` • ${escapeHtml(track.audioQuality)}` : '' }

Recommendations

${renderTrackList(recs, { queueKey: `track:${track.id}:recs` })}
`; state.currentQueue = [track, ...recs]; } catch (error) { els.pageContent.innerHTML = `
Track load failed: ${escapeHtml(error.message || 'Unknown error')}
`; } } async function renderRoute() { const { page, id } = parseRoute(); if (page === 'home') { renderHome(); return; } if (page === 'album' && id) { await renderAlbumPage(id); return; } if (page === 'artist' && id) { await renderArtistPage(id); return; } if (page === 'track' && id) { await renderTrackPage(id); return; } navigate('/home'); } async function runSearch(query) { const trimmed = query.trim(); if (!trimmed) return; setSearchLoading(true); setStatus(`Searching for "${trimmed}"...`, 'neutral'); try { const [tracksRes, albumsRes, artistsRes] = await Promise.all([ api.searchTracks(trimmed).catch(() => ({ items: [] })), api.searchAlbums(trimmed).catch(() => ({ items: [] })), api.searchArtists(trimmed).catch(() => ({ items: [] })), ]); state.query = trimmed; state.tracks = (tracksRes.items || []).map((t) => normalizeTrack(t)); state.albums = albumsRes.items || []; state.artists = artistsRes.items || []; const total = state.tracks.length + state.albums.length + state.artists.length; setStatus(`Found ${total} result${total === 1 ? '' : 's'}.`, 'ok'); navigate('/home'); renderHome(); } catch (error) { state.query = trimmed; state.tracks = []; state.albums = []; state.artists = []; renderHome(); setStatus(`Search failed: ${error.message || 'Unknown error'}`, 'error'); } finally { setSearchLoading(false); } } async function playQueueAt(queue, index) { if (!Array.isArray(queue) || queue.length === 0) return; if (!Number.isInteger(index) || index < 0 || index >= queue.length) return; player.setQueue(queue, index); try { const track = queue[index]; setStatus(`Loading: ${track.title || 'Track'}`, 'neutral'); await player.playCurrent((t) => api.getTrackStream(t.id, 'HI_RES_LOSSLESS')); setStatus(`Playing: ${track.title || 'Track'}`, 'ok'); } catch (error) { setStatus(`Playback failed: ${error.message || 'Unknown error'}`, 'error'); } } async function playDirectTrack(id) { const track = await api.getTrackMetadata(id); await playQueueAt([normalizeTrack(track)], 0); } function getContextQueue(context) { if (context === 'search:tracks') return state.tracks; const { page } = parseRoute(); if (page === 'album' || page === 'artist' || page === 'track') return state.currentQueue; return state.tracks; } function renderDownloads(tasks) { if (!tasks.length) { els.downloadsList.innerHTML = '
  • No active downloads.
  • '; return; } els.downloadsList.innerHTML = tasks .map( (task) => `
  • ${escapeHtml(task.title || 'Download')}
    ${escapeHtml(task.message || task.status)}
    ${task.status === 'downloading' ? `` : ''}
  • ` ) .join(''); } async function ensureLyricsForCurrentTrack() { const track = player.getCurrentTrack(); if (!track) { els.lyricsContent.innerHTML = '
    No track playing.
    '; return; } if (state.currentLyricsTrackId === track.id && state.currentLyrics) { return; } els.lyricsContent.innerHTML = '
    Loading lyrics...
    '; const payload = await lyrics.fetchLyrics(track); state.currentLyrics = payload; state.currentLyricsTrackId = track.id; const currentTime = els.audio.currentTime || 0; const idx = lyrics.getCurrentLineIndex(currentTime, payload.lines); els.lyricsContent.innerHTML = lyrics.renderLyrics(payload, idx); } function toggleLyricsPanel(forceOpen = null) { const open = forceOpen === null ? els.lyricsPanel.classList.contains('hidden') : forceOpen; els.lyricsPanel.classList.toggle('hidden', !open); if (open) { ensureLyricsForCurrentTrack(); } } function bindEvents() { els.form.addEventListener('submit', async (event) => { event.preventDefault(); await runSearch(els.input.value); }); els.input.addEventListener( 'input', debounce(() => { const value = els.input.value.trim(); if (value.length >= 3) runSearch(value); }, 420) ); els.tabs.addEventListener('click', (event) => { const tab = event.target.closest('.tab'); if (!tab) return; setActiveTab(tab.dataset.tab); renderHome(); }); els.backBtn.addEventListener('click', goBack); window.addEventListener('hashchange', renderRoute); els.pageContent.addEventListener('click', async (event) => { const actionEl = event.target.closest('[data-action]'); if (!actionEl) return; const action = actionEl.dataset.action; const id = actionEl.dataset.id; if (action === 'open-album' && id) navigate(`/album/${id}`); if (action === 'open-artist' && id) navigate(`/artist/${id}`); if (action === 'open-track' && id) navigate(`/track/${id}`); if (action === 'play-track') { const index = Number(actionEl.dataset.index || -1); const context = actionEl.dataset.context || ''; const queue = getContextQueue(context); await playQueueAt(queue, index); } if (action === 'play-track-direct' && id) await playDirectTrack(id); if (action === 'play-album') await playQueueAt(state.currentQueue, 0); if (action === 'play-top-tracks') await playQueueAt(state.currentQueue, 0); if (action === 'download-track' && id) { try { const track = await api.getTrackMetadata(id); await downloads.downloadTrack(normalizeTrack(track), 'LOSSLESS'); setStatus('Download started.', 'ok'); } catch (error) { setStatus(`Download failed: ${error.message || 'Unknown error'}`, 'error'); } } if (action === 'download-album') { try { const { page } = parseRoute(); if (page !== 'album') return; const route = parseRoute(); const { album, tracks } = await api.getAlbum(route.id); await downloads.downloadAlbum(album, tracks.map((t) => normalizeTrack(t)), 'LOSSLESS'); setStatus('Album download queued.', 'ok'); } catch (error) { setStatus(`Album download failed: ${error.message || 'Unknown error'}`, 'error'); } } if (action === 'cancel-download' && id) downloads.cancel(id); }); els.playPauseBtn.addEventListener('click', async () => { try { await player.togglePlayPause(); } catch (error) { setStatus(`Playback error: ${error.message || 'Unknown error'}`, 'error'); } }); els.prevBtn.addEventListener('click', async () => { try { await player.playPrev((t) => api.getTrackStream(t.id, 'HI_RES_LOSSLESS')); } catch (error) { setStatus(`Previous track failed: ${error.message || 'Unknown error'}`, 'error'); } }); els.nextBtn.addEventListener('click', async () => { try { await player.playNext((t) => api.getTrackStream(t.id, 'HI_RES_LOSSLESS')); } catch (error) { setStatus(`Next track failed: ${error.message || 'Unknown error'}`, 'error'); } }); els.audio.addEventListener('play', setPlayButtonState); els.audio.addEventListener('pause', setPlayButtonState); player.onTrackChanged = (track) => { updatePlayerMeta(track); state.currentLyricsTrackId = null; if (!els.lyricsPanel.classList.contains('hidden')) { ensureLyricsForCurrentTrack(); } }; player.onTimeUpdate = (currentTime) => { if (els.lyricsPanel.classList.contains('hidden')) return; if (!state.currentLyrics || !state.currentLyrics.synced) return; const idx = lyrics.getCurrentLineIndex(currentTime, state.currentLyrics.lines); els.lyricsContent.innerHTML = lyrics.renderLyrics(state.currentLyrics, idx); }; player.setResolver((track) => api.getTrackStream(track.id, 'HI_RES_LOSSLESS')); els.lyricsBtn.addEventListener('click', () => toggleLyricsPanel()); els.lyricsCloseBtn.addEventListener('click', () => toggleLyricsPanel(false)); els.lyricsContent.addEventListener('click', (event) => { const line = event.target.closest('[data-time]'); if (!line) return; const time = Number(line.dataset.time || 0); if (Number.isFinite(time) && time >= 0) { els.audio.currentTime = time; } }); downloads.setOnChange(renderDownloads); els.downloadsClearBtn.addEventListener('click', () => downloads.clearFinished()); } async function bootstrap() { setStatus('Loading API instances...', 'neutral'); try { await api.initInstances(); setStatus('Ready.', 'ok'); } catch { setStatus('Instance loading failed, using fallback list.', 'error'); } bindEvents(); setPlayButtonState(); if (!window.location.hash) { navigate('/home'); } else { renderRoute(); } } bootstrap();