Greasy Fork

GeniusLyrics

Downloads and shows genius lyrics for Tampermonkey scripts

目前为 2022-12-17 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/406698/1129241/GeniusLyrics.js

// ==UserScript==
// @exclude      *
// ==UserLibrary==
// @name         GeniusLyrics
// @description  Downloads and shows genius lyrics for Tampermonkey scripts
// @version      5.5.0
// @license      GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt
// @copyright    2020, cuzi (https://github.com/cvzi)
// @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 ([email protected])

    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 Reflect, top */

if (typeof module !== 'undefined') {
  module.exports = geniusLyrics
}

function geniusLyrics (custom) { // eslint-disable-line no-unused-vars
  'use strict'

  if (typeof custom !== 'object') {
    if (typeof window !== 'undefined') window.alert('geniusLyrics requires options argument')
    throw new Error('geniusLyrics requires options argument')
  }

  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 hideLyricsWithMessage () {
    const ret = custom.hideLyrics(...arguments)
    if (ret === false) {
      return false
    }
    window.postMessage({ iAm: custom.scriptName, type: 'lyricsDisplayState', visibility: 'hidden' }, '*')
    return ret
  }

  const __SELECTION_CACHE_VERSION__ = 1
  const __REQUEST_CACHE_VERSION__ = 1
  const REQUEST_CACHE_RESPONSE_TEXT_ONLY = true

  const genius = {
    option: {
      autoShow: true,
      themeKey: null,
      cacheHTMLRequest: true
    },
    f: {
      metricPrefix,
      cleanUpSongTitle,
      showLyrics,
      showLyricsAndRemember,
      reloadCurrentLyrics,
      loadLyrics,
      hideLyricsWithMessage,
      rememberLyricsSelection,
      isGreasemonkey,
      forgetLyricsSelection,
      forgetCurrentLyricsSelection,
      getLyricsSelection,
      geniusSearch,
      searchByQuery,
      isScrollLyricsEnabled,
      scrollLyrics,
      config
    },
    current: {
      title: '',
      artists: ''
    },
    iv: {
      main: null
    },
    style: {
      enabled: false
    },
    styleProps: {
    },
    minimizeHit: {
      noImageURL: false,
      noFeaturedArtists: false,
      simpleReleaseDate: false,
      noRawReleaseDate: false,
      shortenArtistName: false,
      fixArtistName: false,
      removeStats: false, // note: true for YoutubeGeniusLyrics only
      noRelatedLinks: false
    },
    debug: false
  }

  function cleanRequestCache () {
    return {
      __VERSION__: __REQUEST_CACHE_VERSION__
    }
  }

  function cleanSelectionCache () {
    return {
      __VERSION__: __SELECTION_CACHE_VERSION__
    }
  }

  let loadingFailed = false
  let requestCache = cleanRequestCache()
  let selectionCache = cleanSelectionCache()
  let theme
  let annotationsEnabled = true
  let autoScrollEnabled = false
  const onMessage = {}

  function isFakeWindow () {
    // window is not window in Spotify Web App
    return (window instanceof window.constructor) === false
  }
  function getTrueWindow () {
    // this can bypass Spotify's window Proxy Object and obtain the original window object
    return new Function('return window')() // eslint-disable-line no-new-func
  }

  let trueWindow = isFakeWindow() ? getTrueWindow() : window
  const setTimeout = trueWindow.setTimeout.bind(trueWindow)
  const setInterval = trueWindow.setInterval.bind(trueWindow)
  const clearTimeout = trueWindow.clearTimeout.bind(trueWindow)
  const clearInterval = trueWindow.clearInterval.bind(trueWindow)
  trueWindow = null

  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(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/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(/\((master|stereo|mono|anniversary|digital|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.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 parsePreloadedStateData (obj, parent) {
  // Convert genius' JSON represenation of lyrics to DOM object
    if ('children' in obj) {
      for (const child of obj.children) {
        if (typeof (child) === 'string') {
          if (child) {
            parent.appendChild(document.createTextNode(child))
          }
        } else {
          const node = parent.appendChild(document.createElement(child.tag))
          if ('data' in child) {
            for (const key in child.data) {
              node.dataset[key] = child.data[key]
            }
          }
          if ('attributes' in child) {
            for (const attr in child.attributes) {
              let value = child.attributes[attr]
              if ((attr === 'href' || attr === 'src') && (!value.startsWith('http') && !value.startsWith('#'))) {
                value = `https://genius.com${value}`
              }
              node.setAttribute(attr, value)
            }
          }
          parsePreloadedStateData(child, node)
        }
      }
    }
    return parent
  }

  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 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 is converted to: ', ret)
    return ret
  }

  function loadRequestCache (storedValue) {
    // global requestCache
    if (storedValue === '{}') {
      requestCache = cleanRequestCache()
    } else {
      requestCache = JSON.parse(storedValue)
      if (!requestCache.__VERSION__) {
        requestCache.__VERSION__ = 0
      }
    }
    if (requestCache.__VERSION__ !== __REQUEST_CACHE_VERSION__) {
      requestCache = cleanRequestCache()
      custom.GM.setValue('requestcache', JSON.stringify(requestCache))
    }
  }

  function loadSelectionCache (storedValue) {
    // global selectionCache
    if (storedValue === '{}') {
      selectionCache = cleanSelectionCache()
    } else {
      selectionCache = JSON.parse(storedValue)
      if (!selectionCache.__VERSION__) {
        selectionCache.__VERSION__ = 0
      }
    }
    if (selectionCache.__VERSION__ !== __SELECTION_CACHE_VERSION__) {
      if (selectionCache.__VERSION__ === 0) {
        selectionCache = convertSelectionCacheV0toV1(selectionCache)
        selectionCache.__VERSION__ = __SELECTION_CACHE_VERSION__
      } else {
        selectionCache = cleanSelectionCache()
      }
      custom.GM.setValue('selectioncache', JSON.stringify(selectionCache))
    }
  }

  function loadCache () {
    Promise.all([
      custom.GM.getValue('selectioncache', '{}'),
      custom.GM.getValue('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)
    }

    let headers = {
      Referer: obj.url,
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
      Host: getHostname(obj.url),
      'User-Agent': navigator.userAgent
    }
    if (obj.headers) {
      headers = Object.assign(headers, obj.headers)
    }

    return custom.GM.xmlHttpRequest({
      url: obj.url,
      method: obj.method ? obj.method : 'GET',
      data: obj.data,
      headers,
      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 (REQUEST_CACHE_RESPONSE_TEXT_ONLY === 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)
            custom.GM.setValue('requestcache', JSON.stringify(requestCache))
          }
        }
        obj.load(cacheObject, cacheResult)
      }
    })
  }

  function getSelectionCacheKey (title, artists) {
    title = title.replace(/\s+/g, ' ') // space, \n, \t, ...
    artists = artists.replace(/\s+/g, ' ')
    const selectionCacheKey = `${title}\t${artists}`
    return selectionCacheKey
  }

  function rememberLyricsSelection (title, artists, jsonHit) {
    const selectionCacheKey = getSelectionCacheKey(title, artists)
    if (typeof jsonHit === 'object') {
      jsonHit = JSON.stringify(jsonHit)
    }
    if (typeof jsonHit !== 'string') {
      return
    }
    selectionCache[selectionCacheKey] = jsonHit
    custom.GM.setValue('selectioncache', JSON.stringify(selectionCache))
  }

  function forgetLyricsSelection (title, artists) {
    const selectionCacheKey = getSelectionCacheKey(title, artists)
    if (selectionCacheKey in selectionCache) {
      delete selectionCache[selectionCacheKey]
      custom.GM.setValue('selectioncache', JSON.stringify(selectionCache))
    }
  }

  function forgetCurrentLyricsSelection () {
    const title = genius.current.title
    const artists = genius.current.artists
    if (typeof title === 'string' && typeof artists === 'string') {
      forgetLyricsSelection(title, artists)
      return true
    }
    return false
  }

  function getLyricsSelection (title, artists) {
    const selectionCacheKey = getSelectionCacheKey(title, artists)
    if (selectionCacheKey in selectionCache) {
      return JSON.parse(selectionCache[selectionCacheKey])
    } 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 modifyHits (hits) {
    // 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 !== 'complete') return false
      return true
    })

    for (const hit of hits) {
      const result = hit.result
      if (!result) return
      const primaryArtist = result.primary_artist || 0
      const minimizeHit = genius.minimizeHit
      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 (result.language === '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
      }

      if (hits.length > 1) {
        if (hit.type === 'song') {
          hit._order = 2600
        } else {
          hit._order = 1300
        }
        if (hit.result.language === 'romanization') {
          hit._order -= 50
        }
        if (hit.result.updated_by_human_at) {
          hit._order += 400
        }
        if (hit.result.language === 'en') {
          // possible translation for non-english songs
          // if all results are en, no different for hit._order reduction
          hit._order -= 1000
        }
      }
    }

    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
      error: function geniusSearchOnError (response) {
        window.alert(custom.scriptName + '\n\nError in geniusSearch(' + JSON.stringify(query) + ', ' + ('name' in cb ? cb.name : 'cb') + '):\n' + response)
        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') {
            window.alert(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)
          return jsonData
        } else {
          window.alert(custom.scriptName + '\n\n' + (errorMsg || 'Error') + ' in geniusSearch(' + JSON.stringify(query) + ', ' + ('name' in cb ? cb.name : 'cb') + '):\n\n' + response.responseText)
          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) {
        window.alert(custom.scriptName + '\n\nError loadGeniusSong(' + JSON.stringify(song) + ', cb):\n' + response)
      },
      load: function loadGeniusSongOnLoad (response, cacheResult) {
        // cacheResult(response)
        cb(response, cacheResult)
      }
    })
  }

  async function delayScrolling (scrollLyricsGeneric) {
    let p1
    let p2 = document.scrollingElement.scrollTop
    do {
      p1 = p2
      await new Promise(r => window.requestAnimationFrame(r)) // eslint-disable-line promise/param-names
      p2 = document.scrollingElement.scrollTop
    } while (`${p1}` !== `${p2}`)
    // the scrollTop is stable now
    window.scrollLyricsBusy = false
    scrollLyricsGeneric(window.latestScrollPos, true)
  }

  function scrollLyricsFunction (lyricsContainerSelector, defaultStaticOffsetTop) {
    // Creates a scroll function for a specific theme
    return function scrollLyricsGeneric (position, force) {
      window.latestScrollPos = position

      if (window.scrollLyricsBusy) return
      window.scrollLyricsBusy = true

      const staticTop = 'staticOffsetTop' in window ? window.staticOffsetTop : defaultStaticOffsetTop

      const div = document.querySelector(lyricsContainerSelector)
      const offset = genius.debug ? sumOffsets(div) : null
      const offsetTop = (div.getBoundingClientRect().top - document.scrollingElement.getBoundingClientRect().top)
      // const containerHeight = document.documentElement.clientHeight

      const lastPos = window.lastScrollTopPosition
      let newScrollTop = staticTop + div.scrollHeight * position + offsetTop
      const maxScrollTop = document.scrollingElement.scrollHeight - document.scrollingElement.clientHeight
      const btns = document.querySelectorAll('#resumeAutoScrollButton, #resumeAutoScrollFromHereButton')

      if (newScrollTop > maxScrollTop) {
        newScrollTop = maxScrollTop
      } else if (newScrollTop < 0) {
        newScrollTop = 0
      }

      if (lastPos > 0 && Math.abs(lastPos - document.scrollingElement.scrollTop) > 5) {
        if (!force && document.visibilityState === 'visible') {
          // the scrolltop is updating by scrollTo({behavior:'smooth'})
          delayScrolling(scrollLyricsGeneric)
          return
        }
        window.staticOffsetTop = staticTop
        window.newScrollTopPosition = newScrollTop
        function setArrowUpDownStyle (arrowUpDown) {
          if (document.scrollingElement.scrollTop - window.newScrollTopPosition < 0) {
            arrowUpDown.style.borderBottom = ''
            arrowUpDown.style.borderTop = '18px solid #222'
            arrowUpDown.style.borderRight = '9px inset transparent'
            arrowUpDown.style.borderLeft = '9px inset transparent'
          } else {
            arrowUpDown.style.borderBottom = '18px solid #222'
            arrowUpDown.style.borderTop = ''
            arrowUpDown.style.borderRight = '9px inset transparent'
            arrowUpDown.style.borderLeft = '9px inset transparent'
          }
        }
        // User scrolled -> stop auto scroll
        if (!document.getElementById('resumeAutoScrollButton')) {
          const resumeButton = document.createElement('div')
          const resumeButtonFromHere = document.createElement('div')
          resumeButton.addEventListener('click', function resumeAutoScroll () {
            resumeButton.classList.remove('btn-show')
            resumeButtonFromHere.classList.remove('btn-show')
            window.lastScrollTopPosition = null
            // Resume auto scrolling
            document.scrollingElement.scrollTo({
              top: window.newScrollTopPosition,
              behavior: 'smooth'
            })
            window.scrollLyricsBusy = false
          })
          resumeButtonFromHere.addEventListener('click', function resumeAutoScrollFromHere () {
            resumeButton.classList.remove('btn-show')
            resumeButtonFromHere.classList.remove('btn-show')
            window.scrollLyricsBusy = false
            // Resume auto scrolling from current position
            if (genius.debug) {
              for (const e of document.querySelectorAll('.scrolllabel')) {
                e.remove()
              }
              window.first = false
            }
            window.lastScrollTopPosition = null
            window.staticOffsetTop += document.scrollingElement.scrollTop - window.newScrollTopPosition
          })
          resumeButton.id = 'resumeAutoScrollButton'
          resumeButton.setAttribute('title', 'Resume auto scrolling')
          const arrowUpDown = resumeButton.appendChild(document.createElement('div'))
          arrowUpDown.style = 'width: 0;height: 0;margin-left: 2px;'
          setArrowUpDownStyle(arrowUpDown)
          resumeButtonFromHere.id = 'resumeAutoScrollFromHereButton'
          resumeButtonFromHere.setAttribute('title', 'Resume auto scrolling from here')
          const arrowRight = resumeButtonFromHere.appendChild(document.createElement('div'))
          arrowRight.style = 'width: 0;height: 0;border-top: 9px inset transparent;border-bottom: 9px inset transparent;border-left: 15px solid #222;margin-left: 2px;'
          setTimeout(() => {
            if (newScrollTop > 0 && newScrollTop < maxScrollTop) {
              resumeButton.classList.add('btn-show')
              resumeButtonFromHere.classList.add('btn-show')
            }
            window.scrollLyricsBusy = false
          }, 40)
          appendElements(document.body, [resumeButton, resumeButtonFromHere])
        } else {
          const arrowUpDown = document.querySelector('#resumeAutoScrollButton div')
          setArrowUpDownStyle(arrowUpDown)
          window.scrollLyricsBusy = false
          setTimeout(() => {
            if (newScrollTop > 0 && newScrollTop < maxScrollTop) {
              btns[0].classList.add('btn-show')
              btns[1].classList.add('btn-show')
            }
            window.scrollLyricsBusy = false
          }, 40)
        }
        return
      }

      if (btns.length === 2) {
        btns[0].classList.remove('btn-show')
        btns[1].classList.remove('btn-show')
      }

      window.lastScrollTopPosition = newScrollTop
      document.scrollingElement.scrollTo({
        top: newScrollTop,
        behavior: 'smooth'
      })

      if (genius.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.innerHTML = `${parseInt(position * 100)}%  -> ${parseInt(newScrollTop)}px`
      }
      window.scrollLyricsBusy = false
    }
  }

  function loadGeniusAnnotations (song, html, annotationsEnabled, cb) {
    let annotations = {}
    if (!annotationsEnabled) {
      // return cb(song, html, {})
      return cb(annotations)
    }
    let m = html.match(/annotation-fragment="\d+"/g)
    if (!m) {
      m = html.match(/href="\/\d+\//g)
      if (!m) {
        // No annotations in source -> skip loading annotations from API
        // return cb(song, html, {})
        return cb(annotations)
      }
    }

    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
      error: function loadGeniusAnnotationsOnError (response) {
        window.alert(custom.scriptName + '\n\nError loadGeniusAnnotations(' + JSON.stringify(song) + ', cb):\n' + response)
        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 = {
    annotationsRemoveAll () {
      for (const a of document.querySelectorAll('.song_body-lyrics .referent,.song_body-lyrics a[class*="referent"]')) {
        let tmpElement
        while ((tmpElement = a.firstChild) !== null) {
          a.parentNode.insertBefore(tmpElement, a)
        }
        a.remove()
      }
    },
    annotationsRemoveAll2 () {
      const referents = document.querySelectorAll('.song_body-lyrics .referent')
      for (const a of referents) {
        let tmpElement
        while ((tmpElement = a.firstChild) !== null) {
          a.parentNode.insertBefore(tmpElement, a)
        }
        a.remove()
      }
      // Remove right column
      document.querySelector('.song_body.column_layout .column_layout-column_span--secondary').remove()
      document.querySelector('.song_body.column_layout .column_layout-column_span--primary').style.width = '100%'
    },
    // Hide footer
    hideFooter895 () {
      const f = document.querySelectorAll('.footer div')
      if (f.length) {
        removeIfExists(f[0])
        removeIfExists(f[1])
      }
    },
    hideSecondaryFooter895 () {
      removeIfExists(document.querySelector('.footer.footer--secondary'))
    },
    // Hide other stuff
    hideStuff235 () {
      const grayBox = document.querySelector('.column_layout-column_span-initial_content>.dfp_unit.u-x_large_bottom_margin.dfp_unit--in_read')
      removeIfExists(grayBox)
      removeIfExists(document.querySelector('.header .header-expand_nav_menu'))
    },
    showAnnotation1234A (t) {
      const es = document.querySelectorAll('.song_body-lyrics .referent--yellow.referent--highlighted')
      for (const e of es) {
        e.classList.remove('referent--yellow', 'referent--highlighted')
      }
      t.classList.add('referent--yellow', 'referent--highlighted')
      if (!('annotations1234' in window)) {
        if (document.getElementById('annotationsdata1234')) {
          window.annotations1234 = JSON.parse(document.getElementById('annotationsdata1234').innerHTML)
        } else {
          window.annotations1234 = {}
          console.warn('No annotation data found #annotationsdata1234')
        }
      }
    },
    // Change links to target=_blank
    targetBlankLinks145A () {
      const as = document.querySelectorAll('body a:not([href|="#"]):not([target="_blank"])')
      for (const a of as) {
        a.target = '_blank'
      }
    },
    targetBlankLinks145B () {
      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')
          }
        }
      }
    }
  }

  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 themes = {
    genius: {
      name: 'Genius (Default)', // obsoleted
      themeKey: 'genius',
      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"]'))
          // 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 clickableTitle037 () {
          const url = document.querySelector('meta[property="og:url"]').content
          const h1 = document.querySelector('h1[class^="SongHeader"]')
          h1.innerHTML = '<a target="_blank" href="' + url + '" style="color:black">' + h1.innerHTML + '</a>'
          const div = document.querySelector('div[class^=SongHeader][class*="__CoverArt"]')
          div.innerHTML = '<a target="_blank" href="' + url + '">' + div.innerHTML + '</a>'
        }
        onload.push(clickableTitle037)

        // Show artwork
        onload.push(function showArtwork () {
          const noscripts = document.querySelectorAll('div[class^="SizedImage__Container"] noscript')
          // noScriptImage
          for (const noscript of noscripts) {
            const div = noscript.parentNode
            div.innerHTML = noscript.innerHTML
            div.querySelector('img').style.left = '0px'
          }
        })
        onload.push(hideStuff)

        // Goto lyrics
        onload.push(function () {
          document.getElementById('lyrics').scrollIntoView()
        })

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

        // Show annotations function
        function getAnnotationsContainer (a) {
          let c = document.getElementById('annotationcontainer958')
          if (!c) {
            c = document.body.appendChild(document.createElement('div'))
            c.setAttribute('id', 'annotationcontainer958')
            const isChrome = navigator.userAgent.indexOf('Chrome') !== -1
            document.head.appendChild(document.createElement('style')).innerHTML = `
            #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;
            }
            #annotationcontainer958 .arrow {
              height:30px;
            }
            #annotationcontainer958 .arrow:before {
              content: "";
              position: absolute;
              width: 0px;
              height: 0px;
              margin-top: 20px;
              ${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 .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;
            }
            #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;
            }
          `
          }
          c.innerHTML = ''

          c.style.display = 'block'
          c.style.opacity = 1.0
          const rect = a.getBoundingClientRect()
          c.style.top = (window.scrollY + rect.top + rect.height + 3) + 'px'

          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.innerHTML = ''
          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.innerHTML = ''
          return [annotationTabBar, annotationContent]
        }
        function switchTab (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')
        }
        function showAnnotation4956 (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 (!('annotations1234' in window)) {
            if (document.getElementById('annotationsdata1234')) {
              window.annotations1234 = JSON.parse(document.getElementById('annotationsdata1234').innerHTML)
            } else {
              window.annotations1234 = {}
              console.log('No annotation data found #annotationsdata1234')
            }
          }

          if (id in window.annotations1234) {
            const [annotationTabBar, annotationContent] = getAnnotationsContainer(this)
            let innerHTMLAddition = ''
            for (const annotation of window.annotations1234[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', switchTab)
              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 += '-'
                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.annotations1234[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) + 'px'
              const elements = annotationContent.querySelectorAll('iframe,img')
              for (const e of elements) {
                e.style.maxWidth = maxWidth
              }
              themeCommon.targetBlankLinks145B() // Change link target to _blank
            }, 100)
          }
        }
        onload.push(function () {
          if (document.getElementById('annotationsdata1234')) {
            window.annotations1234 = JSON.parse(document.getElementById('annotationsdata1234').innerHTML)
          }
        })

        onload.push(themeCommon.targetBlankLinks145B)
        onload.push(() => setTimeout(themeCommon.targetBlankLinks145B, 1000))

        if (!annotationsEnabled) {
          // Remove all annotations
          onload.push(function removeAnnotations135 () {
            document.querySelectorAll('div[class^="SongPage__Section"] a[class^="ReferentFragment"]').forEach(removeTagsKeepText)
          })
        } else {
          // Add click handler to annotations
          for (const a of document.querySelectorAll('div[class^="SongPage__Section"] a[class^="ReferentFragment"]')) {
            a.classList.add('annotated')
            a.addEventListener('click', showAnnotation4956)
          }
          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')
                }
              }
            }
          })
        }

        // Adapt width
        onload.push(function () {
          const bodyWidth = document.body.getBoundingClientRect().width
          document.querySelector('div[class^="Lyrics__Container"]').style.maxWidth = `calc(${bodyWidth}px - 1.5em)`
          document.querySelector('#lyrics-root').style.gridTemplateColumns = 'auto' // class="SongPageGriddesktop__TwoColumn-
        })

        // Open real page if not in frame
        onload.push(function () {
          if (window.top === window) {
            document.location.href = document.querySelector('meta[property="og:url"]').content
          }
        })
        return onload
      },
      combine: function themeGeniusCombineGeniusResources (song, html, annotations, cb) {
        let headhtml = ''

        // Make annotations clickable
        html = html.replace(/annotation-fragment="(\d+)"/g, '$0 data-annotationid="$1"')

        // 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
        headhtml += '\n<script id="annotationsdata1234" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</script>'

        // Scrollbar colors
        // Highlight annotated lines on hover
        headhtml += `
        <style>
          html{
            background-color: #181818;
            scrollbar-color: hsla(0,0%,100%,.3) transparent;
            scrollbar-width: auto;
          }
          .annotated span {
            background-color: #f0f0f0;
          }
          .annotated:hover span, .annotated.highlighted span {
            background-color: #ddd;
          }

          #resumeAutoScrollButton{
            position:fixed; right:65px; top:30%; cursor: pointer;border: 1px solid #d9d9d9;border-radius:100%;padding: 11px; z-index:101; background:white;
          }

          #resumeAutoScrollFromHereButton{
            position:fixed; right:20px; top:30%; cursor: pointer;border: 1px solid #d9d9d9;border-radius:100%;padding: 11px; z-index:101; background:white;
          }

          #resumeAutoScrollButton, #resumeAutoScrollFromHereButton{
            visibility: hidden;
            visibility: collapse;
          }
          #resumeAutoScrollButton.btn-show, #resumeAutoScrollFromHereButton.btn-show{
            visibility: visible;
          }
        </style>`

        // Add to <head>
        html = appendHeadText(html, headhtml)

        return cb(html)
      },
      // scrollLyrics: scrollLyricsFunction('div[class^="Lyrics__Container"]', -200)
      scrollLyrics: scrollLyricsFunction('div[class*="SongPage__LyricsWrapper"]', -120)
    },

    cleanwhite: {
      name: 'Clean white', // secondary theme
      themeKey: 'cleanwhite',
      scripts: function themeCleanWhiteScripts () {
        const onload = []

        // Hide cookies box function
        // var iv45
        // function hideCookieBox458 () {if(document.querySelector(".optanon-allow-all")){document.querySelector(".optanon-allow-all").click(); clearInterval(iv458)}}
        // onload.push(function() { iv458 = setInterval(hideCookieBox458, 500) }

        onload.push(themeCommon.hideFooter895)
        onload.push(themeCommon.hideSecondaryFooter895)
        onload.push(themeCommon.hideStuff235)

        // Show annotations function
        function showAnnotation1234 (ev) {
          ev.preventDefault()
          const id = this.dataset.annotationid
          themeCommon.showAnnotation1234A(this)
          if (id in window.annotations1234) {
            const annotation = window.annotations1234[id][0]
            const main = document.querySelector('.annotationbox')
            main.innerHTML = ''
            main.style.display = 'block'
            const bodyRect = document.body.getBoundingClientRect()
            const elemRect = this.getBoundingClientRect()
            const top = elemRect.top - bodyRect.top + elemRect.height
            main.style.top = top + 'px'
            main.style.left = '5px'
            const div0 = document.createElement('div')
            div0.className = 'annotationcontent'
            main.appendChild(div0)
            let html = '<div class="annotationlabel">$author</div><div class="annotation_rich_text_formatting">$body</div>'
            html = html.replace(/\$body/g, decodeHTML(annotation.body.html)).replace(/\$author/g, decodeHTML(annotation.created_by.name))
            div0.innerHTML = html
            themeCommon.targetBlankLinks145A() // Change link target to _blank
            setTimeout(function () { // hide on click
              document.body.addEventListener('click', hideAnnotationOnClick1234)
            }, 100)
            setTimeout(function () { // Resize iframes and images in frame
              const maxWidth = (document.body.clientWidth - 40) + 'px'
              const elements = main.querySelectorAll('iframe,img')
              for (const e of elements) {
                e.style.maxWidth = maxWidth
              }
            }, 100)
          }
        }
        function hideAnnotationOnClick1234 (ev) {
          let target = ev.target
          while (target) {
            if (target.id === 'annotationbox') {
              return
            }
            if (target.className && target.className.indexOf('referent') !== -1) {
              const id = parseInt(target.dataset.id)
              return showAnnotation1234.call(target, ev, id)
            }
            target = target.parentNode
          }
          document.body.removeEventListener('click', hideAnnotationOnClick1234)
          const main = document.querySelector('.annotationbox')
          main.style.display = 'none'
        }

        // Make song title clickable
        function clickableTitle037 () {
          if (!document.querySelector('.header_with_cover_art-primary_info-title')) {
            return
          }
          const url = document.querySelector('meta[property="og:url"]').content
          const h1 = document.querySelector('.header_with_cover_art-primary_info-title')
          h1.innerHTML = '<a target="_blank" href="' + url + '">' + h1.innerHTML + '</a>'
          // Featuring and album name
          const h2 = document.querySelector('.header_with_cover_art-primary_info-primary_artist').parentNode
          let s1 = ''
          let s2 = ''
          for (const el of document.querySelectorAll('.metadata_unit-label')) {
            if (el.innerText.toLowerCase().indexOf('feat') !== -1) {
              s1 += ' ' + el.parentNode.innerText.trim()
            } else if (el.innerText.toLowerCase().indexOf('album') !== -1) {
              s2 += ' \u2022 ' + el.parentNode.querySelector('a').parentNode.innerHTML.trim()
            }
          }
          h1.innerHTML += s1
          h2.innerHTML += s2
          // Remove other meta like Producer
          removeElements(document.querySelectorAll('h3'))
        }
        onload.push(clickableTitle037)

        onload.push(themeCommon.targetBlankLinks145A)
        onload.push(() => setTimeout(themeCommon.targetBlankLinks145A, 500))

        if (!annotationsEnabled) {
          // Remove all annotations
          onload.push(themeCommon.annotationsRemoveAll)
        } else {
          // Add click handler to annotations
          for (const a of document.querySelectorAll('*[data-annotationid]')) {
            a.addEventListener('click', showAnnotation1234)
          }
        }

        // Open real page if not in frame
        onload.push(function () {
          if (window.top === window) {
            document.location.href = document.querySelector('meta[property="og:url"]').content
          }
        })

        return onload
      },
      combine: function themeCleanWhiteXombineGeniusResources (song, html, annotations, onCombine) {
        let headhtml = ''
        const bodyWidth = document.getElementById('lyricsiframe').style.width || (document.getElementById('lyricsiframe').getBoundingClientRect().width + 'px')

        if (html.indexOf('class="lyrics">') === -1) {
          const doc = new window.DOMParser().parseFromString(html, 'text/html')
          const originalUrl = doc.querySelector('meta[property="og:url"]').content

          if (html.indexOf('__PRELOADED_STATE__ = JSON.parse(\'') !== -1) {
            const jsonStr = html.split('__PRELOADED_STATE__ = JSON.parse(\'')[1].split('\');\n')[0].replace(/\\([^\\])/g, '$1').replace(/\\\\/g, '\\')
            const jData = JSON.parse(jsonStr)

            const root = parsePreloadedStateData(jData.songPage.lyricsData.body, document.createElement('div'))

            // Annotations
            for (const a of root.querySelectorAll('a[data-id]')) {
              a.dataset.annotationid = a.dataset.id
              a.classList.add('referent--yellow')
            }

            const lyricshtml = root.innerHTML

            const h1 = doc.querySelector('div[class^=SongHeader][class*=Column] 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')

            removeIfExists(h1.parentNode.querySelector('div[class^="HeaderTracklist"]'))

            const titlehtml = '<div class="myheader">' + h1.parentNode.outerHTML + '</div>'

            headhtml = `<style>
            body {
              background:#ffffff linear-gradient(to bottom, #fafafa, #ffffff) fixed;
              color:black;
              font-family:Roboto, Arial, sans-serif;
              max-width:${bodyWidth - 20}px;
              overflow-x:hidden;
            }
            .mylyrics {color: black; font-size: 1.3em; line-height: 1.3em;font-weight: 300; padding:0.1em;}
            .mylyrics a:link,.mylyrics a:visited,.mylyrics a:hover{color:black; padding:0; line-height: 1.3em; box-shadow: none;}
            .myheader {font-size: 1.0em; font-weight:300}
            .myheader a:link,.myheader a:visited {color: rgb(96, 96, 96);; font-size:1.0em; font-weight:300; text-decoration:none}
            h1.mytitle {font-size: 1.1em;}
            h1.mytitle a:link,h1.mytitle a:visited {color: rgb(96, 96, 96);; text-decoration:none}
            .referent--yellow.referent--highlighted { opacity:1.0; background-color: transparent; box-shadow: none; color:#1ed760; transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
            .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)}
          </style>`

            // Add annotation data
            headhtml += '\n<script id="annotationsdata1234" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</script>'

            return onCombine(`
          <html>
          <head>
           ${headhtml}
          </head>
          <body>
            ${titlehtml}
            <div class="mylyrics song_body-lyrics">
            ${lyricshtml}
            </div>
            <div class="annotationbox" id="annotationbox"></div>
          </body>
          </html>
          `)
          }

          return onCombine(`<div style="color:black;background:white;font-family:sans-serif">
        <br>
        <h1>&#128561; Oops!</h1>
        <br>
        Sorry, these lyrics seem to use new genius page design.<br>They cannot be shown with the "Clean white theme" (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;">

Error:   Unknown genius page design
URL:     ${document.location.href}
Genius:  ${originalUrl}

</pre><br>
        You can simply post the information on github:<br>
        <a target="_blank" href="https://github.com/cvzi/genius-lyrics-userscript/issues/1">https://github.com/cvzi/genius-lyrics-userscript/issues/1</a>
        <br>
        or via email: <a target="_blank" href="mailto:[email protected]">[email protected]</a>
        <br>
        <br>
        Thanks for your help!
        <br>
        <br>
         </div>`)
        }

        // Make annotations clickable
        const regex = /annotation-fragment="(\d+)"/g
        html = html.replace(regex, '$0 data-annotationid="$1"')

        // Remove cookie consent
        html = html.replace(/<script defer="true" src="https:\/\/cdn.cookielaw.org.+?"/, '<script ')

        // Extract lyrics
        const lyrics = '<div class="mylyrics song_body-lyrics">' + html.split('class="lyrics">')[1].split('</div>')[0] + '</div>'

        // Extract title
        const title = '<div class="header_with_cover_art-primary_info">' + html.split('class="header_with_cover_art-primary_info">')[1].split('</div>').slice(0, 3).join('</div>') + '</div></div>'

        // Remove body content, hide horizontal scroll bar, add lyrics
        const parts = html.split('<body', 2)
        html = parts[0] + '<body' + parts[1].split('>')[0] + '>\n\n' +
      title + '\n\n' + lyrics +
      '\n\n<div class="annotationbox" id="annotationbox"></div><div style="height:5em"></div></body></html>'

        // Add annotation data
        headhtml += '\n<script id="annotationsdata1234" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</script>'

        // CSS
        headhtml += `<style>
          body {
            background:#ffffff linear-gradient(to bottom, #fafafa, #ffffff) fixed;
            color:black;
            font-family:Roboto, Arial, sans-serif;
            overflow-x:hidden;
            max-width:${bodyWidth}px;
          }
          .mylyrics {color: black; font-size: 1.3em; line-height: 1.1em;font-weight: 300; padding:0.1em;}
          .referent {background-color:inherit;box-shadow: none; line-height: 1.1em !important; }
          .windows a.referent {padding:0; line-height: 1.1em; background-color:inherit;box-shadow: none;}
          .windows a.referent:hover {background-color: rgb(230,230,230);border-radius: 2px;}
          .referent:hover {background-color: rgb(230,230,230);border-radius: 2px;}
          .windows a.referent:not(.referent--green):not(.referent--red):not(.referent--highlighted):not(.referent--image) { opacity:1.0; background-color: inherit; box-shadow: none; color:rgb(6, 95, 212); transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
          .referent:not(.referent--green):not(.referent--red):not(.referent--highlighted):not(.referent--image) { opacity:1.0; background-color: inherit; box-shadow: none; color:#2c1cb7; transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
          .windows a.referent:hover:not(.referent--green):not(.referent--red):not(.referent--highlighted):not(.referent--image) { background-color: rgb(230,230,230);border-radius: 2px;}
          .referent--yellow.referent--highlighted { opacity:1.0; background-color: inherit; box-shadow: none; color:#2c1cb7; transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
          .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)}
          .header_with_cover_art-primary_info h1,.header_with_cover_art-primary_info h2,.header_with_cover_art-primary_info h3 {color: gray; font-size: 0.9em; line-height: 1.0em;font-weight: 300; }
          h1.header_with_cover_art-primary_info-title {line-height: 1.1em;}
          h1.header_with_cover_art-primary_info-title a {color: gray; font-size:1.1em}
          h2 a,h2 a.header_with_cover_art-primary_info-primary_artist {color: gray; font-size:1.0em; font-weight:300}
          .header_with_cover_art-primary_info {display:inline-block;color: black;border-radius: 2px;padding:7px 10px 0px 5px;}
        </style>`

        // Add to <head>
        html = appendHeadText(html, headhtml)

        return onCombine(html)
      },
      scrollLyrics: scrollLyricsFunction('.mylyrics', -200)
    },

    spotify: {
      name: 'Spotify', // secondary theme
      themeKey: 'spotify',
      scripts: function themeSpotifyScripts () {
        const onload = []

        // Hide cookies box function
        // var iv458
        // function hideCookieBox458 () {if(document.querySelector(".optanon-allow-all")){document.querySelector(".optanon-allow-all").click(); clearInterval(iv458)}}
        // onload.push(function() { iv458 = setInterval(hideCookieBox458, 500) })

        onload.push(themeCommon.hideFooter895)
        onload.push(themeCommon.hideSecondaryFooter895)
        onload.push(themeCommon.hideStuff235)

        // Show annotations function
        function showAnnotation1234 (ev) {
          ev.preventDefault()
          const id = this.dataset.annotationid
          themeCommon.showAnnotation1234A(this)
          if (id in window.annotations1234) {
            const annotation = window.annotations1234[id][0]
            const main = document.querySelector('.annotationbox')
            main.innerHTML = ''
            main.style.display = 'block'
            const bodyRect = document.body.getBoundingClientRect()
            const elemRect = this.getBoundingClientRect()
            const top = elemRect.top - bodyRect.top + elemRect.height
            main.style.top = top + 'px'
            main.style.left = '5px'
            const div0 = document.createElement('div')
            div0.className = 'annotationcontent'
            main.appendChild(div0)
            let html = '<div class="annotationlabel">$author</div><div class="annotation_rich_text_formatting">$body</div>'
            html = html.replace(/\$body/g, decodeHTML(annotation.body.html)).replace(/\$author/g, decodeHTML(annotation.created_by.name))
            div0.innerHTML = html
            themeCommon.targetBlankLinks145A() // Change link target to _blank
            setTimeout(function () { document.body.addEventListener('click', hideAnnotationOnClick1234) }, 100) // hide on click
          }
        }
        function hideAnnotationOnClick1234 (ev) {
          let target = ev.target
          while (target) {
            if (target.id === 'annotationbox') {
              return
            }
            if (target.className && target.className.indexOf('referent') !== -1) {
              const id = parseInt(target.dataset.id)
              return showAnnotation1234.call(target, ev, id)
            }
            target = target.parentNode
          }
          document.body.removeEventListener('click', hideAnnotationOnClick1234)
          const main = document.querySelector('.annotationbox')
          main.style.display = 'none'
        }

        onload.push(function () {
          if (document.getElementById('annotationsdata1234')) {
            window.annotations1234 = JSON.parse(document.getElementById('annotationsdata1234').innerHTML)
          }
        })

        // Make song title clickable
        function clickableTitle037 () {
          if (!document.querySelector('.header_with_cover_art-primary_info-title')) {
            return
          }
          const url = document.querySelector('meta[property="og:url"]').content
          const h1 = document.querySelector('.header_with_cover_art-primary_info-title')
          h1.innerHTML = '<a target="_blank" href="' + url + '">' + h1.innerHTML + '</a>'
          // Featuring and album name
          const h2 = document.querySelector('.header_with_cover_art-primary_info-primary_artist').parentNode
          let s1 = ''
          let s2 = ''
          for (const el of document.querySelectorAll('.metadata_unit-label')) {
            if (el.innerText.toLowerCase().indexOf('feat') !== -1) {
              s1 += ' ' + el.parentNode.innerText.trim()
            } else if (el.innerText.toLowerCase().indexOf('album') !== -1) {
              s2 += ' \u2022 ' + el.parentNode.querySelector('a').parentNode.innerHTML.trim()
            }
          }
          h1.innerHTML += s1
          h2.innerHTML += s2
          // Remove other meta like Producer
          removeElements(document.querySelectorAll('h3'))
        }
        onload.push(clickableTitle037)

        onload.push(() => setTimeout(themeCommon.targetBlankLinks145A, 1000))

        if (!annotationsEnabled) {
          // Remove all annotations
          onload.push(themeCommon.annotationsRemoveAll)
        } else {
          // Add click handler to annotations
          for (const a of document.querySelectorAll('*[data-annotationid]')) {
            a.addEventListener('click', showAnnotation1234)
          }
        }

        // Open real page if not in frame
        onload.push(function () {
          if (window.top === window) {
            document.location.href = document.querySelector('meta[property="og:url"]').content
          }
        })

        return onload
      },
      combine: function themeSpotifyXombineGeniusResources (song, html, annotations, onCombine) {
        let headhtml = ''
        const bodyWidth = document.getElementById('lyricsiframe').style.width || (document.getElementById('lyricsiframe').getBoundingClientRect().width + 'px')

        if (html.indexOf('class="lyrics">') === -1) {
          const doc = new window.DOMParser().parseFromString(html, 'text/html')
          const originalUrl = doc.querySelector('meta[property="og:url"]').content

          if (html.indexOf('__PRELOADED_STATE__ = JSON.parse(\'') !== -1) {
            const jsonStr = html.split('__PRELOADED_STATE__ = JSON.parse(\'')[1].split('\');\n')[0].replace(/\\([^\\])/g, '$1').replace(/\\\\/g, '\\')
            const jData = JSON.parse(jsonStr)

            const root = parsePreloadedStateData(jData.songPage.lyricsData.body, document.createElement('div'))

            // Annotations
            for (const a of root.querySelectorAll('a[data-id]')) {
              a.dataset.annotationid = a.dataset.id
              a.classList.add('referent--yellow')
            }

            const lyricshtml = root.innerHTML

            const h1 = doc.querySelector('div[class^=SongHeader][class*=Column] 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')

            removeIfExists(h1.parentNode.querySelector('div[class^="HeaderTracklist"]'))

            const titlehtml = '<div class="myheader">' + h1.parentNode.outerHTML + '</div>'

            headhtml = `<style>
            @font-face{font-family:spotify-circular;src:url("https://open.scdn.co/fonts/CircularSpUIv3T-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Light.ttf) format("truetype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular;src:url("https://open.scdn.co/fonts/CircularSpUIv3T-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Book.ttf) format("truetype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular;src:url("https://open.scdn.co/fonts/CircularSpUIv3T-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Bold.ttf) format("truetype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-arabic;src:url("https://open.scdn.co/fonts/CircularSpUIAraOnly-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Light.otf) format("opentype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-arabic;src:url("https://open.scdn.co/fonts/CircularSpUIAraOnly-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Book.otf) format("opentype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-arabic;src:url("https://open.scdn.co/fonts/CircularSpUIAraOnly-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Bold.otf) format("opentype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-hebrew;src:url("https://open.scdn.co/fonts/CircularSpUIHbrOnly-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Light.otf) format("opentype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-hebrew;src:url("https://open.scdn.co/fonts/CircularSpUIHbrOnly-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Book.otf) format("opentype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-hebrew;src:url("https://open.scdn.co/fonts/CircularSpUIHbrOnly-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Bold.otf) format("opentype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-cyrillic;src:url("https://open.scdn.co/fonts/CircularSpUICyrOnly-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Light.otf) format("opentype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-cyrillic;src:url("https://open.scdn.co/fonts/CircularSpUICyrOnly-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Book.otf) format("opentype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-cyrillic;src:url("https://open.scdn.co/fonts/CircularSpUICyrOnly-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Bold.otf) format("opentype");font-weight:600;font-style:normal;font-display:swap}
            html{
              scrollbar-color:hsla(0,0%,100%,.3) transparent;
              scrollbar-width:auto; }
            body {
              background-color: rgba(0, 0, 0, 0);
              color:white;
              max-width: ${bodyWidth - 20}px;
              overflow-x:hidden;
              font-family:spotify-circular,spotify-circular-cyrillic,spotify-circular-arabic,spotify-circular-hebrew,Helvetica Neue,Helvetica,Arial,Hiragino Kaku Gothic Pro,Meiryo,MS Gothic,sans-serif;
            }
            .mylyrics {color: rgb(255,255,255,0.85); font-size: 1.3em; line-height: 1.1em;font-weight: 300; padding:0px 0.1em 0.1em 0.1em;}
            .mylyrics a:link,.mylyrics a:visited,.mylyrics a:hover{color:rgba(255,255,255,0.95)}
            .myheader {font-size: 1.0em; font-weight:300}
            .myheader a:link,.myheader a:visited {color: rgb(255,255,255,0.9); font-size:1.0em; font-weight:300; text-decoration:none}
            h1.mytitle {font-size: 1.1em;}
            h1.mytitle a:link,h1.mytitle a:visited {color: rgb(255,255,255,0.9); text-decoration:none}
            ::-webkit-scrollbar {width: 16px;}
            ::-webkit-scrollbar-thumb {background-color: hsla(0,0%,100%,.3);}
            .referent--yellow.referent--highlighted { opacity:1.0; background-color: transparent; box-shadow: none; color:#1ed760; transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
            .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: rgb(255,255,255,0.6)}
            .annotationbox .annotation_rich_text_formatting a {color: rgb(255,255,255,0.9)}
          </style>`

            // Add annotation data
            headhtml += '\n<script id="annotationsdata1234" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</script>'

            return onCombine(`
          <html>
          <head>
           ${headhtml}
          </head>
          <body>
            ${titlehtml}
            <div class="mylyrics song_body-lyrics">
            ${lyricshtml}
            </div>
            <div class="annotationbox" id="annotationbox"></div>
          </body>
          </html>
          `)
          }

          return onCombine(`<div style="color:black;background:white;font-family:sans-serif">
        <br>
        <h1>&#128561; Oops!</h1>
        <br>
        Sorry, these lyrics seem to use new genius page design.<br>They cannot be shown with the "Spotify theme" (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;">

Error:   Unknown genius page design
Genius:  ${originalUrl}

</pre><br>
        You can simply post the information on github:<br>
        <a target="_blank" href="https://github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues/4">https://github.com/cvzi/Spotify-Genius-Lyrics-userscript/issues/4</a>
        <br>
        or via email: <a target="_blank" href="mailto:[email protected]">[email protected]</a>
        <br>
        <br>
        Thanks for your help!
        <br>
        <br>
         </div>`)
        }

        // Make annotations clickable
        const regex = /annotation-fragment="(\d+)"/g
        html = html.replace(regex, '$0 data-annotationid="$1"')

        // Remove cookie consent
        html = html.replace(/<script defer="true" src="https:\/\/cdn.cookielaw.org.+?"/, '<script ')

        // Extract lyrics
        const lyrics = '<div class="mylyrics song_body-lyrics">' + html.split('class="lyrics">')[1].split('</div>')[0] + '</div>'

        // Extract title
        const title = '<div class="header_with_cover_art-primary_info">' + html.split('class="header_with_cover_art-primary_info">')[1].split('</div>').slice(0, 3).join('</div>') + '</div></div>'

        // Remove body content, hide horizontal scroll bar, add lyrics
        const parts = html.split('<body', 2)
        html = parts[0] + '<body style="overflow-x:hidden;width:100%;" ' + parts[1].split('>')[0] + '>\n\n' +
      title + '\n\n' + lyrics +
      '\n\n<div class="annotationbox" id="annotationbox"></div><div style="height:5em"></div></body></html>'

        // Add annotation data
        headhtml += '\n<script id="annotationsdata1234" type="application/json">' + JSON.stringify(annotations).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</script>'

        // CSS
        headhtml += `<style>
          @font-face{font-family:spotify-circular;src:url("https://open.scdn.co/fonts/CircularSpUIv3T-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Light.ttf) format("truetype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular;src:url("https://open.scdn.co/fonts/CircularSpUIv3T-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Book.ttf) format("truetype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular;src:url("https://open.scdn.co/fonts/CircularSpUIv3T-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIv3T-Bold.ttf) format("truetype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-arabic;src:url("https://open.scdn.co/fonts/CircularSpUIAraOnly-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Light.otf) format("opentype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-arabic;src:url("https://open.scdn.co/fonts/CircularSpUIAraOnly-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Book.otf) format("opentype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-arabic;src:url("https://open.scdn.co/fonts/CircularSpUIAraOnly-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIAraOnly-Bold.otf) format("opentype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-hebrew;src:url("https://open.scdn.co/fonts/CircularSpUIHbrOnly-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Light.otf) format("opentype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-hebrew;src:url("https://open.scdn.co/fonts/CircularSpUIHbrOnly-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Book.otf) format("opentype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-hebrew;src:url("https://open.scdn.co/fonts/CircularSpUIHbrOnly-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUIHbrOnly-Bold.otf) format("opentype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-cyrillic;src:url("https://open.scdn.co/fonts/CircularSpUICyrOnly-Light.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Light.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Light.otf) format("opentype");font-weight:200;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-cyrillic;src:url("https://open.scdn.co/fonts/CircularSpUICyrOnly-Book.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Book.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Book.otf) format("opentype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:spotify-circular-cyrillic;src:url("https://open.scdn.co/fonts/CircularSpUICyrOnly-Bold.woff2") format("woff2"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Bold.woff) format("woff"),url(https://open.scdn.co/fonts/CircularSpUICyrOnly-Bold.otf) format("opentype");font-weight:600;font-style:normal;font-display:swap}
          html{
            scrollbar-color:hsla(0,0%,100%,.3) transparent;
            scrollbar-width:auto; }
          body {
            background-color: rgba(0, 0, 0, 0);
            color:white;
            max-width:${bodyWidth - 20}px;
            overflow-x:hidden;
            font-family:spotify-circular,spotify-circular-cyrillic,spotify-circular-arabic,spotify-circular-hebrew,Helvetica Neue,Helvetica,Arial,Hiragino Kaku Gothic Pro,Meiryo,MS Gothic,sans-serif;
          }
          .mylyrics {color: rgb(255,255,255,0.6); font-size: 1.3em; line-height: 1.1em;font-weight: 300; padding:0.1em;}
          .referent {background-color:transparent;box-shadow: none;  line-height: 1.1em !important; }
          .windows a.referent {padding:0; line-height: 1.1em; background-color:transparent;box-shadow: none;}
          .windows a.referent:hover {background-color: hsla(0,0%,0%,.2);border-radius: 2px;}
          .referent:hover {background-color: hsla(0,0%,0%,.2);border-radius: 2px;}
          .windows a.referent:not(.referent--green):not(.referent--red):not(.referent--highlighted):not(.referent--image) { opacity:1.0; background-color: transparent; box-shadow: none; color:white; transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
          .referent:not(.referent--green):not(.referent--red):not(.referent--highlighted):not(.referent--image) { opacity:1.0; background-color: transparent; box-shadow: none; color:white; transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
          .windows a.referent:hover:not(.referent--green):not(.referent--red):not(.referent--highlighted):not(.referent--image) { background-color: hsla(0,0%,0%,.2);border-radius: 2px;}
          .referent--yellow.referent--highlighted { opacity:1.0; background-color: transparent; box-shadow: none; color:#1ed760; transition: color .2s linear;transition-property: color;transition-duration: 0.2s;transition-timing-function: linear;transition-delay: 0s;}
          .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: rgb(255,255,255,0.6)}
          .annotationbox .annotation_rich_text_formatting a {color: rgb(255,255,255,0.9)}
          .header_with_cover_art-primary_info h1,.header_with_cover_art-primary_info h2,.header_with_cover_art-primary_info h3 {color: rgb(255,255,255,0.5); font-size: 0.9em; line-height: 1.0em;font-weight: 300; }
          h1.header_with_cover_art-primary_info-title {line-height: 1.1em;}
          h1.header_with_cover_art-primary_info-title a {color: rgb(255,255,255,0.9); font-size:1.1em}
          h2 a,h2 a.header_with_cover_art-primary_info-primary_artist {color: rgb(255,255,255,0.9); font-size:1.0em; font-weight:300}
          .header_with_cover_art-primary_info {display:inline-block;background-color: hsla(0,0%,0%,.2);color: #000;border-radius: 2px;padding:7px 10px 0px 5px;}
          ::-webkit-scrollbar {width: 16px;}
          ::-webkit-scrollbar-thumb {background-color: hsla(0,0%,100%,.3);}
        </style>`

        // Add to <head>
        html = appendHeadText(html, headhtml)

        return onCombine(html)
      },
      scrollLyrics: scrollLyricsFunction('.mylyrics', -200)
    }
  }

  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 songTitle = genius.current.title
    const songArtists = genius.current.artists
    if (songTitle && songArtists) {
      const hitFromCache = getLyricsSelection(songTitle, songArtists)
      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 = songArtistsArr.join(' ')
    if (force || beLessSpecific || (!document.hidden && musicIsPlaying && (genius.current.title !== songTitle || genius.current.artists !== songArtists))) {
      const mTitle = genius.current.title = songTitle
      const mArtists = genius.current.artists = songArtists

      const firstArtist = songArtistsArr[0]

      const simpleTitle = songTitle.replace(/\s*-\s*.+?$/, '') // Remove anything following the last dash
      if (beLessSpecific) {
        songArtists = firstArtist
        songTitle = simpleTitle
      }

      if ('onNewSongPlaying' in custom) {
        custom.onNewSongPlaying(songTitle, songArtistsArr)
      }

      const hitFromCache = getLyricsSelection(mTitle, mArtists)
      if (!force && hitFromCache) {
        showLyrics(hitFromCache, 1)
      } else {
        geniusSearch(songTitle + ' ' + songArtists, function geniusSearchCb (r) {
          const hits = r.response.sections[0].hits
          if (hits.length === 0) {
            hideLyricsWithMessage()
            if (!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(mTitle, mArtists)
          } else if (hits.length === 1) {
            showLyricsAndRemember(mTitle, mArtists, hits[0], 1)
          } else if (songArtistsArr.length === 1) {
            // Check if one result is an exact match
            const exactMatches = []
            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) {
              console.log(`Genius Lyrics - exact match is found in ${hits.length} results.`)
              showLyricsAndRemember(mTitle, mArtists, exactMatches[0], hits.length)
            } else {
              multipleResultsFound(hits, mTitle, mArtists)
            }
          } else {
            console.log('Genius Lyrics - lyrics results with multiple artists are found.')
            multipleResultsFound(hits, mTitle, mArtists)
          }
        }, 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.title, genius.current.artists)
        custom.showSearchField(`${genius.current.artists} ${genius.current.title}`)
      })
      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) {
        custom.showSearchField(genius.current.artists + ' ' + genius.current.title)
      })
      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 contentStyling (html) {
    // only if genius.style.enable is set to true by external script
    if (genius.style.enabled !== true) return html
    if (typeof genius.style.setup === 'function') {
      if (genius.style.setup() === false) return html
    }

    const customProperties = Object.entries(genius.styleProps).map(([prop, value]) => {
      return `${prop}: ${value};`
    }).join('\n')

    const css = `
    html {
      margin: 0;
      padding: 0;
      ${customProperties}
    }

    body {
      background-color: var(--ygl-background);
      color: var(--ygl-color);
      font-size: var(--ygl-font-size);
      margin: 0;
      padding: 0;
      padding-top: 50vh;
      padding-bottom: 50vh;
    }

    main, #application {
      --ygl-container-display: none;
    }

    #application {
      padding: 28px;
    }

    div[data-lyrics-container]{
      font-size: var(--ygl-font-size);
    }

    div[class*="SongPageGrid"], div[class*="SongHeader"] {
      background-color: none;
      padding: 0;
      color: var(--ygl-color);
    }

    div[class*="SongPageGrid"], div[class*="SongHeaderWithPrimis__Container"]{
      background-image: none;
    }

    div[data-exclude-from-selection] {
      display: none;
    }

    main[class*="Container"] a[href] {
      color: var(--ygl-color) !important;
    }

    main[class*="Container"] h1[font-size][class] {
      color: var(--ygl-color);
    }

    div[class*="SongHeaderWithPrimis__Left"] {
      display: none;
    }

    div[class*="SongPageGriddesktop"] {
      display: block;
    }
    
    span[class*="LabelWithIcon"] > svg,
    button[class*="LabelWithIcon"] > svg,
    div[class*="Tooltip__Container"] svg{
      fill: currentColor;
    }

    p[class*="__Label"],
    span[class*="__Label"],
    div[class*="__Section"],
    button[class*="__Container"] {
      color: inherit;
      text-decoration: none;
      cursor: inherit;
    }

    div[class*="MetadataStats"] {
      cursor: default;
    }

    div[class*="SongHeaderWithPrimis__Information"] div[class*="HeaderCreditsPrimis__Container"] {
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      gap: 10px;
      align-items: center;
      justify-items: center;
    }

    div[class*="SongHeaderWithPrimis__Information"] {
      margin: 0;
      padding: 0;
      max-width: 100%;
      white-space: normal;
    }

    div[class*="SongHeaderWithPrimis__Bottom"] a[href] {
      padding: 0;
      margin: 0;
    }
    
    div[class*="SongHeaderWithPrimis__Right"]{
      background-color: var(--ygl-infobox-background);
      padding: 18px 26px;
    }

    div[data-lyrics-container][class*="Lyrics__Container"] {
      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: none;
      outline: none;
    }

    a[href][class],
    span[class*="PortalTooltip"],
    div[class*="HeaderCreditsPrimis"],
    div[class*="HeaderArtistAndTracklistPrimis"] {
      font-size: inherit;
    }
    
    div[class*="SongHeaderWithPrimis__Information"] h1 + div[class*="HeaderArtistAndTracklistPrimis"] {
      font-size: 80%;
      margin-top: 10px;
      margin-bottom: 6px;
    }

    div[class*="MetadataStats__Stats"] {  
      display: flex;
      flex-wrap: wrap;
      white-space: nowrap;    
      row-gap: 4px;
      column-gap: 16px;
      white-space: nowrap;
      margin-top: 6px;
    }

    h1,
    div[class*="SongPage__LyricsWrapper"] {
      white-space: normal;
    }
    
    div[class*="MetadataStats__Stats"] > [class] {
      margin-right: 0;
    }

    div[class*="SongHeaderWithPrimis__Information"] div[class*="HeaderCreditsPrimis__List"] {
      font-size: 85%;
    }

    div[class*="SongHeaderWithPrimis__Information"] ~ div[class*="SongHeaderWithPrimis__PrimisContainer"] {
      display: none;
    }

    div[class*="SongHeaderWithPrimis__Information"] {
      --ygl-container-display: '-NULL-';
    }

    div[class*="Footer"],
    div[class*="Leaderboard"] {
      display: none;
    }

    div[class*="SongPage__Section"] #about,
    div[class*="SongPage__Section"] #about ~ *,
    div[class*="SongPage__Section"] #comments,
    div[class*="SongPage__Section"] #comments ~ * {
      display: none;
    }
    
    div[class*="SongPage__Section"] #lyrics-root-pin-spacer {
      padding-top: 12px;
    }

    div[class*="Header"] {
      max-width: unset;
    }

    span[class*="InlineSvg__Wrapper"] > svg {
      fill: currentColor;
    }

    div[class*="SongHeader"] h1[font-size="medium"]{
      font-size: 140%;
    }
    `

    const headhtml = `<style>${css}</style>`

    // Add to <head>
    html = appendHeadText(html, headhtml)
    return html
  }

  let isShowLyricsInterrupted = false
  function interuptMessageHandler (ev) {
    const data = ev.data || 0
    if (data.iAm === custom.scriptName && data.type === 'lyricsDisplayState' && typeof data.visibility === 'string') {
      isShowLyricsInterrupted = data.visibility !== 'loading'
    }
  }

  function showLyrics (songInfo, searchresultsLengths) {
    // 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
    }

    const spinnerHolder = document.createElement('div')
    spinnerHolder.classList.add('loadingspinnerholder')
    let spinner
    if ('createSpinner' in custom) {
      spinner = custom.createSpinner(spinnerHolder)
    } else {
      spinnerHolder.style.left = (iframe.getBoundingClientRect().left + container.clientWidth / 2) + 'px'
      spinnerHolder.style.top = '100px'
      spinner = spinnerHolder.appendChild(document.createElement('div'))
      spinner.classList.add('loadingspinner')
    }
    document.body.appendChild(spinnerHolder)

    function spinnerUpdate (text, title, status, textStatus) {
      if (typeof text === 'string') spinner.textContent = text
      if (typeof title === 'string') spinnerHolder.title = title
      if ('notifyGeniusLoading' in custom && arguments.length > 2) {
        custom.notifyGeniusLoading({
          status,
          textStatus
        })
      }
    }

    window.removeEventListener('message', interuptMessageHandler, false)
    window.addEventListener('message', interuptMessageHandler, false)
    isShowLyricsInterrupted = false

    spinnerUpdate('5', 'Downloading lyrics...', 0, 'start')
    window.postMessage({ iAm: custom.scriptName, type: 'lyricsDisplayState', visibility: 'loading', song: songInfo, searchresultsLengths }, '*')

    function interuptedByExternal () {
      window.removeEventListener('message', interuptMessageHandler, false)
    }
    async function showLyricsRunner () {
      if (isShowLyricsInterrupted === true) return interuptedByExternal()
      let cacheReqResult = null
      let html = await new Promise(resolve => loadGeniusSong(songInfo, function loadGeniusSongCb (response, cacheResult) {
        cacheReqResult = cacheResult // cache the proceeded html only
        resolve(response.responseText)
      }))
      if (isShowLyricsInterrupted === true) return interuptedByExternal()

      if (cacheReqResult !== null) {
        // 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 (isShowLyricsInterrupted === true) 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 (isShowLyricsInterrupted === true) return interuptedByExternal()
        annotations = null
        html = contentStyling(html)
        if (genius.option.cacheHTMLRequest === true) cacheReqResult({ responseText: html }) // note: 1 page consume 2XX KBytes
      }

      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 ???
          custom.onLyricsReady(songInfo, container)
        }
        if (iv > 0) {
          clearInterval(iv)
          iv = 0
        }
        clearTimeout(tv1)
        clearTimeout(tv2)
        iframe.style.opacity = 1.0
        spinnerHolder.remove()
      }

      // event listeners
      addOneMessageListener('genius-iframe-waiting', function () {
        if (iv === 0) {
          return
        }
        ivf() // this is much faster than 1500ms
        clearInterval(iv)
        iv = 0
      })
      addOneMessageListener('htmlwritten', function () {
        if (iv > 0) {
          clearInterval(iv)
          iv = 0
        }
        spinnerUpdate('1', 'Calculating...', 302, 'htmlwritten')
      })
      addOneMessageListener('pageready', function (ev) {
        // 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 }, '*')
      })

      function reloadFrame () {
        // no use if the iframe is detached
        tv1 = 0
        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
        console.debug('tv2')
        clear() // unable to load
        spinnerUpdate(null, null, 902, 'failed')
        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)
          }, 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 = () => {
        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 (iframe.contentWindow && iframe.contentWindow.postMessage) {
          iframe.contentWindow.postMessage({ iAm: custom.scriptName, type: 'writehtml', html, themeKey: genius.option.themeKey }, '*')
        } 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)
    }
    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)
    })
  }

  function isScrollLyricsEnabled () {
    return autoScrollEnabled && ('scrollLyrics' in theme)
  }

  function scrollLyrics (positionFraction) {
    if (isScrollLyricsEnabled() === 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) {
    geniusSearch(query, function geniusSearchCb (r) {
      const hits = r.response.sections[0].hits
      if (hits.length === 0) {
        window.alert(custom.scriptName + '\n\nNo search results')
      } else {
        custom.listSongs(hits, container, query)
      }
    }, function geniusSearchErrorCb () {
      // do nothing
    })
  }

  function config () {
    loadCache()

    // Blur background
    for (const e of document.querySelectorAll('body > *')) {
      e.style.filter = 'blur(4px)'
    }
    const lyricscontainer = document.getElementById('lyricscontainer')
    if (lyricscontainer) {
      lyricscontainer.style.filter = 'blur(1px)'
    }

    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 () {
      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 () {
      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(() => custom.addLyrics(true))
      }
    }
    selectTheme.addEventListener('change', onSelectTheme)

    // 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 () {
      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 () {
      if (checkAutoScrollEnabled.checked !== autoScrollEnabled) {
        autoScrollEnabled = checkAutoScrollEnabled.checked === true
        custom.addLyrics(true)
        custom.GM.setValue('autoscrollenabled', autoScrollEnabled)
      }
    }
    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')))
      }
    }

    // Buttons
    div = win.appendChild(document.createElement('div'))

    const closeButton = div.appendChild(document.createElement('button'))
    closeButton.textContent = 'Close'
    closeButton.addEventListener('click', function onCloseButtonClick () {
      win.remove()
      // Un-blur background
      for (const e of document.querySelectorAll('body > *, #lyricscontainer')) {
        e.style.filter = ''
      }
    })

    // console.dir(selectionCache)
    // console.dir(requestCache)

    const bytes = metricPrefix(JSON.stringify(selectionCache).length + JSON.stringify(requestCache).length, 2, 1024) + 'Bytes'
    const clearCacheButton = div.appendChild(document.createElement('button'))
    clearCacheButton.textContent = `Clear cache (${bytes})`
    clearCacheButton.addEventListener('click', function onClearCacheButtonClick () {
      Promise.all([custom.GM.setValue('selectioncache', '{}'), custom.GM.setValue('requestcache', '{}')]).then(function () {
        clearCacheButton.innerHTML = 'Cleared'
        selectionCache = cleanSelectionCache()
        requestCache = {}
      })
    })

    const debugButton = div.appendChild(document.createElement('button'))
    debugButton.title = 'Do not enable this.'
    debugButton.style.float = 'right'
    const updateDebugButton = function () {
      if (genius.debug) {
        debugButton.innerHTML = 'Debug is on'
        debugButton.style.opacity = '1.0'
      } else {
        debugButton.innerHTML = 'Debug is off'
        debugButton.style.opacity = '0.2'
      }
    }
    updateDebugButton()
    debugButton.addEventListener('click', function onDebugButtonClick () {
      genius.debug = !genius.debug
      custom.GM.setValue('debug', genius.debug).then(function () {
        updateDebugButton()
      })
    })

    // Footer
    div = win.appendChild(document.createElement('div'))
    div.innerHTML = `<p style="font-size:15px;">
      Powered by <a style="font-size:15px;" target="_blank" href="https://github.com/cvzi/genius-lyrics-userscript/">GeniusLyrics.js</a>, Copyright © 2019 <a style="font-size:15px;" href="mailto:[email protected]">cuzi</a>.
      <br>Licensed under the GNU General Public License v3.0</p>`
  }

  function addOneMessageListener (type, cb) {
    let arr = onMessage[type]
    if (!arr) {
      arr = onMessage[type] = []
    }
    arr.push(cb)
  }

  function listenToMessages () {
    window.addEventListener('message', function (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 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 (e) {
      if (typeof e.data === 'object' && 'iAm' in e.data && e.data.iAm === custom.scriptName && e.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')).innerHTML = `
    #myconfigwin39457845 {
      position:absolute;
      top:120px;
      right:10px;
      padding:15px;
      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:0.2em;
    }
    #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 button {
      color:black;
      background:default;
    }
    #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)
      }
    }`

    if ('addCss' in custom) {
      custom.addCss()
    }
  }

  async function mainRunner () {
    // get values from GM
    const values = await Promise.all([
      custom.GM.getValue('debug', genius.debug),
      custom.GM.getValue('theme', genius.option.themeKey),
      custom.GM.getValue('annotationsenabled', annotationsEnabled),
      custom.GM.getValue('autoscrollenabled', autoScrollEnabled)
    ])

    // set up variables
    genius.debug = !!values[0]
    if (Object.prototype.hasOwnProperty.call(themes, values[1])) {
      genius.option.themeKey = values[1]
    } else {
      genius.option.themeKey = Reflect.ownKeys(themes)[0]
      custom.GM.setValue('theme', genius.option.themeKey)
      console.error(`Invalid value for theme key: custom.GM.getValue("theme") = '${values[1]}', using default theme key: '${genius.option.themeKey}'`)
    }
    theme = themes[genius.option.themeKey]
    annotationsEnabled = !!values[2]
    autoScrollEnabled = !!values[3]

    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 (e) {
        if ((((e || 0).data || 0).iAm) === custom.scriptName && e.data.type === 'writehtml') {
          window.removeEventListener('message', msgFn, false)
          msgFn = null
          const { data, source } = e
          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 ('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}`)
    }

    document.documentElement.innerHTML = e.data.html
    const communicationWindow = e.source
    communicationWindow.postMessage({ iAm: custom.scriptName, type: 'htmlwritten' }, '*')

    // clean up
    e = null

    // delay 500ms
    await new Promise(resolve => setTimeout(resolve, 500))

    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
    if ('scrollLyrics' in theme) {
      window.addEventListener('message', function (e) {
        if (typeof e.data !== 'object' || !('iAm' in e.data) || e.data.iAm !== custom.scriptName || e.data.type !== 'scrollLyrics' || !('scrollLyrics' in theme)) {
          return
        }
        theme.scrollLyrics(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
    communicationWindow.postMessage({ iAm: custom.scriptName, type: 'pageready'/* , html: document.documentElement.innerHTML */ }, '*')
    if ('iframeLoadedCallback2' in custom) {
      // after all onload functions
      custom.iframeLoadedCallback2({ document, theme, onload })
    }
  }

  mainRunner()

  return genius
}