NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @exclude * // ==UserLibrary== // @name GeniusLyrics // @description Downloads and shows genius lyrics for Tampermonkey scripts // @version 5.16.0 // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @copyright 2019, cuzi (cuzi@openmail.cc) and contributors // @supportURL https://github.com/cvzi/genius-lyrics-userscript/issues // @icon https://avatars.githubusercontent.com/u/2738430?s=200&v=4 // ==/UserLibrary== // @homepageURL https://github.com/cvzi/genius-lyrics-userscript // ==/UserScript== /* Copyright (C) 2019, cuzi (cuzi@openmail.cc) and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ /* This library requires the following permission in the userscript: * grant GM.xmlHttpRequest * grant GM.getValue * grant GM.setValue * connect genius.com */ /* global Blob, top, HTMLElement, GM_openInTab, crypto, Document */ /* jshint asi: true, esversion: 8 */ if (typeof module !== 'undefined') { module.exports = geniusLyrics } function geniusLyrics (custom) { // eslint-disable-line no-unused-vars 'use strict' const __SELECTION_CACHE_VERSION__ = 7 const __REQUEST_CACHE_VERSION__ = 7 /** @type {globalThis.PromiseConstructor} */ const Promise = (async () => { })().constructor // YouTube polyfill to Promise in older browsers will make the feature being unstable. if (typeof custom !== 'object') { if (typeof window !== 'undefined') window.alert('geniusLyrics requires options argument') throw new Error('geniusLyrics requires options argument') } let _shouldUseLZStringCompression = null const testUseLZStringCompression = async () => { if (typeof _shouldUseLZStringCompression === 'boolean') return _shouldUseLZStringCompression let res = false const isLZStringAvailable = typeof LZString !== 'undefined' && typeof (LZString || 0).compressToUTF16 === 'function' // eslint-disable-line no-undef if (isLZStringAvailable && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') { try { // Browser 2022+ let isEdge = false if (typeof webkitCancelAnimationFrame === 'function' && typeof navigator?.userAgentData === 'object') { // Brave, Chrome, Edge (Browser 2022+) isEdge = (navigator.userAgentData?.brands?.find(e => e.brand.includes('Edge')) || false) } else { // Safari, Firefox } if (!isEdge) { const testFn = async () => { await Promise.resolve() const t = crypto.randomUUID() const r = LZString.decompressFromUTF16(LZString.compressToUTF16(t)) === t // eslint-disable-line no-undef await Promise.resolve() return r } const r = await Promise.race([testFn().catch(() => { }), new Promise(resolve => (AbortSignal.timeout(9).onabort = resolve))]) res = (r === true) } } catch (e) { } } return (_shouldUseLZStringCompression = res) } const elmBuild = (tag, ...contents) => { /** @type {HTMLElement} */ const elm = typeof tag === 'string' ? document.createElement(tag) : tag for (const content of contents) { if (!content || typeof content !== 'object' || (content instanceof Node)) { // eslint-disable-line no-undef elm.append(content) } else if (content.length > 0) { elm.appendChild(elmBuild(...content)) } else if (content.style) { Object.assign(elm.style, content.style) } else if (content.classList) { elm.classList.add(...content.classList) } else if (content.attr) { for (const [attr, val] of Object.entries(content.attr)) elm.setAttribute(attr, val) } else if (content.listener) { for (const [attr, val] of Object.entries(content.listener)) elm.addEventListener(attr, val) } else { Object.assign(elm, content) } } return elm } Array.prototype.forEach.call([ 'GM', 'scriptName', 'domain', 'emptyURL', 'listSongs', 'showSearchField', 'addLyrics', // addLyrics would not immediately add lyrics panel 'hideLyrics', // hideLyrics immediately hide lyrics panel 'getCleanLyricsContainer', 'setFrameDimensions' ], function (valName) { if (!(valName in custom)) { if (typeof window !== 'undefined') window.alert(`geniusLyrics requires parameter ${valName}`) throw new Error(`geniusLyrics() requires parameter ${valName}`) } }) function unScroll () { // unable to do delete window.xxx // only for mainWin window.lastScrollTopPosition = null window.scrollLyricsBusy = false window.staticOffsetTop = null window.latestScrollPos = null window.newScrollTopPosition = null window.isPageAbleForAutoScroll = null } function hideLyricsWithMessage () { const ret = custom.hideLyrics(...arguments) if (ret === false) { // cancelled return false } unScroll() window.postMessage({ iAm: custom.scriptName, type: 'lyricsDisplayState', visibility: 'hidden' }, '*') return ret } function cancelLoading () { window.postMessage({ iAm: custom.scriptName, type: 'cancelLoading' }, '*') } function getUnmodifiedWindowMethods (win) { if (!(win instanceof win.constructor)) { // window in isolated context return win } let removeIframeFn = null let fc = win try { const frameId = 'vanillajs-iframe-v1' let frame = document.getElementById(frameId) if (!frame) { frame = document.createElement('iframe') frame.id = frameId const blobURL = typeof webkitCancelAnimationFrame === 'function' && typeof kagi === 'undefined' ? (frame.src = URL.createObjectURL(new Blob([], { type: 'text/html' }))) : null // avoid Brave Crash frame.sandbox = 'allow-same-origin' // script cannot be run inside iframe but API can be obtained from iframe let n = document.createElement('noscript') // wrap into NOSCRPIT to avoid reflow (layouting) n.appendChild(frame) const root = document.documentElement if (root) { root.appendChild(n) if (blobURL) Promise.resolve().then(() => URL.revokeObjectURL(blobURL)) removeIframeFn = (setTimeout) => { const removeIframeOnDocumentReady = (e) => { e && win.removeEventListener('DOMContentLoaded', removeIframeOnDocumentReady, false) e = n n = win = removeIframeFn = 0 setTimeout ? setTimeout(() => e.remove(), 200) : e.remove() } if (!setTimeout || document.readyState !== 'loading') { removeIframeOnDocumentReady() } else { win.addEventListener('DOMContentLoaded', removeIframeOnDocumentReady, false) } } } } fc = (frame ? frame.contentWindow : null) || win } catch (e) { console.warn(e) } try { const { requestAnimationFrame, setTimeout, setInterval, clearTimeout, clearInterval } = fc const res = { requestAnimationFrame, setTimeout, setInterval, clearTimeout, clearInterval } for (const k in res) res[k] = res[k].bind(win) // necessary if (removeIframeFn) Promise.resolve(res.setTimeout).then(removeIframeFn) return res } catch (e) { if (removeIframeFn) removeIframeFn() throw e } } const { requestAnimationFrame, setTimeout, setInterval, clearTimeout, clearInterval } = getUnmodifiedWindowMethods(window) const genius = { option: { autoShow: true, themeKey: null, romajiPriority: 'low', fontSize: 0, // == 0 : use default value, >= 1 : "px" value useLZCompression: false, shouldUseLZStringCompression: null, cacheHTMLRequest: true, // be careful of cache size if trimHTMLReponseText is false; around 50KB per lyrics including selection cache requestCallbackResponseTextOnly: true, // default true; just need the request text enableStyleSubstitution: false, // default false; some checking are provided but not guaranteed trimHTMLReponseText: true, // make html request to be smaller for caching and window messaging; safe to enable defaultPlaceholder: 'Search genius.com...' // placeholder for input field }, f: { metricPrefix, cleanUpSongTitle, showLyrics, showLyricsAndRemember, reloadCurrentLyrics, loadLyrics, hideLyricsWithMessage, cancelLoading, rememberLyricsSelection, isGreasemonkey, forgetLyricsSelection, forgetCurrentLyricsSelection, getLyricsSelection, geniusSearch, searchByQuery, updateAutoScrollEnabled, isScrollLyricsEnabled, // refer to user setting isScrollLyricsCallable, // refer to content rendering scrollLyrics, config, modalAlert, modalConfirm, closeModalUIs }, current: { // store the title and artists of the current lyrics [cached and able to reload] title: '', // these shall be replaced by CompoundTitle artists: '', // these shall be replaced by CompoundTitle compoundTitle: '', themeSettings: null // currently displayed theme + fontSize }, iv: { main: null // unless setupMain is provided and the interval / looping is controlled externally }, style: { enabled: false // true to make the iframe content more compact and concise; [only work on Genius Default Theme?] }, styleProps: { // if style.enabled, feed the content style into styleProps }, minimizeHit: { // minimize the hit for smaller caches; default all false noImageURL: false, noFeaturedArtists: false, simpleReleaseDate: false, noRawReleaseDate: false, shortenArtistName: false, fixArtistName: false, removeStats: false, // note: true for YoutubeGeniusLyrics only; as YoutubeGeniusLyrics will not show this info noRelatedLinks: false, onlyCompleteLyrics: false }, onThemeChanged: [], debug: false } function cleanRequestCache () { return { __VERSION__: __REQUEST_CACHE_VERSION__ } } function cleanSelectionCache () { return { __VERSION__: __SELECTION_CACHE_VERSION__ } } let askedToSolveCaptcha = false let loadingFailed = false let requestCache = cleanRequestCache() let selectionCache = cleanSelectionCache() let theme let annotationsEnabled = true let autoScrollEnabled = false const onMessage = {} const isLZStringAvailable = typeof LZString !== 'undefined' && typeof (LZString || 0).compressToUTF16 === 'function' // eslint-disable-line no-undef // if (!isLZStringAvailable) throw new Error('LZString is not available. Please update your script.') async function setJV (key, text) { if (isLZStringAvailable && genius.option.useLZCompression && genius.option.shouldUseLZStringCompression) { if (typeof text === 'object') text = JSON.stringify(text) if (typeof text !== 'string') return null const z = 'b\n' + LZString.compressToUTF16(text) // eslint-disable-line no-undef return await custom.GM.setValue(key, z) } else { if (typeof text === 'object') text = JSON.stringify(text) if (typeof text !== 'string') return null const z = 'a\n' + text return await custom.GM.setValue(key, z) } } async function getJVstr (key, d) { const z = await custom.GM.getValue(key, null) if (z === null) return d if (z === '{}') return z if (typeof z !== 'string') return z const j = z.indexOf('\n') if (j <= 0) return z const w = z.substring(0, j) const t = z.substring(j + 1) if (w === 'b') return LZString.decompressFromUTF16(t) // eslint-disable-line no-undef if (w === 'a') return t return t } /* async function getJVobj (key, d) { const z = await custom.GM.getValue(key, null) if (z === null) return d if (z === '{}') return {} const t = LZString.decompressFromUTF16(z) return JSON.parse(t) } */ function measurePlainTextLength (text) { try { return (new TextEncoder().encode(text)).length } catch (e) { return text.length } } function measureJVLength (obj) { let z if (isLZStringAvailable && genius.option.useLZCompression && genius.option.shouldUseLZStringCompression) { z = LZString.compressToUTF16(JSON.stringify(obj)) // eslint-disable-line no-undef } else { z = JSON.stringify(obj) } return measurePlainTextLength(z) } function getHostname (url) { // absolute path if (typeof url === 'string' && url.startsWith('http')) { const query = new URL(url) return query.hostname } // relative path - use <a> or new URL(url, document.baseURI) const a = document.createElement('a') a.href = url return a.hostname } function removeIfExists (e) { if (e && e.remove) { e.remove() } } const removeElements = (typeof window.DocumentFragment.prototype.append === 'function') ? function (elements) { document.createDocumentFragment().append(...elements) } : function (elements) { for (const element of elements) { element.remove() } } function removeTagsKeepText (node) { let tmpNode = null while ((tmpNode = node.firstChild) !== null) { if ('tagName' in tmpNode && tmpNode.tagName !== 'BR') { removeTagsKeepText(tmpNode) } else { node.parentNode.insertBefore(tmpNode, node) } } node.remove() } function decodeHTML (s) { return `${s}`.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') } function metricPrefix (n, decimals, k) { // http://stackoverflow.com/a/18650828 if (n <= 0) { return String(n) } k = k || 1000 const dm = decimals <= 0 ? 0 : decimals || 2 const sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] const i = Math.floor(Math.log(n) / Math.log(k)) return parseFloat((n / Math.pow(k, i)).toFixed(dm)) + sizes[i] } function cleanUpSongTitle (songTitle) { // Remove featuring artists and version info from song title songTitle = songTitle.replace(/\((single|master|studio|stereo|mono|anniversary|digital|edit|edition|naked|original|re|ed|no.*?\d+|mix|version|\d+th|\d{4}|\s|\.|-|\/)+\)/i, '').trim() songTitle = songTitle.replace(/[-‧⋅·ᐧ•‐‒–—―﹘]\s*(single|master|studio|stereo|mono|anniversary|digital|edit|edition|naked|original|re|ed|no.*?\d+|mix|version|\d+th|\d{4}|\s|\.|-|\/)+/i, '').trim() songTitle = songTitle.replace(/fe?a?t\.?u?r?i?n?g?\s+[^)]+/i, '') songTitle = songTitle.replace(/\(\s*\)/, ' ').replace('"', ' ').replace('[', ' ').replace(']', ' ').replace('|', ' ') songTitle = songTitle.replace(/\s\s+/, ' ') songTitle = songTitle.replace(/[\u200B-\u200D\uFEFF]/g, '') // zero width spaces songTitle = songTitle.trim() return songTitle } function sumOffsets (obj) { const sums = { left: 0, top: 0 } while (obj) { sums.left += obj.offsetLeft sums.top += obj.offsetTop obj = obj.offsetParent } return sums } function convertSelectionCacheV0toV1 (selectionCache) { // the old cache key use '--' which is possible to mixed up with the brand name // the new cache key use '\t' as separator const ret = {} const bugKeys = [] function pushBugKey (selectionCacheKey) { const s = selectionCacheKey.split(/\t/) if (s.length !== 2) return const songTitle = s[0] const artists = s[1] // setting simpleTitle as cache key was a bug const simpleTitle = songTitle.replace(/\s*-\s*.+?$/, '') // Remove anything following the last dash if (simpleTitle !== songTitle) { bugKeys.push(`${simpleTitle}\t${artists}`) } } console.warn('Genius Lyrics - old section cache V0 is found: ', selectionCache) for (const originalKey in selectionCache) { if (originalKey === '__VERSION__') continue let k = 0 const selectionCacheKey = originalKey .replace(/[\r\n\t\s]+/g, ' ') .replace(/--/g, () => { k++ return '\t' }) if (k === 1) { pushBugKey(selectionCacheKey) ret[selectionCacheKey] = selectionCache[originalKey] } } for (const bugKey of bugKeys) { delete ret[bugKey] } console.warn('Genius Lyrics - old section cache V0 is converted to V1: ', ret) return ret } function convertSelectionCacheV1toV2 (selectionCache) { // ${title}\t${artists} => ${artists}\t${title} const ret = {} console.warn('Genius Lyrics - old section cache V1 is found: ', selectionCache) for (const originalKey in selectionCache) { if (originalKey === '__VERSION__') continue const s = originalKey.split('\t') const selectionCacheKey = `${s[1]}\t${s[0]}` ret[selectionCacheKey] = selectionCache[originalKey] } console.warn('Genius Lyrics - old section cache V1 is converted to V2: ', ret) return ret } function loadRequestCache (storedValue) { // global requestCache if (storedValue === '{}') { requestCache = cleanRequestCache() } else { try { requestCache = JSON.parse(storedValue) if (!requestCache.__VERSION__) { requestCache.__VERSION__ = 0 } } catch (e) { requestCache = cleanRequestCache() } } if (requestCache.__VERSION__ !== __REQUEST_CACHE_VERSION__) { requestCache = cleanRequestCache() setJV('requestcache', requestCache) } } function loadSelectionCache (storedValue) { // global selectionCache if (storedValue === '{}') { selectionCache = cleanSelectionCache() } else { try { selectionCache = JSON.parse(storedValue) if (!selectionCache.__VERSION__) { selectionCache.__VERSION__ = 0 } } catch (e) { selectionCache = cleanSelectionCache() } } if (selectionCache.__VERSION__ !== __SELECTION_CACHE_VERSION__) { if (selectionCache.__VERSION__ === 0) { selectionCache = convertSelectionCacheV0toV1(selectionCache) selectionCache.__VERSION__ = 1 selectionCache = convertSelectionCacheV1toV2(selectionCache) selectionCache.__VERSION__ = __SELECTION_CACHE_VERSION__ } else if (selectionCache.__VERSION__ === 1) { selectionCache = convertSelectionCacheV1toV2(selectionCache) selectionCache.__VERSION__ = __SELECTION_CACHE_VERSION__ } else { selectionCache = cleanSelectionCache() } setJV('selectioncache', selectionCache) } } function loadCache () { Promise.all([ getJVstr('selectioncache', '{}'), getJVstr('requestcache', '{}'), custom.GM.getValue('optionautoshow', true) ]).then(function (values) { loadSelectionCache(values[0]) loadRequestCache(values[1]) genius.option.autoShow = values[2] === true || values[2] === 'true' /* requestCache = { "cachekey0": "121648565.5\njsondata123", ... } */ const now = (new Date()).getTime() const exp = 2 * 60 * 60 * 1000 for (const prop in requestCache) { if (prop === '__VERSION__') continue // 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 invalidateRequestCache (obj) { const resultCachekey = JSON.stringify(obj) if (resultCachekey in requestCache) { delete requestCache[resultCachekey] } } function getRequestCacheKeyReplacer (key, value) { if (key === 'headers') { return undefined } else if (key === 'url') { if (typeof value !== 'string') return undefined let idx idx = value.lastIndexOf('/') value = `~${idx}${value.substring(idx)}` idx = value.indexOf('?') if (idx > 0) { value = value.substring(0, idx + 1) + decodeURIComponent(value.substring(idx + 1)).replace(/\s+/g, '-') } } return value } function getRequestCacheKey (obj) { return JSON.stringify(obj, getRequestCacheKeyReplacer) } function request (obj) { const cachekey = getRequestCacheKey(obj) if (cachekey in requestCache) { return obj.load(JSON.parse(requestCache[cachekey].split('\n')[1]), null) } const method = obj.method ? obj.method : 'GET' let headers = { Referer: obj.url, // 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', Host: getHostname(obj.url), 'User-Agent': navigator.userAgent } if (method === 'POST') headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' if (obj.responseType === 'json') headers['Accept'] = 'application/json' // eslint-disable-line dot-notation if (obj.headers) { headers = Object.assign(headers, obj.headers) } const cookiePartition = {} if (obj.url.startsWith('https://genius.com/')) { cookiePartition.topLevelSite = 'https://genius.com' } const req = { url: obj.url, method, data: obj.data, headers, cookiePartition, onerror: obj.error ? obj.error : function xmlHttpRequestGenericOnError (response) { console.error('xmlHttpRequestGenericOnError: ' + response) }, onload: function xmlHttpRequestOnLoad (response) { const time = (new Date()).toJSON() let cacheObject = null if (typeof obj.preProcess === 'function') { const proceed = obj.preProcess.call(this, response) if (typeof proceed === 'object') { cacheObject = proceed } } if (cacheObject === null) { // only if preProcess is undefined or preProcess() does not return a object if (genius.option.requestCallbackResponseTextOnly === true) { // only cache responseText cacheObject = { responseText: response.responseText } } else { // full object const newObject = Object.assign({}, response) newObject.responseText = response.responseText // key 'responseText' is not enumerable cacheObject = newObject } } // only cache when the callback call this function function cacheResult (cacheObject) { if (cacheObject !== null) { requestCache[cachekey] = time + '\n' + JSON.stringify(cacheObject) setJV('requestcache', requestCache) } } obj.load(cacheObject, cacheResult) } } if (obj.responseType) req.responseType = obj.responseType if (obj.responseType === 'json') req.overrideMimeType = 'application/json; charset=utf-8' return custom.GM.xmlHttpRequest(req) } function generateCompoundTitle (title, artists) { title = title.replace(/\s+/g, ' ') // space, \n, \t, ... artists = artists.replace(/\s+/g, ' ') return `${artists}\t${title}` } function displayTextOfCompoundTitle (compoundTitle) { return compoundTitle.replace('\t', ' ') } function rememberLyricsSelection (title, artists, jsonHit) { const compoundTitleKey = artists === null ? title : generateCompoundTitle(title, artists) if (typeof jsonHit === 'object') { jsonHit = JSON.stringify(jsonHit) } if (typeof jsonHit !== 'string') { return } selectionCache[compoundTitleKey] = jsonHit setJV('selectioncache', selectionCache) } function forgetLyricsSelection (title, artists) { const compoundTitleKey = artists === null ? title : generateCompoundTitle(title, artists) if (compoundTitleKey in selectionCache) { delete selectionCache[compoundTitleKey] setJV('selectioncache', selectionCache) } } function forgetCurrentLyricsSelection () { const ctitle = genius.current.compoundTitle if (typeof ctitle === 'string') { forgetLyricsSelection(ctitle, null) return true } return false } function getLyricsSelection (title, artists) { const compoundTitleKey = artists === null ? title : generateCompoundTitle(title, artists) if (compoundTitleKey in selectionCache) { return JSON.parse(selectionCache[compoundTitleKey]) } else { return false } } function ReleaseDateComponent (components) { if (!components) return if (components.year - components.month - components.day > 0) { // avoid NaN return `${components.year}.${components.month < 10 ? '0' : ''}${components.month}.${components.day < 10 ? '0' : ''}${components.day}` } return null } function removeSymbolsAndWhitespace (s) { return s.replace(/[\s\p{P}$+<=>^`|~]/gu, '') } function getHitResultType (result) { if (typeof (result.language || 0) === 'string') { if (result.language === 'romanization') return 'romanization' if (result.language === 'romanisation') return 'romanization' if (result.language === 'translation') return 'translation' } const primaryArtist = result.primary_artist || 0 if (primaryArtist) { if (typeof primaryArtist.slug === 'string' && (primaryArtist.slug || '').startsWith('Genius-')) { if (/Genius-[Rr]omani[zs]ations?/.test(primaryArtist.slug)) { return 'romanization' } if (/Genius-[Tt]ranslations?/.test(primaryArtist.slug)) { return 'translation' } } if (typeof primaryArtist.name === 'string' && (primaryArtist.name || '').startsWith('Genius')) { if (/Genius\s+[Rr]omani[zs]ations?/.test(primaryArtist.name)) { return 'romanization' } if (/Genius\s+[Tt]ranslations?/.test(primaryArtist.name)) { return 'translation' } } } const path = result.path || 0 if (typeof path === 'string') { if (/\b[Gg]enius\b\S+\bromani[zs]ations?\b/.test(path)) return 'romanization' if (/\b[Gg]enius\b\S+\btranslations?\b/.test(path)) return 'translation' } return '' } function modifyHits (hits, query) { // the original hits store too much and not in a proper ordering // only song.result.url is neccessary // There are few instrumental music existing in Genius // No lyrics will be provided for instrumental music in Genius hits = hits.filter(hit => { if (hit.result.instrumental === true) return false if (hit.result.lyrics_state === 'unreleased') return false if (genius.minimizeHit.onlyCompleteLyrics === true && hit.result.lyrics_state !== 'complete') return false const primary_artist = (hit.result.primary_artist || 0).name || 0 // eslint-disable-line camelcase if (primary_artist.startsWith('Deleted') && primary_artist.endsWith('Artist')) return false // eslint-disable-line camelcase return true }) const removeZeroWidthSpaceAndTrimStringsInObject = function (obj) { // Recursively traverse object, and remove zero width spaces and trim string values if (obj !== null && typeof obj === 'object') { Object.entries(obj).forEach(([key, value]) => { obj[key] = removeZeroWidthSpaceAndTrimStringsInObject(value) }) } else if (typeof obj === 'string') { return obj.replace(/[\u200B-\u200D\uFEFF]/g, '').trim() } return obj } for (const hit of hits) { const result = hit.result if (!result) return const primaryArtist = result.primary_artist || 0 const minimizeHit = genius.minimizeHit const hitResultType = getHitResultType(hit.result) delete hit.highlights // always [] delete result.annotation_count // always 0 delete result.pyongs_count // always null if (minimizeHit.noImageURL) { // if the script does not require the images, remove to save storage delete result.header_image_thumbnail_url delete result.header_image_url delete result.song_art_image_thumbnail_url delete result.song_art_image_url } if (minimizeHit.noRelatedLinks) { delete result.relationships_index_url } if (minimizeHit.noFeaturedArtists) { // it can be a band of 35 peoples which is wasting storage delete result.featured_artists } if (primaryArtist) { if (minimizeHit.noImageURL) { delete primaryArtist.header_image_url delete primaryArtist.image_url } if (minimizeHit.noRelatedLinks) { delete primaryArtist.api_path delete primaryArtist.url delete primaryArtist.is_meme_verified delete primaryArtist.is_verified delete primaryArtist.index_character delete primaryArtist.slug } } // reduce release date storage if (minimizeHit.simpleReleaseDate && 'release_date_components' in result) { const c = ReleaseDateComponent(result.release_date_components) if (c !== null) { result.release_date = c } } if (minimizeHit.noRawReleaseDate) { delete result.release_date_components delete result.release_date_for_display delete result.release_date_with_abbreviated_month_for_display } if (minimizeHit.shortenArtistName && primaryArtist && typeof primaryArtist.name === 'string' && typeof result.artist_names === 'string') { // if it is a brand the title could be very long as it compose it with the full member names if (primaryArtist.name.length < result.artist_names.length) { result.artist_names = primaryArtist.name } } if (minimizeHit.fixArtistName) { if (hitResultType === 'romanization' && result.title === result.title_with_featured && result.artist_names === primaryArtist.name) { // Example: "なとり (Natori) - Overdose (Romanized)" const split = result.title.split(' - ') if (split.length === 2) { result.artist_names = split[0] primaryArtist.name = split[0] result.title = split[1] result.title_with_featured = split[1] } } } if (minimizeHit.removeStats) { delete result.stats } // Remove zero width spaces in strings and trim strings removeZeroWidthSpaceAndTrimStringsInObject(result) if (hits.length > 1) { if (hit.type === 'song') { hit._order = 2600 } else { hit._order = 1300 } if (hitResultType === 'romanization') { if (genius.option.romajiPriority === 'low') { hit._order -= 50 } else if (genius.option.romajiPriority === 'high') { hit._order += 50 } } if (hit.result.updated_by_human_at) { hit._order += 400 } if (hitResultType === 'translation') { // possible translation for non-english songs // if all results are en, no different for hit._order reduction hit._order -= 1000 } // Sort hits by comparing to the query if (query) { query = query.toLowerCase() const queryNoSymbols = removeSymbolsAndWhitespace(query) const title = result.title.toLowerCase() const artist = primaryArtist ? primaryArtist.name.toLowerCase() : '' const titleNoSymbols = removeSymbolsAndWhitespace(title) const artistNoSymbols = removeSymbolsAndWhitespace(artist) if (artist && `${artist} ${title}` === query) { hit._order += 10 } else if (titleNoSymbols && artistNoSymbols && artistNoSymbols + titleNoSymbols === queryNoSymbols) { hit._order += 9 } else { if (query.indexOf(title) !== -1) { hit._order += 4 } else if (titleNoSymbols && queryNoSymbols.indexOf(titleNoSymbols) !== -1) { hit._order += 3 } if (primaryArtist && query.indexOf(primaryArtist.name) !== -1) { hit._order += 4 } else if (artistNoSymbols && queryNoSymbols.indexOf(artistNoSymbols) !== -1) { hit._order += 3 } } } } } if (hits.length > 1) { hits.sort((a, b) => { // if order is the same, compare the entry id (greater is more recent) return (b._order - a._order) || (b.result.id - a.result.id) || 0 }) } // console.log(hits) return hits } function geniusSearch (query, cb, cbError) { console.log('Genius Search Query', query) let requestObj = { url: 'https://genius.com/api/search/song?page=1&q=' + encodeURIComponent(query), headers: { 'X-Requested-With': 'XMLHttpRequest' }, t: 'search', // differentiate with other types of requesting responseType: 'json', error: function geniusSearchOnError (response) { console.error(response) modalAlert(custom.scriptName + '\n\nError in geniusSearch(' + JSON.stringify(query) + ', ' + ('name' in cb ? cb.name : 'cb') + '):' + '\nRequest status:' + ('status' in response ? response.status : 'unknown') + ' ' + ('statusText' in response ? response.statusText : '') + ('finalUrl' in response ? '\nUrl: ' + response.finalUrl : '')) invalidateRequestCache(requestObj) if (typeof cbError === 'function') cbError() requestObj = null }, preProcess: function geniusSearchPreProcess (response) { let jsonData = null let errorMsg = '' try { jsonData = JSON.parse(response.responseText) } catch (e) { errorMsg = e } if (jsonData !== null) { const section = (((jsonData || 0).response || 0).sections[0] || 0) const hits = section.hits || 0 if (typeof hits !== 'object') { modalAlert(custom.scriptName + '\n\n' + 'Incorrect Response Format' + ' in geniusSearch(' + JSON.stringify(query) + ', ' + ('name' in cb ? cb.name : 'cb') + '):\n\n' + response.responseText) invalidateRequestCache(requestObj) if (typeof cbError === 'function') cbError() requestObj = null return } section.hits = modifyHits(hits, query) return jsonData } else { if (response.responseText.startsWith('<') && !askedToSolveCaptcha) { askedToSolveCaptcha = true captchaHint(response.responseText) } console.debug(custom.scriptName + '\n\n' + (errorMsg || 'Error') + ' in geniusSearch(' + JSON.stringify(query) + ', ' + ('name' in cb ? cb.name : 'cb') + '):\n\n' + response.responseText) // log into the console window for copying invalidateRequestCache(requestObj) if (typeof cbError === 'function') cbError() requestObj = null } }, load: function geniusSearchOnLoad (jsonData, cacheResult) { if (typeof cacheResult === 'function') cacheResult(jsonData) cb(jsonData) } } request(requestObj) } function loadGeniusSong (song, cb) { request({ url: song.result.url, theme: `${genius.option.themeKey}`, // different theme, differnt html cache error: function loadGeniusSongOnError (response) { console.error(response) modalAlert(custom.scriptName + '\n\nError loadGeniusSong(' + JSON.stringify(song) + ', cb):\n' + '\nRequest status:' + ('status' in response ? response.status : 'unknown') + ' ' + ('statusText' in response ? response.statusText : '') + ('finalUrl' in response ? '\nUrl: ' + response.finalUrl : '')) }, load: function loadGeniusSongOnLoad (response, cacheResult) { // cacheResult(response) cb(response, cacheResult) } }) } async function waitForStableScrollTop () { let p1 let p2 = document.scrollingElement.scrollTop const ct = Date.now() do { p1 = p2 await getRafPromise().then() // eslint-disable-line promise/param-names p2 = document.scrollingElement.scrollTop if (Date.now() - ct > 2800) break } while (`${p1}` !== `${p2}`) } function delay (ms) { return new Promise(resolve => setTimeout(resolve, ms)) // eslint-disable-line promise/param-names } function setArrowUpDownStyle (resumeButton) { if (!resumeButton) return const oldAttribute = resumeButton.getAttribute('arrow-icon') const newAttribute = (document.scrollingElement.scrollTop - window.newScrollTopPosition < 0) ? 'up' : 'down' if (oldAttribute !== newAttribute) { resumeButton.setAttribute('arrow-icon', newAttribute) } } async function onResumeAutoScrollClick () { const resumeAutoScrollButtonContainer = document.querySelector('#resumeAutoScrollButtonContainer') if (resumeAutoScrollButtonContainer === null || typeof window.newScrollTopPosition !== 'number') return window.scrollLyricsBusy = true window.lastScrollTopPosition = null resumeAutoScrollButtonContainer.classList.remove('btn-show') // Resume auto scrolling document.scrollingElement.scrollTo({ top: window.newScrollTopPosition, behavior: 'smooth' }) await delay(100) if (document.visibilityState === 'visible') { await waitForStableScrollTop() } window.scrollLyricsBusy = false } function onResumeAutoScrollFromHereClick () { const resumeAutoScrollButtonContainer = document.querySelector('#resumeAutoScrollButtonContainer') if (resumeAutoScrollButtonContainer === null || typeof window.staticOffsetTop !== 'number' || typeof window.newScrollTopPosition !== 'number') return window.scrollLyricsBusy = true resumeAutoScrollButtonContainer.classList.remove('btn-show') // Resume auto scrolling from current position if (genius.debug) { for (const e of document.querySelectorAll('.scrolllabel')) { e.remove() } window.first = false } window.lastScrollTopPosition = null let newScrollTop = window.newScrollTopPosition let count = 4 while (+newScrollTop.toFixed(1) !== +document.scrollingElement.scrollTop.toFixed(1)) { window.staticOffsetTop += document.scrollingElement.scrollTop - newScrollTop newScrollTop = getNewScrollTop().newScrollTop if (--count === 0) break } setTimeout(() => { window.scrollLyricsBusy = false }, 30) } function getNewScrollTop (div) { const staticTop = typeof window.staticOffsetTop === 'number' ? window.staticOffsetTop : theme.defaultStaticOffsetTop div = div || document.querySelector(theme.scrollableContainer) const offsetTop = (div.getBoundingClientRect().top - document.scrollingElement.getBoundingClientRect().top) const iframeHeight = document.scrollingElement.clientHeight const position = window.latestScrollPos const newScrollTop = staticTop + (div.scrollHeight - iframeHeight) * position + offsetTop return { newScrollTop, iframeHeight, staticTop } } async function scrollLyricsGeneric (position) { window.latestScrollPos = position if (window.scrollLyricsBusy) return window.scrollLyricsBusy = true if (document.visibilityState === 'visible') { await waitForStableScrollTop() } const div = document.querySelector(theme.scrollableContainer) const offset = genius.debug ? sumOffsets(div) : null const lastPos = window.lastScrollTopPosition let { newScrollTop, iframeHeight, staticTop } = getNewScrollTop(div) const maxScrollTop = document.scrollingElement.scrollHeight - iframeHeight let btnContainer = document.querySelector('#resumeAutoScrollButtonContainer') async function showButtons () { const staticTopChanged = window.staticOffsetTop !== staticTop window.newScrollTopPosition = newScrollTop if (staticTopChanged) { window.staticOffsetTop = staticTop } // User scrolled -> stop auto scroll if (!btnContainer) { const resumeButton = document.createElement('div') const resumeButtonFromHere = document.createElement('div') const resumeAutoScrollButtonContainer = document.createElement('div') resumeAutoScrollButtonContainer.id = 'resumeAutoScrollButtonContainer' resumeButton.addEventListener('click', onResumeAutoScrollClick, false) resumeButtonFromHere.addEventListener('click', onResumeAutoScrollFromHereClick, false) resumeButton.id = 'resumeAutoScrollButton' resumeButton.setAttribute('title', 'Resume auto scrolling') resumeButton.appendChild(document.createElement('div')) setArrowUpDownStyle(resumeButton) resumeButtonFromHere.id = 'resumeAutoScrollFromHereButton' resumeButtonFromHere.setAttribute('title', 'Resume auto scrolling from here') resumeButtonFromHere.appendChild(document.createElement('div')) appendElements(resumeAutoScrollButtonContainer, [resumeButton, resumeButtonFromHere]) document.body.appendChild(resumeAutoScrollButtonContainer) btnContainer = resumeAutoScrollButtonContainer } else { const resumeButton = document.querySelector('#resumeAutoScrollButton') setArrowUpDownStyle(resumeButton) } await Promise.resolve(0) // wait for DOM // if (newScrollTop > 0 && newScrollTop < maxScrollTop) { btnContainer.classList.add('btn-show') // } await Promise.resolve(0) // wait for DOM window.scrollLyricsBusy = false } function isShowButtonRequired () { if (typeof lastPos === 'number' && lastPos >= 0 && Math.abs(lastPos - document.scrollingElement.scrollTop) > 5) { // lastPos !== null showButtons() return true } return false } function smoothScroll () { window.lastScrollTopPosition = newScrollTop document.scrollingElement.scrollTo({ top: newScrollTop, behavior: 'smooth' }) } function debug () { if (!window.first) { window.first = true for (let i = 0; i < 11; i++) { const label = document.body.appendChild(document.createElement('div')) label.classList.add('scrolllabel') label.textContent = (`${i * 10}% + ${window.staticOffsetTop}px`) label.style.position = 'absolute' label.style.top = `${offset.top + window.staticOffsetTop + div.scrollHeight * 0.1 * i}px` label.style.color = 'rgba(255,0,0,0.5)' label.style.zIndex = 1000 } let label = document.body.appendChild(document.createElement('div')) label.classList.add('scrolllabel') label.textContent = `Start @ offset.top + window.staticOffsetTop = ${offset.top}px + ${window.staticOffsetTop}px` label.style.position = 'absolute' label.style.top = `${offset.top + window.staticOffsetTop}px` label.style.left = '200px' label.style.color = '#008000a6' label.style.zIndex = 1000 label = document.body.appendChild(document.createElement('div')) label.classList.add('scrolllabel') label.textContent = `Base @ offset.top = ${offset.top}px` label.style.position = 'absolute' label.style.top = `${offset.top}px` label.style.left = '200px' label.style.color = '#008000a6' label.style.zIndex = 1000 } let indicator = document.getElementById('scrollindicator') if (!indicator) { indicator = document.body.appendChild(document.createElement('div')) indicator.classList.add('scrolllabel') indicator.id = 'scrollindicator' indicator.style.position = 'absolute' indicator.style.left = '150px' indicator.style.color = '#00dbff' indicator.style.zIndex = 1000 } indicator.style.top = `${offset.top + window.staticOffsetTop + div.scrollHeight * position}px` indicator.textContent = `${parseInt(position * 100)}% -> ${parseInt(newScrollTop)}px` } let bool2 = true if (((newScrollTop < 0 || newScrollTop > maxScrollTop))) { if (newScrollTop < 0) newScrollTop = 0 else if (newScrollTop > maxScrollTop) newScrollTop = maxScrollTop bool2 = (lastPos === 0 || lastPos === maxScrollTop) && lastPos === newScrollTop } if (bool2 && isShowButtonRequired()) { return } if (btnContainer) { btnContainer.classList.remove('btn-show') } smoothScroll() if (genius.debug) { debug() } if (document.visibilityState === 'visible') { await waitForStableScrollTop() } window.scrollLyricsBusy = false } function loadGeniusAnnotations (song, html, annotationsEnabled, cb) { let annotations = {} if (!annotationsEnabled) { // return cb(song, html, {}) return cb(annotations) } if (html.indexOf('ReferentFragment-') === -1) { console.log('No annotations in source -> skip loading annotations from API') // No annotations in source -> skip loading annotations from API // return cb(song, html, {}) return cb(annotations) } const m = html.match(/href="\/\d+\//g) const ids = m.map((s) => `ids[]=${s.match(/\d+/)[0]}`) const apiurl = 'https://genius.com/api/referents/multi?text_format=html%2Cplain&' + ids.join('&') request({ url: apiurl, headers: { 'X-Requested-With': 'XMLHttpRequest' }, t: 'annotations', // differentiate with other types of requesting responseType: 'json', error: function loadGeniusAnnotationsOnError (response) { console.error(response) modalAlert(custom.scriptName + '\n\nError loadGeniusAnnotations(' + JSON.stringify(song) + ', cb):\n' + '\nRequest status:' + ('status' in response ? response.status : 'unknown') + ' ' + ('statusText' in response ? response.statusText : '') + ('finalUrl' in response ? '\nUrl: ' + response.finalUrl : '')) cb(annotations) }, preProcess: function loadGeniusAnnotationsPreProcess (response) { const r = JSON.parse(response.responseText).response annotations = {} if (typeof r.referents.length === 'number') { for (const referent of r.referents) { for (const annotation of referent.annotations) { if (annotation.referent_id in annotations) { annotations[annotation.referent_id].push(annotation) } else { annotations[annotation.referent_id] = [annotation] } } } } else { for (const refId in r.referents) { const referent = r.referents[refId] for (const annotation of referent.annotations) { if (annotation.referent_id in annotations) { annotations[annotation.referent_id].push(annotation) } else { annotations[annotation.referent_id] = [annotation] } } } } return annotations }, load: function loadGeniusAnnotationsOnLoad (annotations, cacheResult) { if (typeof cacheResult === 'function') cacheResult(annotations) cb(annotations) } }) } const themeCommon = { lyricsAppInit () { let application = document.querySelector('#application') if (application !== null) { application.classList.add('app11') } application = null }, // Change links to target=_blank targetBlankLinks () { const originalUrl = document.querySelector('meta[property="og:url"]') ? document.querySelector('meta[property="og:url"]').content : null const as = document.querySelectorAll('body a[href]:not([href|="#"]):not([target="_blank"])') for (const a of as) { const href = a.getAttribute('href') if (!href.startsWith('#')) { a.target = '_blank' if (!href.startsWith('http')) { a.href = 'https://genius.com' + href } else if (href.startsWith(custom.domain)) { a.href = href.replace(custom.domain, 'https://genius.com') } } else if (originalUrl) { // Convert internal anchor to external anchor a.target = '_blank' a.href = originalUrl + a.hash } } }, setScrollUpdateLocation () { document.addEventListener('scroll', scrollUpdateLocationHandler, false) }, getAnnotationsContainer (a) { let c = document.getElementById('annotationcontainer958') if (!c) { c = document.body.appendChild(document.createElement('div')) c.setAttribute('id', 'annotationcontainer958') themeCommon.setScrollUpdateLocation(c) } c.textContent = '' c.style.display = 'block' c.style.opacity = 1.0 setAnnotationsContainerTop(c, a, true) const arrow = c.querySelector('.arrow') || c.appendChild(document.createElement('div')) arrow.className = 'arrow' let annotationTabBar = c.querySelector('.annotationtabbar') if (!annotationTabBar) { annotationTabBar = c.appendChild(document.createElement('div')) annotationTabBar.classList.add('annotationtabbar') } annotationTabBar.textContent = '' annotationTabBar.style.display = 'block' let annotationContent = c.querySelector('.annotationcontent') if (!annotationContent) { annotationContent = c.appendChild(document.createElement('div')) annotationContent.classList.add('annotationcontent') } annotationContent.style.display = 'block' annotationContent.textContent = '' return [annotationTabBar, annotationContent] }, annotationSwitchTab (ev) { const id = this.dataset.annotid const selectedElements = document.querySelectorAll('#annotationcontainer958 .annotationtabbar .tabbutton.selected, #annotationcontainer958 .annotationtab.selected') for (const e of selectedElements) { e.classList.remove('selected') } this.classList.add('selected') document.querySelector(`#annotationcontainer958 .annotationtab[id="annottab_${id}"]`).classList.add('selected') }, showAnnotation (ev) { ev.preventDefault() // Annotation id const m = this.href.match(/\/(\d+)\//) if (!m) { return } const id = m[1] // Highlight const highlightedElements = document.querySelectorAll('.annotated.highlighted') for (const e of highlightedElements) { e.classList.remove('highlighted') } this.classList.add('highlighted') // Load all annotations if (!('annotations_userscript' in window)) { if (document.getElementById('annotationsdata_for_userscript')) { window.annotations_userscript = JSON.parse(document.getElementById('annotationsdata_for_userscript').innerHTML) } else { window.annotations_userscript = {} console.log('No annotation data found #annotationsdata_for_userscript') } } if (id in window.annotations_userscript) { const [annotationTabBar, annotationContent] = themeCommon.getAnnotationsContainer(this) let innerHTMLAddition = '' for (const annotation of window.annotations_userscript[id]) { // Example for multiple annotations: https://genius.com/72796/ const tabButton = annotationTabBar.appendChild(document.createElement('div')) tabButton.dataset.annotid = annotation.id tabButton.classList.add('tabbutton') tabButton.addEventListener('click', themeCommon.annotationSwitchTab) if (annotation.state === 'verified') { tabButton.textContent = ('Verified annotation') } else { tabButton.textContent = 'Genius annotation' } let hint = '' if ('accepted_by' in annotation && !annotation.accepted_by) { hint = '<span class="redhint">⚠ This annotation is unreviewed</span><br>' } let header = '<div class="annotationheader" style="float:right">' let author = false if (annotation.authors.length === 1) { if (annotation.authors[0].name) { author = decodeHTML(annotation.authors[0].name) header += `<a href="${annotation.authors[0].url}">${author}</a>` } else { author = decodeHTML(annotation.created_by.name) header += `<a href="${annotation.created_by.url}">${author}</a>` } } else { header += `<span title="Created by ${annotation.created_by.name}">${annotation.authors.length} Contributors</span>` } header += '</div><br style="clear:right">' let footer = '<div class="annotationfooter">' footer += `<div title="Direct link to the annotation"><a href="${annotation.share_url}">🔗 Share</a></div>` if (annotation.pyongs_count) { footer += `<div title="Pyongs"> ⚡ ${annotation.pyongs_count}</div>` } if (annotation.comment_count) { footer += `<div title="Comments"> 💬 ${annotation.comment_count}</div>` } footer += '<div title="Total votes">' if (annotation.votes_total > 0) { footer += '+' footer += annotation.votes_total footer += '👍' } else if (annotation.votes_total < 0) { footer += annotation.votes_total footer += '👎' } else { footer += annotation.votes_total + '👍 👎' } footer += '</div>' footer += '<br style="clear:right"></div>' let body = '' if ('body' in annotation && annotation.body) { body = decodeHTML(annotation.body.html) } if ('being_created' in annotation && annotation.being_created) { if (author) { body = author + ' is currently annotating this line.<br><br>' + body } else { body = 'This line is currently being annotated.<br><br>' + body } } innerHTMLAddition += ` <div class="annotationtab" id="annottab_${annotation.id}"> ${hint} ${header} ${body} ${footer} </div>` } annotationContent.innerHTML += innerHTMLAddition annotationTabBar.appendChild(document.createElement('br')).style.clear = 'left' if (window.annotations_userscript[id].length === 1) { annotationTabBar.style.display = 'none' } annotationTabBar.querySelector('.tabbutton').classList.add('selected') annotationContent.querySelector('.annotationtab').classList.add('selected') // Resize iframes and images in frame setTimeout(function () { const maxWidth = (document.body.clientWidth - 40) const elements = annotationContent.querySelectorAll('iframe,img') for (const e of elements) { if (e.parentNode.nodeName === 'P' && e.parentNode.childElementCount === 1) { e.parentNode.classList.add('annotation-img-parent-p') e.style.maxWidth = `${maxWidth - 60}px` } else { e.style.maxWidth = `${maxWidth}px` } } themeCommon.targetBlankLinks() // Change link target to _blank }, 100) } }, removeAnnotations () { document.querySelectorAll('#lyrics-root a[class^="ReferentFragment"]').forEach(removeTagsKeepText) }, addAnnotationHandling () { try { window.annotations_userscript = JSON.parse(document.getElementById('annotationsdata_for_userscript').innerHTML) } catch (e) { console.log('Could not load annotations data from script tag:', e) return } // Add click handler to annotations for (const a of document.querySelectorAll('#lyrics-root a[class^="ReferentFragment"]')) { a.classList.add('annotated') a.addEventListener('click', themeCommon.showAnnotation) } document.body.addEventListener('click', function (e) { // Hide annotation container on click outside of it const annotationcontainer = document.getElementById('annotationcontainer958') if (annotationcontainer && !e.target.classList.contains('.annotated') && e.target.closest('.annotated') === null) { if (e.target.closest('#annotationcontainer958') === null) { annotationcontainer.style.display = 'none' annotationcontainer.style.opacity = 0.0 for (const e of document.querySelectorAll('.annotated.highlighted')) { e.classList.remove('highlighted') } } } }) }, setCustomFontSize () { if (genius.option.fontSize && genius.option.fontSize > 0) { if (document.getElementById('lyrics_text_div')) { document.getElementById('lyrics_text_div').style.fontSize = `${genius.option.fontSize}px` } for (const div of document.querySelectorAll('div[data-lyrics-container="true"]')) { div.style.fontSize = `${genius.option.fontSize}px` } } }, themeError (themeName, errorMsg, originalUrl, song) { return `<div style="color:black;background:white;font-family:sans-serif"> <br> <h1>😱 Oops!</h1> <br> Sorry, could not transform the genius page<br>The lyrics cannot be shown with the theme "${themeName}" (yet)<br> Could you inform the author of this program about the problem and provide the following information:<br> <pre style="color:black; background:silver; border:1px solid black; width:95%; overflow:auto;margin-left: 5px;padding: 0px 5px;"> themeName: ${themeName} Error: ${errorMsg} URL: ${document.location.href} Genius: ${originalUrl} Song: ${'result' in song && 'full_title' in song.result ? song.result.full_title : JSON.stringify(song)} Browser: ${navigator.userAgent} </pre><br> You can simply post the information on github:<br> <a target="_blank" href="https://github.com/cvzi/genius-lyrics-userscript/issues/">https://github.com/cvzi/genius-lyrics-userscript/issues/</a> <br> or via email: <a target="_blank" href="mailto:cuzi@openmail.cc">cuzi@openmail.cc</a> <br> <br> Thanks for your help! <br> <br> </div>` }, fixInstrumentalBridge () { for (const div of document.querySelectorAll('div[data-lyrics-container="true"]')) { let innerHTML = div.innerHTML const before = innerHTML innerHTML = innerHTML.replace(/<br><br>\[Instrumental Bridge\]<br><br>/g, '<br><br>[Instrumental Bridge]<a id="Instrumental-Bridge"></a><br><br>') if (before !== innerHTML) { div.innerHTML = innerHTML } } }, extractLyrics (html, song) { /* Extract the lyrics and title/album header from genius page html */ const doc = 'trustedTypes' in window ? Document.parseHTMLUnsafe(window.trustedTypes.createPolicy('ignorePolicy', { createHTML: (x) => x }).createHTML(html)) : Document.parseHTMLUnsafe(html) const originalUrl = doc.querySelector('meta[property="og:url"]') ? doc.querySelector('meta[property="og:url"]').content : null const lyricsContainers = Array.from(doc.querySelectorAll('#lyrics-root [class*=Lyrics-]:not([class*=Sidebar])')) const lyricsPlaceHolder = doc.querySelector('[class*="LyricsPlaceholder__Container"]') if (lyricsContainers.length === 0 && !lyricsPlaceHolder) { return { error: true, errorHtml: themeCommon.themeError( theme.name, 'Neither "Lyrics-" nor "LyricsPlaceholder__Container" found', originalUrl, song ) } } doc.querySelectorAll('[class*="LyricsFooter__Container"]').forEach(e => e.remove()) doc.querySelectorAll('[class*="LyricsEditdesktop__Container"]').forEach(e => e.remove()) const bodyWidth = parseInt(document.getElementById('lyricsiframe').style.width || (document.getElementById('lyricsiframe').getBoundingClientRect().width + 'px')) // Change album links from anchor to real url const albumLinkA = doc.querySelector('[class*="PrimaryAlbum-"][href^="https://genius.com/albums/"]') if (albumLinkA) { doc.querySelectorAll('[href="#primary-album"]').forEach(a => { a.href = albumLinkA.href a.target = '_blank' if (!a.previousSibling.textContent.endsWith(' ')) { // add a space before album name a.parentNode.insertBefore(document.createTextNode(' '), a) } }) } // Insert album art const metaImageUrl = doc.querySelector('meta[property="og:image"][content]') const sizedImage = doc.querySelector('div[class*="SongHeader-"] img[class*="SizedImage-"]:not([src])') if (sizedImage && metaImageUrl) { sizedImage.src = metaImageUrl.content sizedImage.style = 'max-width: 7em;max-height: 7em;' } let lyricsHtml if (lyricsContainers.length > 0) { lyricsHtml = '<div class="SongPage__Section" id="lyrics_text_div">' + lyricsContainers.map(e => e.outerHTML).join('\n') + '</div>' } else if (lyricsPlaceHolder) { lyricsHtml = '<div class="SongPage__Section">' + lyricsPlaceHolder.outerHTML + '</div>' } const h1 = doc.querySelector('div[class^=SongHeader] h1') const titleNode = h1.firstChild const titleA = h1.appendChild(document.createElement('a')) titleA.href = originalUrl titleA.target = '_blank' titleA.appendChild(titleNode) h1.classList.add('mytitle') h1.parentNode.querySelectorAll('a[href^=https]').forEach(a => (a.target = '_blank')) doc.querySelectorAll('div[class^=SongHeader] [class*="InlineSvg-"]').forEach(e => e.remove()) h1.parentNode.querySelectorAll('[class*="HeaderCredits__"]').forEach(e => e.remove()) removeIfExists(h1.parentNode.querySelector('div[class^="HeaderTracklist"]')) const headerHtml = '<div class="myheader">' + h1.parentNode.outerHTML + '</div>' return { error: false, lyricsHtml, headerHtml, bodyWidth } } } function appendHeadText (html, headhtml) { // Add to <head> const idxHead = html.indexOf('</head>') if (idxHead > 5) { html = html.substring(0, idxHead) + headhtml + html.substring(idxHead) } else { html = `<head>${headhtml}</head>${html}` } return html } const isChrome = navigator.userAgent.indexOf('Chrome') !== -1 const iframeCSSCommon = ` html { --egl-btn-half-border-size: 7px; --egl-btn-color: #222; /* this is intended to give some space to see the first line at the vertical center */ --egl-page-pt: 50vh; /* this is intended to give some space to see the last line at the vertical center */ --egl-page-pb: 50vh; visibility: collapse; } html.v { visibility: visible; } html .genius-scrollable{ scroll-behavior: smooth; } html.instant-scroll .genius-scrollable{ scroll-behavior: auto; } #resumeAutoScrollButtonContainer{ position: fixed; right: 20px; top: 30%; z-index: 101; display: flex; flex-direction: row; gap: 4px; } #resumeAutoScrollButtonContainer #resumeAutoScrollButton, #resumeAutoScrollButtonContainer #resumeAutoScrollFromHereButton{ cursor: pointer; border: 1px solid #d9d9d9; border-radius:100%; background:white; display: flex; justify-content: center; align-content: center; justify-items: center; align-items: center; padding: calc(1.732*var(--egl-btn-half-border-size) + 3px); contain: strict; } #resumeAutoScrollButtonContainer { visibility: hidden; pointer-events: none; visibility: collapse; /* if collapse is supported, hidden + no pointer events */ } #resumeAutoScrollButtonContainer.btn-show { visibility: visible; pointer-events: initial; } #resumeAutoScrollButton > div:only-child { position: absolute; contain: strict; } #resumeAutoScrollButton[arrow-icon="up"] > div:only-child { border-top: calc(1.732*var(--egl-btn-half-border-size)) solid var(--egl-btn-color); border-right: var(--egl-btn-half-border-size) inset transparent; border-bottom: 0; border-left: var(--egl-btn-half-border-size) inset transparent; } #resumeAutoScrollButton[arrow-icon="down"] > div:only-child { border-top: 0; border-right: var(--egl-btn-half-border-size) inset transparent; border-bottom: calc(1.732*var(--egl-btn-half-border-size)) solid var(--egl-btn-color); border-left: var(--egl-btn-half-border-size) inset transparent; } #resumeAutoScrollFromHereButton > div:only-child { position: absolute; contain: strict; border-top: var(--egl-btn-half-border-size) inset transparent; border-right: 0; border-bottom: var(--egl-btn-half-border-size) inset transparent; border-left: calc(1.732*var(--egl-btn-half-border-size)) solid var(--egl-btn-color); } body div[class*="__Container"] iframe, body div[class*="__Container"] div[class*="_preview"] iframe, body div[class*="__Container"] div[class*="embed"] iframe { /* override .hPdMCA .embedly_preview iframe {display: block;} */ display: none; position: absolute; top: 0; right: 0; width: 0; height: 0; } div[class*="ExpandableContent__Content"] { mask: none !important; max-height: none !important; overflow:auto !important; } #lyrics-root div[class*="Lyrics-"] { grid-column: 1 / -1; } div[class*="SidebarLyrics-"], div[class*="RightSidebar-"], div[class*="InreadContainer-"], div[class*="LyricsHeader-"], div[class*="PageFooter-"], div[class*="ExpandableContent__ButtonContainer"], div[class*="About-"], div[class*="QuestionList-"], #questions, div[class*=SongComments-], div[class*="AppleMusicPlayer"], div[class*="MusicVideo"], div[class*="ShareButtons"], div[class*="StickyContributorToolbar"], div[class*="StickyNavSentinel"], button[class*="SmallButton-"], div[class*="UnreviewedAnnotation__Container"] { display: none; } div[class*="InnerSectionDivider"] { margin-top:5pt !important; margin-bottom:10pt !important; padding-bottom:10pt !important; } @keyframes appDomAppended { 0% { background-position-x: 1px; } 100% { background-position-x: 2px; } } @keyframes appDomAppended2 { 0% { background-position-x: 3px; } 100% { background-position-x: 4px; } } @keyframes songHeaderDomAppended { 0% { background-position-x: 1px; } 100% { background-position-x: 2px; } } @keyframes headerArtistAndTracklistDOMAppended { 0% { background-position-x: 1px; } 100% { background-position-x: 2px; } } @keyframes headerSongTitleDOMAppended { 0% { background-position-x: 1px; } 100% { background-position-x: 2px; } } #application { animation: appDomAppended 1ms linear 0s 1 normal forwards; } #application.app11 { animation: appDomAppended2 1ms linear 0s 1 normal forwards; } #application.app11 div.LSongHeader__Container:not(.genius-lyrics-header-container) div.LSongHeader__CenterInfo, #application.app11 span#lyrics_rendered { animation: songHeaderDomAppended 1ms linear 0s 1 normal forwards; } span#lyrics_rendered { position:fixed; top:-10px; left:-10px; height:1px; width:1px; } #application.app11 div.LUNDETERMINED__Container .LHeaderArtistAndTracklist__Artist { animation: headerArtistAndTracklistDOMAppended 1ms linear 0s 1 normal forwards; /* animation removed once the class is determined */ } #application.app11 .LSongHeader__Title { animation: headerSongTitleDOMAppended 1ms linear 0s 1 normal forwards; } /* CSS for annotation container */ #annotationcontainer958 { opacity:0.0; display:none; transition:opacity 500ms; position:absolute; background:linear-gradient(to bottom, #FFF1, 5px, white); color:black; font: 100 1.125rem / 1.5 "Programme", sans-serif; max-width:95%; min-width:60%; margin:10px; z-index:4; } #annotationcontainer958 .arrow { height:10px; background: transparent; } #annotationcontainer958 .arrow:before { content: ""; position: absolute; width: 0px; height: 0px; top:0%; margin-top: 6px; ${isChrome ? 'margin-left: calc(50% - 15px);' : 'inset: -1rem 0px 0px 50%;'} border-style: solid; border-width: 0px 25px 20px; border-color: transparent transparent rgb(170, 170, 170); } #annotationcontainer958[location-dir="up"] .arrow { height:0px; } #annotationcontainer958[location-dir="up"] .arrow:before { top:100%; transform: rotate(180deg); margin-top:0px; } #annotationcontainer958 .annotationcontent { background-color:#E9E9E9; padding:5px; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; border-top-right-radius: 0px; border-top-left-radius: 0px; box-shadow: #646464 5px 5px 5px; scrollbar-color: #7d8fe885 transparent; } #annotationcontainer958 .annotationcontent a { color: var(--egl-link-color); } #annotationcontainer958 .annotationtab { display:none } #annotationcontainer958 .annotationtab.selected { display:block } #annotationcontainer958 .annotationtabbar .tabbutton { background-color:#d0cece; cursor:pointer; user-select:none; padding: 1px 7px; margin: 0px 3px; border-radius: 5px 5px 0px 0px; box-shadow: #0000004f 2px -2px 3px; float:left } #annotationcontainer958 .annotationtabbar .tabbutton.selected { background-color:#E9E9E9; } #annotationcontainer958 .annotationcontent .annotationfooter { user-select: none; } #annotationcontainer958 .annotationcontent .annotationfooter > div { float: right; min-width: 20%; text-align: center; } #annotationcontainer958 .annotationcontent .redhint { color:#ff146470; padding:.1rem 0.7rem; } #annotationcontainer958 .annotationcontent .annotation-img-parent-p { display: flex; justify-content: center; align-content: center; margin: 6px; } #annotationcontainer958 .annotationcontent .annotation-img-parent-p > img[src][width][height]:only-child{ object-fit: contain; height: auto; } #annotationcontainer958[location-dir="down"]{ transform: ''; top: calc(var(--annotation-container-syrt) + var(--annotation-container-rh) + 3px); } #annotationcontainer958[location-dir="up"]{ transform: translateY(-100%); top: calc(var(--annotation-container-syrt) - 3px - 18px); window.scrollY + rect.top - 3 - 18); } [data-lyrics-container="true"] + [data-exclude-from-selection="true"] { display: none; } a#Instrumental-Bridge { line-height: 420%; } ` function setAnnotationsContainerTop (c, a, isContentChanged) { const rect = a.getBoundingClientRect() const bodyH = document.scrollingElement.clientHeight const upSpace = Math.max(rect.top, 0) const downSpace = bodyH - Math.min(rect.bottom, bodyH) if (isContentChanged) { c.style.setProperty('--annotation-container-syrt', `${window.scrollY + rect.top}px`) c.style.setProperty('--annotation-container-rh', `${rect.height}px`) } if (downSpace > upSpace) { c.setAttribute('location-dir', 'down') } else { c.setAttribute('location-dir', 'up') } } function scrollUpdateLocationHandler () { getRafPromise(() => { let c = document.querySelector('#annotationcontainer958[style*="display: block;"]') if (c !== null) { let a = document.querySelector('.annotated.highlighted') if (a !== null) { setAnnotationsContainerTop(c, a, false) } a = null } c = null }) } async function scrollToBegining () { document.documentElement.classList.add('instant-scroll') await new Promise(resolve => setTimeout(resolve, 100)) const isContentStylesIsAdded = !!document.querySelector('style#egl-contentstyles') if (isContentStylesIsAdded) { theme.scrollableContainer = 'html #application' // theme.scrollableContainer = '.LSongHeader__Outer_Container' } let scrollable = document.querySelector(theme.scrollableContainer) if (isScrollLyricsEnabled()) { // scrollable.scrollIntoView(true) } else if (scrollable) { const innerTopElement = isContentStylesIsAdded // ? scrollable.querySelector('.genius-lyrics-header-content') ? scrollable.querySelector('.LSongHeader__Outer_Container') : scrollable.firstElementChild scrollable = (innerTopElement || scrollable) // scrollable.scrollIntoView(true) } else { return } scrollable.classList.add('genius-scrollable') await Promise.resolve(0) // allow CSS rule changed scrollable.scrollIntoView(true) // alignToTop = true await Promise.resolve(0) // allow DOM scrollTop changed document.documentElement.classList.remove('instant-scroll') } const themes = { genius: { name: 'Genius (Default)', themeKey: 'genius', scrollableContainer: 'html #application', defaultStaticOffsetTop: 0, scripts: function themeGeniusScripts () { const onload = [] function pushIfAny (arr, element) { if (element) { arr.push(element) } } function hideStuff () { let removals = [] // Hide "Manage Lyrics" and "Click here to go to the old song page" pushIfAny(removals, document.querySelector('div[class^="LyricsControls_"]')) // Hide "This is a work in progress" pushIfAny(removals, document.getElementById('top')) // Header leaderboard/nav pushIfAny(removals, document.querySelector('div[class^="Leaderboard"]')) pushIfAny(removals, document.querySelector('div[class^="StickyNav"]')) pushIfAny(removals, document.querySelector('div[class^="StickyNavSentinel"]')) pushIfAny(removals, document.querySelector('div[class^="Pyong"]')) pushIfAny(removals, document.querySelector('div[class^="Button-"]')) pushIfAny(removals, document.querySelector('div[class^="QuestionList-"]')) pushIfAny(removals, document.querySelector('div[class^="SidebarLyrics-"]')) removals.push(...document.querySelectorAll('div[class^="InreadContainer-"]')) removals.push(...document.querySelectorAll('div[class*="RightSidebar-"]')) pushIfAny(removals, document.querySelector('div[class^="AppleMusicPlayer"]')) pushIfAny(removals, document.querySelector('div[class^="MusicVideo"]')) pushIfAny(removals, document.querySelector('div[class^="ShareButtons"]')) pushIfAny(removals, document.querySelector('div[class^="StickyContributorToolbar"]')) removals.push(...document.querySelectorAll('button[class^="SmallButton-"]')) pushIfAny(removals, document.querySelector('div[class^="SongDescription-"] div[class^="SongDescription-"]')) // Footer except copyright hint let divs divs = document.querySelectorAll('div[class^="PageGriddesktop"] div[class^="PageFooterdesktop"]') for (const div of divs) { if (div.innerHTML.indexOf('©') === -1) { removals.push(div) } } divs = document.querySelectorAll('div[class^="PageGriddesktop"]') for (const div of divs) { div.className = '' } // Ads divs = document.querySelectorAll('div[class^="InreadAd__Container"],div[class^="InreadAddesktop__Container"]') for (const div of divs) { removals.push(div) } divs = document.querySelectorAll('div[class^="SidebarAd__Container"]') for (const div of divs) { removals.push(div.parentNode) } if (removals.length > 0) { removeElements(removals) } removals.length = 0 removals = null } // Make song title clickable function clickableTitle () { const url = document.querySelector('meta[property="og:url"]').content const h1 = document.querySelector('h1[class^="SongHeader"]') h1.innerHTML = '<a target="_blank" href="' + url + '">' + h1.innerHTML + '</a>' const img = document.querySelector('div[class^=SongHeader] img[src]') if (img) { img.parentNode.innerHTML = '<a target="_blank" href="' + url + '">' + img.innerHTML + '</a>' } // Fix album link const albumLinkA = document.querySelector('[class*="PrimaryAlbum-"][href^="https://genius.com/albums/"]') if (albumLinkA) { document.querySelectorAll('[href="#primary-album"]').forEach(a => { a.href = albumLinkA.href a.target = '_blank' if (!a.previousSibling.textContent.endsWith(' ')) { // add a space before album name a.parentNode.insertBefore(document.createTextNode(' '), a) } }) } } onload.push(clickableTitle) // Show artwork onload.push(function showArtwork () { const noscripts = document.querySelectorAll('div[class^="SizedImage-"] noscript') // noScriptImage for (const noscript of noscripts) { const div = noscript.parentNode div.innerHTML = noscript.innerHTML div.querySelector('img').style.left = '0px' } // Song artwork const metaImageUrl = document.querySelector('meta[property="og:image"][content]') const sizedImage = document.querySelector('div[class*="SongHeader-"] img[class*="SizedImage-"]:not([src])') if (sizedImage && metaImageUrl) { sizedImage.src = metaImageUrl.content sizedImage.style = 'max-width: 7em;max-height: 7em;' } }) onload.push(hideStuff) // fixInstrumentalBridge onload.push(themeCommon.fixInstrumentalBridge) // Make expandable content buttons work function expandContent () { const button = this const content = button.parentNode.querySelector('div[class*="__Content"]') || button.parentNode.parentNode.querySelector('div[class*="__Expandable"]') for (const className of content.classList) { if (className.indexOf('__Content') === -1 && className.indexOf('__Expandable') === -1) { content.classList.remove(className) } } button.remove() } onload.push(function makeExpandablesWork () { const divs = document.querySelectorAll('div[class*="__Container"]') for (const div of divs) { const button = div.querySelector('button[class^="Button"]') if (button) { button.addEventListener('click', expandContent) } } }) onload.push(themeCommon.targetBlankLinks) onload.push(() => setTimeout(themeCommon.targetBlankLinks, 1000)) // fixInstrumentalBridge onload.push(themeCommon.fixInstrumentalBridge) // Handle annotations if (!annotationsEnabled) { // Remove all annotations onload.push(themeCommon.removeAnnotations) } else { onload.push(themeCommon.addAnnotationHandling) } onload.push(() => { Promise.resolve(0).then(() => { document.documentElement.classList.add('v') }) }) // Set custom fontSize onload.push(themeCommon.setCustomFontSize) // Goto lyrics onload.push(scrollToBegining) return onload }, combine: function themeGeniusCombineGeniusResources (song, html, annotations, cb) { let headhtml = '' // Change design html = html.split('<div class="leaderboard_ad_container">').join('<div class="leaderboard_ad_container" style="width:0px;height:0px">') // Remove cookie consent html = html.replace(/<script defer="true" src="https:\/\/cdn.cookielaw.org.+?"/, '<script ') // Add base for relative hrefs headhtml += '\n<base href="https://genius.com/" target="_blank">' // Add annotation data if (annotationsEnabled) { headhtml += '\n<script id="annotationsdata_for_userscript" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</script>' } // Scrollbar colors // Highlight annotated lines on hover headhtml += ` <style> html{ background-color: #181818 !important; scrollbar-color: hsla(0,0%,100%,.3) transparent !important; scrollbar-width: auto; } .annotated span { background-color: #c0c0c060 !important; text-decoration: none !important; } .annotated:hover span, .annotated.highlighted span { background-color: #ddd !important; text-decoration: none !important; } .annotated.highlighted span { filter: drop-shadow(0px 0px 5px #555); } a[href].annotated { padding: 5px 0px !important; /* make the whole <a> clickable; including gap between lines*/ } html div[class*="SongPage__LyricsWrapper"] { padding-top: var(--egl-page-pt); padding-bottom: var(--egl-page-pb); } ${iframeCSSCommon} </style>` // Add to <head> html = appendHeadText(html, headhtml) return cb(html) } }, cleanwhite: { name: 'Clean white', // secondary theme themeKey: 'cleanwhite', scrollableContainer: '.lyrics_body_pad', defaultStaticOffsetTop: 0, scripts: function themeCleanWhiteScripts () { const onload = [] // fixInstrumentalBridge onload.push(themeCommon.fixInstrumentalBridge) // Handle annotations if (!annotationsEnabled) { // Remove all annotations onload.push(themeCommon.removeAnnotations) } else { onload.push(themeCommon.addAnnotationHandling) } onload.push(themeCommon.targetBlankLinks) onload.push(() => setTimeout(themeCommon.targetBlankLinks, 1000)) onload.push(() => { Promise.resolve(0).then(() => { document.documentElement.classList.add('v') }) }) // Set custom fontSize onload.push(themeCommon.setCustomFontSize) // Goto lyrics onload.push(scrollToBegining) return onload }, combine: function themeCleanWhiteCombineGeniusResources (song, html, annotations, onCombine) { const result = themeCommon.extractLyrics(html, song) if (result.error) { return onCombine(result.errorHtml) } const { lyricsHtml, headerHtml, bodyWidth } = result let headhtml = ` <link rel="stylesheet" href="//fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&family=YouTube+Sans:wght@300..900&display=swap"> <style> body { background:#ffffff linear-gradient(to bottom, #fafafa, #ffffff) fixed !important; color:black; font-family:'Youtube Sans', Roboto, Arial, sans-serif; max-width:${bodyWidth - 20}px; overflow-x:hidden; } .mylyrics {color: black; margin-top:1em;} .mylyrics a:link,.mylyrics a:visited,.mylyrics a:hover{color:black; } .myheader a:link,.myheader a:visited {color: rgb(96, 96, 96);} .myheader { border-bottom: 1px solid #0002; padding-bottom: 1em; margin: 0 10px; max-width: ${bodyWidth - 20 - 20}px; } h1.mytitle a:link,h1.mytitle a:visited {color: rgb(96, 96, 96);} .annotationbox {position:absolute; display:none; max-width:95%; min-width: 160px;padding: 3px 7px;margin: 2px 0 0;background-color: rgba(245, 245, 245, 0.98);background-clip: padding-box;border: 1px solid rgba(0,0,0,.15);border-radius: .25rem;} .annotationbox .annotationlabel {display:block;color:rgb(10, 10, 10);border-bottom:1px solid rgb(200,200,200);padding: 0;font-weight:600} .annotationbox .annotation_rich_text_formatting {color: black} .annotationbox .annotation_rich_text_formatting a {color: rgb(6, 95, 212)} *[class*=HeaderArtistAndTracklist] { font-size:smaller; } *[class*=HeaderArtistAndTracklist] [class*=StyledLink] { padding-left:0.3em; } div[class*="HeaderArtistAndTracklistPrimis"] /* desktop_react_atf */ { display:none; } div.LHeaderMetaCredits__Container { display:none; } html .lyrics_body_pad{ padding-top: var(--egl-page-pt); padding-bottom: var(--egl-page-pb); } h1,h2,h3,h4,h5,h6 { margin:0; } ${iframeCSSCommon} </style>` // Add annotation data headhtml += '\n<script id="annotationsdata_for_userscript" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</script>' return onCombine(` <html> <head> ${headhtml} </head> <body> <div id="application"> <main> <div class="lyrics_body_pad"> ${headerHtml} <div id="lyrics-root" class="mylyrics song_body-lyrics"> ${lyricsHtml} </div> </div> <div class="annotationbox" id="annotationbox"></div> <span id="lyrics_rendered"></span> </main> </div> </body> </html> `) } }, spotify: { name: 'Spotify', // secondary theme themeKey: 'spotify', scrollableContainer: '.lyrics_body_pad', defaultStaticOffsetTop: 0, scripts: function themeSpotifyScripts () { const onload = [] // fixInstrumentalBridge onload.push(themeCommon.fixInstrumentalBridge) // Handle annotations if (!annotationsEnabled) { // Remove all annotations onload.push(themeCommon.removeAnnotations) } else { onload.push(themeCommon.addAnnotationHandling) } onload.push(themeCommon.targetBlankLinks) onload.push(() => setTimeout(themeCommon.targetBlankLinks, 1000)) onload.push(() => { Promise.resolve(0).then(() => { document.documentElement.classList.add('v') }) }) // Set custom fontSize onload.push(themeCommon.setCustomFontSize) // Goto lyrics onload.push(scrollToBegining) return onload }, combine: function themeSpotifyCombineGeniusResources (song, html, annotations, onCombine) { const result = themeCommon.extractLyrics(html, song) if (result.error) { return onCombine(result.errorHtml) } const { lyricsHtml, headerHtml, bodyWidth } = result let headhtml = '' const spotifyOriginalCSS = document.head.querySelector('link[rel="stylesheet"][href*="spotifycdn.com"][href*="web-player"]') if (spotifyOriginalCSS) { headhtml += spotifyOriginalCSS.outerHTML } headhtml += `<style> html{ scrollbar-color:hsla(0,0%,100%,.3) transparent; scrollbar-width:auto; } body { background-color: rgb(21, 21, 21) !important; color:white; max-width: ${bodyWidth - 20}px; overflow-x:hidden; font-family:CircularSp,CircularSp-Arab,CircularSp-Hebr,CircularSp-Cyrl,CircularSp-Grek,CircularSp-Deva,'HelveticaNeue',Arial,sans-serif; padding:10px; } .mylyrics {color: #bebebe; margin-top:1em;} .mylyrics a:link,.mylyrics a:visited,.mylyrics a:hover{color:#f3f3f3} .myheader { border-bottom: 1px solid #FFF2; padding-bottom: 1em; margin: 0 10px; max-width: ${bodyWidth - 20 - 20}px; } .myheader a:link,.myheader a:visited {color: #f3f3f3; } h1.mytitle a:link,h1.mytitle a:visited {color: #bebebe; } ::-webkit-scrollbar-thumb {background-color: hsla(0,0%,100%,.3);} .annotationbox {position:absolute; display:none; max-width:95%; min-width: 160px;padding: 3px 7px;margin: 2px 0 0;background-color: #282828;background-clip: padding-box;border: 1px solid rgba(0,0,0,.15);border-radius: .25rem;} .annotationbox .annotationlabel {display:inline-block;background-color: hsla(0,0%,100%,.6);color: #000;border-radius: 2px;padding: 0 .3em;} .annotationbox .annotation_rich_text_formatting {color: black} .annotationbox .annotation_rich_text_formatting a {color: black)} div[class*="HeaderArtistAndTracklistPrimis"] { display:none; } div.LHeaderMetaCredits__Container { display:none; } h1,h2,h3,h4,h5,h6 { margin:0; } html .lyrics_body_pad{ padding-top: var(--egl-page-pt); padding-bottom: var(--egl-page-pb); } ${iframeCSSCommon} </style>` // Add annotation data headhtml += '\n<script id="annotationsdata_for_userscript" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</script>' return onCombine(` <html> <head> ${headhtml} </head> <body> <div id="application"> <main> <div class="lyrics_body_pad"> ${headerHtml} <div id="lyrics-root" class="mylyrics song_body-lyrics"> ${lyricsHtml} </div> </div> <div class="annotationbox" id="annotationbox"></div> <span id="lyrics_rendered"></span> </main> </div> </body> </html> `) } } } genius.option.themeKey = Object.keys(themes)[0] theme = themes[genius.option.themeKey] function combineGeniusResources (song, html, annotations, cb) { return theme.combine(song, html, annotations, cb) } function reloadCurrentLyrics () { // this is for special use - if the iframe is moved to another container, the content will be re-rendered. // As the lyrics is lost, it requires reloading const compoundTitle = genius.current.compoundTitle if (compoundTitle) { const hitFromCache = getLyricsSelection(compoundTitle, null) if (hitFromCache) { showLyrics(hitFromCache, 1) return true } } return false } function multipleResultsFound (hits, mTitle, mArtists) { // Multiple matches and no one exact match // or multiple artists multiple results if ('autoSelectLyrics' in custom) { const ret = custom.autoSelectLyrics(hits, mTitle, mArtists) if (ret && ret.hit) { showLyricsAndRemember(mTitle, mArtists, ret.hit, hits.length) return } } // let user decide custom.listSongs(hits) } function loadLyrics (force, beLessSpecific, songTitle, songArtistsArr, musicIsPlaying) { let songArtists = null let compoundTitle = null let queryType = 0 let simpleTitle = null let firstArtist = null if (typeof songTitle === 'string' && (songArtistsArr || 0).length >= 0) { songArtists = songArtistsArr.join(' ') compoundTitle = generateCompoundTitle(songTitle, songArtists) queryType = 1 simpleTitle = songTitle.replace(/\s*-\s*.+?$/, '') // Remove anything following the last dash firstArtist = songArtistsArr[0] if (beLessSpecific) { songArtists = firstArtist songTitle = simpleTitle } } else if (typeof songTitle === 'string' && songArtistsArr === null) { compoundTitle = songTitle queryType = 2 beLessSpecific = false } const themeSettings = `${genius.option.themeKey} ${genius.option.fontSize}` if (force || beLessSpecific || (!document.hidden && musicIsPlaying && (genius.current.compoundTitle !== compoundTitle)) || genius.current.themeSettings !== themeSettings) { const mCTitle = genius.current.compoundTitle = compoundTitle genius.current.themeSettings = themeSettings if ('onNewSongPlaying' in custom) { custom.onNewSongPlaying(songTitle, songArtistsArr) } function isFuzzyMatched (hits) { // if first hit's _order is the only highest, consider it as fuzzy matched if (!hits) return null return hits[0] && hits[1] && hits[0]._order > hits[1]._order && hits[1]._order > 0 } function resultMsg (hits, ...args) { console.log(...args) console.log(hits) } const hitFromCache = getLyricsSelection(mCTitle, null) if (!force && hitFromCache) { showLyrics(hitFromCache, 1) } else { geniusSearch(displayTextOfCompoundTitle(mCTitle), function geniusSearchCb (r) { const hits = r.response.sections[0].hits if (hits.length === 0) { hideLyricsWithMessage() if (queryType === 1 && !beLessSpecific && (firstArtist !== songArtists || simpleTitle !== songTitle)) { // Try again with only the first artist or the simple title custom.addLyrics(!!force, true) } else if (force) { custom.showSearchField() } else { // No results if ('onNoResults' in custom) { custom.onNoResults(songTitle, songArtistsArr) } } // invalidate previous cache if any forgetLyricsSelection(mCTitle, null) } else if (hits.length === 1) { showLyricsAndRemember(mCTitle, null, hits[0], 1) } else if (queryType === 2 || songArtistsArr.length === 1) { // Check if one result is an exact match const exactMatches = [] if (queryType === 1) { for (const hit of hits) { // hit sorted by _order if (hit.result.title.toLowerCase() === songTitle.toLowerCase() && hit.result.primary_artist.name.toLowerCase() === songArtistsArr[0].toLowerCase()) { exactMatches.push(hit) } } } if (exactMatches.length === 1) { resultMsg(hits, `Genius Lyrics - exact match is found in ${hits.length} results.`) showLyricsAndRemember(mCTitle, null, exactMatches[0], hits.length) } else if (isFuzzyMatched(hits)) { resultMsg(hits, `Genius Lyrics - fuzzy match is found in ${hits.length} results.`) showLyricsAndRemember(mCTitle, null, hits[0], hits.length) } else { multipleResultsFound(hits, mCTitle, null) } } else { if (isFuzzyMatched(hits)) { resultMsg(hits, `Genius Lyrics - fuzzy match is found in ${hits.length} results.`) showLyricsAndRemember(mCTitle, null, hits[0], hits.length) } else { resultMsg(hits, 'Genius Lyrics - lyrics results with multiple artists are found.', hits.length, songArtistsArr) multipleResultsFound(hits, mCTitle, null) } } }, function geniusSearchErrorCb () { // do nothing }) } } } function appendElements (target, elements) { if (typeof target.append === 'function') { target.append(...elements) } else { for (const element of elements) { target.appendChild(element) } } } function isGreasemonkey () { return 'info' in custom.GM && 'scriptHandler' in custom.GM.info && custom.GM.info.scriptHandler === 'Greasemonkey' } function setupLyricsDisplayDOM (song, searchresultsLengths) { // getCleanLyricsContainer const container = custom.getCleanLyricsContainer() container.className = '' // custom.getCleanLyricsContainer might forget to clear the className if the element is reused container.classList.add('genius-lyrics-result-shown') if (isGreasemonkey()) { container.innerHTML = '<h2>This script only works in <a target="_blank" href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Tampermonkey</a></h2>Greasemonkey is no longer supported because of this <a target="_blank" href="https://github.com/greasemonkey/greasemonkey/issues/2574">bug greasemonkey/issues/2574</a> in Greasemonkey.' return } let elementsToBeAppended = [] let separator = document.createElement('span') separator.setAttribute('class', 'second-line-separator') separator.setAttribute('style', 'padding:0px 3px') separator.textContent = '•' const bar = document.createElement('div') bar.setAttribute('class', 'lyricsnavbar') bar.style.fontSize = '0.7em' bar.style.userSelect = 'none' // Resize button if ('initResize' in custom) { const resizeButton = document.createElement('span') resizeButton.style.fontSize = '1.8em' resizeButton.style.cursor = 'ew-resize' resizeButton.textContent = '⇹' resizeButton.addEventListener('mousedown', custom.initResize) elementsToBeAppended.push(resizeButton, separator.cloneNode(true)) } // Hide button const hideButton = document.createElement('span') hideButton.classList.add('genius-lyrics-hide-button') hideButton.style.cursor = 'pointer' hideButton.textContent = 'Hide' hideButton.addEventListener('click', function hideButtonClick (ev) { genius.option.autoShow = false // Temporarily disable showing lyrics automatically on song change if (genius.iv.main > 0) { clearInterval(genius.iv.main) genius.iv.main = 0 } hideLyricsWithMessage() }) elementsToBeAppended.push(hideButton, separator.cloneNode(true)) // Config button const configButton = document.createElement('span') configButton.classList.add('genius-lyrics-config-button') configButton.style.cursor = 'pointer' configButton.textContent = 'Options' configButton.addEventListener('click', function configButtonClick (ev) { config() }) elementsToBeAppended.push(configButton) if (searchresultsLengths === 1) { // Wrong lyrics button const wrongLyricsButton = document.createElement('span') wrongLyricsButton.classList.add('genius-lyrics-wronglyrics-button') wrongLyricsButton.style.cursor = 'pointer' wrongLyricsButton.textContent = 'Wrong lyrics' wrongLyricsButton.addEventListener('click', function wrongLyricsButtonClick (ev) { removeElements(document.querySelectorAll('.loadingspinnerholder')) forgetLyricsSelection(genius.current.compoundTitle, null) const searchFieldText = displayTextOfCompoundTitle(genius.current.compoundTitle) custom.showSearchField(searchFieldText) }) elementsToBeAppended.push(separator.cloneNode(true), wrongLyricsButton) } else if (searchresultsLengths > 1) { // Back button const backbutton = document.createElement('span') backbutton.classList.add('genius-lyrics-back-button') backbutton.style.cursor = 'pointer' // searchresultsLengths === true is always false for searchresultsLengths > 1 // if (searchresultsLengths === true) { // backbutton.textContent = 'Back to search results' // } else { backbutton.textContent = `Back to search (${searchresultsLengths - 1} other result${searchresultsLengths === 2 ? '' : 's'})` // } backbutton.addEventListener('click', function backbuttonClick (ev) { const searchFieldText = displayTextOfCompoundTitle(genius.current.compoundTitle) custom.showSearchField(searchFieldText) }) elementsToBeAppended.push(separator.cloneNode(true), backbutton) } const iframe = document.createElement('iframe') iframe.id = 'lyricsiframe' iframe.style.opacity = 0.1 // clean up separator = null // flush to DOM tree appendElements(bar, elementsToBeAppended) appendElements(container, [bar, iframe]) // clean up elementsToBeAppended.length = 0 elementsToBeAppended = null return { container, bar, iframe } } function defaultCSS (html) { // independent of iframe or main window // use with contentStyling // cache might have REPXn // if(genius.option.enableStyleSubstitution !== true) return html /* CSS minimized via https://css-minifier.com/ with discard invalid CSS 3.0; high moderate readability, smaller size */ const defaultCSSTexts = [ ` @font-face{font-family:'Programme';src:url(https://assets.genius.com/fonts/programme_bold.woff2?1671208854) format("woff2"),url(https://assets.genius.com/fonts/programme_bold.woff?1671208854) format("woff");font-style:normal;font-weight:700} @font-face{font-family:'Programme';src:url(https://assets.genius.com/fonts/programme_normal.woff2?1671208854) format("woff2"),url(https://assets.genius.com/fonts/programme_normal.woff?1671208854) format("woff");font-style:normal;font-weight:400} @font-face{font-family:'Programme';src:url(https://assets.genius.com/fonts/programme_normal_italic.woff2?1671208854) format("woff2"),url(https://assets.genius.com/fonts/programme_normal_italic.woff?1671208854) format("woff");font-style:italic;font-weight:400} @font-face{font-family:'Programme';src:url(data:font/woff2;base64,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) format("woff2"),url(https://assets.genius.com/fonts/programme_light.woff?1671208854) format("woff");font-style:normal;font-weight:100} @font-face{font-family:'Programme';src:url(https://assets.genius.com/fonts/programme_light_italic.woff2?1671208854) format("woff2"),url(https://assets.genius.com/fonts/programme_light_italic.woff?1671208854) format("woff");font-style:italic;font-weight:100} `] // there are three pieces of texts in defaultCSSTexts. First one is the font files imported in Genius.com which shall be the same no matter whether it is WithPrimis or not. // the 2nd and 3rd are used to make the styling which is similar to the Genius.com // if there are matched, REPX1 or REPX2 will be assigned for caching. // svg might be also replaced if the same svg is found in the pre-defined svg in this UserScript. html = html .replace('<style id="REPX1"></style>', () => { return `<style>${defaultCSSTexts[0]}</style>` // font-face }).replace(/<svg([^><]+)><svg-repx(\d+) v1 \/><\/svg>/g, (a, w, d) => { d = +d if (d >= 0) { let text = defaultSVGBoxs[d] if (typeof text === 'string') { return `<svg${w}>` + text.substring(5) } text = null } return '' }) return html } function contentStyling () { // contentStyling is to generate a specific css for the styling matching the main window // mainly background-color and text colors // (this is part of the contentStylingIframe) // only if genius.style.enable is set to true by external script if (genius.style.enabled !== true) return null if (typeof genius.style.setup === 'function') { if (genius.style.setup() === false) return null } const customProperties = Object.entries(genius.styleProps).map(([prop, value]) => { return `${prop}: ${value};` }).join('\n') const css = ` html { margin: 0; padding: 0; ${customProperties} } ` return css } function contentStylingIframe (html, contentStyle) { // contentStylingIframe is a function to customize the styling to the html. As the original styles are removed and this is a generic style to apply to every lyrics // this can make the cache size of lyrics become small and guarantee the style align all the time. // however, this contracts the original way that GeniusLyrics.js used. // the original way is to adopt the Genius.com 's style as much as possible. // the new way is to extract the lyrics and song/album info only to make the cache and then apply the own styles according to the website (YouTube/Spotify) if (!contentStyle) return html const css = ` body { ${contentStyle.includes('--egl-background') ? 'background-color: var(--egl-background);' : ''} ${contentStyle.includes('--egl-color') ? 'color: var(--egl-color);' : ''} ${contentStyle.includes('--egl-font-size') ? 'font-size: var(--egl-font-size);' : ''} margin: 0; padding: 0; } html { --egl-page-pt: 50vh; --egl-page-pb: 50vh; --egl-page-offset-top: 30vh; } html body { /* padding-top: var(--egl-page-offset-top); */ } #application { ${contentStyle.includes('--egl-background') ? 'background-color: var(--egl-background);' : ''} } [class*="SongHeader-"][class*="HeaderArtistAndTracklist"] { flex-wrap: wrap; } h1[class*="SongHeader"] { font-size: 140%; } [class*="LabelWithIcon__Container"], [class*="MetadataStats__LabelWithIcon"] { font-size: inherit; } [class*="LabelWithIcon__Container"] svg, [class*="MetadataStats__LabelWithIcon"] svg, svg[class*="MetadataTooltip__InfoIcon"] { height: 1.2em; width: 1.2em; font-size: inherit; } .LHeaderMetaCredits__Selection { white-space: nowrap; word-break: normal; /* .grjXi, .glevmK */ font-weight: 100; font-size: .75rem; line-height: 1.33; } .LDESKTOPONLY__Grid{ display: grid; grid-template-columns: repeat(2,1fr); -webkit-column-gap: 0.75rem; } [class].LDESKTOPONLY__Grid{ column-gap: 28px; } a[href^="#"][class="HeaderBio__Wrapper"]{ /* .eFqCnd */ line-height: 1.33; } body #annotationcontainer958 { ${contentStyle.includes('--egl-font-size') ? 'font-size: var(--egl-font-size);' : ''} } .annotationcontent { max-height: 30vh; overflow: auto; } main, #application { --egl-container-display: none; /* default hide; override by info conatiner */ } #application { padding: 28px; /* looks better to give some space away from the iframe */ } [class*="LabelWithIcon__Container"] [class*="LabelWithIcon__Label"] { font-size:inherit; /* general */ } div[class].LSongHeader__CenterInfo { color: inherit; /* for dekstop_react */ } div#lyrics-root[class*="Lyrics__Root"] { font-size: inherit; /* for lyrics text */ } body .LHeaderMetaCredits__List, body .HeaderTracklist__Container { font-size: inherit; /* revert 1rem */ } body .LHeaderMetaCredits__Selection{ margin-bottom:0; /* revert mb */ } #application:not(:hover) [data-lyrics-container="true"]::selection { /* no selection when the cursor moved out */ color: inherit; background: inherit; } div[class*="SongPageGrid"], div[class*="SongHeader"] { background-color: transparent; padding: 0; } div[class*="SongPageGrid"], div[class].LSongHeader__Container { background-image: none; /* no header background image */ } div[data-exclude-from-selection] { display: none; } main[class*="Container"] h1[font-size][class*="SongHeaderWithPrimis__Title"] { margin-bottom: 8px; } div[class].LSongHeader__Left { display: none; /* just empty space */ } div[class].LSongHeader__Bottom { margin-bottom:1em; } div[class*="SongPageGriddesktop"] { display: block; } span[class*="LabelWithIcon"]>svg, button[class*="LabelWithIcon"]>svg, div[class*="Tooltip__Container"] svg, span[class*="InlineSvg-"]>svg { fill: currentColor; /* dynamic color instead of black */ } p[class*="__Label"], span[class*="__Label"], div[class*="__Section"], button[class*="__Container"] { color: inherit; /* follow parent styles */ text-decoration: none; cursor: inherit; /* follow parent styles */ } div[class*="MetadataStats"] { cursor: default; /* no pointer */ } div[class*="MetadataStats"] [class] { cursor: inherit; } div[class*="MetadataStats__Container"] { font-size: 0.75rem; } div[class].LSongHeader__CenterInfo { margin: 0; padding: 0; max-width: 100%; white-space: normal; display: flex; flex-direction: column; } div[class].LSongHeader__CenterInfo .LHeaderMetaCredits__Container{ /* using flexbox with wrapping */ display: flex; flex-direction: row; flex-wrap: wrap; gap: 0px; align-items: center; justify-items: center; } div[data-lyrics-container][class*="Lyrics__Container"], #lyrics-root div[class=*="Lyrics-"] { padding: 0; } body .annotated span, body .annotated span:hover, body a[href], body a[href]:hover, body .annotated a[href], body .annotated a[href]:hover, body a[href]:focus-visible, body .annotated a[href]:focus-visible, body .annotated:hover span, body .annotated.highlighted span { background-color: transparent; outline: none; } body .annotated span:hover, body .annotated a[href]:hover, body .annotated a[href]:focus-visible, body .annotated:hover span, body .annotated.highlighted span { text-decoration: underline; } a[href][class], span[class*="PortalTooltip"], div[class*="HeaderCreditsPrimis"] /* desktop_react_atf */, div[class*="HeaderArtistAndTracklistPrimis"] /* desktop_react_atf */, div[class*="HeaderTracklist__Container"] /* desktop_react */ { font-size: inherit; } div[class].LSongHeader__CenterInfo h1+div[class*="HeaderArtistAndTracklistPrimis"] /* desktop_react_atf */ { font-size: 80%; margin-top: 10px; margin-bottom: 3px; } .LSongHeader__CenterInfo .LSongHeader__Title + span[class*="PortalTooltip__Container"] /* desktop_react */ { font-size: 80%; display:inline-flex; margin-top: 10px; margin-bottom: 3px; } .LSongHeader__CenterInfo .LSongHeader__Title + span[class*="PortalTooltip__Container"] + div[class*="HeaderTracklist__Container"] /* desktop_react */ { font-size: 80%; display:inline-flex; margin-top: 10px; margin-bottom: 3px; } .LSongHeader__CenterInfo .LSongHeader__Title + span[class*="PortalTooltip__Container"] + div[class*="HeaderTracklist__Container"]::before /* desktop_react */ { content: "•"; margin: 0 .5em; display: inline-block; float: left; } .LSongHeader__CenterInfo .LSongHeader__Title + span[class*="PortalTooltip__Container"] + div[class*="HeaderTracklist__Container"] [class*="HeaderTracklist__AlbumWrapper"] /* desktop_react */ { display: inline-flex; flex-direction: row; flex-wrap: nowrap; } .LSongHeader__CenterInfo [class], div[class].LSongHeader__Container { color: inherit; } .LSongHeader__CenterInfo { --egl-container-display: '-NULL-'; /* all under info would not hide */ } div[class*="Footer"], div[class*="Leaderboard"] { display: none; /* unnessary info */ } div[class*="SongPage__Section"] #about, div[class*="SongPage__Section"] #about~*, div[class*="SongPage__Section"] #comments, div[class*="SongPage__Section"] #comments~* { display: none; /* unnessary info */ } div[class*="SongPage__Section"] #lyrics-root-pin-spacer { padding-top: 12px; /* look better */ } div[class*="Header"] { max-width: unset; /* default just 50%; want full width */ } div[class*="SongHeader"] h1 { font-size: 200%; /* by default */ white-space: break-spaces; } div[class*="SongHeader"] h1[font-size="medium"] { font-size: 140%; /* make song header title smaller */ white-space: break-spaces; } div[class*="SongHeader"] h1[font-size="xSmallHeadline"] { font-size: 120%; /* make song header title bigger */ white-space: break-spaces; } /* the following shall apply with padding-top: XXX */ /* the content might be hidden if height > XXX */ /* the max-height allow the header box to be scrolled if height > XXX */ disabled.genius-lyrics-header-container { position: relative; /* set 100% width for inner absolute box */ } disabled.genius-lyrics-header-container > * { /* main purpose for adding class using CSS event triggering; avoid :has() */ --genius-lyrics-header-content-display: none; display: var(--genius-lyrics-header-content-display); /* none by default */ } disabled.genius-lyrics-header-container > .genius-lyrics-header-content { ${contentStyle.includes('--egl-infobox-background') ? 'background-color: var(--egl-infobox-background);' : ''} /* give some color to info container background */ padding: 18px 26px; /* looks better */ --genius-lyrics-header-content-display: '--NULL--'; /* override none */ position: absolute; width: 100%; /* related to .genius-lyrics-header-container which is padded */ transform: translateY(-100%); /* 100% height refer to the element itself dim */ max-height: calc( var(--egl-page-offset-top) + var(--egl-page-pt) ); display: flex; flex-direction: column; overflow: auto; height: auto; word-break: break-word; } #lyrics-root div[class=*="Lyrics-"] { word-break: keep-all; /* not only a single lyrics character get wrapped. the whole lyrics word will be wrapped */ } .LHeaderMetaCredits__Selection { /* flexbox for header info */ display: flex; flex-direction: row; flex-wrap: wrap; column-gap: 18px; row-gap: 2px; width: 100%; justify-content: start; padding: 4px 0px; } div[class*="LyricsEditdesktop__"] { display: none; } div[class*="HeaderArtistAndTracklistPrimis__Container"] div[class*="HeaderArtistAndTracklistPrimis__Tracklist"]>a[href], /* desktop_react_atf */ div[class*="HeaderTracklist__Container"] div[class*="HeaderTracklist__Album"] a[href], /* desktop_react */ div[class*="HeaderArtistAndTracklistdesktop__Container"] div[class*="HeaderArtistAndTracklistdesktop__Tracklist"] a[href] /* new desktop */ { margin: 6px; } body button { color: inherit; } .LSongHeader__CenterInfo a[href][class]{ padding: 0; margin: 0; } .LSongHeader__CenterInfo a[href="#about"] { font-weight: 300; font-size: 85%; opacity: 0.65; /* coloring only */ } h1, div[class*="SongPage__LyricsWrapper"] { white-space: normal; } disabled.genius-lyrics-header-content div[class*="MetadataStats__Stats"] { /* using flexbox with wrapping */ display: flex; flex-wrap: wrap; /* element blocks with wrapping */ white-space: nowrap; /* text itself no wrapping */ row-gap: 4px; column-gap: 16px; margin-top: 6px; width: 100%; /* force the div to be full width */ font-size: 85%; } [class*="Tooltip__Children"], [class*="Tooltip__Container"] { display: inline-flex; flex-direction: row; align-items: center; justify-content: center; justify-items: center; align-content: center; align-self: center; justify-self: center; } disabled.genius-lyrics-header-content div[class*="MetadataStats__Stats"] > [class] { margin-right: 0; display: flex; justify-content: center; align-items: center; column-gap: 2px; } disabled.genius-lyrics-header-content div[class*="MetadataStats__Stats"] button[class*="LabelWithIcon__Container"] { display: flex; justify-content: center; cursor: default; } disabled.genius-lyrics-header-content div[class*="MetadataStats__Stats"] span[class*="LabelWithIcon__Container"] { display: flex; flex-direction: row; flex-wrap: nowrap; justify-content: center; align-items: center; } .LHeaderMetaCredits__Label[class] { font-size: 0.92em; /* instead of 0.75rem */ align-self: center; } #lyrics-root [class*="Lyrics__Title"]{ font-size: 0.7rem; /* looks better */ margin: 12px 4px; /* looks better */ color: inherit; opacity: 0.89; /* lighter in color */ } .LSongHeader__Title a[href][style*="color"] { color: inherit !important; /* important color set by inherit instead */ } .LSongHeader__Title a[href][class] { color: inherit; /* if not forced color, inherit color */ } disabled.genius-lyrics-header-content div[class].LHeaderMetaCredits__List{ font-size: 92%; } disabled.genius-lyrics-header-content div[class].LHeaderMetaCredits__List span, disabled.genius-lyrics-header-content div[class].LHeaderMetaCredits__List button{ font-size: inherit; } button[class*="List__More"] { cursor: inherit; color: inherit; text-decoration: inherit; font-size: inherit; } [data-lyrics-container="true"] a[class], [data-lyrics-container="true"] span[class] { color: inherit; } /* desktop_react_atf */ .LSongHeader__CenterInfo ~ div[class*="SongHeaderWithPrimis__PrimisContainer"] /* desktop_react_atf */ { display: none; } div[class*="HeaderArtistAndTracklistPrimis__Container"] /* desktop_react_atf */ { flex-wrap: wrap; column-gap: 6px; row-gap: 2px; } .LSongHeader__CenterInfo div[class*="SongHeaderWithPrimis__Bottom"] /* desktop_react_atf */ { display: flex; flex-direction: column; } disabled.genius-lyrics-header-content a[href][class*="Link__"] { color: var(--egl-link-color); font-weight: 100; text-decoration: none; } div[class*="HeaderMetadata__ReleaseDate"] { font-size: inherit; } /* anchor links like #about, #primary-album */ disabled.genius-lyrics-header-content a[href^="#"][class*="Link_"] { --egl-link-color: '--NULL--'; pointer-events: none; text-decoration: inherit; } disabled.genius-lyrics-header-content a[href^="#"][class*="Link_"] * { pointer-events: all; /* text can be selected */ text-decoration: inherit; } disabled.genius-lyrics-header-content a[href^="#"][class*="Link_"] > span[class*="InlineSvg"]:last-child, disabled.genius-lyrics-header-content a[href^="#"][class*="Link_"] > span[class*="InlineSvg"]:last-child > svg { display: none; } /* only for #about Read More */ disabled.genius-lyrics-header-content a[href^="#"][class*="Link_"] > span[class*="HeaderBio__ViewBio"]:last-child, disabled.genius-lyrics-header-content a[href^="#"][class*="Link_"] > span[class*="HeaderBio__ViewBio"]:last-child svg { display: none; } [class].LHeaderMetaCredits__List /* desktop_react_atf */ { justify-content: center; align-items: center; display: inline-flex; flex-direction: row; column-gap: 6px; } [class].fTHPLE, [class].LSongHeader__Title{ color: inherit; } [class*="SongHeaderdesktop__PrimisContainer"] { height: auto; /* override fixed height */ } [class*="SongHeaderdesktop__Information"], [class*="SongHeaderdesktop__SongDetails"] { row-gap: 0; /* remove unnecessary row gap */ } [class*="LyricsFooter__Container"] { display:none; } div[class*="SidebarLyrics-"], div[class*="RightSidebar-"], div[class*="InreadContainer-"], div[class*="LyricsHeader-"], div[class*="PageFooter-"], div[class*="ExpandableContent__ButtonContainer"], div[class*="About-"], div[class*="QuestionList-"], #questions, div[class*=SongComments-], div[class*="AppleMusicPlayer"], div[class*="MusicVideo"], div[class*="ShareButtons"], div[class*="StickyContributorToolbar"], div[class*="StickyNavSentinel"], button[class*="SmallButton-"], div[class*="UnreviewedAnnotation__Container"] { display: none; } ` const contentStyleByDefault = ` html{ --egl-link-color: hsl(206,100%,40%); } ` const headhtml = ` <style id="egl-contentstyles"> ${contentStyleByDefault} ${contentStyle} ${css} </style> ` // Add to <head> html = appendHeadText(html, headhtml) return html } let isShowLyricsInterrupted = false let isShowLyricsIsCancelledByUser = false function interuptMessageHandler (ev) { const data = (ev || 0).data || 0 if (data.iAm === custom.scriptName && data.type === 'lyricsDisplayState' && typeof data.visibility === 'string') { isShowLyricsInterrupted = data.visibility !== 'loading' } } // store all the svgs displayed in the lyrics panel; reduce cache size const defaultSVGBoxs = [ '<svg><path d="M11.7 2.9s0-.1 0 0c-.8-.8-1.7-1.2-2.8-1.2-1.1 0-2.1.4-2.8 1.1-.2.2-.3.4-.5.6v.1c0 .1.1.1.1.1.4-.2.9-.3 1.4-.3 1.1 0 2.2.5 2.9 1.2h1.6c.1 0 .1-.1.1-.1V2.9c.1 0 0 0 0 0zm-.1 4.6h-1.5c-.8 0-1.4-.6-1.5-1.4.1 0 0-.1 0-.1-.3 0-.6.2-.8.4v.2c-.6 1.8.1 2.4.9 2.4h1.1c.1 0 .1.1.1.1v.4c0 .1.1.1.1.1.6-.1 1.2-.4 1.7-.8V7.6c.1 0 0-.1-.1-.1z"></path><path d="M11.6 11.9s-.1 0 0 0c-.1 0-.1 0 0 0-.1 0-.1 0 0 0-.8.3-1.6.5-2.5.5-3.7 0-6.8-3-6.8-6.8 0-.9.2-1.7.5-2.5 0-.1-.1-.1-.2-.1h-.1C1.4 4.2.8 5.7.8 7.5c0 3.6 2.9 6.4 6.4 6.4 1.7 0 3.3-.7 4.4-1.8V12c.1 0 0-.1 0-.1zm13.7-3.1h3.5c.8 0 1.4-.5 1.4-1.3v-.2c0-.1-.1-.1-.1-.1h-4.8c-.1 0-.1.1-.1.1v1.4c-.1 0 0 .1.1.1zm5.1-6.7h-5.2c-.1 0-.1.1-.1.1v1.4c0 .1.1.1.1.1H29c.8 0 1.4-.5 1.4-1.3v-.2c.1-.1.1-.1 0-.1z"></path><path d="M30.4 12.3h-6.1c-1 0-1.6-.6-1.6-1.6V1c0-.1-.1-.1-.1-.1-1.1 0-1.8.7-1.8 1.8V12c0 1.1.7 1.8 1.8 1.8H29c.8 0 1.4-.6 1.4-1.3v-.1c.1 0 .1-.1 0-.1zm12 0c-.6-.1-.9-.6-.9-1.3V1.1s0-.1-.1-.1H41c-.9 0-1.5.6-1.5 1.5v9.9c0 .9.6 1.5 1.5 1.5.8 0 1.4-.6 1.5-1.5 0-.1 0-.1-.1-.1zm8.2 0h-.2c-.9 0-1.4-.4-1.8-1.1l-4.5-7.4-.1-.1c-.1 0-.1.1-.1.1V8l2.8 4.7c.4.6.9 1.2 2 1.2 1 0 1.7-.5 2-1.4 0-.2-.1-.2-.1-.2zm-.9-3.8c.1 0 .1-.1.1-.1V1.1c0-.1 0-.1-.1-.1h-.4c-.9 0-1.5.6-1.5 1.5v3.1l1.7 2.8c.1 0 .1.1.2.1zm13 3.8c-.6-.1-.9-.6-.9-1.2v-10c0-.1 0-.1-.1-.1h-.3c-.9 0-1.5.6-1.5 1.5v9.9c0 .9.6 1.5 1.5 1.5.8 0 1.4-.6 1.5-1.5l-.2-.1zm18.4-.5H81c-.7.3-1.5.5-2.5.5-1.6 0-2.9-.5-3.7-1.4-.9-1-1.4-2.4-1.4-4.2V1c0-.1 0-.1-.1-.1H73c-.9 0-1.5.6-1.5 1.5V8c0 3.7 2 5.9 5.4 5.9 1.9 0 3.4-.7 4.3-1.9v-.1c0-.1 0-.1-.1-.1z"></path><path d="M81.2.9h-.3c-.9 0-1.5.6-1.5 1.5v5.7c0 .7-.1 1.3-.3 1.8 0 .1.1.1.1.1 1.4-.3 2.1-1.4 2.1-3.3V1c0-.1-.1-.1-.1-.1zm12.7 7.6l1.4.3c1.5.3 1.6.8 1.6 1.2 0 .1.1.1.1.1 1.1-.1 1.8-.7 1.8-1.5s-.6-1.2-1.9-1.5l-1.4-.3c-3.2-.6-3.8-2.3-3.8-3.6 0-.7.2-1.3.6-1.9v-.2c0-.1-.1-.1-.1-.1-1.5.7-2.3 1.9-2.3 3.4-.1 2.3 1.3 3.7 4 4.1zm5.2 3.2c-.1.1-.1.1 0 0-.9.4-1.8.6-2.8.6-1.6 0-3-.5-4.3-1.4-.3-.3-.5-.6-.5-1 0-.1 0-.1-.1-.1s-.3-.1-.4-.1c-.4 0-.8.2-1.1.6-.2.3-.4.7-.3 1.1.1.4.3.7.6 1 1.4 1 2.8 1.5 4.5 1.5 2 0 3.7-.7 4.5-1.9v-.1c0-.1 0-.2-.1-.2z"></path><path d="M94.1 3.2c0 .1.1.1.1.1h.2c1.1 0 1.7.3 2.4.8.3.2.6.3 1 .3s.8-.2 1.1-.6c.2-.3.3-.6.3-.9 0-.1 0-.1-.1-.1-.2 0-.3-.1-.5-.2-.8-.6-1.4-.9-2.6-.9-1.2 0-2 .6-2 1.4.1 0 .1 0 .1.1z"></path></svg>', '<svg><path d="M21.48 20.18L14.8 13.5a8.38 8.38 0 1 0-1.43 1.4l6.69 6.69zM2 8.31a6.32 6.32 0 1 1 6.32 6.32A6.32 6.32 0 0 1 2 8.31z"></path></svg>', '<svg><path d="M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z"></path></svg>', '<svg><path d="M12.917 2.042H10.75V.958H9.667v1.084H5.333V.958H4.25v1.084H2.083C1.487 2.042 1 2.529 1 3.125v10.833c0 .596.488 1.084 1.083 1.084h10.834c.595 0 1.083-.488 1.083-1.084V3.125c0-.596-.488-1.083-1.083-1.083zm0 11.916H2.083V6.375h10.834v7.583zm0-8.666H2.083V3.125H4.25v1.083h1.083V3.125h4.334v1.083h1.083V3.125h2.167v2.167z" stroke-width="0.096"></path></svg>', '<svg><path d="M16.27 13.45L12 10.58V4.46H9.76v7.25L15 15.25z"></path><path d="M11 2a9 9 0 1 1-9 9 9 9 0 0 1 9-9m0-2a11 11 0 1 0 11 11A11 11 0 0 0 11 0z"></path></svg>', '<svg><path d="M12.55 6.76a4 4 0 1 0 0-4.59 4.41 4.41 0 0 1 0 4.59zm3.07 2.91v5.17H22V9.66l-6.38.01M7 9a4.43 4.43 0 0 0 3.87-2.23 4.41 4.41 0 0 0 0-4.59 4.47 4.47 0 0 0-8.38 2.3A4.48 4.48 0 0 0 7 9zm-7 1.35v6.12h13.89v-6.14l-6.04.01-7.85.01"></path></svg>', '<svg><path d="M0 7l6.16-7 3.3 7H6.89S5.5 12.1 5.5 12.17h5.87L6.09 22l.66-7H.88l2.89-8z"></path></svg>', '<svg><path d="M6.5037 26.1204a14.0007 14.0007 0 0 0 17.6775-1.7412 13.9997 13.9997 0 0 0 1.7412-17.6775A14.0004 14.0004 0 0 0 11.5505.7487a13.9992 13.9992 0 0 0-7.1682 3.8316 14.0002 14.0002 0 0 0 2.1213 21.54ZM7.615 4.5022a11.9998 11.9998 0 0 1 16.6443 16.6443 12 12 0 0 1-12.3186 5.1028A12.0005 12.0005 0 0 1 3.1951 9.8875 12.0018 12.0018 0 0 1 7.615 4.5022Zm6.6667 1.9775a1.4996 1.4996 0 0 0-1.4711 1.7928 1.5027 1.5027 0 0 0 .7624 1.0293 1.5002 1.5002 0 0 0 2.063-1.9668 1.4997 1.4997 0 0 0-.2937-.4158 1.4992 1.4992 0 0 0-1.0606-.4394Zm1 14v-8h-4v2h2v6h-3v2h8v-2h-3Z"></path></svg>', '<svg><path d="M10.66 10.91L0 1.5 1.32 0l9.34 8.24L20 0l1.32 1.5-10.66 9.41"></path></svg>', '<svg><path d="M8.09 3.81c-1.4 0-1.58.84-1.58 1.67v1.3h3.35L9.49 11h-3v9H2.33v-9H0V6.88h2.42V3.81C2.42 1.3 3.81 0 6.6 0H10v3.81z"></path></svg>', '<svg><path d="M20 1.89l-2.3 2.16v.68a12.28 12.28 0 0 1-3.65 8.92c-5 5.13-13.1 1.76-14.05.81 0 0 3.78.14 5.81-1.76A4.15 4.15 0 0 1 2.3 9.86h2S.81 9.05.81 5.81A11 11 0 0 0 3 6.35S-.14 4.05 1.49.95a11.73 11.73 0 0 0 8.37 4.19A3.69 3.69 0 0 1 13.51 0a3.19 3.19 0 0 1 2.57 1.08 12.53 12.53 0 0 0 3.24-.81l-1.75 1.89A10.46 10.46 0 0 0 20 1.89z"></path></svg>', '<svg><path d="M0 7l6.16-7 3.3 7H6.89S5.5 12.1 5.5 12.17h5.87L6.09 22l.66-7H.88l2.89-8z"></path></svg>', '<svg><path d="M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z"></path></svg>', '<svg><circle cx="74" cy="10" r="9"></circle></svg>', '<svg><path d="M8.09 3.81c-1.4 0-1.58.84-1.58 1.67v1.3h3.35L9.49 11h-3v9H2.33v-9H0V6.88h2.42V3.81C2.42 1.3 3.81 0 6.6 0H10v3.81z"></path></svg>', '<svg><path d="M20 1.89l-2.3 2.16v.68a12.28 12.28 0 0 1-3.65 8.92c-5 5.13-13.1 1.76-14.05.81 0 0 3.78.14 5.81-1.76A4.15 4.15 0 0 1 2.3 9.86h2S.81 9.05.81 5.81A11 11 0 0 0 3 6.35S-.14 4.05 1.49.95a11.73 11.73 0 0 0 8.37 4.19A3.69 3.69 0 0 1 13.51 0a3.19 3.19 0 0 1 2.57 1.08 12.53 12.53 0 0 0 3.24-.81l-1.75 1.89A10.46 10.46 0 0 0 20 1.89z"></path></svg>', '<svg><path d="M10 0c2.724 0 3.062 0 4.125.06.83.017 1.65.175 2.426.467.668.254 1.272.65 1.77 1.162.508.498.902 1.1 1.153 1.768.292.775.45 1.595.467 2.424.06 1.063.06 1.41.06 4.123 0 2.712-.06 3.06-.06 4.123-.017.83-.175 1.648-.467 2.424-.52 1.34-1.58 2.402-2.922 2.92-.776.293-1.596.45-2.425.468-1.063.06-1.41.06-4.125.06-2.714 0-3.062-.06-4.125-.06-.83-.017-1.65-.175-2.426-.467-.668-.254-1.272-.65-1.77-1.162-.508-.498-.902-1.1-1.153-1.768-.292-.775-.45-1.595-.467-2.424C0 13.055 0 12.708 0 9.995c0-2.712 0-3.04.06-4.123.017-.83.175-1.648.467-2.424.25-.667.645-1.27 1.153-1.77.5-.507 1.103-.9 1.77-1.15C4.225.234 5.045.077 5.874.06 6.958 0 7.285 0 10 0zm0 1.798h.01c-2.674 0-2.992.06-4.046.06-.626.02-1.245.15-1.83.377-.434.16-.828.414-1.152.746-.337.31-.602.69-.775 1.113-.222.595-.34 1.224-.348 1.858-.06 1.064-.06 1.372-.06 4.045s.06 2.99.06 4.044c.007.633.125 1.262.347 1.857.17.434.434.824.775 1.142.31.33.692.587 1.113.754.596.222 1.224.34 1.86.348 1.063.06 1.37.06 4.045.06 2.674 0 2.992-.06 4.046-.06.635-.008 1.263-.126 1.86-.348.87-.336 1.56-1.025 1.897-1.897.217-.593.332-1.218.338-1.848.06-1.064.06-1.372.06-4.045s-.06-2.99-.06-4.044c-.01-.623-.128-1.24-.347-1.827-.16-.435-.414-.83-.745-1.152-.318-.34-.71-.605-1.143-.774-.596-.222-1.224-.34-1.86-.348-1.063-.06-1.37-.06-4.045-.06zm0 3.1c1.355 0 2.655.538 3.613 1.496.958.958 1.496 2.257 1.496 3.61 0 2.82-2.288 5.108-5.11 5.108-2.822 0-5.11-2.287-5.11-5.107 0-2.82 2.288-5.107 5.11-5.107zm0 8.415c.878 0 1.72-.348 2.34-.97.62-.62.97-1.46.97-2.338 0-1.827-1.482-3.31-3.31-3.31s-3.31 1.483-3.31 3.31 1.482 3.308 3.31 3.308zm6.51-8.633c0 .658-.533 1.192-1.192 1.192-.66 0-1.193-.534-1.193-1.192 0-.66.534-1.193 1.193-1.193.316 0 .62.126.844.35.223.223.35.526.35.843z"></path></svg>', '<svg><path d="M19.81 3A4.32 4.32 0 0 0 19 1a2.86 2.86 0 0 0-2-.8C14.21 0 10 0 10 0S5.8 0 3 .2A2.87 2.87 0 0 0 1 1a4.32 4.32 0 0 0-.8 2S0 4.51 0 6.06V8a30 30 0 0 0 .2 3 4.33 4.33 0 0 0 .8 2 3.39 3.39 0 0 0 2.2.85c1.46.14 5.9.19 6.68.2h.4c1 0 4.35 0 6.72-.21a2.87 2.87 0 0 0 2-.84 4.32 4.32 0 0 0 .8-2 30.31 30.31 0 0 0 .2-3.21V6.28A30.31 30.31 0 0 0 19.81 3zM7.94 9.63V4l5.41 2.82z"></path></svg>', '<svg><path d="M0 10h24v4h-24z"></path></svg>', '<svg><path d="M6.5037 26.1204a14.0007 14.0007 0 0 0 17.6775-1.7412 13.9997 13.9997 0 0 0 1.7412-17.6775A14.0004 14.0004 0 0 0 11.5505.7487a13.9992 13.9992 0 0 0-7.1682 3.8316 14.0002 14.0002 0 0 0 2.1213 21.54ZM7.615 4.5022a11.9998 11.9998 0 0 1 16.6443 16.6443 12 12 0 0 1-12.3186 5.1028A12.0005 12.0005 0 0 1 3.1951 9.8875 12.0018 12.0018 0 0 1 7.615 4.5022Zm6.6667 1.9775a1.4996 1.4996 0 0 0-1.4711 1.7928 1.5027 1.5027 0 0 0 .7624 1.0293 1.5002 1.5002 0 0 0 2.063-1.9668 1.4997 1.4997 0 0 0-.2937-.4158 1.4992 1.4992 0 0 0-1.0606-.4394Zm1 14v-8h-4v2h2v6h-3v2h8v-2h-3Z"></path></svg>', '<svg><path d="m11 4.12 7.6 13.68H3.4L11 4.12M11 0 0 19.8h22L11 0z"></path><path d="M10 8.64h2v4.51h-2zm1 5.45a1.13 1.13 0 0 1 1.13 1.15A1.13 1.13 0 1 1 11 14.09z"></path></svg>', '<svg><path d="M16.52 21.29H6V8.5l.84-.13a3.45 3.45 0 0 0 1.82-1.09 13.16 13.16 0 0 0 .82-1.85c1.06-2.69 2-4.78 3.52-5.31a2.06 2.06 0 0 1 1.74.17c2.5 1.42 1 5 .16 6.95-.11.27-.25.6-.31.77a.78.78 0 0 0 .6.36h4.1a2.29 2.29 0 0 1 2.37 2.37c0 .82-1.59 5.4-2.92 9.09a2.39 2.39 0 0 1-2.22 1.46zm-8.52-2h8.56a.48.48 0 0 0 .31-.17c1.31-3.65 2.73-7.82 2.79-8.44 0-.22-.1-.32-.37-.32h-4.1A2.61 2.61 0 0 1 12.54 8 4.29 4.29 0 0 1 13 6.46c.45-1.06 1.64-3.89.7-4.43-.52 0-1.3 1.4-2.38 4.14a10 10 0 0 1-1.13 2.38A5.28 5.28 0 0 1 8 10.11zM0 8.4h4.86v12.96H0z"></path></svg>', '<svg><path d="M8 21.36a2.12 2.12 0 0 1-1.06-.29c-2.5-1.42-1-5-.16-6.95.11-.27.25-.6.31-.77a.78.78 0 0 0-.6-.36H2.37A2.29 2.29 0 0 1 0 10.64c0-.82 1.59-5.4 2.92-9.09A2.39 2.39 0 0 1 5.1.07h10.56v12.79l-.84.13A3.45 3.45 0 0 0 13 14.08a13.16 13.16 0 0 0-.82 1.85c-1.06 2.69-2 4.79-3.49 5.31a2.06 2.06 0 0 1-.69.12zM5.1 2.07a.48.48 0 0 0-.31.17C3.48 5.89 2.07 10.06 2 10.68c0 .22.1.32.37.32h4.1a2.61 2.61 0 0 1 2.61 2.4 4.29 4.29 0 0 1-.48 1.51c-.46 1.09-1.65 3.89-.7 4.42.52 0 1.3-1.4 2.38-4.14a10 10 0 0 1 1.13-2.38 5.27 5.27 0 0 1 2.25-1.56V2.07zM16.76 0h4.86v12.96h-4.86z"></path></svg>', '<svg><path d="M19.29 1.91v11.46H7.69l-.57.7L5 16.64v-3.27H1.91V1.91h17.38M21.2 0H0v15.28h3.12V22l5.48-6.72h12.6V0z"></path><path d="M4.14 4.29h12.93V6.2H4.14zm0 4.09h12.93v1.91H4.14z"></path></svg>', '<svg><path d="M16.03 7.39v12.7H1.91V7.39H0V22h17.94V7.39h-1.91"></path><path d="M8.08 3.7v11.81h1.91V3.63l2.99 2.98 1.35-1.35L9.07 0 3.61 5.46l1.36 1.35L8.08 3.7"></path></svg>', '<svg><path d="M11 2c4 0 7.26 3.85 8.6 5.72-1.34 1.87-4.6 5.73-8.6 5.73S3.74 9.61 2.4 7.73C3.74 5.86 7 2 11 2m0-2C4.45 0 0 7.73 0 7.73s4.45 7.73 11 7.73 11-7.73 11-7.73S17.55 0 11 0z"></path><path d="M11 5a2.73 2.73 0 1 1-2.73 2.73A2.73 2.73 0 0 1 11 5m0-2a4.73 4.73 0 1 0 4.73 4.73A4.73 4.73 0 0 0 11 3z"></path></svg>', '<svg><path d="M24 10h-10v-10h-4v10h-10v4h10v10h4v-10h10z"></path></svg>', '<svg><path d="M21.82,20.62,17,15.83l3.59-3.59L17.55,9.17l-3.36.12L10.09,5.19v-3L7.91,0,0,7.91l2.16,2.16L5,10.25,9.1,14.37,9,17.73l3.08,3.08,3.59-3.59L20.43,22ZM11,16.94l.12-3.36L5.85,8.34,3,8.16l-.25-.25L7.91,2.77,8.13,3V6l5.27,5.27,3.36-.12,1.09,1.09L12.06,18Z"></path></svg>', '<svg><path d="M20.07,1.93V20.07H1.93V1.93H20.07M22,0H0V22H22V0Z"></path><path d="M7.24,8.38l4.07-4.66L13.5,8.38H11.8s-.92,3.35-.92,3.4h3.88l-3.49,6.5s.44-4.61.44-4.66H7.82L9.74,8.38Z"></path></svg>', '<svg><path d="M16,13.05v-6a7.05,7.05,0,0,0-14.11,0v6H0v6.66H6.65a2.29,2.29,0,0,0,4.57,0h6.65V13.05Zm-12.2-6a5.15,5.15,0,1,1,10.3,0v6H3.79ZM1.9,17.81V15.23H16v2.58Z"></path></svg>', '<svg><path d="M0,0V15.34H22V0ZM12.32,8.2,11,9.47,9.68,8.2,8.3,6.88l-5.18-5H18.88l-5.18,5ZM6.82,8.1,1.9,12.17V3.37ZM8.21,9.42,11,12.1l2.79-2.68,4.86,4H3.35Zm7-1.33L20.1,3.37v8.8Z"></path></svg>', '<svg><path d="M20.07,1.93V20.07H1.93V1.93H20.07M22,0H0V22H22V0Z"></path><path d="M3.83,16.29V5.71h2.1V16.29Z"></path><path d="M16.35,16.57l-.65-.71a5.23,5.23,0,0,1-2.83.71A5.43,5.43,0,0,1,7.26,11a5.45,5.45,0,0,1,5.62-5.57A5.45,5.45,0,0,1,18.5,11,5.23,5.23,0,0,1,17,14.82l1.47,1.75ZM12.88,7.29A3.55,3.55,0,0,0,9.36,11a3.56,3.56,0,0,0,3.57,3.69,3.27,3.27,0,0,0,1.48-.28l-1.93-2.12,2.13-.16,1.09,1.22A3.74,3.74,0,0,0,16.4,11,3.55,3.55,0,0,0,12.88,7.29Z"></path></svg>', '<svg><path fill-rule="evenodd" d="M4 16.483A9 9 0 1 0 14 1.518 9 9 0 0 0 4 16.483Zm.714-13.897a7.714 7.714 0 1 1 8.572 12.828A7.714 7.714 0 0 1 4.714 2.586Zm3.643 6.678 3.594 3.593.906-.906L9.643 8.73V3.214H8.357v6.05Z" clip-rule="evenodd"></path></svg>', '<svg><path fill-rule="evenodd" d="M20.418 2.53a13.655 13.655 0 0 1 4.806 6.192.818.818 0 0 1 0 .556A13.655 13.655 0 0 1 13 18 13.655 13.655 0 0 1 .776 9.278a.818.818 0 0 1 0-.556A13.655 13.655 0 0 1 13 0c2.667.1 5.246.98 7.418 2.53ZM2.421 9C4.08 13.148 8.664 16.364 13 16.364S21.918 13.148 23.58 9C21.917 4.852 17.335 1.636 13 1.636S4.082 4.852 2.42 9Zm7.852-4.082a4.91 4.91 0 1 1 5.454 8.164 4.91 4.91 0 0 1-5.454-8.164Zm.909 6.803a3.272 3.272 0 1 0 3.636-5.442 3.272 3.272 0 0 0-3.636 5.442Z" clip-rule="evenodd"></path></svg>', '<svg><path fill-rule="evenodd" d="M3.577 0H18v14.423h-2.394V4.083L1.689 18 0 16.31 13.916 2.395H3.576V0Z" clip-rule="evenodd"></path></svg>', '<svg><path d="M15.923 1.385h-2.77V0H11.77v1.385H6.231V0H4.846v1.385h-2.77c-.76 0-1.384.623-1.384 1.384v13.846c0 .762.623 1.385 1.385 1.385h13.846c.762 0 1.385-.623 1.385-1.385V2.77c0-.761-.623-1.384-1.385-1.384Zm0 15.23H2.077V6.923h13.846v9.692Zm0-11.077H2.077V2.77h2.77v1.385H6.23V2.769h5.538v1.385h1.385V2.769h2.77v2.77Z"></path></svg>', '<svg><path fill-rule="evenodd" d="M11.335 2.6v1.333H9.2A11.76 11.76 0 0 1 6.588 9.02a9.654 9.654 0 0 0 3.413 2.247l-.473 1.226a11.279 11.279 0 0 1-3.84-2.56 12.314 12.314 0 0 1-3.853 2.574l-.5-1.24a11.227 11.227 0 0 0 3.44-2.28 10.98 10.98 0 0 1-2-3.72h1.4A9 9 0 0 0 5.7 8.053a9.807 9.807 0 0 0 2.127-4.12H.668V2.6h4.667v-2h1.333v2h4.667Zm7.997 16h-1.433l-1.067-2.667h-4.567L11.2 18.6H9.765l4-10h1.567l4 10Zm-4.787-8.373L12.8 14.6h3.5l-1.754-4.373Z" clip-rule="evenodd"></path></svg>', '<svg><path d="M4.488 7 0 0h8.977L4.488 7Z"></path></svg>', '<svg><path d="M4 16.483A9 9 0 1 0 14 1.516 9 9 0 0 0 4 16.483Zm.714-13.897a7.714 7.714 0 1 1 8.572 12.828A7.714 7.714 0 0 1 4.714 2.586ZM9 3.857a.964.964 0 1 0 0 1.928.964.964 0 0 0 0-1.928Zm.643 9V7.714H7.07V9h1.286v3.857H6.428v1.286h5.143v-1.286H9.643Z"></path></svg>', '<svg><path d="M4.488.5 0 7.5h8.977L4.488.5Z"></path></svg>', '<svg><path fill-rule="evenodd" d="M9 .5a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm0 16.714a7.715 7.715 0 1 1 0-15.43 7.715 7.715 0 0 1 0 15.43Zm.643-12.857H8.357v7.072h1.286V4.357ZM8.464 13.52a.964.964 0 1 1 1.072 1.603.964.964 0 0 1-1.072-1.603Z" clip-rule="evenodd"></path></svg>', '<svg><path fill-rule="evenodd" d="M4 2.017a9 9 0 1 1 10 14.966A9 9 0 0 1 4 2.017Zm.714 13.897a7.715 7.715 0 1 0 8.572-12.829 7.715 7.715 0 0 0-8.572 12.83ZM4.5 9.765l3.214 3.215L13.5 7.195l-.91-.91-4.876 4.877-2.306-2.305-.908.909Z" clip-rule="evenodd"></path></svg>', '<svg><path fill-rule="evenodd" d="M3.214 11.671h.643a1.287 1.287 0 0 1 1.286 1.286v1.286a1.287 1.287 0 0 1-1.286 1.286h-.643V18.1H1.93v-2.57h-.643A1.287 1.287 0 0 1 0 14.243v-1.286a1.287 1.287 0 0 1 1.286-1.286h.643V.101h1.285v11.57Zm-1.928 2.572h2.571v-1.286H1.286v1.286Zm9-11.571h-.643V.1H8.357v2.572h-.643A1.287 1.287 0 0 0 6.43 3.957v1.286a1.287 1.287 0 0 0 1.285 1.286h.643V18.1h1.286V6.53h.643a1.287 1.287 0 0 0 1.285-1.286V3.957a1.287 1.287 0 0 0-1.285-1.285Zm0 2.571H7.714V3.957h2.572v1.286Zm6.428 2.571h-.643V.1h-1.285v7.714h-.643A1.287 1.287 0 0 0 12.857 9.1v1.286a1.287 1.287 0 0 0 1.286 1.286h.643V18.1h1.285v-6.429h.643A1.287 1.287 0 0 0 18 10.386V9.1a1.287 1.287 0 0 0-1.286-1.286Zm0 2.572h-2.571V9.1h2.571v1.286Z" clip-rule="evenodd"></path></svg>', '<svg><path d="M17.51 5.827c.654-.654.654-1.636 0-2.29L14.563.59c-.655-.655-1.637-.655-2.291 0L0 12.864V18.1h5.236L17.51 5.827Zm-4.092-4.09 2.946 2.945-2.455 2.454-2.945-2.945 2.454-2.455ZM1.636 16.463v-2.946l8.182-8.182 2.946 2.946-8.182 8.182H1.636Z"></path></svg>', '<svg><path fill-rule="evenodd" d="M2.948.1h10.97v1.371H2.948V.101ZM15.29 2.843H1.578v1.372H15.29V2.843Zm.567 15.257H2.144a1.373 1.373 0 0 1-1.371-1.37v-9.6a1.373 1.373 0 0 1 1.37-1.37h13.713a1.373 1.373 0 0 1 1.371 1.37v9.599a1.373 1.373 0 0 1-1.37 1.371ZM2.144 7.13v9.599h13.712V7.13H2.144Z" clip-rule="evenodd"></path></svg>', '<svg><path d="M6.5 10.68.04.605h12.92L6.5 10.68z"></path></svg>', '<svg><path d="M16.58 20.73H2V6.15h9.07l2-2H0v18.58h18.58V8.75l-2 2v9.98z"></path><path d="M18.65 0l-4.16 4.15-2 2L8 10.66l-1.59 5.25 5.19-1.6 5-5 2-2 3.71-3.71zm-2.07 7.38l-5.71 5.71-1.23.38-.82-.82.38-1.26 5.25-5.23 2-2L18.65 2l1.67 1.67-1.74 1.71z"></path></svg>' ] // note: the script can detect that the fetched svg might be missing in the defaultSVGBoxs, // but if those SVGs are no longer used in all lyrics / theme, there will be no warning or logging to alert the developer. // in such a case, they will just remains as trash code. // ( the icon usages are highly dependent on the lyrics and themes ) /* eslint-enable quotes, comma-dangle, indent */ function normalizeClassNameInner (d, k1, k2) { // in normal version and WithPrimis version, they share some similarity of the Layout Design. // This is a case by case study to make the generalized class names for the content and styling control in GeniusLyrics.js switch (d) { case 'SongHeaderdesktop__Column': return 'LDESKTOPONLY__Column' case 'SongHeaderdesktop__Container': case 'SongHeaderWithPrimis__Container': return 'LSongHeader__Container' case 'SongHeaderdesktop__Sentinel': case 'SongHeaderWithPrimis__Sentinel': return 'LSongHeader__Sentinel' case 'SongHeaderdesktop__Left': case 'SongHeaderWithPrimis__Left': return 'LSongHeader__Left' case 'SongHeaderdesktop__Right': case 'SongHeaderWithPrimis__Right': return 'LSongHeader__Right' case 'SongHeaderdesktop__Bottom': case 'SongHeaderWithPrimis__Bottom': return 'LSongHeader__Bottom' case 'SongHeaderdesktop__Center': case 'SongHeaderWithPrimis__Information': return 'LSongHeader__CenterInfo' case 'SongHeaderdesktop__Title': case 'SongHeaderWithPrimis__Title': return 'LSongHeader__Title' case 'SongHeaderdesktop__HiddenMask': case 'SongHeaderWithPrimis__HiddenMask': return 'LSongHeader__Title' case 'HeaderArtistAndTracklistPrimis__Container': case 'PortalTooltip__Container': return 'LUNDETERMINED__Container' // set to LHeaderArtistAndTracklist__Container after DOM loaded case 'SongHeaderdesktop__Artist': case 'HeaderArtistAndTracklistPrimis__Artist': return 'LHeaderArtistAndTracklist__Artist' case 'HeaderMetadata__Container': case 'HeaderCreditsPrimis__Container': return 'LHeaderMetaCredits__Container' case 'HeaderMetadata__Grid': return 'LDESKTOPONLY__Grid' case 'HeaderMetadata__Section': case 'HeaderCreditsPrimis__Section': return 'LHeaderMetaCredits__Selection' case 'HeaderMetadata__Label': case 'HeaderCreditsPrimis__Label': return 'LHeaderMetaCredits__Label' case 'HeaderMetadata__List': case 'HeaderCreditsPrimis__List': return 'LHeaderMetaCredits__List' /* desktop_react_atf SongHeaderWithPrimis__Bottom HeaderBio__Wrapper SongHeaderWithPrimis__HeaderBio HeaderBio__ViewBio desktop_react HeaderMetadata__Section HeaderMetadata__Label HeaderMetadata__ReleaseDate HeaderMetadata__Section HeaderMetadata__ViewCredits Link__StyledLink */ default: // if (k1 === 'SongHeaderdesktop' || k1 === 'SongHeaderWithPrimis') { // return `LSongHeader__${k2}` // } // return null // ignore all not recognized } return null } function normalizeClassNames (htmlText) { htmlText = htmlText.replace(/\s+class="([a-zA-Z0-9\-_\s]+)"/g, (m, a) => { if (!a.includes('__')) return m // ignore without __ const arr = [] a.replace(/([A-Za-z]+)__([A-Za-z]+)/g, (d, k1, k2) => { const newClassName = normalizeClassNameInner(d, k1, k2) if (newClassName) { arr.push(newClassName) } }) if (arr.length === 0) return m return ` class="${a} ${arr.join(' ')}"` }) return htmlText } async function trimHTMLReponseTextFn (htmlText) { /* original: 200 ~ 400 KB trimHTMLReponseText only: 130 ~ 200 KB [Spotify Genius Lyrics] trimHTMLReponseText + enableStyleSubstitution: 25 ~ 50 KB [YouTube Genius Lyrics Simplified Iframe Content] */ const originalHtmlText = htmlText // unicode fix - including various unicodes for "space" and zero-width spaces htmlText = htmlText.replace(/[\t\x20\u0009-\u000D\u0085\u00A0\u1680\u2000-\u200A\u2028-\u2029\u202F\u205F\u3000]+/g, ' ') /* spacing */ // eslint-disable-line no-control-regex htmlText = htmlText.replace(/[\u180E\u200B-\u200D\u2060\uFEFF]/g, '') // reduce blank lines htmlText = htmlText.replace(/[\r\n](\x20*[\r\n])+/g, '\n') // remove metas htmlText = htmlText.replace(/\s*<meta\b[^<>]*(?:(?!>)<[^<>]*)*>\s*/gi, (m) => { if (m.indexOf('og:url') > 0 || m.indexOf('og:image') > 0) return m return '' }) // minimize style htmlText = htmlText.replace(/\s*<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>\s*/gi, (m) => { m = m.replace(/\/\*[^/*]*\*\//g, '') // comments if (genius.option.enableStyleSubstitution) { m = m.replace(/\s[\w\-.[\]="]+\{content:"[^"]*"\}\s*/g, ' ') // content:'xxx' m = m.replace(/\s+!important;/g, ';') // !important // this allows further reduction of html text size, but it shall be used with content styling // since some genius css is removed in the minimized version (default CSS) if (m.indexOf('@font-face') > 0 && m.split('@font-face { font-family: \'Programme\'; ').length === 6) { // font-face console.log('Genius Lyrics - REPX1') return '<style id="REPX1"></style>' } } return m }) // remove all content scripts htmlText = htmlText.replace(/\s*<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>\s*/gi, (m) => { if (m.indexOf('script src=') > 0) return m return '' }) // <link ... /> htmlText = htmlText.replace(/\s*<link\b[^<>]*(?:(?!>)<[^<>]*)*>\s*/gi, (m) => { return '' }) // <noscript>....</noscript> htmlText = htmlText.replace(/\s*<noscript\b[^<]*(?:(?!<\/noscript>)<[^<]*)*<\/noscript>\s*/gi, (m) => { return '' }) // comments tag htmlText = htmlText.replace(/\s*<!--[^\->]+-->\s*/gi, (m) => { return '' }) const om = new Set() htmlText = htmlText.replace(/\s*<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>\s*/gi, (m) => { m = m.trim() const mi = m.indexOf('><') // <svg .... ><.... </svg> if (mi < 0) return m const n = `<svg><${m.substring(mi + 2).trim()}` const match = defaultSVGBoxs.indexOf(n) // array search if (match >= 0) { return `${m.substring(0, mi)}><svg-repx${match} v1 /></svg>` } else { om.add(n) } return m }) if (om.size > 0) { console.log('Genius Lyrics - new SVGs are found', om.size, [...om.keys()]) } // remove all <div style="display: none;"> ... </div> htmlText = htmlText.replace(/<div\b[^<]*(?:(?!<\/div>)<[^<]*)*<\/div>\s*/gi, (m) => { if (m.startsWith('<div style="display: none;">')) return '' return m }) // add normalized classname to adapt both desktop_react and desktop_react_atf htmlText = normalizeClassNames(htmlText) console.log(`Genius Lyrics - HTML text size reduced from ${metricPrefix(measurePlainTextLength(originalHtmlText), 2, 1024)} to ${metricPrefix(measurePlainTextLength(htmlText), 2, 1024)}`) // console.log([htmlText]) // htmlText = response.responseText // structurize(htmlText) return htmlText } function defaultSpinnerDOM (container, bar, iframe) { const spinnerDOM = { createSpinnerHolder: () => { const spinnerHolder = document.createElement('div') spinnerHolder.classList.add('loadingspinnerholder') spinnerDOM.spinnerHolder = spinnerHolder }, createSpinner: () => { let spinner = null const spinnerHolder = spinnerDOM.spinnerHolder if ('createSpinner' in custom) { spinner = custom.createSpinner(spinnerHolder) } else { spinnerHolder.style.left = (iframe.getBoundingClientRect().left + container.clientWidth / 2) + 'px' spinnerHolder.style.top = '100px' spinner = document.createElement('div') spinner.classList.add('loadingspinner') spinnerHolder.appendChild(spinner) } spinnerDOM.spinner = spinner }, displaySpinnerHolder: () => { document.body.appendChild(spinnerDOM.spinnerHolder) }, setStatusTitle: (title) => { const spinnerHolder = spinnerDOM.spinnerHolder spinnerHolder.title = title }, setSpinnerNum: (text) => { const spinner = spinnerDOM.spinner spinner.textContent = text }, remove: () => { const spinnerHolder = spinnerDOM.spinnerHolder spinnerHolder.remove() } } return spinnerDOM } let rafPromise = null const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => { requestAnimationFrame(hRes => { rafPromise = null resolve(hRes) }) })) function showLyrics (songInfo, searchresultsLengths) { // showLyrics const currentFunctionClosureIdentifier = ((window.showLyricsIdentifier || 0) + 1) % 100000000 window.showLyricsIdentifier = currentFunctionClosureIdentifier // if this function closure is no longer valid, they will be not equal. // setup DOMs const { container, bar, iframe } = 'setupLyricsDisplayDOM' in custom ? custom.setupLyricsDisplayDOM(songInfo, searchresultsLengths) : setupLyricsDisplayDOM(songInfo, searchresultsLengths) if (!iframe || iframe.nodeType !== 1 || iframe.closest('html, body') === null) { console.warn('iframe#lyricsiframe is not inserted into the page.') return } iframe.src = custom.emptyURL + '#html:post' custom.setFrameDimensions(container, iframe, bar) if (typeof songInfo === 'object') { // do nothing; assume the object can be passed through postMessage } else { console.warn('The parameter \'songInfo\' in showLyrics() is incorrect.') return } if (typeof searchresultsLengths === 'number') { // do nothing } else { console.warn('The parameter \'searchresultsLengths\' in showLyrics() is incorrect.') return } let spinnerDOM = null if ('customSpinnerDOM' in custom && typeof custom.customSpinnerDOM === 'function') { spinnerDOM = custom.customSpinnerDOM(container, bar, iframe) if (!spinnerDOM || typeof spinnerDOM !== 'object') spinnerDOM = null } if (spinnerDOM === null) { spinnerDOM = defaultSpinnerDOM(container, bar, iframe) } spinnerDOM.createSpinnerHolder() spinnerDOM.createSpinner() spinnerDOM.displaySpinnerHolder() // container.appendChild(spinnerHolder) function spinnerUpdate (text, title, status, textStatus) { if (typeof text === 'string') spinnerDOM.setSpinnerNum(text) if (typeof title === 'string') spinnerDOM.setStatusTitle(title) if ('notifyGeniusLoading' in custom && arguments.length > 2) { custom.notifyGeniusLoading({ status, textStatus }) } } window.removeEventListener('message', interuptMessageHandler, false) window.addEventListener('message', interuptMessageHandler, false) isShowLyricsIsCancelledByUser = false isShowLyricsInterrupted = false let isCancelLoadingEnabled = true addOneMessageListener('cancelLoading', () => { if (window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return if (isCancelLoadingEnabled === false) return // such as user clicking back btn isShowLyricsIsCancelledByUser = true isShowLyricsInterrupted = true unScroll() try { spinnerDOM.remove() } catch (e) { // could be already removed } isCancelLoadingEnabled = false }) function isThisShowLyricsInvalidated () { return isShowLyricsInterrupted === true || window.showLyricsIdentifier !== currentFunctionClosureIdentifier } spinnerUpdate('5', 'Downloading lyrics...', 0, 'start') unScroll() async function updateLyricsDisplayState () { if (document.visibilityState === 'visible') await getRafPromise().then() window.postMessage({ iAm: custom.scriptName, type: 'lyricsDisplayState', visibility: 'loading', song: songInfo, searchresultsLengths }, '*') if (document.visibilityState === 'visible') await getRafPromise().then() } updateLyricsDisplayState() function interuptedByExternal () { window.removeEventListener('message', interuptMessageHandler, false) } async function showLyricsRunner () { try { if (isThisShowLyricsInvalidated()) return interuptedByExternal() let cacheReqResult = null let html = await new Promise(resolve => loadGeniusSong(songInfo, function loadGeniusSongCb (response, cacheResult) { cacheReqResult = cacheResult // not immediately cache this html; cache the proceeded html only resolve(response.responseText) })) if (isThisShowLyricsInvalidated()) return interuptedByExternal() if (cacheReqResult !== null) { if (genius.option.trimHTMLReponseText === true) { html = await trimHTMLReponseTextFn(html) if (isThisShowLyricsInvalidated()) return interuptedByExternal() } // not obtained from cache spinnerUpdate('4', 'Downloading annotations...', 100, 'donwloading') let annotations = await new Promise(resolve => loadGeniusAnnotations(songInfo, html, annotationsEnabled, function loadGeniusAnnotationsCb (annotations) { resolve(annotations) })) if (isThisShowLyricsInvalidated()) return interuptedByExternal() spinnerUpdate('3', 'Composing page...', 200, 'pageComposing') html = await new Promise(resolve => combineGeniusResources(songInfo, html, annotations, function combineGeniusResourcesCb (html) { // in fact `combineGeniusResources` is synchronous resolve(html) })) if (isThisShowLyricsInvalidated()) return interuptedByExternal() annotations = null // cache the html text with annotations // note: 1 page consume 2XX KB // if trimHTMLReponseText is used, trim to 25KB ~ 50KB if (genius.option.cacheHTMLRequest === true) cacheReqResult({ responseText: html }) } const contentStyle = contentStyling() || '' // obtained from the main window, to be passed to iframe spinnerUpdate('3', 'Loading page...', 300, 'pageLoading') // obtain the iframe detailed information let tv1 = 0 let tv2 = 0 let iv = 0 const clear = function () { // a. clear() when LyricsReady (success) // b. clear() when failed (after 30s) window.removeEventListener('message', interuptMessageHandler, false) if ('onLyricsReady' in custom) { // only on success ???; not reliable custom.onLyricsReady(songInfo, container) } if (iv > 0) { clearInterval(iv) iv = 0 } clearTimeout(tv1) clearTimeout(tv2) iframe.style.opacity = 1.0 try { spinnerDOM.remove() } catch (e) { // could be already removed } isCancelLoadingEnabled = false } // event listeners addOneMessageListener('genius-iframe-waiting', async function () { if (isShowLyricsIsCancelledByUser || window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return if (iv === 0) { return } await ivf() // this is much faster than 1500ms clearInterval(iv) iv = 0 }) addOneMessageListener('htmlwritten', async function () { if (isShowLyricsIsCancelledByUser || window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return if (iv > 0) { clearInterval(iv) iv = 0 } if (document.visibilityState === 'visible') await getRafPromise().then() spinnerUpdate('1', 'Calculating...', 302, 'htmlwritten') }) addOneMessageListener('pageready', function (ev) { if (isShowLyricsIsCancelledByUser || window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return // note: this is not called after the whole page is rendered // console.log(ev.data) clear() // loaded spinnerUpdate(null, null, 901, 'complete') window.postMessage({ iAm: custom.scriptName, type: 'lyricsDisplayState', visibility: 'loaded', lyricsSuccess: true }, '*') unScroll() setTimeout(() => { // delay required due to scrollToBegining() is changing the scrollTop window.isPageAbleForAutoScroll = true }, 240) }) addOneMessageListener('iframeContentRendered', function (ev) { if (isShowLyricsIsCancelledByUser || window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return unScroll() }) function reloadFrame () { // no use if the iframe is detached tv1 = 0 if (window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return if (isShowLyricsIsCancelledByUser) return console.debug('tv1') iframe.src = 'data:text/html,%3Ch1%3ELoading...%21%3C%2Fh1%3E' setTimeout(function () { iframe.src = custom.emptyURL + '#html:post' }, 400) } // After 15 seconds, try to reload the iframe tv1 = setTimeout(reloadFrame, 15000) function fresh () { tv2 = 0 if (window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return if (isShowLyricsIsCancelledByUser) return console.debug('tv2') clear() // unable to load spinnerUpdate(null, null, 902, 'failed') unScroll() window.postMessage({ iAm: custom.scriptName, type: 'lyricsDisplayState', visibility: 'loaded', lyricsSuccess: false }, '*') if (!loadingFailed) { console.debug('try again fresh') loadingFailed = true hideLyricsWithMessage() setTimeout(function () { custom.addLyrics(true) // new function closure }, 100) } } // After 30 seconds, try again fresh (only once) tv2 = setTimeout(fresh, 30000) function unableToProcess (msg) { clearInterval(iv) iv = 0 console.warn(msg) clearTimeout(tv1) clearTimeout(tv2) // iframe is probrably detached from the page if (tv2 > 0) { fresh() } } const ivf = async () => { if (window.showLyricsIdentifier !== currentFunctionClosureIdentifier) return if (iv === 0) { return } if (isShowLyricsInterrupted === true) { // this is possible if the lyrics was hidden by other function calling unableToProcess('Genius Lyrics - showLyrics() was interrupted') } spinnerUpdate('2', 'Rendering...', 301, 'pageRendering') if (document.visibilityState === 'visible') await getRafPromise().then() const iframeContentWin = iframe.contentWindow || 0 if ((iframeContentWin.location || 0).hash && iframeContentWin.postMessage) { // (iframeContentWin.location||0).hash === '#html:post' iframeContentWin.postMessage({ iAm: custom.scriptName, type: 'writehtml', html, contentStyle, themeKey: genius.option.themeKey, fontSize: genius.option.fontSize }, '*') } else if (iframe.closest('html, body') === null) { // unlikely as interupter_lyricsDisplayState is checked unableToProcess('iframe#lyricsiframe was removed from the page. No contentWindow could be found.') } else { // console.debug('iframe.contentWindow is ', iframe.contentWindow) } } iv = setInterval(ivf, 1500) } catch (e) { console.warn(e) } } showLyricsRunner() } function showLyricsAndRemember (title, artists, hit, searchresultsLengths) { showLyrics(hit, searchresultsLengths) // store the selection Promise.resolve(0).then(() => { return JSON.stringify(hit) }).then(jsonHit => { rememberLyricsSelection(title, artists, jsonHit) }) } async function updateAutoScrollEnabled () { const newValue = await custom.GM.getValue('autoscrollenabled') autoScrollEnabled = newValue } function isScrollLyricsEnabled () { return autoScrollEnabled // note: if iframe is not ready, still no action } function isScrollLyricsCallable () { return autoScrollEnabled && window.isPageAbleForAutoScroll === true // note: if iframe is not ready, still no action } function scrollLyrics (positionFraction) { if (isScrollLyricsCallable() === false) { return } // Relay the event to the iframe const iframe = document.getElementById('lyricsiframe') const contentWindow = (iframe || 0).contentWindow if (contentWindow && typeof contentWindow.postMessage === 'function') { contentWindow.postMessage({ iAm: custom.scriptName, type: 'scrollLyrics', position: positionFraction }, '*') } } function searchByQuery (query, container, callback) { geniusSearch(query, function geniusSearchCb (r) { const hits = r.response.sections[0].hits if (hits.length === 0) { if (typeof callback === 'function') { const res = { hits, status: 200 } callback(res) } else { modalAlert(custom.scriptName + '\n\nNo search results') } } else { if (typeof callback === 'function') { const res = { hits, status: 200 } callback(res) } else { custom.listSongs(hits, container, query) } } }, function geniusSearchErrorCb () { if (typeof callback === 'function') { const res = { status: 500 } callback(res) } // do nothing }) } async function captchaHint (responseText) { if (document.querySelector('#mycaptchahint897454') !== null) return // avoid showing duplicating option window if (await custom.GM.getValue('noMoreCaptchaHint', false)) return if (typeof GM_openInTab === 'function') { GM_openInTab('https://genius.com/', { active: true }) } // Background overlay if (!document.getElementById('myoverlay7658438')) { const bg = document.body.appendChild(document.createElement('div')) bg.setAttribute('id', 'myoverlay7658438') } // Blur background for (const e of document.querySelectorAll('body > *')) { e.style.filter = 'blur(4px)' } const win = document.body.appendChild(document.createElement('div')) win.setAttribute('id', 'mycaptchahint897454') let div = win.appendChild(document.createElement('div')) div.innerHTML = `genius.com has blocked you.<br>Please open <a style="color:#0066ff; text-decoration:underline;" target="_blank" href="https://genius.com">genius.com</a> and solve the captcha/prove you are not a robot.<br> Then reload the page.` div.style = 'font-size:30px; width:70%' div.appendChild(document.createElement('br')) const reloadButton = div.appendChild(document.createElement('span')) reloadButton.textContent = 'Reload page' reloadButton.style = 'font-size:20px; background-color:#0066ff; color:white; padding:5px 10px; border-radius:10px; cursor:pointer;' reloadButton.addEventListener('click', function () { requestCache = cleanRequestCache() setJV('requestcache', requestCache).then(() => { window.location.reload() }) }) div.appendChild(document.createElement('br')) const closeButton = div.appendChild(document.createElement('span')) closeButton.textContent = "Don't show this hint again" closeButton.style = 'font-size:20px; background-color:#88aaff; color:white; padding:5px 10px; border-radius:10px; cursor:pointer;' closeButton.addEventListener('click', function () { document.querySelectorAll('#mycaptchahint897454').forEach(d => d.remove()) document.querySelectorAll('#myoverlay7658438').forEach(d => d.remove()) // Un-blur background for (const e of document.querySelectorAll('body > *')) { e.style.filter = '' } custom.GM.setValue('noMoreCaptchaHint', true) }) div = win.appendChild(document.createElement('div')) div.appendChild(document.createElement('br')) div.appendChild(document.createTextNode('Error text (in case you want to report a bug):')) div.appendChild(document.createElement('br')) div.appendChild(document.createElement('textarea')).value = responseText } function config () { if (document.querySelector('#myconfigwin39457845') !== null) return // avoid showing duplicating option window // Background overlay if (!document.getElementById('myoverlay7658438')) { const bg = document.body.appendChild(document.createElement('div')) bg.setAttribute('id', 'myoverlay7658438') bg.addEventListener('click', function () { document.querySelectorAll('#myconfigwin39457845_close_button').forEach(b => b.focus()) }) } // Blur background for (const e of document.querySelectorAll('body > *')) { e.style.filter = 'blur(1px)' } loadCache() const clearCacheFn = () => { return Promise.all([custom.GM.setValue('selectioncache', '{}'), custom.GM.setValue('requestcache', '{}')]).then(function () { selectionCache = cleanSelectionCache() requestCache = {} }) } const win = document.body.appendChild(document.createElement('div')) win.setAttribute('id', 'myconfigwin39457845') const h1 = document.createElement('h1') win.appendChild(h1) h1.textContent = 'Options' if ('scriptIssuesURL' in custom) { const a = document.createElement('a') a.href = custom.scriptIssuesURL win.appendChild(a) a.textContent = ('scriptIssuesTitle' in custom ? custom.scriptIssuesTitle : custom.scriptIssuesURL) } // Switch: Show automatically let div = win.appendChild(document.createElement('div')) div.classList.add('divAutoShow') const checkAutoShow = div.appendChild(document.createElement('input')) checkAutoShow.type = 'checkbox' checkAutoShow.id = 'checkAutoShow748' checkAutoShow.checked = genius.option.autoShow === true custom.GM.getValue('optionautoshow', checkAutoShow.checked === true).then(function (v) { // Get real value, genius.option.autoShow might have been changed temporarily genius.option.autoShow = v === true || v === 'true' checkAutoShow.checked = genius.option.autoShow }) const onAutoShow = function onAutoShowListener (evt) { const checkAutoShow = evt.target custom.GM.setValue('optionautoshow', checkAutoShow.checked === true) genius.option.autoShow = checkAutoShow.checked === true } checkAutoShow.addEventListener('click', onAutoShow) checkAutoShow.addEventListener('change', onAutoShow) let label = div.appendChild(document.createElement('label')) label.setAttribute('for', 'checkAutoShow748') label.textContent = ' Automatically show lyrics when new song starts' div.appendChild(document.createElement('br')) div.appendChild(document.createTextNode('(if you disable this, a small button will appear in the top right corner to show the lyrics)')) // Select: Theme div = win.appendChild(document.createElement('div')) div.textContent = 'Theme: ' const selectTheme = div.appendChild(document.createElement('select')) for (const key in themes) { const option = selectTheme.appendChild(document.createElement('option')) option.value = key if (genius.option.themeKey === key) { option.selected = true } option.textContent = themes[key].name } const onSelectTheme = function onSelectThemeListener (evt) { const selectTheme = evt.target const hasChanged = genius.option.themeKey !== selectTheme.selectedOptions[0].value if (hasChanged) { genius.option.themeKey = selectTheme.selectedOptions[0].value theme = themes[genius.option.themeKey] custom.GM.setValue('theme', genius.option.themeKey).then(() => { if (genius.onThemeChanged) { for (const f of genius.onThemeChanged) { f() } } custom.addLyrics() }) } } selectTheme.addEventListener('change', onSelectTheme) // Font size div = win.appendChild(document.createElement('div')) label = div.appendChild(document.createElement('label')) label.setAttribute('for', 'inputFontSize748') label.textContent = 'Font size: ' const inputFontSize = div.appendChild(document.createElement('input')) inputFontSize.type = 'number' inputFontSize.value = genius.option.fontSize inputFontSize.min = 0 inputFontSize.max = 99 inputFontSize.id = 'inputFontSize748' inputFontSize.style.maxWidth = '5em' const onFontSizeChanged = function onFontSizeChangeListener (evt) { genius.option.fontSize = Math.max(0, parseInt(inputFontSize.value) || 0) custom.GM.setValue('fontsize', genius.option.fontSize).then(() => { if (genius.onThemeChanged) { for (const f of genius.onThemeChanged) { f() } } custom.addLyrics() }) } inputFontSize.addEventListener('change', onFontSizeChanged) // Switch: Show annotations div = win.appendChild(document.createElement('div')) const checkAnnotationsEnabled = div.appendChild(document.createElement('input')) checkAnnotationsEnabled.type = 'checkbox' checkAnnotationsEnabled.id = 'checkAnnotationsEnabled748' checkAnnotationsEnabled.checked = annotationsEnabled === true const onAnnotationsEnabled = function onAnnotationsEnabledListener (evt) { const checkAnnotationsEnabled = evt.target if (checkAnnotationsEnabled.checked !== annotationsEnabled) { annotationsEnabled = checkAnnotationsEnabled.checked === true custom.addLyrics(true) custom.GM.setValue('annotationsenabled', annotationsEnabled) } } checkAnnotationsEnabled.addEventListener('click', onAnnotationsEnabled) checkAnnotationsEnabled.addEventListener('change', onAnnotationsEnabled) label = div.appendChild(document.createElement('label')) label.setAttribute('for', 'checkAnnotationsEnabled748') label.textContent = ' Show annotations' // Switch: Automatic scrolling div = win.appendChild(document.createElement('div')) const checkAutoScrollEnabled = div.appendChild(document.createElement('input')) checkAutoScrollEnabled.type = 'checkbox' checkAutoScrollEnabled.id = 'checkAutoScrollEnabled748' checkAutoScrollEnabled.checked = autoScrollEnabled === true const onAutoScrollEnabled = function onAutoScrollEnabledListener (evt) { const checkAutoScrollEnabled = evt.target const newValue = checkAutoScrollEnabled.checked === true if (newValue !== autoScrollEnabled) { custom.GM.setValue('autoscrollenabled', newValue).then(() => { // note: custom.addLyrics(true) shall not be required in both coding implementation in Spotify / YouTube / YouTube Music updateAutoScrollEnabled() // autoScrollEnabled = checkAutoScrollEnabled.checked === true // custom.addLyrics(true) }) } } checkAutoScrollEnabled.addEventListener('click', onAutoScrollEnabled) checkAutoScrollEnabled.addEventListener('change', onAutoScrollEnabled) label = div.appendChild(document.createElement('label')) label.setAttribute('for', 'checkAutoScrollEnabled748') label.textContent = ' Automatic scrolling' // Custom buttons if ('config' in custom) { for (const f of custom.config) { f(win.appendChild(document.createElement('div'))) } } // Select: RomajiPriority div = win.appendChild(document.createElement('div')) div.textContent = 'Romaji: ' const selectRomajiPriority = div.appendChild(document.createElement('select')) const romajiPriorities = [ { text: 'Low Priority', value: 'low' }, { text: 'High Priority', value: 'high' } ] for (const o of romajiPriorities) { const option = selectRomajiPriority.appendChild(document.createElement('option')) option.value = `${o.value}` if (`${genius.option.romajiPriority}` === `${o.value}`) { option.selected = true } option.textContent = o.text } const onSelectRomajiPriority = function onSelectRomajiListener (evt) { const selectRomajiPriority = evt.target const val = selectRomajiPriority.selectedOptions[0].value const hasChanged = genius.option.romajiPriority !== val if (hasChanged) { genius.option.romajiPriority = val custom.GM.setValue('romajipriority', genius.option.romajiPriority).then(() => { // cache is required to clear for the reselection clearCacheFn().then(() => { // Callback = ? }) }) } } selectRomajiPriority.addEventListener('change', onSelectRomajiPriority) // Select: RomajiPriority div = win.appendChild(document.createElement('div')) div.textContent = 'LZCompression: ' const selectLZCompression = div.appendChild(document.createElement('select')) const lzCompressionOptions = [ { text: 'Enabled', value: 'true' }, { text: 'Disabled', value: 'false' } ] for (const o of lzCompressionOptions) { const option = selectLZCompression.appendChild(document.createElement('option')) option.value = `${o.value}` if (`${genius.option.useLZCompression}` === `${o.value}`) { option.selected = true } option.textContent = o.text } const onSelectLZCompression = function onSelectLZCompressionListener (evt) { const selectLZCompression = evt.target const val = (selectLZCompression.selectedOptions[0].value === 'true') const hasChanged = genius.option.useLZCompression !== val if (hasChanged) { genius.option.useLZCompression = val custom.GM.setValue('useLZCompression', genius.option.useLZCompression).then(() => { // Nil }) } } selectLZCompression.addEventListener('change', onSelectLZCompression) selectLZCompression.disabled = true testUseLZStringCompression().then((r) => (selectLZCompression.disabled = !r)) // Buttons div = win.appendChild(document.createElement('div')) const closeButton = div.appendChild(document.createElement('button')) closeButton.textContent = 'Close' closeButton.setAttribute('id', 'myconfigwin39457845_close_button') closeButton.addEventListener('click', function onCloseButtonClick () { document.querySelectorAll('#myconfigwin39457845').forEach(d => d.remove()) document.querySelectorAll('#myoverlay7658438').forEach(d => d.remove()) // Un-blur background for (const e of document.querySelectorAll('body > *')) { e.style.filter = '' } }) // console.dir(selectionCache) // console.dir(requestCache) const bytes = metricPrefix(measureJVLength(selectionCache) + measureJVLength(requestCache), 2, 1024) + 'Bytes' const clearCacheButton = div.appendChild(document.createElement('button')) clearCacheButton.textContent = `Clear cache (${bytes})` clearCacheButton.addEventListener('click', function onClearCacheButtonClick (evt) { const clearCacheButton = evt.target clearCacheFn().then(function () { clearCacheButton.textContent = 'Cleared' }) }) const debugButton = div.appendChild(document.createElement('button')) debugButton.title = 'Do not enable this.' debugButton.style.float = 'right' const updateDebugButton = function (debugButton) { if (genius.debug) { debugButton.textContent = 'Debug is on' debugButton.style.opacity = '1.0' } else { debugButton.textContent = 'Debug is off' debugButton.style.opacity = '0.2' } } updateDebugButton(debugButton) debugButton.addEventListener('click', function onDebugButtonClick (evt) { const debugButton = evt.target genius.debug = !genius.debug custom.GM.setValue('debug', genius.debug).then(function () { updateDebugButton(debugButton) }) }) // Footer div = elmBuild('div', ['p', { style: { 'font-size': '15px' } }, 'Powered by ', ['a', { style: { 'font-size': '15px' } }, { attr: { target: '_blank', href: 'https://github.com/cvzi/genius-lyrics-userscript/' } }, 'GeniusLyrics.js' ], 'Copyright © 2019 ', ['a', { style: { 'font-size': '15px' } }, { attr: { href: 'mailto:cuzi@openmail.cc' } }, 'cuzi' ], ' and contributors.', ['br'], 'Licensed under the GNU General Public License v3.0' ]) div = win.appendChild(div) } function closeModalUIs () { document.querySelectorAll('.modal_ui_genius_lyrics_overlay').forEach(div => div.remove()) } function modalAlert (text, buttons = { OK: true }) { return new Promise(function (resolve) { const buttonMap = (obj, mapFn) => { const arr = [] let i = 0 if (obj) { for (const key in obj) { arr.push(mapFn(key, obj[key], i++)) } } return arr } const bg = elmBuild('div', { classList: ['modal_ui_genius_lyrics_overlay'], listener: { click: function () { this.querySelector('button').focus() } } }, ['div', { classList: ['modal_ui_genius_lyrics_dialog_box'] }, text, ['div', { classList: ['modal_ui_genius_lyrics_dialog_buttons_holder'] }, ...buttonMap(buttons, (key, value, i) => { return ['button', { classList: ['modal_ui_genius_lyrics_dialog_button'] }, { listener: { click: () => { bg.remove() resolve(value) } } }, { attr: { tabindex: i } }, key] }) ] ] ) document.body.appendChild(bg) bg.querySelector('button[tabindex="0"]').focus() }) } function modalConfirm (text) { return modalAlert(text, { OK: true, Cancel: false }) } function addOneMessageListener (type, cb) { let arr = onMessage[type] if (!arr) { arr = onMessage[type] = [] } arr.push(cb) } function listenToMessagesHandler (e) { const data = ((e || 0).data || 0) if (data.iAm !== custom.scriptName) { return } let arr = onMessage[data.type] if (arr && arr.length > 0) { let tmp = [...arr] arr.length = 0 arr = null for (const cb of tmp) { if (typeof cb === 'function') { cb(e) } } tmp = null } } function listenToMessages () { window.addEventListener('message', listenToMessagesHandler, false) } function unlistenToMessages () { window.removeEventListener('message', listenToMessagesHandler, false) } function pageKeyboardEvent (keyParams, fct) { document.addEventListener('keypress', function onKeyPress (ev) { if (ev.key === keyParams.key && ev.shiftKey === keyParams.shiftKey && ev.ctrlKey === keyParams.ctrlKey && ev.altKey === keyParams.altKey) { let e = ev.target while (e) { // Filter input, textarea, etc. if (typeof e.value !== 'undefined') { console.log(e) console.log(e.value) return } e = e.parentNode } return fct(ev) } }) } function toggleLyrics () { const isLyricsIframeExist = !!document.getElementById('lyricsiframe') if (genius.iv.main > 0) { clearInterval(genius.iv.main) genius.iv.main = 0 } if (!isLyricsIframeExist) { genius.option.autoShow = true // Temporarily enable showing lyrics automatically on song change if ('main' in custom) { custom.setupMain ? custom.setupMain(genius) : (genius.iv.main = setInterval(custom.main, 2000)) } // if ('addLyrics' in custom) { // custom.addLyrics(true) // } custom.addLyrics(true) } else { genius.option.autoShow = false // Temporarily disable showing lyrics automatically on song change // if ('hideLyrics' in custom) { // custom.hideLyrics() // } hideLyricsWithMessage() } } function addKeyboardShortcut (keyParams) { window.addEventListener('message', function (ev) { const data = (ev || 0).data || 0 if (data.iAm === custom.scriptName && data.type === 'togglelyrics') { toggleLyrics() } }) pageKeyboardEvent(keyParams, function (ev) { toggleLyrics() }) } function addKeyboardShortcutInFrame (keyParams) { pageKeyboardEvent(keyParams, function (ev) { if (window.parent) { window.parent.postMessage({ iAm: custom.scriptName, type: 'togglelyrics' }, '*') } }) } function addCss () { document.head.appendChild(document.createElement('style')).textContent = ` #mycaptchahint897454 { position:fixed; top:120px; right:10px; padding:15px; background:white; border-radius:10%; border:2px solid black; color:black; z-index:104; font-size:1.2em } #myoverlay7658438 { display: block; position: fixed; background-color: rgba(0,0,0,0.5); top: 0; left: 0; width: 100%; height: 100%; z-index: 102; user-select: none; filter:blur(1px); } #myconfigwin39457845 { position:fixed; top:120px; left:50px; padding:30px 10px; background:white; border-radius:10%; border:2px solid black; color:black; z-index:103; font-size:1.2em } #myconfigwin39457845 h1 { font-size:1.9em; padding:0em 0.2em; margin:0; } #myconfigwin39457845 a:link, #myconfigwin39457845 a:visited { font-size:1.2em; text-decoration:underline; color:#7847ff; cursor:pointer; } #myconfigwin39457845 a:hover { font-size:1.2em; text-decoration:underline; color:#dd65ff; } #myconfigwin39457845 input[type=text], #myconfigwin39457845 input[type=number] { color:black; background-color: white; } #myconfigwin39457845 button { color:black; font-family: sans-serif; background-color: #e9e9ed; border-radius: 5px; border: 1px solid #8f8f9d; font-size: 14px; cursor: pointer; padding: 1px 4px; margin: auto 2px; } #myconfigwin39457845 button:focus { border-color:#1a1dff; background-color:#d0d0d7; } #myconfigwin39457845 button:hover { border-color:black; background-color:#d0d0d7; } #myconfigwin39457845 div { margin:2px 0; padding:5px; border-radius: 5px; background-color: #EFEFEF } .loadingspinner { color:rgb(255, 255, 100); text-align:center; pointer-events: none; width: 2.5em; height: 2.5em; border: 0.4em solid transparent; border-color: rgb(255, 255, 100) #181818 #181818 #181818; border-radius: 50%; animation: loadingspin 2s ease infinite } @keyframes loadingspin { 25% { transform: rotate(90deg) } 50% { transform: rotate(180deg) } 75% { transform: rotate(270deg) } 100% { transform: rotate(360deg) } } .modal_ui_genius_lyrics_overlay { display: block; position: fixed; background-color: rgba(0,0,0,0.5); top: 0; left: 0; width: 100%; height: 100%; z-index: 999; user-select: none; } .modal_ui_genius_lyrics_dialog_box { display: block; position: fixed; background-color: #bbb; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 20px; border-radius: 10px; box-shadow: 0 0 10px 0 rgba(0,0,0,0.5); z-index: 1000; width: 400px; height: auto; text-align: center; font-size: 20px; line-height: 1.5; font-family: sans-serif; color: black; word-break: break-word; overflow-wrap: break-word; white-space: pre-wrap; overflow: auto; max-height: 80%; max-width: 80%; user-select: text; } .modal_ui_genius_lyrics_dialog_buttons_holder { margin-top :20px; } .modal_ui_genius_lyrics_dialog_button { margin: 0 10px; padding: 10px; border-radius: 5px; border: 2px solid #777; background-color: #ddd; color: black; font-family: sans-serif; font-size: 16px; cursor: pointer } .modal_ui_genius_lyrics_dialog_button:focus { border-color:#1a1dff; } .modal_ui_genius_lyrics_dialog_button:hover { border-color:black; } ` if ('addCss' in custom) { custom.addCss() } } async function getGMValues (o) { // GM.getValues will be soon avaible in TM & VM due to MV3 (TM issue #2045), tally with chrome.storage const entries = Object.entries(o) const values = await Promise.all(entries.map(entry => custom.GM.getValue(entry[0], entry[1]))) return Object.fromEntries(values.map((val, idx) => [entries[idx][0], val])) } async function mainRunner () { // get values from GM const values = await getGMValues({ debug: genius.debug, theme: genius.option.themeKey, annotationsenabled: annotationsEnabled, autoscrollenabled: autoScrollEnabled, romajipriority: genius.option.romajiPriority, fontsize: genius.option.fontSize, useLZCompression: genius.option.useLZCompression }) // disable useLZCompression if the browser could not perform LZString in a good condition const shouldUseLZStringCompression = await testUseLZStringCompression() if (shouldUseLZStringCompression === false) { values.useLZCompression = false } genius.option.shouldUseLZStringCompression = shouldUseLZStringCompression // set up variables genius.debug = !!values.debug if (Object.prototype.hasOwnProperty.call(themes, values.theme)) { genius.option.themeKey = values.theme } else { genius.option.themeKey = Object.getOwnPropertyNames(themes)[0] custom.GM.setValue('theme', genius.option.themeKey) console.error(`Invalid value for theme key: custom.GM.getValue("theme") = '${values.theme}', using default theme key: '${genius.option.themeKey}'`) } theme = themes[genius.option.themeKey] annotationsEnabled = !!values.annotationsenabled autoScrollEnabled = !!values.autoscrollenabled genius.option.romajiPriority = values.romajipriority genius.option.fontSize = Math.max(0, parseInt(values.fontsize) || 0) genius.option.useLZCompression = values.useLZCompression if (genius.onThemeChanged) { for (const f of genius.onThemeChanged) { f() } } // If debug mode, clear cache if (genius.debug) { await Promise.all([custom.GM.setValue('selectioncache', '{}'), custom.GM.setValue('requestcache', '{}')]).then(function () { selectionCache = cleanSelectionCache() requestCache = {} console.log('selectionCache and requestCache cleared') }) } const isMessaging = document.location.href.startsWith(`${custom.emptyURL}#html:post`) // top if (!isMessaging) { listenToMessages() loadCache() addCss() if ('main' in custom) { custom.setupMain ? custom.setupMain(genius) : (genius.iv.main = setInterval(custom.main, 2000)) } if ('onResize' in custom) { window.addEventListener('resize', custom.onResize) } if ('toggleLyricsKey' in custom) { addKeyboardShortcut(custom.toggleLyricsKey) } return } // iframe let e = await new Promise(resolve => { // only receive 'writehtml' message once let msgFn = function (ev) { const data = (ev || 0).data || 0 if (data.iAm === custom.scriptName && data.type === 'writehtml') { window.removeEventListener('message', msgFn, false) msgFn = null const { data, source } = ev resolve({ data, source }) } } window.addEventListener('message', msgFn, false) try { // faster than setInterval top.postMessage({ iAm: custom.scriptName, type: 'genius-iframe-waiting' }, '*') } catch (e) { // in case top is not accessible from iframe } }) if (document.visibilityState === 'visible') await getRafPromise().then() if ('themeKey' in e.data && Object.prototype.hasOwnProperty.call(themes, e.data.themeKey)) { genius.option.themeKey = e.data.themeKey theme = themes[genius.option.themeKey] console.debug(`Theme activated in iframe: ${theme.name}`) } let html = e.data.html html = defaultCSS(html) let contentStyle = e.data.contentStyle if (typeof contentStyle === 'string' && contentStyle.length > 0) { html = contentStylingIframe(html, contentStyle) } contentStyle = null document.documentElement.innerHTML = html const communicationWindow = e.source // top if (document.visibilityState === 'visible') await getRafPromise().then() communicationWindow.postMessage({ iAm: custom.scriptName, type: 'htmlwritten' }, '*') if (document.visibilityState === 'visible') await getRafPromise().then() // clean up e = null function addClassNameToInfoHeader (evTarget) { /** @type {HTMLElement | null} */ const elm = (evTarget || 0) if (elm && elm.matches('div.LSongHeader__CenterInfo')) { const elmSongHeaderContainer = elm.closest('.LSongHeader__Right').parentNode if (!elmSongHeaderContainer || elmSongHeaderContainer.classList.contains('genius-lyrics-header-container')) { return } let p = elm while (p) { const t = p.parentNode if (t === elmSongHeaderContainer) break p = t } if (p !== null) { let children = [...elmSongHeaderContainer.children] let wrapper = elmSongHeaderContainer.appendChild(document.createElement('div')) appendElements(wrapper, children) elmSongHeaderContainer.classList.add('genius-lyrics-header-container') wrapper.classList.add('genius-lyrics-header-content') // for flexbox children = null wrapper = null } } } function addClassNameToHeaderArtistAndTracklist (evTarget) { /** @type {HTMLElement | null} */ const elm = (evTarget || 0) if (elm && elm.matches('.LHeaderArtistAndTracklist__Artist')) { const container = elm.closest('div.LUNDETERMINED__Container') if (container) { container.classList.remove('LUNDETERMINED__Container') container.classList.add('LHeaderArtistAndTracklist__Container') } } } function addClassNameHeaderOuter (evTarget) { /** @type {HTMLElement | null} */ const elm = (evTarget || 0) if (elm && elm.matches('.LSongHeader__Title')) { let lastMatchParent = null for (let parent = elm.parentNode; parent instanceof HTMLElement; parent = parent.parentNode) { if (parent.querySelector('#lyrics-root')) { break } lastMatchParent = parent } if (lastMatchParent !== null && lastMatchParent.nodeName === 'DIV') { lastMatchParent.classList.add('LSongHeader__Outer') lastMatchParent.parentNode.classList.add('LSongHeader__Outer_Container') } } } function cssTriggeringHook (resolve) { document.addEventListener('animationstart', (ev) => { const evTarget = ev.target if (ev.animationName === 'appDomAppended' || ev.animationName === 'appDomAppended2') { resolve() Promise.resolve(0).then(() => { communicationWindow.postMessage({ iAm: custom.scriptName, type: 'iframeLyricsAppRendered' }, '*') // iframeWin -> iframeWin }) if (ev.animationName === 'appDomAppended') { evTarget.classList.add('app11') } } if (ev.animationName === 'songHeaderDomAppended') { Promise.resolve(0).then(() => { communicationWindow.postMessage({ iAm: custom.scriptName, type: 'iframeContentRendered' }, '*') // iframeWin -> mainWin }) Promise.resolve(evTarget).then(addClassNameToInfoHeader) } if (ev.animationName === 'headerArtistAndTracklistDOMAppended') { Promise.resolve(evTarget).then(addClassNameToHeaderArtistAndTracklist) } if (ev.animationName === 'headerSongTitleDOMAppended') { Promise.resolve(evTarget).then(addClassNameHeaderOuter) } }, true) } // page rendered via CSS rendering const race1 = new Promise(resolve => { cssTriggeringHook(resolve) themeCommon.lyricsAppInit() }) // delay 500ms as a backup const race2 = new Promise(resolve => setTimeout(resolve, 500)) await Promise.race([race1, race2]) // page is rendered or 500ms after written html unlistenToMessages() // remove message handler removeElements(document.querySelectorAll('iframe')) // remove all embeded iframes inside #lyricsiframe // communicationWindow.postMessage({ iAm: custom.scriptName, type: 'lyricsAppInit', html: document.documentElement.innerHTML }, '*') const onload = theme.scripts() if ('iframeLoadedCallback1' in custom) { // before all onload functions and allow modification of theme and onload from external custom.iframeLoadedCallback1({ document, theme, onload }) } for (const func of onload) { try { func() } catch (e) { console.error(`Error in iframe onload ${func.name || func}: ${e}`) } } // Scroll lyrics event window.addEventListener('message', function (e) { if (typeof e.data !== 'object' || !('iAm' in e.data) || e.data.iAm !== custom.scriptName || e.data.type !== 'scrollLyrics') { return } scrollLyricsGeneric(e.data.position) }) if ('toggleLyricsKey' in custom) { addKeyboardShortcutInFrame(custom.toggleLyricsKey) } // this page is generated by code; pageready does not mean the page is fully rendered if (document.visibilityState === 'visible') await getRafPromise().then() communicationWindow.postMessage({ iAm: custom.scriptName, type: 'pageready'/* , html: document.documentElement.innerHTML */ }, '*') if (document.visibilityState === 'visible') await getRafPromise().then() if ('iframeLoadedCallback2' in custom) { // after all onload functions custom.iframeLoadedCallback2({ document, theme, onload }) } } mainRunner() return genius }