// ==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 4 // @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== const isFirefox = typeof InstallTrigger !== 'undefined' 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 = 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, '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', data: obj.data, headers: headers, onerror: obj.error ? obj.error : function xmlHttpRequestGenericOnError (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 myScripts () { const script = [] const onload = [] // Define globals script.push('var iv458,annotations1234;') script.push('function removeIfExists (e) { if(e && e.remove) { e.remove() }}') 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){removeIfExists(f[0]);removeIfExists(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()') // Hide other stuff script.push('function hideStuff235 () {') script.push(' const grayBox = document.querySelector(".column_layout-column_span-initial_content>.dfp_unit.u-x_large_bottom_margin.dfp_unit--in_read"); removeIfExists(grayBox)') script.push(' removeIfExists(document.querySelector(".header .header-expand_nav_menu"))') script.push('}') onload.push('hideStuff235()') // Show annotations function script.push('function checkAnnotationHeight458() {') script.push(' const annot = document.querySelector(".song_body.column_layout .column_layout-column_span.column_layout-column_span--secondary .column_layout-flex_column-fill_column")') script.push(' const arrow = annot.querySelector(".annotation_sidebar_arrow")') script.push(' if (arrow.offsetTop > arrow.nextElementSibling.clientHeight) {') script.push(' arrow.nextElementSibling.style.paddingTop = (10 + parseInt(arrow.nextElementSibling.style.paddingTop) + arrow.offsetTop - arrow.nextElementSibling.clientHeight) + "px"') script.push(' }') script.push('}') script.push('function showAnnotation1234(ev, id) {') script.push(' ev.preventDefault()') 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(typeof annotations1234 == "undefined") {') script.push(' annotations1234 = JSON.parse(document.getElementById("annotationsdata1234").innerHTML)') script.push(' }') 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(' targetBlankLinks145 (); // Change link target to _blank') script.push(' window.setTimeout(checkAnnotationHeight458, 200) // Change link target to _blank') 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()') // Change links to target=_blank script.push('function targetBlankLinks145 () {') script.push(' const as = document.querySelectorAll(\'body a:not([href|="#"]):not([target=_blank])\')') script.push(' as.forEach(function(a) {') script.push(' a.target = "_blank";') script.push(' })') script.push('}') onload.push('window.setTimeout(targetBlankLinks145, 1000)') // Open real page if not in frame onload.push('if(top==window) {document.location.href = document.querySelector("meta[property=\'og:url\']").content}') return [script, onload] } function combineGeniusResources (song, html, annotations, cb) { const [script, onload] = myScripts() let headhtml = '' // Make annotations clickable const regex = /annotation-fragment="(\d+)"/g html = html.replace(regex, 'onclick="showAnnotation1234.call(this, event, $1)"') // Change design html = html.split('