//js/lyrics.js
import { getTrackTitle, getTrackArtists } from './utils.js';
export class LyricsManager {
constructor(api) {
this.api = api;
this.currentLyrics = null;
this.syncedLyrics = [];
this.lyricsCache = new Map();
}
async fetchLyrics(trackId) {
if (this.lyricsCache.has(trackId)) {
return this.lyricsCache.get(trackId);
}
try {
const response = await this.api.fetchWithRetry(`/lyrics/?id=${trackId}`);
const data = await response.json();
if (Array.isArray(data) && data.length > 0) {
const lyricsData = data[0];
this.lyricsCache.set(trackId, lyricsData);
return lyricsData;
}
return null;
} catch (error) {
console.error('Failed to fetch lyrics:', 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: 'text/plain' });
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 function createLyricsPanel() {
const panel = document.createElement('div');
panel.id = 'lyrics-panel';
panel.className = 'lyrics-panel hidden';
panel.innerHTML = `
`;
document.body.appendChild(panel);
return panel;
}
export function showSyncedLyricsPanel(lyricsData, audioPlayer, panel) {
const content = panel.querySelector('.lyrics-content');
const syncedLyrics = lyricsData.subtitles
? parseSyncedLyricsSimple(lyricsData.subtitles)
: null;
if (syncedLyrics && syncedLyrics.length > 0) {
// Render synced lyrics
content.innerHTML = '';
syncedLyrics.forEach((line, index) => {
const lineEl = document.createElement('p');
lineEl.className = 'lyrics-line synced-line';
lineEl.textContent = line.text || '♪';
lineEl.dataset.index = index;
lineEl.dataset.time = line.time;
content.appendChild(lineEl);
});
let currentLineIndex = -1;
const updateLyrics = () => {
const currentTime = audioPlayer.currentTime;
const newIndex = getCurrentLineIndex(syncedLyrics, currentTime);
if (newIndex !== currentLineIndex) {
currentLineIndex = newIndex;
content.querySelectorAll('.synced-line').forEach((line, index) => {
line.classList.remove('active', 'upcoming', 'past');
if (index === currentLineIndex) {
line.classList.add('active');
// Smooth scroll to active line
line.scrollIntoView({ behavior: 'smooth', block: 'center' });
} else if (index === currentLineIndex + 1) {
line.classList.add('upcoming');
} else if (index < currentLineIndex) {
line.classList.add('past');
}
});
}
};
// Store the update function so we can remove it later
panel.lyricsUpdateHandler = updateLyrics;
audioPlayer.addEventListener('timeupdate', updateLyrics);
// Initial update
updateLyrics();
} else if (lyricsData.lyrics) {
// Fallback to static lyrics
const lines = lyricsData.lyrics.split('\n');
content.innerHTML = lines.map(line =>
`${line || ' '}
`
).join('');
} else {
content.innerHTML = 'No lyrics available
';
}
}
export function clearLyricsPanelSync(audioPlayer, panel) {
if (panel.lyricsUpdateHandler) {
audioPlayer.removeEventListener('timeupdate', panel.lyricsUpdateHandler);
panel.lyricsUpdateHandler = null;
}
}
export function showKaraokeView(track, lyricsData, audioPlayer) {
const view = document.createElement('div');
view.id = 'karaoke-view';
view.className = 'karaoke-view';
const syncedLyrics = lyricsData.subtitles
? parseSyncedLyricsSimple(lyricsData.subtitles)
: [];
view.innerHTML = `
${getTrackTitle(track)}
${getTrackArtists(track)}
`;
document.body.appendChild(view);
const lyricsContainer = view.querySelector('#karaoke-lyrics');
syncedLyrics.forEach((line, index) => {
const lineEl = document.createElement('div');
lineEl.className = 'karaoke-line';
lineEl.textContent = line.text;
lineEl.dataset.index = index;
lineEl.dataset.time = line.time;
lyricsContainer.appendChild(lineEl);
});
let currentLineIndex = -1;
const updateLyrics = () => {
const currentTime = audioPlayer.currentTime;
const newIndex = getCurrentLineIndex(syncedLyrics, currentTime);
if (newIndex !== currentLineIndex) {
currentLineIndex = newIndex;
document.querySelectorAll('.karaoke-line').forEach((line, index) => {
line.classList.remove('active', 'upcoming', 'past');
if (index === currentLineIndex) {
line.classList.add('active');
} else if (index === currentLineIndex + 1) {
line.classList.add('upcoming');
} else if (index < currentLineIndex) {
line.classList.add('past');
}
});
if (currentLineIndex >= 0) {
const activeLine = lyricsContainer.children[currentLineIndex];
if (activeLine) {
activeLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
};
// Use timeupdate event for better sync
audioPlayer.addEventListener('timeupdate', updateLyrics);
// Initial update
updateLyrics();
view.querySelector('#close-karaoke-btn').addEventListener('click', () => {
audioPlayer.removeEventListener('timeupdate', updateLyrics);
view.remove();
});
return view;
}
function parseSyncedLyricsSimple(subtitles) {
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 };
}
return null;
}).filter(Boolean);
}
function getCurrentLineIndex(syncedLyrics, currentTime) {
let currentIndex = -1;
for (let i = 0; i < syncedLyrics.length; i++) {
if (currentTime >= syncedLyrics[i].time) {
currentIndex = i;
} else {
break;
}
}
return currentIndex;
}