Skip to content

Instantly share code, notes, and snippets.

@sglkc
Last active May 9, 2024 01:40
Show Gist options
  • Save sglkc/9dfc4a6a2216f1f03ff8d73da7498fc5 to your computer and use it in GitHub Desktop.
Save sglkc/9dfc4a6a2216f1f03ff8d73da7498fc5 to your computer and use it in GitHub Desktop.
Spotify lyrics for FREE accounts FOR FREE!

Spotify free lyrics

Earlier this month (May 2024), Spotify has disabled full lyrics for free accounts, and that's sad, because I want to enjoy free things :(

This script is for Spotify Web Player and is not thoroughly tested! Please report any bugs in the comment section ✨

Warning

NOT EVERY SONG IS LISTED! Don't expect "unpopular" or local songs to work.

How to use

  1. Copy the everything from the script below
  2. Go to Spotify Web Player and open the lyrics (mic icon below)
  3. Open the console by pressing Ctrl + Shift + J
  4. Paste everything and press Enter!
  5. Repeat the steps if you had refreshed the page

Limitations

These are the current limitation of the script, it might be possible to solve them though.

  1. Incomplete database (obv, Spotify doesn't have all lyrics anyway)
  2. Can't click on lyrics to skip part (too complicated?)
  3. Can't scroll to active lyrics (might cause bad UX)

Acknowledgement

shameless plug

Please try my extension for more lyrics customization!

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)
@sglkc
Copy link
Author

sglkc commented May 8, 2024

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

It requires a token which means more steps to do, I'm trying to make this as simple as possible so anyone can use it. Also this is just a proof of concept so I can continue on the extension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment