// ==UserScript== // @name Spotify Genius Lyrics // @description Show lyrics from genius.com on the Spotify web player // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @copyright 2019, cuzi (https://github.com/cvzi // @supportURL https://github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues // @version 1 // @include https://open.spotify.com/* // @grant GM.xmlHttpRequest // @grant GM.setValue // @grant GM.getValue // @connect genius.com // @namespace https://greasyfork.org/users/20068 // @downloadURL none // ==/UserScript== var requestCache = {} var selectionCache = {} var currentTitle = '' var currentArtists = '' function getHostname (url) { const a = document.createElement('a') a.href = url return a.hostname } function metricPrefix (bytes, precision) { // http://stackoverflow.com/a/18650828 bytes = parseInt(bytes, 10) if (bytes === 0) { return '0' } var k = 1024 var sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] var i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toPrecision(precision)) + sizes[i] } async function loadCache () { selectionCache = JSON.parse(await GM.getValue('selectioncache', '{}')) requestCache = JSON.parse(await GM.getValue('requestcache', '{}')) /* requestCache = { "cachekey0": "121648565.5\njsondata123", ... } */ for (var prop in requestCache) { // Delete cached values, that are older than 2 hours let time = JSON.parse(requestCache[prop].split('\n')[0]) if ((new Date()).getTime() - (new Date(time)).getTime() > 2 * 60 * 60 * 1000) { delete requestCache[prop] } } } function request (obj) { const cachekey = JSON.stringify(obj) if (cachekey in requestCache) { return obj.load(JSON.parse(requestCache[cachekey].split('\n')[1])) } let headers = { 'Referer': obj.url, 'data': obj.data, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Host': getHostname(obj.url), 'User-Agent': navigator.userAgent } if (obj.headers) { headers = Object.assign(headers, obj.headers) } return GM.xmlHttpRequest({ url: obj.url, method: obj.method ? obj.method : 'GET', headers: headers, onerror: obj.error ? obj.error : function genericOnError (response) { console.log(response) }, onload: function xmlHttpRequestOnLoad (response) { const time = (new Date()).toJSON() // Chrome fix: Otherwise JSON.stringify(requestCache) omits responseText var newobj = {} for (var key in response) { newobj[key] = response[key] } newobj.responseText = response.responseText requestCache[cachekey] = time + '\n' + JSON.stringify(newobj) GM.setValue('requestcache', JSON.stringify(requestCache)) obj.load(response) } }) } function rememberLyricsSelection (title, artists, jsonHit) { const cachekey = title + '--' + artists selectionCache[cachekey] = jsonHit GM.setValue('selectioncache', JSON.stringify(selectionCache)) } function getLyricsSelection (title, artists) { const cachekey = title + '--' + artists if (cachekey in selectionCache) { return JSON.parse(selectionCache[cachekey]) } else { return false } } function onResize (ev) { let iframe = document.getElementById('lyricsiframe') if (iframe) { iframe.style.width = document.getElementById('lyricscontainer').clientWidth - 1 + 'px' iframe.style.height = document.querySelector('.Root__nav-bar .navBar').clientHeight + 'px' } } function geniusSearch (query, cb) { request({ url: 'https://genius.com/api/search/song?page=1&q=' + encodeURIComponent(query), headers: { 'X-Requested-With': 'XMLHttpRequest' }, error: function geniusSearchOnError (response) { alert('Error geniusSearch(' + JSON.stringify(query) + ', cb):\n' + response) }, load: function geniusSearchOnLoad (response) { cb(JSON.parse(response.responseText)) } }) } function loadGeniusAssets (song, annotations, cb) { let script = [] let onload = [] let headhtml = '' // Define globals script.push('var iv458,annotations1234;') // Hide cookies box function script.push('function hideCookieBox458() {if(document.querySelector(".optanon-allow-all")){document.querySelector(".optanon-allow-all").click(); clearInterval(iv458)}}') script.push('function decodeHTML652(s) { return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">") }') onload.push('iv458 = window.setInterval(hideCookieBox458, 500)') // Show annotations function script.push('function showAnnotation1234(id) { if(id in annotations1234) { let annotation = annotations1234[id]; let main = document.querySelector(".column_layout-column_span-initial_content"); main.querySelector(".annotation_label h3").innerHTML = decodeHTML652(annotation.created_by.name); main.querySelector(".rich_text_formatting").innerHTML = decodeHTML652(annotation.body.html); }}') onload.push('annotations1234 = JSON.parse(document.getElementById("annotationsdata1234").innerHTML);') request({ url: song.result.url, error: function loadGeniusAssetsOnError (response) { alert('Error loadGeniusAssets(' + JSON.stringify(song) + ', cb):\n' + response) }, load: function loadGeniusAssetsOnLoad (response) { let html = response.responseText // Make annotations clickable const regex = /annotation-fragment="(\d+)"/g html = html.replace(regex, 'onclick="showAnnotation1234($1)"') // Add onload attribute to body let parts = html.split('' // Add annotation data headhtml += '\n' // Add to parts = html.split('') html = parts[0] + '\n' + headhtml + '\n' + parts.slice(1).join('') cb(html) } }) } function loadGeniusAnnotations (song, cb) { const apiurl = 'https://genius.com/api/referents/?text_format=html&song_id=' + song.result.id // TODO multiple pages e.g. https://genius.com/api/referents/?text_format=html&song_id=81159&page=1 and https://genius.com/api/referents/?text_format=html&song_id=81159&page=2 and ... request({ url: apiurl, headers: { 'X-Requested-With': 'XMLHttpRequest' }, error: function loadGeniusAnnotationsOnError (response) { alert('Error loadGeniusAnnotations(' + JSON.stringify(song) + ', cb):\n' + response) }, load: function loadGeniusAnnotationsOnLoad (response) { const r = JSON.parse(response.responseText).response const annotations = {} r.referents.forEach(function forEachReferent (referent) { referent.annotations.forEach(function forEachAnnotation (annotation) { annotations[annotation.id] = annotation }) }) cb(song, annotations) } }) } function getCleanLyricsContainer () { if (!document.getElementById('lyricscontainer')) { const topContainer = document.querySelector('.Root__top-container') topContainer.style.width = '70%' topContainer.style.float = 'left' const container = document.createElement('div') container.id = 'lyricscontainer' container.style = 'min-height: 100%; width: 30%; position: relative; z-index: 1; float:left; ' topContainer.parentNode.insertBefore(container, topContainer.nextSibling) } else { document.getElementById('lyricscontainer').innerHTML = '' } return document.getElementById('lyricscontainer') } function hideLyrics () { if (document.getElementById('lyricscontainer')) { document.getElementById('lyricscontainer').parentNode.removeChild(document.getElementById('lyricscontainer')) const topContainer = document.querySelector('.Root__top-container') topContainer.style.width = '100%' topContainer.style.removeProperty('float') } } function showLyrics (song, searchresultsLengths) { const container = getCleanLyricsContainer() if (searchresultsLengths) { const bar = document.createElement('div') bar.style.fontSize = '0.7em' container.appendChild(bar) const backbutton = document.createElement('a') backbutton.href = '#' if (searchresultsLengths === true) { backbutton.appendChild(document.createTextNode('Back to search results')) } else { backbutton.appendChild(document.createTextNode('Back to search (' + (searchresultsLengths - 1) + ' other result' + (searchresultsLengths === 2 ? '' : 's') + ')')) } backbutton.addEventListener('click', function backbuttonClick (ev) { ev.preventDefault() addLyrics(true) }) bar.appendChild(backbutton) } const iframe = document.createElement('iframe') iframe.id = 'lyricsiframe' container.appendChild(iframe) const spinner = '
' iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(spinner) iframe.style.width = container.clientWidth - 1 + 'px' iframe.style.height = document.querySelector('.Root__nav-bar .navBar').clientHeight + 'px' loadGeniusAnnotations(song, function loadGeniusAnnotationsCb (song, annotations) { loadGeniusAssets(song, annotations, function loadGeniusAssetsCb (html) { iframe.src = 'data:text/html;charset=utf-8,' + encodeURIComponent(html) }) }) } function listSongs (hits) { const container = getCleanLyricsContainer() const trackhtml = '
🅖
📄
$title
' container.innerHTML = '
    ' const ol = container.querySelector('ol.tracklist') const searchresultsLengths = hits.length const title = currentTitle const artists = currentArtists const onclick = function onclick () { rememberLyricsSelection(title, artists, this.dataset.hit) showLyrics(JSON.parse(this.dataset.hit), searchresultsLengths) } hits.forEach(function forEachHit (hit) { let li = document.createElement('li') li.setAttribute('class', 'tracklist-row') li.setAttribute('role', 'button') li.innerHTML = trackhtml.replace(/\$title/g, hit.result.title_with_featured).replace(/\$artist/g, hit.result.primary_artist.name).replace(/\$lyrics_state/g, hit.result.lyrics_state).replace(/\$stats\.pageviews/g, metricPrefix(hit.result.stats.pageviews, 1)) li.dataset.hit = JSON.stringify(hit) li.addEventListener('click', onclick) ol.appendChild(li) }) } function addLyrics (force) { const songTitle = document.querySelector('.track-info__name.ellipsis-one-line').innerText const songArtistsArr = [] document.querySelector('.track-info__artists.ellipsis-one-line').querySelectorAll('a[href^="/artist/"]').forEach((e) => songArtistsArr.push(e.innerText)) const songArtists = songArtistsArr.join(' ') if (force || currentTitle !== songTitle || currentArtists !== songArtists) { currentTitle = songTitle currentArtists = songArtists let hitFromCache = getLyricsSelection(songTitle, songArtists) if (!force && hitFromCache) { showLyrics(hitFromCache, true) } else { geniusSearch(songTitle + ' ' + songArtists, function geniusSearchCb (r) { const hits = r.response.sections[0].hits if (hits.length === 0) { hideLyrics() } else if (hits.length === 1) { showLyrics(hits[0]) } else { listSongs(hits) } }) } } } function main () { if (document.querySelector('.now-playing')) { addLyrics() } } loadCache() window.setInterval(main, 2000) window.addEventListener('resize', onResize)