// ==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 2 // @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 = '' var resizeLeftContainer var resizeContainer var optionCurrentSize = 30.0 var optionAutoShow = true var mainIv function getHostname (url) { const a = document.createElement('a') a.href = url return a.hostname } function metricPrefix (n, decimals, k) { // http://stackoverflow.com/a/18650828 if (n <= 0) { return String(n) } k = k || 1000 let dm = decimals <= 0 ? 0 : decimals || 2 let sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] let i = Math.floor(Math.log(n) / Math.log(k)) return parseFloat((n / Math.pow(k, i)).toFixed(dm)) + sizes[i] } function loadCache () { Promise.all([GM.getValue('selectioncache', '{}'), GM.getValue('requestcache', '{}'), GM.getValue('optioncurrentsize', 30.0), GM.getValue('optionautoshow', true),]).then(function(values) { selectionCache = JSON.parse(values[0]) requestCache = JSON.parse(values[1]) optionCurrentSize = values[2] optionAutoShow = values[3] /* requestCache = { "cachekey0": "121648565.5\njsondata123", ... } */ const now = (new Date()).getTime() const exp = 2 * 60 * 60 * 1000 for (let prop in requestCache) { // Delete cached values, that are older than 2 hours const time = JSON.parse(requestCache[prop].split('\n')[0]) if ((now - (new Date(time)).getTime()) > exp) { 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 forgetLyricsSelection (title, artists) { const cachekey = title + '--' + artists if (cachekey in selectionCache) { delete selectionCache[cachekey] 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 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 loadGeniusSong (song, cb) { request({ url: song.result.url, error: function loadGeniusSongOnError (response) { alert('Error loadGeniusSong(' + JSON.stringify(song) + ', cb):\n' + response) }, load: function loadGeniusSongOnLoad (response) { cb(response.responseText) } }) } function loadGeniusAnnotations (song, html, cb) { const regex = /annotation-fragment="\d+"/g let m = html.match(regex) if(!m) { // No annotations, skip loading from API return cb(song, html, {}) } m = m.map((s) => s.match(/\d+/)[0]) const ids = m.map((id) => 'ids[]='+id) const apiurl = 'https://genius.com/api/referents/multi?text_format=html%2Cplain&' + ids.join("&") 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 = {} if (r.referents.forEach) { r.referents.forEach(function forEachReferent (referent) { referent.annotations.forEach(function forEachAnnotation (annotation) { annotations[annotation.id] = annotation }) }) } else { for (let refId in r.referents) { const referent = r.referents[refId] referent.annotations.forEach(function forEachAnnotation (annotation) { annotations[annotation.id] = annotation }) } } cb(song, html, annotations) } }) } function combineGeniusResources(song, html, annotations, cb) { let script = [] let onload = [] let headhtml = '' // Define globals script.push('var iv458,annotations1234;') script.push('function decodeHTML652 (s) { return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">") }') // Hide cookies box function script.push('function hideCookieBox458 () {if(document.querySelector(".optanon-allow-all")){document.querySelector(".optanon-allow-all").click(); clearInterval(iv458)}}') onload.push('iv458 = window.setInterval(hideCookieBox458, 500)') // Hide footer script.push('function hideFooter895 () {let f = document.querySelectorAll(".footer div"); if(f.length){f[0].parentNode.removeChild(f[0]);f[1].parentNode.removeChild(f[1])}}') script.push('function hideSecondaryFooter895 () {if(document.querySelector(".footer.footer--secondary")){document.querySelector(".footer.footer--secondary").parentNode.removeChild(document.querySelector(".footer.footer--secondary"))}}') onload.push('hideFooter895()') onload.push('hideSecondaryFooter895()') // Show annotations function script.push('function showAnnotation1234(ev, id) {') script.push(' document.querySelectorAll(".song_body-lyrics .referent--yellow.referent--highlighted").forEach((e) => e.className = e.className.replace(/\\breferent--yellow\\b/, "").replace(/\\breferent--highlighted\\b/, ""))') script.push(' this.className += " referent--yellow referent--highlighted";') script.push(' if(id in annotations1234) {') script.push(' let annotation = annotations1234[id];') script.push(' let main = document.querySelector(".song_body.column_layout .column_layout-column_span.column_layout-column_span--secondary");') script.push(' main.style.paddingRight = 0;') script.push(' main.innerHTML = "";') script.push(' const div0 = document.createElement("div");') script.push(' div0.className = "column_layout-flex_column-fill_column";') script.push(' main.appendChild(div0);') script.push(' const arrowTop = this.offsetTop;') script.push(' const paddingTop = window.scrollY - main.offsetTop - main.parentNode.offsetTop;') script.push(' let html = \'
\';') script.push(' html += \'\\n \';') script.push(' html = html.replace(/\\$body/g, decodeHTML652(annotation.body.html)).replace(/\\$author/g, decodeHTML652(annotation.created_by.name));') script.push(' div0.innerHTML = html;') script.push(' }') script.push('}') onload.push('annotations1234 = JSON.parse(document.getElementById("annotationsdata1234").innerHTML);') // Make song title clickable script.push('function clickableTitle037() { let url = document.querySelector("meta[property=\'og:url\']").content; ') script.push(' let h1 = document.querySelector(\'.header_with_cover_art-primary_info-title\'); h1.innerHTML = \'\' + h1.innerHTML + \'\'') script.push(' let div = document.querySelector(\'.header_with_cover_art-cover_art .cover_art\'); div.innerHTML = \'\' + div.innerHTML + \'\'') script.push('}') onload.push('clickableTitle037()') // Open real page if not in frame onload.push('if(top==window) {document.location.href = document.querySelector("meta[property=\'og:url\']").content}') // Make annotations clickable const regex = /annotation-fragment="(\d+)"/g html = html.replace(regex, 'onclick="showAnnotation1234.call(this, event, $1)"') // Change design html = html.split('