// ==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
}