|
const cachedLyrics = {} |
|
|
|
async function getLyrics(interval) { |
|
|
|
// Delete previous generated lyrics if any |
|
if ('generateLyricsInterval' in window) { |
|
document.querySelectorAll('[data-timestamp]').forEach(e => e.remove()) |
|
} |
|
|
|
const $ = (selector) => document.querySelector(selector) |
|
const tsToMs = (ts) => { |
|
const [min, sec] = ts.trim().split(':') |
|
return Number(min) * 60 * 1000 + Number(sec) * 1000 |
|
} |
|
|
|
const songsUrl = new URL('https://music.xianqiao.wang/neteaseapiv2/search') |
|
const lyricsUrl = new URL('https://music.xianqiao.wang/neteaseapiv2/lyric') |
|
const timestampElement = $('[data-testid="playback-position"]') |
|
const lyricElement = $('[data-testid="fullscreen-lyric"]').cloneNode() |
|
const lyricContainer = $(':has(> [data-testid="fullscreen-lyric"])') |
|
const title = $('[data-testid="context-item-link"]').textContent |
|
const artist = $('[data-testid="context-item-info-artist"]').textContent |
|
const keywords = `${artist} ${title}` |
|
|
|
// Styling for lyrics and hiding blocking elements |
|
const styles = `<style> |
|
/* overlay */ main div[style] > :is(:nth-last-child(1), :nth-last-child(2)), |
|
/* gradient */ div:has(> [data-testid="fullscreen-lyric"])::after |
|
{ display: none!important; } |
|
/* active */ .active-lyric |
|
{ color: var(--lyrics-color-active); } |
|
/* passed */ [data-testid="fullscreen-lyric"]:has(~ .active-lyric) |
|
{ color: var(--lyrics-color-passed)!important; } |
|
/* inactive */ .active-lyric ~ [data-testid] |
|
{ color: var(--lyrics-color-inactive)!important; } |
|
/* unused */ .${lyricElement.className.replace(' ', '.')} |
|
{ display: none!important; } |
|
</style>` |
|
|
|
document.head.insertAdjacentHTML('beforeend', styles) |
|
|
|
let lyricsDetail, lyrics |
|
|
|
songsUrl.searchParams.set('type', 1) |
|
songsUrl.searchParams.set('limit', 1) |
|
songsUrl.searchParams.set('keywords', keywords) |
|
|
|
// BEGIN Check if lyrics is already cached |
|
if (keywords in cachedLyrics) { |
|
lyrics = cachedLyrics[keywords] |
|
} else { |
|
// Try to fetch the song and retrieve the lyrics |
|
try { |
|
const songsResponse = await fetch(songsUrl) |
|
const songs = await songsResponse.json() |
|
|
|
if (!songs.result.songCount) |
|
return console.error(`Nothing found for keyword: "${keywords}"`) |
|
|
|
lyricsUrl.searchParams.set('id', songs.result.songs[0].id) |
|
|
|
const lyricsResponse = await fetch(lyricsUrl) |
|
lyricsDetail = await lyricsResponse.json() |
|
} catch (error) { |
|
return console.error('Error fetching API', error) |
|
} |
|
|
|
const lyricsText = lyricsDetail.lrc.lyric.trim().split('\n') |
|
const incomplete = ['needDesc', 'uncollected'].some(v => v in lyricsDetail) |
|
|
|
if (incomplete) console.warn('Found lyric is incomplete') |
|
|
|
// Parse raw lyrics to object with lyric timestamp |
|
lyrics = lyricsText |
|
.map((lyric) => { |
|
const [timeTag, timeString] = lyric.match(/^\[([\d:\.]+)\]/) || [] |
|
|
|
if (!timeTag || !timeString) { |
|
return { ms: 0, text: lyric } |
|
} |
|
|
|
const text = lyric.replace(timeTag, '') |
|
const ms = tsToMs(timeString) |
|
|
|
return { ms, text } |
|
}) |
|
.filter((lyric) => lyric.text !== '') |
|
|
|
cachedLyrics[keywords] = lyrics |
|
} |
|
// END Check if lyrics is already cached |
|
|
|
// Add generated lyrics to the container |
|
lyrics.forEach(({ ms, text }) => { |
|
const element = lyricElement.cloneNode() |
|
element.textContent = text |
|
element.dataset.timestamp = ms |
|
element.className = '' |
|
lyricContainer.insertAdjacentElement('beforeend', element) |
|
}) |
|
|
|
// This function is used to highlight active lyric using timestamps |
|
// Return interval id to clear later |
|
return setInterval(() => { |
|
if (!location.pathname.startsWith('/lyrics')) return |
|
|
|
// Detect song updates or empty lyrics |
|
const titleNew = $('[data-testid="context-item-link"]').textContent |
|
const artistNew = $('[data-testid="context-item-info-artist"]').textContent |
|
const generatedElement = $('[data-timestamp]') |
|
|
|
if (titleNew !== title || artistNew !== artist || !generatedElement) { |
|
clearInterval(window.generateLyricsInterval) |
|
getLyrics().then(id => window.generateLyricsInterval = id) |
|
} |
|
|
|
const currentMs = tsToMs(timestampElement.textContent) |
|
let activeLyric |
|
|
|
for (const lyric of lyrics) { |
|
if (lyric.ms > currentMs) break; |
|
activeLyric = lyric |
|
} |
|
|
|
if (currentMs !== activeLyric.ms) { |
|
$('.active-lyric')?.classList?.remove('active-lyric') |
|
$(`[data-timestamp="${activeLyric.ms}"]`)?.classList?.add('active-lyric') |
|
} |
|
}, interval) |
|
} |
|
|
|
getLyrics(500).then(id => window.generateLyricsInterval = id) |
do you think its possible to use musixmatch api to load in translations/romaji/lyrics from their database directly and just inject into spotify ?
there seems to be allot of projects with musixmatch
https://github.com/topics/musixmatch-api