Lyrics Update
This commit is contained in:
parent
a85262a25a
commit
f227c4c00d
3 changed files with 193 additions and 109 deletions
63
js/app.js
63
js/app.js
|
|
@ -221,35 +221,18 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const mode = nowPlayingSettings.getMode();
|
||||
|
||||
if (mode === 'karaoke') {
|
||||
lyricsPanel.classList.add('hidden');
|
||||
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
||||
alert('We have updated how lyrics work, with this, we have unfortunately disabled karaoke for the time being.')
|
||||
|
||||
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
|
||||
if (lyricsData) {
|
||||
showKaraokeView(player.currentTrack, lyricsData, audioPlayer);
|
||||
} else {
|
||||
alert('No lyrics available for this track');
|
||||
}
|
||||
} else if (mode === 'lyrics') {
|
||||
const isHidden = lyricsPanel.classList.contains('hidden');
|
||||
lyricsPanel.classList.toggle('hidden');
|
||||
|
||||
if (isHidden) {
|
||||
const content = lyricsPanel.querySelector('.lyrics-content');
|
||||
content.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
|
||||
|
||||
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
|
||||
|
||||
if (lyricsData) {
|
||||
lyricsManager.currentLyrics = lyricsData;
|
||||
showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel);
|
||||
} else {
|
||||
content.innerHTML = '<div class="lyrics-error">Failed to load lyrics</div>';
|
||||
}
|
||||
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel);
|
||||
} else {
|
||||
// Clear sync when hiding
|
||||
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
||||
}
|
||||
|
||||
} else if (mode === 'cover') {
|
||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||
if (overlay && overlay.style.display === 'flex') {
|
||||
|
|
@ -280,13 +263,31 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
||||
});
|
||||
|
||||
document.getElementById('download-lrc-btn')?.addEventListener('click', (e) => {
|
||||
document.getElementById('download-lrc-btn')?.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (lyricsManager.currentLyrics && player.currentTrack) {
|
||||
lyricsManager.downloadLRC(lyricsManager.currentLyrics, player.currentTrack);
|
||||
if (!player.currentTrack) {
|
||||
alert('No track is currently playing');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = e.target.closest('#download-lrc-btn');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id, player.currentTrack);
|
||||
|
||||
if (lyricsData) {
|
||||
lyricsManager.downloadLRC(lyricsData, player.currentTrack);
|
||||
} 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;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('download-current-btn')?.addEventListener('click', () => {
|
||||
if (player.currentTrack) {
|
||||
handleTrackAction('download', player.currentTrack, player, api, lyricsManager, 'track', ui);
|
||||
|
|
@ -309,19 +310,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
if (!lyricsPanel.classList.contains('hidden')) {
|
||||
const mode = nowPlayingSettings.getMode();
|
||||
if (mode === 'lyrics') {
|
||||
const content = lyricsPanel.querySelector('.lyrics-content');
|
||||
content.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
|
||||
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
||||
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel);
|
||||
|
||||
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
|
||||
|
||||
if (lyricsData) {
|
||||
lyricsManager.currentLyrics = lyricsData;
|
||||
// Clear old sync before showing new
|
||||
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
||||
showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel);
|
||||
} else {
|
||||
content.innerHTML = '<div class="lyrics-error">No lyrics available for this track</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ async function downloadTracksToZip(zip, tracks, folderName, api, quality, lyrics
|
|||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id);
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
|
|
@ -399,7 +399,7 @@ export async function downloadDiscography(artist, api, quality, lyricsManager =
|
|||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id);
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
|
||||
if (lrcContent) {
|
||||
|
|
@ -521,7 +521,7 @@ export async function downloadTrackWithMetadata(track, quality, api, lyricsManag
|
|||
|
||||
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
|
||||
try {
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id);
|
||||
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
|
||||
if (lyricsData) {
|
||||
lyricsManager.downloadLRC(lyricsData, track);
|
||||
}
|
||||
|
|
|
|||
233
js/lyrics.js
233
js/lyrics.js
|
|
@ -7,33 +7,95 @@ export class LyricsManager {
|
|||
this.currentLyrics = null;
|
||||
this.syncedLyrics = [];
|
||||
this.lyricsCache = new Map();
|
||||
this.componentLoaded = false;
|
||||
this.amLyricsElement = null;
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
async fetchLyrics(trackId) {
|
||||
if (this.lyricsCache.has(trackId)) {
|
||||
return this.lyricsCache.get(trackId);
|
||||
async ensureComponentLoaded() {
|
||||
if (this.componentLoaded) return;
|
||||
|
||||
if (typeof customElements !== 'undefined' && customElements.get('am-lyrics')) {
|
||||
this.componentLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.api.fetchWithRetry(`/lyrics/?id=${trackId}`);
|
||||
const data = await response.json();
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/@uimaxbai/am-lyrics@0.5.4/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);
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
const lyricsData = data[0];
|
||||
this.lyricsCache.set(trackId, lyricsData);
|
||||
return lyricsData;
|
||||
async fetchLyrics(trackId, track = null) {
|
||||
// LRCLIB
|
||||
if (track) {
|
||||
if (this.lyricsCache.has(trackId)) {
|
||||
return this.lyricsCache.get(trackId);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch lyrics:', error);
|
||||
return null;
|
||||
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*(.+)/);
|
||||
|
|
@ -82,7 +144,6 @@ export class LyricsManager {
|
|||
|
||||
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) {
|
||||
|
|
@ -119,64 +180,92 @@ export function createLyricsPanel() {
|
|||
return panel;
|
||||
}
|
||||
|
||||
export function showSyncedLyricsPanel(lyricsData, audioPlayer, panel) {
|
||||
export async function showSyncedLyricsPanel(track, 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 = '<div class="lyrics-loading">Loading lyrics...</div>';
|
||||
|
||||
const lyricsManager = new LyricsManager();
|
||||
|
||||
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 || '';
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
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%';
|
||||
|
||||
content.appendChild(amLyrics);
|
||||
lyricsManager.amLyricsElement = 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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 =>
|
||||
`<p class="lyrics-line">${line || ' '}</p>`
|
||||
).join('');
|
||||
} else {
|
||||
content.innerHTML = '<div class="lyrics-error">No lyrics available</div>';
|
||||
|
||||
audioPlayer.addEventListener('timeupdate', updateTime);
|
||||
audioPlayer.addEventListener('play', () => {
|
||||
baseTimeMs = audioPlayer.currentTime * 1000;
|
||||
lastTimestamp = performance.now();
|
||||
tick();
|
||||
});
|
||||
audioPlayer.addEventListener('pause', () => {
|
||||
if (lyricsManager.animationFrameId) {
|
||||
cancelAnimationFrame(lyricsManager.animationFrameId);
|
||||
}
|
||||
});
|
||||
audioPlayer.addEventListener('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();
|
||||
}
|
||||
|
||||
panel.lyricsCleanup = () => {
|
||||
if (lyricsManager.animationFrameId) {
|
||||
cancelAnimationFrame(lyricsManager.animationFrameId);
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load lyrics:', error);
|
||||
content.innerHTML = '<div class="lyrics-error">Failed to load lyrics! :(</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,6 +274,10 @@ export function clearLyricsPanelSync(audioPlayer, panel) {
|
|||
audioPlayer.removeEventListener('timeupdate', panel.lyricsUpdateHandler);
|
||||
panel.lyricsUpdateHandler = null;
|
||||
}
|
||||
if (panel.lyricsCleanup) {
|
||||
panel.lyricsCleanup();
|
||||
panel.lyricsCleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function showKaraokeView(track, lyricsData, audioPlayer) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue