//js/lyrics.js import { getTrackTitle, getTrackArtists, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js'; import { sidePanelManager } from './side-panel.js'; export class LyricsManager { constructor(api) { this.api = api; this.currentLyrics = null; this.syncedLyrics = []; this.lyricsCache = new Map(); this.componentLoaded = false; this.amLyricsElement = null; this.animationFrameId = null; } async ensureComponentLoaded() { if (this.componentLoaded) return; if (typeof customElements !== 'undefined' && customElements.get('am-lyrics')) { this.componentLoaded = true; return; } return new Promise((resolve, reject) => { const script = document.createElement('script'); script.type = 'module'; script.src = 'https://cdn.jsdelivr.net/npm/@uimaxbai/am-lyrics@0.6.2/dist/src/am-lyrics.min.js'; script.onload = () => { if (typeof customElements !== 'undefined') { customElements.whenDefined('am-lyrics') .then(() => { this.componentLoaded = true; resolve(); }) .catch(reject); } else { resolve(); } }; script.onerror = () => reject(new Error('Failed to load lyrics component')); document.head.appendChild(script); }); } async fetchLyrics(trackId, track = null) { // LRCLIB if (track) { if (this.lyricsCache.has(trackId)) { return this.lyricsCache.get(trackId); } try { const artist = Array.isArray(track.artists) ? track.artists.map(a => a.name || a).join(', ') : track.artist?.name || ''; const title = track.title || ''; const album = track.album?.title || ''; const duration = track.duration ? Math.round(track.duration) : null; if (!title || !artist) { console.warn('Missing required fields for LRCLIB'); return null; } const params = new URLSearchParams({ track_name: title, artist_name: artist }); if (album) params.append('album_name', album); if (duration) params.append('duration', duration.toString()); const response = await fetch(`https://lrclib.net/api/get?${params.toString()}`); if (response.ok) { const data = await response.json(); if (data.syncedLyrics) { const lyricsData = { subtitles: data.syncedLyrics, lyricsProvider: 'LRCLIB' }; this.lyricsCache.set(trackId, lyricsData); return lyricsData; } } } catch (error) { console.warn('LRCLIB fetch failed:', error); } } return null; } parseSyncedLyrics(subtitles) { if (!subtitles) return []; const lines = subtitles.split('\n').filter(line => line.trim()); return lines.map(line => { const match = line.match(/\[(\d+):(\d+)\.(\d+)\]\s*(.+)/); if (match) { const [, minutes, seconds, centiseconds, text] = match; const timeInSeconds = parseInt(minutes) * 60 + parseInt(seconds) + parseInt(centiseconds) / 100; return { time: timeInSeconds, text: text.trim() }; } return null; }).filter(Boolean); } generateLRCContent(lyricsData, track) { if (!lyricsData || !lyricsData.subtitles) return null; const trackTitle = getTrackTitle(track); const trackArtist = getTrackArtists(track); let lrc = `[ti:${trackTitle}]\n`; lrc += `[ar:${trackArtist}]\n`; lrc += `[al:${track.album?.title || 'Unknown Album'}]\n`; lrc += `[by:${lyricsData.lyricsProvider || 'Unknown'}]\n`; lrc += '\n'; lrc += lyricsData.subtitles; return lrc; } downloadLRC(lyricsData, track) { const lrcContent = this.generateLRCContent(lyricsData, track); if (!lrcContent) { alert('No synced lyrics available for this track'); return; } const blob = new Blob([lrcContent], { type: 'application/octet-stream' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${getTrackArtists(track)} - ${getTrackTitle(track)}.lrc`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } getCurrentLine(currentTime) { if (!this.syncedLyrics || this.syncedLyrics.length === 0) return -1; let currentIndex = -1; for (let i = 0; i < this.syncedLyrics.length; i++) { if (currentTime >= this.syncedLyrics[i].time) { currentIndex = i; } else { break; } } return currentIndex; } } export async function openLyricsPanel(track, audioPlayer, lyricsManager) { // If no manager provided, create a temp one const manager = lyricsManager || new LyricsManager(); const renderControls = (container) => { container.innerHTML = ` `; container.querySelector('#close-side-panel-btn').addEventListener('click', () => { sidePanelManager.close(); clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); }); container.querySelector('#download-lrc-btn').addEventListener('click', async (e) => { const btn = e.currentTarget; btn.disabled = true; try { const lyricsData = await manager.fetchLyrics(track.id, track); if (lyricsData) { manager.downloadLRC(lyricsData, track); } else { alert('No synced lyrics available for download'); } } catch (error) { console.error('Failed to download lyrics!', error); alert('Failed to Download Lyrics! check the console for more information.') } finally { btn.disabled = false; } }); }; const renderContent = async (container) => { container.innerHTML = '
Loading lyrics...
'; try { await manager.ensureComponentLoaded(); const title = track.title; const artist = getTrackArtists(track); const album = track.album?.title; const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined; const isrc = track.isrc || ''; container.innerHTML = ''; const amLyrics = document.createElement('am-lyrics'); amLyrics.setAttribute('song-title', title); amLyrics.setAttribute('song-artist', artist); if (album) amLyrics.setAttribute('song-album', album); if (durationMs) amLyrics.setAttribute('song-duration', durationMs); amLyrics.setAttribute('query', `${title} ${artist}`.trim()); if (isrc) amLyrics.setAttribute('isrc', isrc); amLyrics.setAttribute('highlight-color', '#93c5fd'); amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)'); amLyrics.setAttribute('autoscroll', ''); amLyrics.setAttribute('interpolate', ''); amLyrics.style.height = '100%'; amLyrics.style.width = '100%'; container.appendChild(amLyrics); manager.amLyricsElement = amLyrics; // Clean up any previous sync clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); setupLyricsSync(track, audioPlayer, sidePanelManager.panel, manager, amLyrics); } catch (error) { console.error('Failed to load lyrics:', error); container.innerHTML = '
Failed to load lyrics! :(
'; } }; sidePanelManager.open('lyrics', 'Lyrics', renderControls, renderContent); } function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) { let baseTimeMs = 0; let lastTimestamp = performance.now(); const updateTime = () => { const currentMs = audioPlayer.currentTime * 1000; baseTimeMs = currentMs; lastTimestamp = performance.now(); amLyrics.currentTime = currentMs; }; const tick = () => { if (!audioPlayer.paused) { const now = performance.now(); const elapsed = now - lastTimestamp; const nextMs = baseTimeMs + elapsed; amLyrics.currentTime = nextMs; lyricsManager.animationFrameId = requestAnimationFrame(tick); } }; const onPlay = () => { baseTimeMs = audioPlayer.currentTime * 1000; lastTimestamp = performance.now(); tick(); }; const onPause = () => { if (lyricsManager.animationFrameId) { cancelAnimationFrame(lyricsManager.animationFrameId); } }; audioPlayer.addEventListener('timeupdate', updateTime); audioPlayer.addEventListener('play', onPlay); audioPlayer.addEventListener('pause', onPause); audioPlayer.addEventListener('seeked', updateTime); // Store handlers for removal panel.lyricsUpdateHandler = updateTime; panel.lyricsPlayHandler = onPlay; panel.lyricsPauseHandler = onPause; panel.lyricsSeekHandler = updateTime; amLyrics.addEventListener('line-click', (e) => { if (e.detail && e.detail.timestamp) { audioPlayer.currentTime = e.detail.timestamp / 1000; audioPlayer.play(); } }); if (!audioPlayer.paused) { tick(); } panel.lyricsCleanup = () => { if (lyricsManager.animationFrameId) { cancelAnimationFrame(lyricsManager.animationFrameId); } audioPlayer.removeEventListener('timeupdate', updateTime); audioPlayer.removeEventListener('play', onPlay); audioPlayer.removeEventListener('pause', onPause); audioPlayer.removeEventListener('seeked', updateTime); }; } export function clearLyricsPanelSync(audioPlayer, panel) { if (panel && panel.lyricsCleanup) { panel.lyricsCleanup(); panel.lyricsCleanup = null; } } export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) { container.innerHTML = '
Loading lyrics...
'; try { await lyricsManager.ensureComponentLoaded(); const title = track.title; const artist = getTrackArtists(track); const album = track.album?.title; const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined; const isrc = track.isrc || ''; container.innerHTML = ''; const amLyrics = document.createElement('am-lyrics'); amLyrics.setAttribute('song-title', title); amLyrics.setAttribute('song-artist', artist); if (album) amLyrics.setAttribute('song-album', album); if (durationMs) amLyrics.setAttribute('song-duration', durationMs); amLyrics.setAttribute('query', `${title} ${artist}`.trim()); if (isrc) amLyrics.setAttribute('isrc', isrc); amLyrics.setAttribute('highlight-color', '#93c5fd'); amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)'); amLyrics.setAttribute('autoscroll', ''); amLyrics.setAttribute('interpolate', ''); amLyrics.style.height = '100%'; amLyrics.style.width = '100%'; container.appendChild(amLyrics); setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager, amLyrics); return amLyrics; } catch (error) { console.error('Failed to load lyrics in fullscreen:', error); container.innerHTML = '
Failed to load lyrics
'; return null; } } function setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager, amLyrics) { let baseTimeMs = 0; let lastTimestamp = performance.now(); const updateTime = () => { const currentMs = audioPlayer.currentTime * 1000; baseTimeMs = currentMs; lastTimestamp = performance.now(); amLyrics.currentTime = currentMs; }; const tick = () => { if (!audioPlayer.paused) { const now = performance.now(); const elapsed = now - lastTimestamp; const nextMs = baseTimeMs + elapsed; amLyrics.currentTime = nextMs; lyricsManager.fullscreenAnimationFrameId = requestAnimationFrame(tick); } }; const onPlay = () => { baseTimeMs = audioPlayer.currentTime * 1000; lastTimestamp = performance.now(); tick(); }; const onPause = () => { if (lyricsManager.fullscreenAnimationFrameId) { cancelAnimationFrame(lyricsManager.fullscreenAnimationFrameId); lyricsManager.fullscreenAnimationFrameId = null; } }; audioPlayer.addEventListener('timeupdate', updateTime); audioPlayer.addEventListener('play', onPlay); audioPlayer.addEventListener('pause', onPause); audioPlayer.addEventListener('seeked', updateTime); container.lyricsUpdateHandler = updateTime; container.lyricsPlayHandler = onPlay; container.lyricsPauseHandler = onPause; container.lyricsSeekHandler = updateTime; container.lyricsCleanup = () => { if (lyricsManager.fullscreenAnimationFrameId) { cancelAnimationFrame(lyricsManager.fullscreenAnimationFrameId); lyricsManager.fullscreenAnimationFrameId = null; } audioPlayer.removeEventListener('timeupdate', updateTime); audioPlayer.removeEventListener('play', onPlay); audioPlayer.removeEventListener('pause', onPause); audioPlayer.removeEventListener('seeked', updateTime); }; amLyrics.addEventListener('line-click', (e) => { if (e.detail && e.detail.timestamp) { audioPlayer.currentTime = e.detail.timestamp / 1000; audioPlayer.play(); } }); if (!audioPlayer.paused) { tick(); } } export function clearFullscreenLyricsSync(container) { if (container && container.lyricsCleanup) { container.lyricsCleanup(); container.lyricsCleanup = null; } }