Greasy Fork

Greasy Fork is available in English.

Bandcamp script (Deluxe Edition)

A discography player for bandcamp.com, manager of your played albums and various other improvements and tools

当前为 2020-10-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name             Bandcamp script (Deluxe Edition)
// @description      A discography player for bandcamp.com, manager of your played albums and various other improvements and tools
// @namespace        https://openuserjs.org/users/cuzi
// @author           cuzi
// @copyright        2019, cuzi (https://openuserjs.org/users/cuzi)
// @supportURL       https://github.com/cvzi/Bandcamp-script-deluxe-edition/issues
// @contributionURL  https://buymeacoff.ee/cuzi
// @contributionURL  https://ko-fi.com/cuzicvzi
// @icon             https://raw.githubusercontent.com/cvzi/Bandcamp-script-deluxe-edition/master/images/icon.png
// @license          MIT
// @version          1.13
// @require          https://unpkg.com/[email protected]/dist/index.min.js
// @require          https://openuserjs.org/src/libs/cuzi/GeniusLyrics.js
// @run-at           document-start
// @grant            GM.xmlHttpRequest
// @grant            GM.setValue
// @grant            GM.getValue
// @grant            GM.notification
// @grant            GM.download
// @grant            GM.registerMenuCommand
// @grant            GM.addStyle
// @grant            unsafeWindow
// @connect          bandcamp.com
// @connect          *.bandcamp.com
// @connect          bcbits.com
// @connect          *.bcbits.com
// @connect          genius.com
// @connect          *
// @include          https://*
// ==/UserScript==

// ==OpenUserJS==
// @author           cuzi
// ==/OpenUserJS==

/* globals geniusLyrics, JSON5, GM, unsafeWindow, MediaMetadata, MouseEvent, Response */

// TODO Mark as played automatically when played

const BACKUP_REMINDER_DAYS = 35
const TRALBUM_CACHE_HOURS = 2
const CHROME = navigator.userAgent.indexOf('Chrome') !== -1
const CAMPEXPLORER = document.location.hostname === 'campexplorer.io'
const BANDCAMPDOMAIN = document.location.hostname === 'bandcamp.com' || document.location.hostname.endsWith('.bandcamp.com')
var BANDCAMP = BANDCAMPDOMAIN
const NOEMOJI = CHROME && navigator.userAgent.match(/Windows (NT)? [4-9]/i)
const DEFAULTSKIPTIME = 10 /* Seek time to skip in seconds by default */
const SCRIPT_NAME = 'Bandcamp script (Deluxe Edition)'
var darkModeInjected = false

const allFeatures = {
  discographyplayer: {
    name: 'Enable player on discography page',
    default: true
  },
  albumPageVolumeBar: {
    name: 'Enable volume slider/shuffle/repeat on album page',
    default: true
  },
  albumPageAutoRepeatAll: {
    name: 'Always "repeat all" on album page',
    default: false
  },
  albumPageLyrics: {
    name: 'Show lyrics from genius.com on album page',
    default: true
  },
  markasplayed: {
    name: 'Show "mark as played" link on discography player',
    default: true
  },
  markasplayedEverywhere: {
    name: 'Show "mark as played" link everywhere',
    default: true
  },
  /* markasplayedAuto: {
    name: '(NOT YET IMPLEMENTED) Automatically "mark as played" once a song was played for',
    default: false
  }, */
  thetimehascome: {
    name: 'Circumvent "The time has come to open thy wallet" limit',
    default: true
  },
  albumPageDownloadLinks: {
    name: 'Show download links on album page',
    default: true
  },
  discographyplayerDownloadLink: {
    name: 'Show download link on discography player',
    default: true
  },
  discographyplayerSidebar: {
    name: 'Show discography player as a sidebar on the right',
    default: false
  },
  discographyplayerPersist: {
    name: 'Recover discography player on next page',
    default: true
  },
  backupReminder: {
    name: 'Remind me to backup my played albums every month',
    default: true
  },
  nextSongNotifications: {
    name: 'Show a notification when a new song starts',
    default: false
  },
  releaseReminder: {
    name: 'Show new releases that I have saved',
    default: true
  },
  darkMode: {
    name: (CHROME ? '🅳🅐🆁🅺🅼🅞🅳🅴' : '🅳🅰🆁🅺🅼🅾🅳🅴') + ' - enable <a href="https://userstyles.org/styles/171538/bandcamp-in-dark">dark theme by Simonus</a>',
    default: false
  }
}

const moreSettings = {
  darkMode: {
    true: async function populateDarkModeSettings (container) {
      let darkModeValue = await GM.getValue('darkmode', '1')
      const onChange = async function () {
        const input = this
        window.setTimeout(() => parentQuery(input, 'fieldset').classList.add('breathe'), 0)
        document.getElementById('bcsde_mode_auto_status').innerHTML = ''
        document.getElementById('bcsde_mode_const_time_from').classList.remove('errorblink')
        document.getElementById('bcsde_mode_const_time_to').classList.remove('errorblink')
        if (document.getElementById('bcsde_mode_always').checked) {
          darkModeValue = '1'
        } else if (document.getElementById('bcsde_mode_const_time').checked) {
          let from = document.getElementById('bcsde_mode_const_time_from').value
          let to = document.getElementById('bcsde_mode_const_time_to').value
          const mFrom = from.match(/([0-2]?\d:[0-5]\d)/)
          const mTo = to.match(/([0-2]?\d:[0-5]\d)/)
          if (mFrom && mTo) {
            from = mFrom[1]
            to = mTo[1]
            document.getElementById('bcsde_mode_const_time_from').value = from
            document.getElementById('bcsde_mode_const_time_to').value = to
            darkModeValue = `2#${from}->${to}`
          } else {
            if (!mFrom) {
              document.getElementById('bcsde_mode_const_time_from').classList.add('errorblink')
            }
            if (!mTo) {
              document.getElementById('bcsde_mode_const_time_to').classList.add('errorblink')
            }
          }
        } else if (document.getElementById('bcsde_mode_auto').checked) {
          let myPosition = null
          let sunData = null
          try {
            myPosition = await getGPSLocation()
            sunData = suntimes(new Date(), myPosition.latitude, myPosition.longitude)
          } catch (e) {
            document.getElementById('bcsde_mode_auto_status').innerHTML = 'Error:\n' + e
          }
          if (myPosition && sunData) {
            const data = Object.assign(myPosition, sunData)
            darkModeValue = '3#' + JSON.stringify(data)
            document.getElementById('bcsde_mode_auto_status').innerHTML = `Source:   ${data.source}
Location: ${data.latitude}, ${data.longitude}
Sunrise:  ${data.sunrise.toLocaleTimeString()}
Sunset:   ${data.sunset.toLocaleTimeString()}`
          }
        }
        await GM.setValue('darkmode', darkModeValue)
        window.setTimeout(() => parentQuery(input, 'fieldset').classList.remove('breathe'), 50)
      }

      const radioAlways = container.appendChild(document.createElement('input'))
      radioAlways.setAttribute('type', 'radio')
      radioAlways.setAttribute('name', 'mode')
      radioAlways.setAttribute('value', 'always')
      radioAlways.setAttribute('id', 'bcsde_mode_always')
      radioAlways.checked = darkModeValue.startsWith('1')
      radioAlways.addEventListener('change', onChange)
      const labelAlways = container.appendChild(document.createElement('label'))
      labelAlways.setAttribute('for', 'bcsde_mode_always')
      labelAlways.appendChild(document.createTextNode('Always'))

      container.appendChild(document.createElement('br'))

      const radioConstTime = container.appendChild(document.createElement('input'))
      radioConstTime.setAttribute('type', 'radio')
      radioConstTime.setAttribute('name', 'mode')
      radioConstTime.setAttribute('value', 'const_time')
      radioConstTime.setAttribute('id', 'bcsde_mode_const_time')
      radioConstTime.checked = darkModeValue.startsWith('2')
      radioConstTime.addEventListener('change', onChange)

      let [from, to] = ['22:00', '06:00']
      if (darkModeValue.startsWith('2')) {
        [from, to] = darkModeValue.substring(2).split('->')
      }
      const labelConstTime = container.appendChild(document.createElement('label'))
      labelConstTime.setAttribute('for', 'bcsde_mode_const_time')
      labelConstTime.appendChild(document.createTextNode('Time'))
      const labelConstTimeFrom = container.appendChild(document.createElement('label'))
      labelConstTimeFrom.setAttribute('for', 'bcsde_mode_const_time_from')
      labelConstTimeFrom.appendChild(document.createTextNode(' from '))
      const inputConstTimeFrom = container.appendChild(document.createElement('input'))
      inputConstTimeFrom.setAttribute('type', 'text')
      inputConstTimeFrom.setAttribute('value', from)
      inputConstTimeFrom.setAttribute('id', 'bcsde_mode_const_time_from')
      inputConstTimeFrom.addEventListener('change', onChange)
      const labelConstTimeTo = container.appendChild(document.createElement('label'))
      labelConstTimeTo.setAttribute('for', 'bcsde_mode_const_time_to')
      labelConstTimeTo.appendChild(document.createTextNode(' to '))
      const inputConstTimeTo = container.appendChild(document.createElement('input'))
      inputConstTimeTo.setAttribute('type', 'text')
      inputConstTimeTo.setAttribute('value', to)
      inputConstTimeTo.setAttribute('id', 'bcsde_mode_const_time_to')
      inputConstTimeTo.addEventListener('change', onChange)

      container.appendChild(document.createElement('br'))

      const radioAuto = container.appendChild(document.createElement('input'))
      radioAuto.setAttribute('type', 'radio')
      radioAuto.setAttribute('name', 'mode')
      radioAuto.setAttribute('value', 'auto')
      radioAuto.setAttribute('id', 'bcsde_mode_auto')
      radioAuto.checked = darkModeValue.startsWith('3')
      radioAuto.addEventListener('change', onChange)
      const labelAuto = container.appendChild(document.createElement('label'))
      labelAuto.setAttribute('for', 'bcsde_mode_auto')
      labelAuto.appendChild(document.createTextNode('Auto (sunset till sunrise)'))
      const preAutoStatus = container.appendChild(document.createElement('pre'))
      preAutoStatus.setAttribute('id', 'bcsde_mode_auto_status')
      preAutoStatus.setAttribute('style', 'font-family:monospace')

      return 'Dark theme details'
    }
  },
  discographyplayerSidebar: {
    true: function checkScreenSize (container) {
      if (!window.matchMedia('(min-width: 1600px)').matches) {
        const span = container.appendChild(document.createElement('span'))
        span.appendChild(document.createTextNode('Your screen/browser window is not wide enough for this option. Width of at least 1600px required'))
        container.style.opacity = 1
      } else {
        container.style.opacity = 0
      }
      return fullfill()
    },
    false: function removeContainerAboutScreenSize (container) {
      container.style.opacity = 0
      return fullfill()
    }
  }

}

var player, audio, currentDuration, timeline, playhead, bufferbar
var onPlayHead = false

const spriteRepeatShuffle = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACUAAABgCAMAAACt1UvuAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAA2UExURQAAAP////39/Tw8PP///////w4ODv////7+/v7+/k5OTktLS35+fiAgIJSUlAAAABAQECoqKpxAnVsAAAAPdFJOUwAxQ05UJGkKBRchgWiOOufd5UcAAAKrSURBVEjH7ZfrkqQgDIUbFLmphPd/2T2EgNqNzlTt7o+p3dR0d5V+JOGEYzkvZ63nsNY6517XCPIrjIDvXF7qL24ao5QynesIllDKE1MpJdom1UDBQIQlE+HmEipVIk+6cqVqQYivlq/loBJFDa6WnaitbbnMtFHnOF1niDJJX14pPa+cOm0l3Vohyuus8xpkj9ih1nPke6iaO6KV323XqwhRON4tQ3GedakNYYQqslaO+yv9xs64Lh2rX8sWeSISzVWTk8ROJmmU9MTl1PvEnHBmzXRSzvhhuqJAzjlJY9eJCVWljKwcESbL+fbTYK0NWx0IGodyvKCACqp6VqMNlguhktbxMqHdI5k7ps1SsiTxPO0YDgojkZPIysl+617cy8rUkIfPflMY4IaKLZfHhSoPn782iQJC5tIX2nfNQseGG4eoe3T1+kXh7j1j/H6W9TbC65ZxR2S0frKePUWYlhbY/hTkvL6aiKPApCRTeoxNTvUTI16r1DqPAqrGVR0UT/ojwGByJ6qO8S32HQ6wJ8r4TwFdyGnx7kzVM8l/nZpwRwkm1GAKC+5oKflMzY3aUm4rBpSsd17pVv2Bsn739ivqFWK2bhD2TE0wwTKM3Knu2puo1PJ8blqu7TEXVY1wgvGQwYN6HKJR0WGjYqxheN/lCpOzd/GlHX+gHyEe/SE/qpyV+sKPfqdEhzVv/OjwwC3zlefnnR+9YW+5Zz86fzjw3o+f1NCP9oMa+fGeOvnR2brH/378B/xI9A0/UjUjSfyOH2GzCDOuKavyUUM/eryMFjNOIMrHD/1o4di0GlCkp8IP/RjwglRSCKX9yI845VGXqwc18KOtWq3mSr35EQVnHbnzC3X144I3d7Wj6xuq+hH7gwz4PvY48GP9p8i2Vzus/dt+pB/nx18MUmsLM2EHrwAAAABJRU5ErkJggg==")'

function humanDuration (duration) {
  let hours = parseInt(duration / 3600)
  if (!hours) {
    hours = ''
  } else {
    hours += ':'
  }
  duration %= 3600
  let minutes = parseInt(duration / 60)
  minutes = (minutes < 10 ? '0' : '') + minutes
  duration %= 60
  let seconds = parseInt(duration)
  if (duration - seconds >= 0.5) {
    seconds++
  }
  seconds = (seconds < 10 ? '0' : '') + seconds
  return `${hours}${minutes}:${seconds}`
}

function addLogVolume (mediaElement) {
  if (!Object.hasOwnProperty.call(mediaElement, 'logVolume')) {
    Object.defineProperty(mediaElement, 'logVolume', {
      get () {
        return Math.log((Math.E - 1) * this.volume + 1)
      },
      set (percentage) {
        this.volume = (Math.exp(percentage) - 1) / (Math.E - 1)
      }
    })
  }
}

function randomIndex (max) {
  // Random int from interval [0,max)
  return Math.floor(Math.random() * Math.floor(max))
}

function padd (n, width, filler) {
  let s
  for (s = n.toString(); s.length < width; s = filler + s) {}
  return s
}

function metricPrefix (n, decimals, k) {
  // From 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 fixFilename (s) {
  const forbidden = '*"/\\[]:|,<>?\n\t\0'.split('')
  forbidden.forEach(function (char) {
    s = s.replace(char, '')
  })
  return s
}

function fullfill (x) {
  return new Promise(resolve => resolve(x))
}

const stylesToInsert = []
function addStyle (css) {
  if (GM.addStyle && css) {
    return GM.addStyle(css)
  } else {
    if (css) {
      stylesToInsert.push(css)
    }
    const head = (document.head ? document.head : document.documentElement)
    if (head) {
      let style = document.createElement('style')
      if (style) {
        while (stylesToInsert.length) {
          head.append(style)
          style.type = 'text/css'
          style.appendChild(document.createTextNode(stylesToInsert.shift()))
          style = document.createElement('style')
        }
        return fullfill(style)
      }
    }
    // document was not ready, wait
    return new Promise((resolve) => window.setTimeout(() => addStyle(false).then(resolve), 100))
  }
}

function css2rgb (colorStr) {
  const div = document.body.appendChild(document.createElement('div'))
  div.style.color = colorStr
  const m = window.getComputedStyle(div).color.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i)
  div.remove()
  if (m) {
    m.shift()
    return m
  }
  return null
}

function base64encode (s) {
  // from https://gist.github.com/stubbetje/229984
  const base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('')
  const l = s.length
  let o = ''
  for (let i = 0; i < l; i++) {
    const byte0 = s.charCodeAt(i++) & 0xff
    const byte1 = s.charCodeAt(i++) & 0xff
    const byte2 = s.charCodeAt(i) & 0xff
    o += base64[byte0 >> 2]
    o += base64[((byte0 & 0x3) << 4) | (byte1 >> 4)]
    const t = i - l
    if (t >= 0) {
      if (t === 0) {
        o += base64[((byte1 & 0x0f) << 2) | (byte2 >> 6)]
        o += base64[64]
      } else {
        o += base64[64]
        o += base64[64]
      }
    } else {
      o += base64[((byte1 & 0x0f) << 2) | (byte2 >> 6)]
      o += base64[byte2 & 0x3f]
    }
  }
  return o
}

function decodeHTMLentities (input) {
  return new window.DOMParser().parseFromString(input, 'text/html').documentElement.textContent
}

function timeSince (date) {
  // From https://stackoverflow.com/a/3177838/10367381
  const seconds = Math.floor((new Date() - date) / 1000)
  let interval = Math.floor(seconds / 31536000)
  if (interval > 1) {
    return interval + ' years'
  }
  interval = Math.floor(seconds / 2592000)
  if (interval > 1) {
    return interval + ' months'
  }
  interval = Math.floor(seconds / 86400)
  if (interval > 1) {
    return interval + ' days'
  }
  interval = Math.floor(seconds / 3600)
  if (interval > 1) {
    return interval + ' hours'
  }
  interval = Math.floor(seconds / 60)
  if (interval > 1) {
    return interval + ' minutes'
  }
  return Math.floor(seconds) + ' seconds'
}

function nowInTimeRange (range) {
  // Format: range = 'hh:mm->hh:mm'
  const m = range.match(/(\d{1,2}):(\d{1,2})->(\d{1,2}):(\d{1,2})/)
  const [fromHours, fromMinutes, toHours, toMinutes] = [parseInt(m[1]), parseInt(m[2]), parseInt(m[3]), parseInt(m[4])]
  const now = new Date()
  const from = new Date()
  from.setHours(fromHours)
  from.setMinutes(fromMinutes)
  const to = new Date()
  to.setHours(toHours)
  to.setMinutes(toMinutes)
  if (to - from < 0) {
    to.setDate(to.getDate() + 1)
  }
  return now > from && now < to
}

function nowInBetween (from, to) {
  const [fromHours, fromMinutes, toHours, toMinutes] = [from.getHours(), from.getMinutes(), to.getHours(), to.getMinutes()]
  const now = new Date()
  from = new Date()
  from.setHours(fromHours)
  from.setMinutes(fromMinutes)
  to = new Date()
  to.setHours(toHours)
  to.setMinutes(toMinutes)
  if (to - from < 0) {
    from.setDate(from.getDate() - 1)
  }
  return now > from && now < to
}

function loadCrossSiteImage (url) {
  return new Promise(function downloadCrossSiteImage (resolve, reject) {
    var canvas = document.createElement('canvas')
    var ctx = canvas.getContext('2d')

    var img0 = document.createElement('img') // Load the image in a <img> to get the dimensions
    img0.addEventListener('load', function onImgLoad () {
      if (img0.height === 0 || img0.width === 0) {
        reject(new Error('loadCrossSiteImage("$url") Error: Could not load image in <img>'))
        return
      }
      canvas.height = img0.height
      canvas.width = img0.width
      // Download image data
      GM.xmlHttpRequest({
        method: 'GET',
        overrideMimeType: 'text/plain; charset=x-user-defined',
        url: url,
        onload: function (resp) {
          // Create a data url image
          var dataurl = 'data:image/jpeg;base64,' + base64encode(resp.responseText)
          var img1 = document.createElement('img')
          img1.addEventListener('load', function () {
            // Load data url image into canvas
            ctx.drawImage(img1, 0, 0)
            resolve(canvas)
          })
          img1.src = dataurl
        },
        onerror: function (response) {
          console.log('loadCrossSiteImage("' + url + '") Error: ' + response.status + '\n' + ('error' in response ? response.error : ''))
          reject(new Error('error' in response ? response.error : 'loadCrossSiteImage failed'))
        }
      })
    })
    img0.src = url
  })
}

function removeViaQuerySelector (parent, selector) {
  if (typeof selector === 'undefined') {
    selector = parent
    parent = document
  }
  for (let el = parent.querySelector(selector); el; el = parent.querySelector(selector)) {
    el.remove()
  }
}

function firstChildWithText (parent) {
  for (let i = 0; i < parent.childNodes.length; i++) {
    const node = parent.childNodes[i]
    if (node.nodeType === window.Node.TEXT_NODE && node.nodeValue.trim()) {
      return node
    } else if (node.childNodes.length) {
      const r = firstChildWithText(node)
      if (r) {
        return r
      }
    }
  }
  return false
}

function parentQuery (node, q) {
  const parents = [node.parentElement]
  node = node.parentElement.parentElement
  while (node) {
    const lst = node.querySelectorAll(q)
    for (let i = 0; i < lst.length; i++) {
      if (parents.indexOf(lst[i]) !== -1) {
        return lst[i]
      }
    }
    parents.push(node)
    node = node.parentElement
  }
  return null
}

function suntimes (date, lat, lng) {
  // According to "Predicting Sunrise and Sunset Times" by Donald A. Teets:
  // https://www.maa.org/sites/default/files/teets09010341463.pdf
  lat = lat * Math.PI / 180.0
  const dayOfYear = Math.round((date - new Date(date).setMonth(0, 0)) / 86400000)
  const sunDist = 149598000.0
  const radius = 6378.0
  const epsilon = 0.409
  const thetha = 2 * Math.PI / 365.25 * (dayOfYear - 80)
  const n = 720 - 10 * Math.sin(2 * thetha) + 8 * Math.sin(2 * Math.PI / 365.25 * dayOfYear)
  const z = sunDist * Math.sin(thetha) * Math.sin(epsilon)
  const rp = Math.sqrt(sunDist * sunDist - z * z)
  const t0 = 1440 / (2 * Math.PI) * Math.acos((radius - z * Math.sin(lat)) / (rp * Math.cos(lat)))
  const sunriseMin = n - t0 - 5 - (4.0 * lng % 15.0) - date.getTimezoneOffset()
  const sunsetMin = sunriseMin + 2 * t0
  const sunrise = new Date(date)
  sunrise.setHours(sunriseMin / 60, Math.round(sunriseMin % 60))
  const sunset = new Date(date)
  sunset.setHours(sunsetMin / 60, Math.round(sunsetMin % 60))
  return { sunrise: sunrise, sunset: sunset }
}

function fromISO6709 (s) {
  // Format: s = '+-DDMM+-DDDMM'
  // Format: s = '+-DDMMSS+-DDDMMSS'
  function convert (iso, negative) {
    const mm = iso % 100
    const dd = iso / 100
    return (dd + mm / 60) * (negative ? -1 : 1)
  }

  const m = s.match(/([+-])(\d+)([+-])(\d+)/)
  const lat = convert(parseInt(m[2]), m[1] === '-')
  const lng = convert(parseInt(m[4]), m[3] === '-')

  return { latitude: lat, longitude: lng }
}

function getGPSLocation () {
  return new Promise(function downloadCrossSiteImage (resolve, reject) {
    navigator.geolocation.getCurrentPosition(function onSuccess (position) {
      resolve({
        source: `navigator.geolocation@${new Date(position.timestamp).toLocaleString()}`,
        latitude: position.coords.latitude,
        longitude: position.coords.longitude
      })
    }, function onError (err) {
      console.log('getGPSLocation Error:')
      console.log(err)
      const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
      console.log('getGPSLocation: Timezone: ' + tz)
      GM.xmlHttpRequest({
        url: 'https://raw.githubusercontent.com/iospirit/NSTimeZone-ISCLLocation/master/zone.tab',
        onload: function (response) {
          if (response.responseText.indexOf(tz) !== -1) {
            const line = response.responseText.split(tz)[0].split('\n').pop()
            const myPosition = fromISO6709(line)
            myPosition.source = 'Browser timezone ' + tz
            resolve(myPosition)
          } else if (response.status !== 200) {
            reject(new Error('Could not download time zone locations: http status=' + response.status))
          } else {
            reject(new Error('Unkown time zone location: ' + tz))
          }
        },
        onerror: function (response) {
          reject(new Error('Could not download time zone locations: ' + response.error))
        }
      })
    })
  })
}

const _dateOptions = { year: 'numeric', month: 'short', day: 'numeric' }
const _dateOptionsWithoutYear = { month: 'short', day: 'numeric' }
const _dateOptionsNumericWithoutYear = { year: '2-digit', month: '2-digit', day: '2-digit' }
function dateFormater (date) {
  if (date.getFullYear() === (new Date()).getFullYear()) {
    return date.toLocaleDateString(undefined, _dateOptionsWithoutYear)
  } else {
    return date.toLocaleDateString(undefined, _dateOptions)
  }
}
function dateFormaterRelease (date) {
  return date.toLocaleDateString(undefined, _dateOptionsWithoutYear) + ', ' + date.getFullYear()
}
function dateFormaterNumeric (date) {
  return date.toLocaleDateString(undefined, _dateOptionsNumericWithoutYear)
}

function getEnabledFeatures (enabledFeaturesValue) {
  for (const feature in allFeatures) {
    allFeatures[feature].enabled = allFeatures[feature].default
  }
  if (enabledFeaturesValue !== false) {
    const enabledFeatures = JSON.parse(enabledFeaturesValue)
    if (enabledFeatures.constructor === Object) {
      for (const feature in enabledFeatures) {
        if (feature in allFeatures) {
          allFeatures[feature].enabled = enabledFeatures[feature].enabled
        }
      }
    }
  }
  return allFeatures
}

function findUserProfileUrl () {
  if (document.querySelector('#collection-main a')) {
    return document.querySelector('#collection-main a').href
  }
  return 'https://bandcamp.com/login'
}

var ivRestoreVolume
function getStoredVolume (callbackIfVolumeExists) {
  GM.getValue('volume', '0.7').then(str => {
    return parseFloat(str)
  }).then(function storedVolumeLoaded (volume) {
    if (!Number.isNaN(volume) && volume > 0.0) {
      callbackIfVolumeExists(volume)
    }
  })
}
function restoreVolume () {
  getStoredVolume(function getStoredVolumeCallback (volume) {
    const restoreVolumeInterval = function restoreInterval () {
      const audios = document.querySelectorAll('audio,video')
      if (audios.length > 0) {
        let paused = true
        audios.forEach(function (media) {
          addLogVolume(media)
          paused = paused && media.paused
          media.logVolume = volume
        })
        if (!paused) {
          // Clear interval once audio is actually playing
          window.clearInterval(ivRestoreVolume)
        }
        // Update volume bar on tag player (by double clicking mute button)
        const muteWrapper = document.querySelector('.vol-icon-wrapper')
        if (muteWrapper) {
          const mouseDownEvent = new MouseEvent('mousedown', { view: unsafeWindow, bubbles: true, cancelable: true })
          muteWrapper.dispatchEvent(mouseDownEvent)
          muteWrapper.dispatchEvent(mouseDownEvent)
        }
      }
    }
    restoreVolumeInterval()
    ivRestoreVolume = window.setInterval(restoreVolumeInterval, 3000)
  })
  window.setTimeout(function clearRestoreInterval () {
    window.clearInterval(ivRestoreVolume)
  }, 10000)
}

function findPreviousAlbumCover (currentUrl) {
  const currentKey = albumKey(currentUrl)
  const as = document.querySelectorAll('.music-grid .music-grid-item a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]')
  let last = false
  let found = false
  for (let i = 0; i < as.length; i++) {
    if (last && albumKey(as[i].href) === currentKey) {
      found = last
      break
    }
    last = as[i]
  }
  if (found) {
    return playAlbumFromCover.apply(found, null)
  }
  return false
}
function findNextAlbumCover (currentUrl) {
  const currentKey = albumKey(currentUrl)
  const as = document.querySelectorAll('.music-grid .music-grid-item a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]')
  let isNext = false
  for (let i = 0; i < as.length; i++) {
    if (isNext) {
      playAlbumFromCover.apply(as[i], null)
      return true
    }
    if (albumKey(as[i].href) === currentKey) {
      isNext = true
    }
  }
  return false
}
function musicPlayerNextSong (next) {
  const current = player.querySelector('.playlist .playing')
  if (!next) {
    next = current.nextElementSibling
    while (next) {
      if ('file' in next.dataset) {
        break
      }
      next = next.nextElementSibling
    }
  }
  if (next) {
    current.classList.remove('playing')
    next.classList.add('playing')
    musicPlayerPlaySong(next)
  } else {
    // End of playlist reached
    if (findNextAlbumCover(current.dataset.albumUrl) === false) {
      const notloaded = player.querySelector('.playlist .playlistheading a.notloaded')
      if (notloaded) {
        // Unloaded albums in playlist
        const url = notloaded.href
        notloaded.remove()
        cachedTralbumData(url).then(function onCachedTralbumDataLoaded (TralbumData) {
          if (TralbumData) {
            addAlbumToPlaylist(TralbumData, 0)
          } else {
            playAlbumFromUrl(url)
          }
        })
      } else {
        audio.pause()
        audio.currentTime -= 1
        musicPlayerOnTimeUpdate()
        window.alert('End of playlist reached')
      }
    }
  }
}
var ivSlideInNextSong
function musicPlayerPlaySong (next, startTime) {
  currentDuration = next.dataset.duration
  player.querySelector('.durationDisplay .current').innerHTML = '-'
  player.querySelector('.durationDisplay .total').innerHTML = humanDuration(currentDuration)
  audio.src = next.dataset.file
  if (typeof startTime !== 'undefined' && startTime !== false) {
    audio.currentTime = startTime
  }
  bufferbar.classList.remove('bufferbaranimation')
  window.setTimeout(function bufferbaranimationWidth () {
    bufferbar.style.width = '0px'
    window.setTimeout(function bufferbaranimationClass () {
      bufferbar.classList.add('bufferbaranimation')
    }, 10)
  }, 0)

  const key = albumKey(next.dataset.albumUrl)

  // Meta
  const currentlyPlaying = document.querySelector('.currentlyPlaying')
  const nextInRow = player.querySelector('.nextInRow')
  nextInRow.querySelector('.cover').href = next.dataset.albumUrl
  nextInRow.querySelector('.cover img').src = next.dataset.albumCover
  nextInRow.querySelector('.info .link').href = next.dataset.albumUrl
  nextInRow.querySelector('.info .title').innerHTML = next.dataset.title
  nextInRow.querySelector('.info .artist').innerHTML = next.dataset.artist
  nextInRow.querySelector('.info .album').innerHTML = next.dataset.album

  // Favicon
  musicPlayerFavicon(next.dataset.albumCover.replace(/_\d.jpg$/, '_3.jpg'))

  // Wishlist
  const collectWishlist = player.querySelector('.collect-wishlist')
  collectWishlist.dataset.albumUrl = next.dataset.albumUrl
  player.querySelectorAll('.collect-wishlist>*').forEach(function (e) { e.style.display = 'none' })
  if (next.dataset.isPurchased === 'true') {
    player.querySelector('.collect-wishlist .wishlist-own').style.display = 'inline-block'
    collectWishlist.dataset.wishlist = 'own'
  } else if (next.dataset.inWishlist === 'true') {
    player.querySelector('.collect-wishlist .wishlist-collected').style.display = 'inline-block'
    collectWishlist.dataset.wishlist = 'collected'
  } else {
    player.querySelector('.collect-wishlist .wishlist-add').style.display = 'inline-block'
    collectWishlist.dataset.wishlist = 'add'
  }

  // Played/Listened
  const collectListened = player.querySelector('.collect-listened')
  if (allFeatures.markasplayed.enabled && collectListened) {
    collectListened.dataset.albumUrl = next.dataset.albumUrl
    player.querySelectorAll('.collect-listened>*').forEach(function (e) { e.style.display = 'none' })
    GM.getValue('myalbums', '{}').then(function myalbumsLoaded (str) {
      const myalbums = JSON.parse(str)
      if (key in myalbums && 'listened' in myalbums[key] && myalbums[key].listened) {
        player.querySelector('.collect-listened .listened').style.display = 'inline-block'
        const date = new Date(myalbums[key].listened)
        const since = timeSince(date)
        player.querySelector('.collect-listened .listened').title = since + ' ago\nClick to mark as NOT played'
        collectListened.dataset.listened = myalbums[key].listened
      } else {
        player.querySelector('.collect-listened .mark-listened').style.display = 'inline-block'
        collectListened.dataset.listened = false
      }
    })
  } else if (collectListened) {
    collectListened.remove()
  }

  // Notification
  if (allFeatures.nextSongNotifications.enabled && 'notification' in GM) {
    GM.notification({
      title: document.location.host,
      text: next.dataset.title + '\nby ' + next.dataset.artist + '\nfrom ' + next.dataset.album,
      image: next.dataset.albumCover,
      highlight: false,
      silent: true,
      timeout: 3000,
      onclick: musicPlayerNext
    })
  }

  // Media hub
  if ('mediaSession' in navigator) {
    navigator.mediaSession.metadata = new MediaMetadata({
      title: next.dataset.title,
      artist: next.dataset.artist,
      album: next.dataset.album,
      artwork: [{
        src: next.dataset.albumCover,
        sizes: '350x350',
        type: 'image/jpeg'
      }]
    })
    navigator.mediaSession.setActionHandler('previoustrack', musicPlayerPrev)
    navigator.mediaSession.setActionHandler('nexttrack', musicPlayerNext)

    navigator.mediaSession.setActionHandler('play', _ => audio.play())
    navigator.mediaSession.setActionHandler('pause', _ => audio.pause())

    navigator.mediaSession.setActionHandler('seekbackward', function (event) {
      const skipTime = event.seekOffset || DEFAULTSKIPTIME
      audio.currentTime = Math.max(audio.currentTime - skipTime, 0)
      musicPlayerUpdatePositionState()
    })
    navigator.mediaSession.setActionHandler('seekforward', function (event) {
      const skipTime = event.seekOffset || DEFAULTSKIPTIME
      audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration || currentDuration)
      musicPlayerUpdatePositionState()
    })

    try {
      navigator.mediaSession.setActionHandler('stop', _ => musicPlayerClose())
    } catch (error) {
      console.log('Warning! The "stop" media session action is not supported.')
    }

    try {
      navigator.mediaSession.setActionHandler('seekto', function (event) {
        if (event.fastSeek && ('fastSeek' in audio)) {
          audio.fastSeek(event.seekTime)
          return
        }
        audio.currentTime = event.seekTime
        musicPlayerUpdatePositionState()
      })
    } catch (error) {
      console.log('Warning! The "seekto" media session action is not supported.')
    }
  }

  // Download link
  const downloadLink = player.querySelector('.downloadlink')
  if (allFeatures.discographyplayerDownloadLink.enabled) {
    downloadLink.href = next.dataset.file
    downloadLink.download = (next.dataset.trackNumber > 9 ? '' : '0') + next.dataset.trackNumber + '. ' + fixFilename(next.dataset.artist + ' - ' + next.dataset.title) + '.mp3'
    downloadLink.style.display = 'block'
  } else {
    downloadLink.style.display = 'none'
  }

  // Show "playing" indication on album covers
  const coverLinkPattern = albumPath(next.dataset.albumUrl)
  document.querySelectorAll('img.albumIsCurrentlyPlaying').forEach(img => img.classList.remove('albumIsCurrentlyPlaying'))
  document.querySelectorAll('.albumIsCurrentlyPlayingIndicator').forEach(div => div.remove())
  document.querySelectorAll('a[href*="' + coverLinkPattern + '"] img').forEach(function (img) {
    let node = img
    while (node) {
      if (node.id === 'discographyplayer') {
        return
      }
      if (node === document.body) {
        break
      }
      node = node.parentNode
    }
    img.classList.add('albumIsCurrentlyPlaying')
    if (!img.parentNode.querySelector('.albumIsCurrentlyPlayingIndicator')) {
      const indicator = img.parentNode.appendChild(document.createElement('div'))
      indicator.classList.add('albumIsCurrentlyPlayingIndicator')
      indicator.addEventListener('click', function (ev) {
        ev.preventDefault()
        musicPlayerPlay()
      })
      indicator.appendChild(document.createElement('div')).classList.add('currentlyPlayingBg')
      indicator.appendChild(document.createElement('div')).classList.add('currentlyPlayingIcon')
    }
  })

  // Animate
  if (allFeatures.discographyplayerSidebar.enabled && window.matchMedia('(min-width: 1600px)').matches) {
    // Slide up
    currentlyPlaying.style.marginTop = -parseInt(currentlyPlaying.clientHeight + 1) + 'px'
    nextInRow.style.height = '99%'
    nextInRow.style.width = '99%'
    clearTimeout(ivSlideInNextSong)
    ivSlideInNextSong = window.setTimeout(function slideInSongInterval () {
      currentlyPlaying.remove()
      const clone = nextInRow.cloneNode(true)
      clone.style.height = '0%'
      clone.className = 'nextInRow'
      nextInRow.className = 'currentlyPlaying'
      nextInRow.parentNode.appendChild(clone)
    }, 600)
  } else {
    // Slide to the left
    currentlyPlaying.style.marginLeft = -parseInt(currentlyPlaying.clientWidth + 1) + 'px'
    nextInRow.style.height = '99%'
    nextInRow.style.width = '99%'

    clearTimeout(ivSlideInNextSong)

    ivSlideInNextSong = window.setTimeout(function slideInSongInterval () {
      currentlyPlaying.remove()
      const clone = nextInRow.cloneNode(true)
      clone.style.width = '0%'
      clone.className = 'nextInRow'
      nextInRow.className = 'currentlyPlaying'
      nextInRow.parentNode.appendChild(clone)
    }, 7 * 1000)
  }

  window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({ block: 'nearest' }), 200)
}

function musicPlayerPlay () {
  if (audio.paused) {
    audio.play().then(_ => musicPlayerUpdatePositionState())
    musicPlayerCookieChannelSendStop()
  } else {
    audio.pause()
  }
}
function musicPlayerStop () {
  if (!audio.paused) {
    audio.pause()
  }
}
function musicPlayerPrev () {
  musicPlayerShowBusy()
  const current = player.querySelector('.playlist .playing')
  let prev = current.previousElementSibling
  while (prev) {
    if ('file' in prev.dataset) {
      break
    }
    prev = prev.previousElementSibling
  }
  if (prev) {
    musicPlayerNextSong(prev)
  }
}
function musicPlayerNext () {
  musicPlayerShowBusy()
  musicPlayerNextSong()
}
function musicPlayerPrevAlbum () {
  audio.pause()
  window.setTimeout(function musicPlayerPrevAlbumTimeout () {
    musicPlayerShowBusy()
    const url = player.querySelector('.playlist .playing').dataset.albumUrl
    if (!findPreviousAlbumCover(url)) {
      // Find previous album in playlist
      let prev = false
      const as = player.querySelectorAll('.playlist .playlistheading a')
      for (let i = 0; i < as.length; i++) {
        if (albumKey(as[i].href) === albumKey(url)) {
          if (i > 0) {
            prev = as[i - 1]
          }
          break
        }
      }
      if (prev) {
        prev.parentNode.click()
      } else {
        // Just play first song in playlist
        player.querySelector('.playlist .playlistentry').click()
      }
    }
  }, 10)
}
function musicPlayerNextAlbum () {
  audio.pause()
  window.setTimeout(function musicPlayerNextAlbumTimeout () {
    musicPlayerShowBusy()
    const r = findNextAlbumCover(player.querySelector('.playlist .playing').dataset.albumUrl)
    if (r === false) {
      // Find next album in playlist
      let reachedPlaying = false
      let found = false
      const lis = player.querySelectorAll('.playlist li')
      for (let i = 0; i < lis.length; i++) {
        if (reachedPlaying && lis[i].classList.contains('playlistheading')) {
          lis[i].click()
          found = true
          break
        } else if (lis[i].classList.contains('playing')) {
          reachedPlaying = true
        }
      }
      if (!found) {
        audio.play().then(_ => musicPlayerUpdatePositionState())
        window.alert('End of playlist reached')
      }
    }
  }, 10)
}

function musicPlayerOnTimelineClick (ev) {
  musicPlayerMovePlayHead(ev)
  const timelineWidth = timeline.offsetWidth - playhead.offsetWidth
  const clickPercent = (ev.clientX - timeline.getBoundingClientRect().left) / timelineWidth
  audio.currentTime = currentDuration * clickPercent
}

function musicPlayerOnTimeUpdate () {
  const playpause = player.querySelector('.playpause')
  const timelineWidth = timeline.offsetWidth - playhead.offsetWidth
  const playPercent = timelineWidth * (audio.currentTime / currentDuration)
  playhead.style.marginLeft = playPercent + 'px'
  if (audio.currentTime === currentDuration) {
    playpause.querySelector('.play').style.display = 'none'
    playpause.querySelector('.busy').style.display = ''
    playpause.querySelector('.pause').style.display = 'none'
    if ('mediaSession' in navigator) {
      navigator.mediaSession.playbackState = 'none'
    }
  } else if (audio.paused) {
    playpause.querySelector('.play').style.display = ''
    playpause.querySelector('.busy').style.display = 'none'
    playpause.querySelector('.pause').style.display = 'none'
    if (document.title.startsWith('\u25B6\uFE0E ')) {
      document.title = document.title.substring(3)
    }
    if ('mediaSession' in navigator) {
      navigator.mediaSession.playbackState = 'paused'
    }
  } else {
    playpause.querySelector('.play').style.display = 'none'
    playpause.querySelector('.busy').style.display = 'none'
    playpause.querySelector('.pause').style.display = ''
    if (!document.title.startsWith('\u25B6\uFE0E ')) {
      document.title = '\u25B6\uFE0E ' + document.title
    }
    if ('mediaSession' in navigator) {
      navigator.mediaSession.playbackState = 'playing'
    }
  }
  player.querySelector('.durationDisplay .current').innerHTML = humanDuration(audio.currentTime)
}

function musicPlayerUpdateBufferBar () {
  if (currentDuration) {
    if (audio.buffered.length > 0) {
      bufferbar.style.width = Math.min(100, 1 + parseInt(100 * audio.buffered.end(0) / currentDuration)) + '%'
    } else {
      bufferbar.style.width = '100%'
    }
  } else {
    bufferbar.style.width = '0px'
  }
}

function musicPlayerShowBusy (ev) {
  const playpause = player.querySelector('.playpause')
  playpause.querySelector('.play').style.display = 'none'
  playpause.querySelector('.busy').style.display = ''
  playpause.querySelector('.pause').style.display = 'none'
}

function musicPlayerMovePlayHead (event) {
  const newMargLeft = event.clientX - timeline.getBoundingClientRect().left
  const timelineWidth = timeline.offsetWidth - playhead.offsetWidth
  if (newMargLeft >= 0 && newMargLeft <= timelineWidth) {
    playhead.style.marginLeft = newMargLeft + 'px'
  }
  if (newMargLeft < 0) {
    playhead.style.marginLeft = '0px'
  }
  if (newMargLeft > timelineWidth) {
    playhead.style.marginLeft = timelineWidth + 'px'
  }
}
function musicPlayerOnPlayheadMouseDown () {
  onPlayHead = true
  window.addEventListener('mousemove', musicPlayerMovePlayHead, true)
  audio.removeEventListener('timeupdate', musicPlayerOnTimeUpdate, false)
}

function musicPlayerOnPlayheadMouseUp (event) {
  if (onPlayHead) {
    musicPlayerMovePlayHead(event)
    window.removeEventListener('mousemove', musicPlayerMovePlayHead, true)
    // change current time
    const timelineWidth = timeline.offsetWidth - playhead.offsetWidth

    const clickPercent = (event.clientX - timeline.getBoundingClientRect().left) / timelineWidth
    audio.currentTime = currentDuration * clickPercent
    audio.addEventListener('timeupdate', musicPlayerOnTimeUpdate, false)
  }
  onPlayHead = false
}

function musicPlayerOnVolumeClick (ev) {
  const volSlider = player.querySelector('.vol-slider')
  const sliderWidth = volSlider.offsetWidth
  const percent = (ev.clientX - volSlider.getBoundingClientRect().left) / sliderWidth
  audio.logVolume = percent > 0.9 ? 1.0 : percent
  GM.setValue('volume', audio.logVolume)
}
function musicPlayerOnVolumeWheel (ev) {
  ev.preventDefault()
  const direction = Math.min(Math.max(-1.0, ev.deltaY), 1.0)
  audio.logVolume = Math.min(Math.max(0.0, audio.logVolume - 0.05 * direction), 1.0)
  GM.setValue('volume', audio.logVolume)
}
function musicPlayerOnMuteClick (ev) {
  if (audio.logVolume < 0.01) {
    if ('lastvolume' in audio.dataset && audio.dataset.lastvolume) {
      audio.logVolume = audio.dataset.lastvolume
      GM.setValue('volume', audio.logVolume)
    } else {
      audio.logVolume = 1.0
    }
  } else {
    audio.dataset.lastvolume = audio.logVolume
    audio.logVolume = 0.0
  }
}

function musicPlayerOnVolumeChanged (ev) {
  const icons = ['\uD83D\uDD07', '\uD83D\uDD08', '\uD83D\uDD09', '\uD83D\uDD0A']
  const percent = audio.logVolume
  const volSlider = player.querySelector('.vol-slider')
  volSlider.querySelector('.vol-amt').style.width = parseInt(100 * percent) + '%'
  const volIconWrapper = player.querySelector('.vol-icon-wrapper')
  volIconWrapper.title = 'Mute (' + parseInt(percent * 100) + '%)'
  if (percent < 0.05) {
    volIconWrapper.innerHTML = icons[0]
  } else if (percent < 0.3) {
    volIconWrapper.innerHTML = icons[1]
  } else if (percent < 0.8) {
    volIconWrapper.innerHTML = icons[2]
  } else {
    volIconWrapper.innerHTML = icons[3]
  }
}

function musicPlayerOnEnded (ev) {
  musicPlayerNextSong()
  window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({ block: 'nearest' }), 200)
}
function musicPlayerOnPlaylistClick (ev) {
  musicPlayerNextSong(this)
}
function musicPlayerOnPlaylistHeadingClick (ev) {
  const a = this.querySelector('a[href]')
  if (a && a.classList.contains('notloaded')) {
    const url = a.href
    this.remove()
    cachedTralbumData(url).then(function onCachedTralbumDataLoaded (TralbumData) {
      if (TralbumData) {
        addAlbumToPlaylist(TralbumData, 0)
      } else {
        playAlbumFromUrl(url)
      }
    })
  } else if (a && this.nextElementSibling) {
    this.nextElementSibling.click()
  }
}
function musicPlayerFavicon (url) {
  removeViaQuerySelector(document.head, 'link[rel*=icon]')
  const link = document.createElement('link')
  link.type = 'image/x-icon'
  link.rel = 'shortcut icon'
  link.href = url
  document.head.appendChild(link)
}

function musicPlayerCollectWishlistClick (ev) {
  ev.preventDefault()

  if (player.querySelector('.collect-wishlist').dataset === 'own') {
    return
  }

  const url = player.querySelector('.collect-wishlist').dataset.albumUrl

  player.querySelectorAll('.collect-wishlist>*').forEach(function (e) { e.style.display = 'none' })

  window.open(url + '#collect-wishlist')
}

async function musicPlayerCollectListenedClick (ev) {
  ev.preventDefault()

  const collectListened = player.querySelector('.collect-listened')

  const url = collectListened.dataset.albumUrl

  setTimeout(function musicPlayerCollectListenedResetTimeout () {
    player.querySelectorAll('.collect-listened>*').forEach(function (e) { e.style.display = 'none' })
    player.querySelector('.collect-listened .listened-saving').style.display = 'inline-block'
    player.querySelector('.collect-listened').style.cursor = 'wait'
  }, 0)

  let albumData = await myAlbumsGetAlbum(url)
  if (!albumData) {
    albumData = await myAlbumsNewFromUrl(url, {})
  }

  if (albumData.listened) {
    albumData.listened = false
  } else {
    albumData.listened = (new Date()).toJSON()
  }

  collectListened.dataset.listened = albumData.listened

  await myAlbumsUpdateAlbum(albumData)

  player.querySelectorAll('.collect-listened>*').forEach(function (e) { e.style.display = 'none' })
  if (albumData.listened) {
    player.querySelector('.collect-listened .listened').style.display = 'inline-block'
  } else {
    player.querySelector('.collect-listened .mark-listened').style.display = 'inline-block'
  }
  player.querySelector('.collect-listened').style.cursor = ''

  setTimeout(makeAlbumLinksGreat, 100)
}

function musicPlayerUpdatePositionState () {
  if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
    console.log('Updating position state...')
    navigator.mediaSession.setPositionState({
      duration: audio.duration || currentDuration || 180,
      playbackRate: audio.playbackRate,
      position: audio.currentTime
    })
  }
}

function musicPlayerCookieChannel (onStopEventCb) {
  if (!BANDCAMPDOMAIN) {
    return
  }
  window.addEventListener('message', function onMessage (event) {
    // Receive messages from the cookie channel event handler
    if (event.origin === document.location.protocol + '//' + document.location.hostname &&
    event.data && typeof (event.data) === 'object' && 'discographyplayerCookiechannelPlaylist' in event.data &&
    event.data.discographyplayerCookiechannelPlaylist.length >= 2 && event.data.discographyplayerCookiechannelPlaylist[1] === 'stop') {
      onStopEventCb(event.data.discographyplayerCookiechannelPlaylist)
    }
  })
  var script = document.createElement('script')
  script.innerHTML = `
  var channel = new Cookie.CommChannel('playlist')
  channel.send('stop')
  channel.subscribe(function(a,b) {
    window.postMessage({'discographyplayerCookiechannelPlaylist': b}, document.location.href)
    })
  channel.startListening()
  window.addEventListener('message', function onMessage (event) {
    // Receive messages from the user script
    if (event.origin === document.location.protocol + '//' + document.location.hostname
    && event.data && typeof(event.data) === 'object' && 'discographyplayerCookiechannelPlaylist' in event.data
    && event.data.discographyplayerCookiechannelPlaylist === 'sendstop') {
      channel.send('stop')
    }
  })
  window.addEventListener('unload', function(event) {
    channel.cleanup()
  })
  `
  document.head.appendChild(script)
}
function musicPlayerCookieChannelSendStop (onStopEventCb) {
  if (BANDCAMPDOMAIN) {
    window.postMessage({ discographyplayerCookiechannelPlaylist: 'sendstop' }, document.location.href)
  }
}

function musicPlayerSaveState () {
  let startPlaybackIndex = false
  const playlistEntries = player.querySelectorAll('.playlist .playlistentry')
  for (let i = 0; i < playlistEntries.length; i++) {
    if (playlistEntries[i].classList.contains('playing')) {
      startPlaybackIndex = i
      break
    }
  }
  const startPlaybackTime = audio.currentTime
  return GM.setValue('musicPlayerState', JSON.stringify({
    time: (new Date().getTime()),
    htmlPlaylist: player.querySelector('.playlist').innerHTML,
    startPlayback: !audio.paused,
    startPlaybackIndex: startPlaybackIndex,
    startPlaybackTime: startPlaybackTime
  }))
}

function musicPlayerRestoreState (state) {
  if (!allFeatures.discographyplayerPersist.enabled) {
    return
  }
  if (state.time + 1000 * 30 < (new Date().getTime())) {
    // Saved state expires after 30 seconds
    return
  }

  // Re-create music player
  musicPlayerCreate()
  player.querySelector('.playlist').innerHTML = state.htmlPlaylist
  const playlistEntries = player.querySelectorAll('.playlist .playlistentry')
  playlistEntries.forEach(function addPlaylistEntryOnClick (li) {
    li.addEventListener('click', musicPlayerOnPlaylistClick)
  })
  player.querySelectorAll('.playlist .playlistheading').forEach(function addPlaylistHeadingEntryOnClick (li) {
    li.addEventListener('click', musicPlayerOnPlaylistHeadingClick)
  })
  if (state.startPlaybackIndex !== false) {
    player.querySelectorAll('.playlist .playing').forEach(function (el) {
      el.classList.remove('playing')
    })
    playlistEntries[state.startPlaybackIndex].classList.add('playing')
    window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({ block: 'nearest' }), 200)
  }
  // Start playback
  if (state.startPlayback && state.startPlaybackIndex !== false) {
    musicPlayerPlaySong(playlistEntries[state.startPlaybackIndex], state.startPlaybackTime)
  }
}

function musicPlayerToggleMinimize (ev, hide) {
  if (hide || player.style.bottom !== '-57px') {
    player.style.bottom = '-57px'
    this.classList.add('minimized')
  } else {
    player.style.bottom = '0px'
    this.classList.remove('minimized')
  }
}

function musicPlayerClose () {
  if (player) {
    player.style.display = 'none'
  }
  if (audio) {
    audio.pause()
  }
  document.querySelectorAll('img.albumIsCurrentlyPlaying').forEach(img => img.classList.remove('albumIsCurrentlyPlaying'))
  document.querySelectorAll('.albumIsCurrentlyPlayingIndicator').forEach(div => div.remove())
}

function musicPlayerCreate () {
  if (player) {
    player.style.display = 'block'
    return
  }

  musicPlayerCookieChannel(musicPlayerStop)

  const img1px = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOsmLZvJgAFwQJn5VVZ5QAAAABJRU5ErkJggg=='

  const listenedListUrl = findUserProfileUrl() + '#listened-tab'

  const checkSymbol = NOEMOJI ? '✓' : '✔'

  player = document.createElement('div')
  document.body.appendChild(player)
  player.id = 'discographyplayer'
  player.innerHTML = `
<div class="col col25 nowPlaying">
  <div class="currentlyPlaying">
    <a class="cover" target="_blank" href="#">
      <img src="${img1px}">
    </a>
    <div class="info">
      <a class="link" target="_blank" href="#">
        <div class="title">◧◩◨▧■□▩</div>
        <div class="artist">by <span>◩▧◧□ ◩◨▧ ■◩▩</span></div>
        <div>from <span class="album">◨■■▩ ▧◨□</span></div>
      </a>
    </div>
  </div>
  <div class="nextInRow">
    <a class="cover" target="_blank" href="#">
      <img src="${img1px}">
    </a>
    <div class="info">
      <a class="link" target="_blank" href="#">
        <div class="title">◧◩◨▧■□▩</div>
        <div>by <span class="artist">◩▧◧□ ◩◨▧ ■◩▩</span></div>
        <div>from <span class="album">◨■■▩ ▧◨□</span></div>
      </a>
    </div>
  </div>
</div>
<div class="col col25 colcontrols">
  <audio autoplay="autoplay" preload="auto"></audio>
  <div class="audioplayer">
    <div id="timeline">
      <div id="bufferbar" class="bufferbaranimation"></div>
      <div id="playhead"></div>
    </div>
    <div class="controls">

      <div class="prevalbum" title="Previous album">
        <div class="arrowbutton prevalbum-icon"></div>
      </div>

      <div class="prev" title="Previous song">
        <div class="arrowbutton prev-icon"></div>
      </div>

      <div class="playpause" title="Play/Pause">
        <div class="play" style="display: none;"></div>
        <div class="busy" style="display: none;"></div>
        <div class="pause" style=""></div>
      </div>

      <div class="next" title="Next song">
        <div class="arrowbutton next-icon"></div>
      </div>

      <div class="nextalbum" title="Next album">
        <div class="arrowbutton nextalbum-icon"></div>
      </div>
    </div>
    <div class="durationDisplay"><span class="current">-</span>/<span class="total">-</span></div>

    <a class="downloadlink" title="Download mp3">
      ⭳
    </a>
    <br class="clb">
  </div>
</div>
<div class="col col35">
  <ol class="playlist"></ol>
</div>
<div class="col col15 colcontrols colvolumecontrols">

  <div class="vol">
      <div class="vol-icon-wrapper" title="Mute">
          🔊
      </div>
      <div class="vol-slider">
          <div class="vol-amt" style="width: 100%;"></div>
          <div class="vol-bg"></div>
      </div>
  </div>

  <div class="collect">
    <div class="collect-wishlist">
      <a class="wishlist-default" href="https://bandcamp.com/wishlist">Wishlist</a>

      <span class="wishlist-add" title="Add this album to your wishlist">
        <span class="bc-ui2 icon add-item-icon"></span>
        <span class="add-item-label">Add to wishlist</span>
      </span>
      <span class="wishlist-collected" title="Remove this album from your wishlist">
        <span class="bc-ui2 icon collected-item-icon"></span>
        <span>In Wishlist</span>
      </span>
      <span class="wishlist-own" title="You own this album">
        <span class="bc-ui2 icon own-item-icon"></span>
        <span>You own this</span>
      </span>
      <span class="wishlist-saving">
        Saving....
      </span>
    </div>
    <div class="collect-listened">
      <a class="listened-default" href="${listenedListUrl}">
        Played albums
        </a>
      <span class="listened" title="Mark album as NOT played">
        <span class="listened-symbol">${checkSymbol}</span>
        <span class="listened-label">Played</span>
      </span>
      <span class="mark-listened" title="Mark album as played">
        <span class="mark-listened-symbol">${checkSymbol}</span>
        <span class="mark-listened-label">Mark as played</span>
      </span>
      <span class="listened-saving">
        Saving...
      </span>
    </div>
  </div>

  <br class="cll">
  <div class="minimizebutton">
    <span class="minimized" title="Maximize player">&uarr;</span>
    <span class="maximized" title="Minimize player">&darr;</span>
  </div>
  <div class="closebutton" title="Close player">x</div>
</div>`

  addStyle(`
.cll{
  clear:left;
}
.clb{
  clear:both;
}
#discographyplayer{
  z-index:1010;
  position:fixed;
  bottom:0px;
  height:83px;
  width:100%;
  padding-top:3px;
  background:white;
  color:#505958;
  border-top: 1px solid rgba(0,0,0,0.15);
  font: 13px/1.231 "Helvetica Neue",Helvetica,Arial,sans-serif;
  transition: bottom 500ms
}
#discographyplayer a:link,#discographyplayer a:visited{
  color: #0687f5;
  text-decoration: none;
  cursor: pointer;
}
#discographyplayer a:hover {
  color: #0687f5;
  text-decoration: underline;
  cursor: pointer;
}
#discographyplayer .nowPlaying .info,#discographyplayer .nowPlaying .cover {
    display: inline-block;
    vertical-align: top;
}
#discographyplayer .nowPlaying img {
    width: 60px;
    height: 60px;
    margin-top: 4px;
    margin-left: 4px;
    margin-bottom: 4px;
}
#discographyplayer .nowPlaying .info {
    line-height: 18px;
    margin-left: 8px;
    margin-top: 8px;
    max-width: calc(100% - 76px);

    border: 0px solid black;
    padding: 0px;
    width: auto;
    max-height: auto;
    overflow-y: hidden;
}
#discographyplayer .nowPlaying .info .title, #discographyplayer .nowPlaying .info .album {
  font-size: 13px;
  font-weight: normal;
  color: #0687f5;
  margin:0;
  padding:0;
}
#discographyplayer .currentlyPlaying{
  display:inline-block;
  vertical-align: top;
  overflow: hidden;
  transition: margin-left 3s ease-in-out;
  width:99%;
}
#discographyplayer .nextInRow {
  display:inline-block;
  vertical-align: top;
  width:0%;
  overflow: hidden;
  transition: width 6s ease-in-out;
}
#discographyplayer .durationDisplay{
  margin-top:24px;
  float:left;
}
#discographyplayer .downloadlink:link{
  display:block;
  float:right;
  margin-top: 10px;
  font-size:15px;
  padding: 0px 3px;
  color: rgb(6, 135, 245);
  border:1px solid rgb(6, 135, 245);
  transition: color 300ms ease-in-out, border-color 300ms ease-in-out;
}
#discographyplayer .downloadlink:hover{
  text-decoration:none
}
#discographyplayer .downloadlink.downloading{
  color:#f0f;
  border-color:#f0f;
  animation: downloadrotation 3s infinite linear;
  cursor:wait;
}
@keyframes downloadrotation {
  from {transform: rotate(0deg)}
  to {transform: rotate(359deg)}
}
#discographyplayer .controls{
  margin-top: 10px;
  width: auto;
  float:left;
}
#discographyplayer .controls > *{
  display:inline-block;
  cursor: pointer;
  border: 1px solid #d9d9d9;
  padding: 11px;
  margin-right: 4px;
  height: 18px;
  width: 17px;
}
#discographyplayer .playpause .play {
  width: 0;
  height: 0;
  border-top: 9px inset transparent;
  border-bottom: 9px inset transparent;
  border-left: 15px solid rgb(34, 34, 34);
  cursor: pointer;
  margin-left: 2px;
}
#discographyplayer .playpause .pause {
  border: 0;
  border-left: 5px solid #2d2d2d;
  border-right: 5px solid #2d2d2d;
  height: 18px;
  width: 4px;
  margin-right: 2px;
  margin-left: 1px;
}
#discographyplayer .playpause .busy {
  background-image: url(https://bandcamp.com/img/playerbusy-noborder.gif);
  background-position: 50% 50%;
  background-repeat: no-repeat;
  border: none;
  height: 30px;
  margin: 0px 0px 0px -3px;
  width: 25px;
  overflow: hidden;
  background-size: contain;
}
#discographyplayer .arrowbutton {
  border: 0;
  height: 13px;
  width: 20px;
  margin-top: 4px;
  background: url(https://bandcamp.com/img/nextprev.png) 0px 0px / 40px 12px no-repeat transparent;
  background-position-x: 0px;
  cursor: pointer;
}
#discographyplayer .arrowbutton.next-icon {
  background-position: 100% 0px;
}
#discographyplayer .arrowbutton.prev-icon {

}
#discographyplayer .arrowbutton.prevalbum-icon {
  border-right: 3px solid #2d2d2d;
}
#discographyplayer .arrowbutton.nextalbum-icon {
  background-position: 100% 0px;
  border-left: 3px solid #2d2d2d;
}
#timeline{
  width: 100%;
  background: rgba(50,50,50,0.4);
  margin-top:5px;
  border-left:1px solid black;
  border-right:1px solid black;
}
#playhead{
  width:10px;
  height:10px;
  border-radius: 50%;
  background:rgba(50,50,50,1.0);
  cursor:pointer;
}
.bufferbaranimation{
  transition: width 1s;
}
#bufferbar{
  position:absolute;
  width:0px;
  height:10px;
  background:rgba(0,0,0,0.1);
}
#discographyplayer .playlist{
  width:100%;
  display:inline-block;
  max-height:80px;
  overflow:auto;
  list-style:none;
  margin:0px;
  padding: 0px 5px 0px 5px;
  scrollbar-color: rgba(50,50,50,0.4) white;
}
#discographyplayer .playlist .playlistentry {
  cursor:pointer;
  margin:1px 0px
}
#discographyplayer .playlist .playlistentry .duration {
  float:right
}
#discographyplayer .playlist .playing{
  background:#619aa950
}
#discographyplayer .playlist .playlistheading{
  background:rgba(50,50,50,0.4);
  margin:3px 0px
}
#discographyplayer .playlist .playlistheading a:link,#discographyplayer .playlist .playlistheading a:hover,#discographyplayer .playlist .playlistheading a:visited{
  color:#EEE;
  cursor:pointer
}
#discographyplayer .playlist .playlistheading a.notloaded{
  color:#CCC
}
#discographyplayer .playlist .playlistheading.notloaded{
  cursor:copy
}
#discographyplayer .vol{
  float:left;
  position: relative;
  width: 100px;
  margin-left: 1em;
  margin-top: 1em;
}
#discographyplayer .vol-icon-wrapper{
  font-size: 20px;
  cursor: pointer;
  width:27px;
}
#discographyplayer .vol-slider {
  width: 60px;
  height: 10px;
  position: relative;
  cursor: pointer;
}
#discographyplayer .vol > * {
  display: inline-block;
  vertical-align: middle;
}
#discographyplayer .vol-bg {
  background: rgba(50, 50, 50, 0.4);
  width: 100%;
  margin-top: 4px;
  height: 3px;
  position: absolute;
}
#discographyplayer .vol-amt {
  margin-top: 4px;
  height: 3px;
  position: absolute;
  background: rgba(50, 50, 50, 1);
}
#discographyplayer .vol-control-outer {
  height: 100%;
  position: relative;
  margin-left: -3px;
  margin-right: 5px;
}
#discographyplayer .collect{
  float:left;
  margin-left: 1em;
}
#discographyplayer .collect-wishlist {
  cursor:default;
  margin-top:0.5em;
}
#discographyplayer .collect-wishlist .wishlist-add {
  cursor:pointer;
}
#discographyplayer .collect-listened {
  cursor:pointer;
  margin-top:0.5em;
  margin-left: 2px;
}
#discographyplayer .collect .icon{
  height: 13px;
  width: 14px;
  display: inline-block;
  position: relative;
  top: 2px;
}
#discographyplayer .collect .add-item-icon{
  background-position: 0px -73px;
}
#discographyplayer .collect .collected-item-icon{
  background-position: -28px -73px;
}
#discographyplayer .collect .own-item-icon{
  background-position: -42px -73px;
}
#discographyplayer .collect .wishlist-add,#discographyplayer .collect .wishlist-collected,#discographyplayer .collect .wishlist-own,#discographyplayer .collect .wishlist-saving{
  display:none;
}
#discographyplayer .collect .wishlist-add:hover .add-item-icon{
  background-position: -56px -73px;
}
#discographyplayer .collect .wishlist-add:hover .add-item-label{
  text-decoration:underline;
}
#discographyplayer .collect .listened,#discographyplayer .collect .mark-listened, #discographyplayer .collect .listened-saving{
  display:none;
}
#discographyplayer .collect .listened .listened-symbol{
  color:rgb(0,220,50);
  text-shadow:1px 0px #DDD,-1px 0px #DDD,0px -1px #DDD,0px 1px #DDD
}
#discographyplayer .collect .mark-listened .mark-listened-symbol{
  color:#FFF;
  text-shadow:1px 0px #959595,-1px 0px #959595,0px -1px #959595,0px 1px #959595
}
#discographyplayer .collect .mark-listened:hover .mark-listened-symbol{
  text-shadow:1px 0px #0AF,-1px 0px #0AF,0px -1px #0AF,0px 1px #0AF
}
#discographyplayer .collect .mark-listened:hover .mark-listened-label {
  text-decoration:underline;
}
#discographyplayer .closebutton,#discographyplayer .minimizebutton {
  position: absolute;
  top: 1px;
  right: 1px;
  border: 1px solid #505958;
  color: #505958;
  font-size: 10px;
  box-shadow: 0px 0px 2px #505958;
  cursor: pointer;
  opacity:0.0;
  transition: opacity 300ms;
  min-width:8px;
  min-height:13px;
  text-align:center;
}
#discographyplayer .minimizebutton {
  right:13px;
}
#discographyplayer .minimizebutton .minimized {
  display:none
}
#discographyplayer .minimizebutton.minimized .maximized {
  display:none
}
#discographyplayer .minimizebutton.minimized .minimized {
  display:inline
}
#discographyplayer:hover .closebutton, #discographyplayer:hover .minimizebutton {
  opacity:1.0
}
#discographyplayer .col {
  float: left;
  min-height: 1px;
  position: relative;
}
#discographyplayer .col25 {
  width: 25%;
}
#discographyplayer .col35 {
  width: 35%;
}
#discographyplayer .col30 {
  width: 30%;
}
#discographyplayer .col15 {
  width: 14%;
}
#discographyplayer .col20 {
  width: 20%;
}
#discographyplayer .colcontrols {
  user-select: none
}
#discographyplayer .colvolumecontrols {
  margin-left:10px
}

.albumIsCurrentlyPlaying {
  border:2px solid lime
}
.music-grid-item .albumIsCurrentlyPlaying {
  border:none
}

.albumIsCurrentlyPlayingIndicator {
  display:none;
}

.music-grid-item .albumIsCurrentlyPlayingIndicator {
    position: absolute;
    display:block;
    width: 74px;
    height: 54px;
    left: 50%;
    top: 50%;
    margin-left: -36px;
    margin-top: -27px;
    opacity: 0.5;
    transition: opacity 0.2s;
}
.albumIsCurrentlyPlayingIndicator:hover {
  opacity: 0.0;
}
.albumIsCurrentlyPlayingIndicator .currentlyPlayingBg {
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background: #000;
    border-radius: 4px;
}
.albumIsCurrentlyPlayingIndicator .currentlyPlayingIcon {
    position: absolute;
    width: 10px;
    height: 20px;
    left: 28px;
    top: 17px;
    border-width: 0px 5px;
    border-color: #fff;
    border-style: solid;
}
`)

  if (allFeatures.discographyplayerSidebar.enabled) {
    // Sidebar discographyplayer
    addStyle(`
@media (min-width: 1600px) {
  #menubar-wrapper:hover {
    z-index:1100;
  }
  #discographyplayer {
    display: block;
    bottom: 0px;
    height: 100vh;
    max-height: 100vh;
    width: calc((100vw - 915px - 35px) / 2);
    right: 0px;
    border-left: 1px solid #0007;
    padding-left: 1px;
  }
  #discographyplayer .playlist {
    height: calc(100vh - 80px - 80px - 50px - 13px);
    max-height: calc(100vh - 80px - 80px - 50px - 13px);
  }
  #discographyplayer .playlist .playlistentry {
    overflow-x:hidden;
  }
  #discographyplayer .col25 {
    width: 98%;
  }
  #discographyplayer .col.nowPlaying {
    height: 70px;
  }
  #discographyplayer .col.col25.colcontrols {
    height: 85px;
  }
  #discographyplayer .col35 {
    width: 97%;
  }
  #discographyplayer .col15 {
    width: 96%;
  }
  #discographyplayer .colvolumecontrols {
    height: 50px
  }
  #playhead, #bufferbar {
    height: 25px;
    border-radius: 0;
  }
  #discographyplayer .audioplayer a.downloadlink {
    position: fixed;
    bottom: 5px;
    right: 5px;
    z-index: 10;
  }
  #discographyplayer .minimizebutton {
    display:none;
  }
  #discographyplayer .currentlyPlaying{
    transition: margin-top 1s ease-in-out;
    width:99%;
    height:99%;
  }
  #discographyplayer .nextInRow {
    height:0%;
    width:99%;
    transition: height 1s ease-in-out;
  }
}
    `)
  }

  audio = player.querySelector('audio')
  addLogVolume(audio)
  getStoredVolume(function setVolumeCallback (volume) { audio.logVolume = volume })
  playhead = player.querySelector('#playhead')
  bufferbar = player.querySelector('#bufferbar')
  timeline = player.querySelector('#timeline')

  player.querySelector('.minimizebutton').addEventListener('click', musicPlayerToggleMinimize)
  player.querySelector('.closebutton').addEventListener('click', musicPlayerClose)

  audio.addEventListener('ended', musicPlayerOnEnded)
  audio.addEventListener('timeupdate', musicPlayerOnTimeUpdate)
  audio.addEventListener('volumechange', musicPlayerOnVolumeChanged)
  audio.addEventListener('canplaythrough', function onCanPlayThrough () {
    currentDuration = audio.duration
    player.querySelector('.durationDisplay .total').innerHTML = humanDuration(currentDuration)
  })

  timeline.addEventListener('click', musicPlayerOnTimelineClick, false)
  playhead.addEventListener('mousedown', musicPlayerOnPlayheadMouseDown, false)
  window.addEventListener('mouseup', musicPlayerOnPlayheadMouseUp, false)

  player.querySelector('.prevalbum').addEventListener('click', musicPlayerPrevAlbum)
  player.querySelector('.prev').addEventListener('click', musicPlayerPrev)
  player.querySelector('.playpause').addEventListener('click', musicPlayerPlay)
  player.querySelector('.next').addEventListener('click', musicPlayerNext)
  player.querySelector('.nextalbum').addEventListener('click', musicPlayerNextAlbum)

  player.querySelector('.vol-slider').addEventListener('click', musicPlayerOnVolumeClick)
  player.querySelector('.vol').addEventListener('wheel', musicPlayerOnVolumeWheel, false)
  player.querySelector('.vol-icon-wrapper').addEventListener('click', musicPlayerOnMuteClick)

  player.querySelector('.collect-wishlist').addEventListener('click', musicPlayerCollectWishlistClick)
  player.querySelector('.collect-listened').addEventListener('click', musicPlayerCollectListenedClick)

  player.querySelector('.downloadlink').addEventListener('click', function onDownloadLinkClick (ev) {
    const addSpinner = (el) => el.classList.add('downloading')
    const removeSpinner = (el) => el.classList.remove('downloading')
    downloadMp3FromLink(ev, this, addSpinner, removeSpinner)
  })
  if (NOEMOJI) {
    player.querySelector('.downloadlink').innerHTML = '↓'
  }

  window.addEventListener('unload', function onPageUnLoad (ev) {
    if (allFeatures.discographyplayerPersist.enabled && player.style.display !== 'none' && !audio.paused) {
      addAllAlbumsAsHeadings()
      musicPlayerSaveState()
    }
  })

  window.setInterval(musicPlayerUpdateBufferBar, 1200)
}

function addHeadingToPlaylist (title, url, albumLoaded) {
  musicPlayerCreate()
  let content = document.createTextNode('💽 ' + title)
  if (url) {
    const a = document.createElement('a')
    a.href = url
    a.target = '_blank'
    a.appendChild(content)
    content = a
    a.className = albumLoaded ? 'loaded' : 'notloaded'
    a.title = 'Open album page'
  }
  const li = document.createElement('li')
  li.appendChild(content)
  li.className = 'playlistheading'
  if (!albumLoaded) {
    li.className += ' notloaded'
    li.title = 'Load album into playlist'
  }
  li.addEventListener('click', musicPlayerOnPlaylistHeadingClick)
  player.querySelector('.playlist').appendChild(li)
}

function addToPlaylist (startPlayback, data) {
  musicPlayerCreate()

  const li = document.createElement('li')
  li.appendChild(document.createTextNode((data.trackNumber > 9 ? '' : '0') + data.trackNumber + '. ' + data.artist + ' - ' + data.title))
  const span = document.createElement('span')
  span.className = 'duration'
  span.appendChild(document.createTextNode(humanDuration(data.duration)))
  li.appendChild(span)
  li.value = data.trackNumber
  li.dataset.file = data.file
  li.dataset.title = data.title
  li.dataset.trackNumber = data.trackNumber
  li.dataset.duration = data.duration
  li.dataset.artist = data.artist
  li.dataset.album = data.album
  li.dataset.albumUrl = data.albumUrl
  li.dataset.albumCover = data.albumCover
  li.dataset.inWishlist = data.inWishlist
  li.dataset.isPurchased = data.isPurchased

  li.addEventListener('click', musicPlayerOnPlaylistClick)
  li.className = 'playlistentry'
  player.querySelector('.playlist').appendChild(li)

  if (startPlayback) {
    player.querySelectorAll('.playlist .playing').forEach(function (el) {
      el.classList.remove('playing')
    })
    li.classList.add('playing')
    musicPlayerPlaySong(li)
    window.setTimeout(() => player.querySelector('.playlist .playing').scrollIntoView({ block: 'nearest' }), 200)
  }
}

function addAlbumToPlaylist (TralbumData, startPlaybackIndex) {
  let i = 0
  const artist = TralbumData.artist
  const album = TralbumData.current.title
  const albumUrl = document.location.protocol + '//' + albumKey(TralbumData.url)
  const albumCover = `https://f4.bcbits.com/img/a${TralbumData.art_id}_2.jpg`
  addHeadingToPlaylist(album, 'url' in TralbumData ? TralbumData.url : false, true)
  let streamable = 0
  for (const key in TralbumData.trackinfo) {
    const track = TralbumData.trackinfo[key]
    if (!track.file) {
      continue
    }
    const trackNumber = track.track_num
    const file = track.file[Object.keys(track.file)[0]]
    const title = track.title
    const duration = track.duration
    const inWishlist = 'tralbum_collect_info' in TralbumData && 'is_collected' in TralbumData.tralbum_collect_info && TralbumData.tralbum_collect_info.is_collected
    const isPurchased = 'tralbum_collect_info' in TralbumData && 'is_purchased' in TralbumData.tralbum_collect_info && TralbumData.tralbum_collect_info.is_purchased
    addToPlaylist(startPlaybackIndex === i++, {
      file: file,
      title: title,
      trackNumber: trackNumber,
      duration: duration,
      artist: artist,
      album: album,
      albumUrl: albumUrl,
      albumCover: albumCover,
      inWishlist: inWishlist,
      isPurchased: isPurchased
    })
    streamable++
  }
  if (streamable === 0) {
    const li = document.createElement('li')
    li.appendChild(document.createTextNode((NOEMOJI ? '\u27C1' : '\uD83D\uDE22') + ' Album is not streamable'))
    player.querySelector('.playlist').appendChild(li)
  }
  player.querySelectorAll('.playlist .playlistheading a.notloaded').forEach(function (el) {
    // Move unloaded items to the end
    el.parentNode.parentNode.appendChild(el.parentNode)
  })
}

function addAllAlbumsAsHeadings () {
  const as = document.querySelectorAll('.music-grid .music-grid-item a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]')
  const lis = player.querySelectorAll('.playlist .playlistentry')

  const isAlreadyInPlaylist = function (url) {
    for (let i = 0; i < lis.length; i++) {
      if (albumKey(lis[i].dataset.albumUrl) === albumKey(url)) {
        return true
      }
    }
    return false
  }

  for (let i = 0; i < as.length; i++) {
    const url = as[i].href
    // Check if already in playlist
    if (!isAlreadyInPlaylist(url)) {
      const title = ('textContent' in as[i].dataset ? as[i].dataset.textContent : as[i].querySelector('.title').textContent).trim()
      addHeadingToPlaylist(title, url, false)
    }
  }
}

function getTralbumData (url, cb) {
  return new Promise(function getTralbumDataPromise (resolve, reject) {
    GM.xmlHttpRequest({
      method: 'GET',
      url: url,
      onload: function getTralbumDataOnLoad (response) {
        if (!response.responseText || response.responseText.indexOf('400 Bad Request') !== -1) {
          let msg = ''
          try {
            msg = response.responseText.split('<center>')[1].split('</center>')[0]
          } catch (e) {
            msg = response.responseText
          }
          window.alert('An error occured. Please clear your cookies of bandcamp.com and try again.\n\nOriginal error:\n' + msg)
          reject(new Error('Too many cookies'))
          return
        }
        let TralbumData = null
        try {
          if (response.responseText.indexOf('var TralbumData =') !== -1) {
            TralbumData = JSON5.parse(response.responseText.split('var TralbumData =')[1].split('\n};\n')[0].replace(/"\s+\+\s+"/, '') + '\n}')
          } else if (response.responseText.indexOf('data-tralbum="') !== -1) {
            let str = response.responseText.split('data-tralbum="')[1].split('"')[0]
            str = decodeHTMLentities(response.responseText.split('data-tralbum="')[1].split('"')[0])
            TralbumData = JSON.parse(str)
          }
        } catch (e) {
          window.alert('An error occured when parsing TralbumData from url=' + url + '.\n\nOriginal error:\n' + e)
          reject(e)
          return
        }
        if (TralbumData) {
          correctTralbumData(TralbumData, response.responseText)
          resolve(TralbumData)
        } else {
          const msg = 'Could not parse TralbumData from url=' + url
          window.alert(msg)
          reject(new Error(msg))
        }
      },
      onerror: function getTralbumDataOnError (response) {
        console.log('getTralbumData(' + url + ') Error: ' + response.status + '\nResponse:\n' + response.responseText + '\n' + ('error' in response ? response.error : ''))
        reject(new Error('error' in response ? response.error : 'getTralbumData failed'))
      }
    })
  })
}
function correctTralbumData (TralbumDataObj, html) {
  const TralbumData = JSON.parse(JSON.stringify(TralbumDataObj))
  // Corrections for single tracks
  if (TralbumData.current.type === 'track' && TralbumData.current.title.toLowerCase().indexOf('single') === -1) {
    TralbumData.current.title += ' - Single'
  }
  for (let i = 0; i < TralbumData.trackinfo.length; i++) {
    if (TralbumData.trackinfo[i].track_num === null) {
      TralbumData.trackinfo[i].track_num = i + 1
    }
  }
  // Add tags from html
  if (html && html.indexOf('tags-inline-label') !== -1) {
    const m = html.split('tags-inline-label')[1].split('</div>')[0].match(/\/tag\/[^"]+"/g)
    if (m && m.length > 0) {
      TralbumData.tags = []
      m.forEach(function (t) {
        t = t.split('/').pop()
        t = t.substring(0, t.length - 1)
        TralbumData.tags.push(t)
      })
    }
  }
  // Remove stuff we don't use to save storage space
  delete TralbumData.current.require_email_0
  delete TralbumData.current.audit
  delete TralbumData.current.download_pref
  delete TralbumData.current.set_price
  delete TralbumData.current.killed
  delete TralbumData.current.auto_repriced
  delete TralbumData.current.minimum_price_nonzero
  delete TralbumData.current.minimum_price
  delete TralbumData.current.purchase_url
  delete TralbumData.current.new_desc_format
  delete TralbumData.current.private
  delete TralbumData.current.is_set_price
  delete TralbumData.current.require_email
  delete TralbumData.current.upc
  delete TralbumData.packages
  delete TralbumData.last_subscription_item
  delete TralbumData.last_subscription_item
  delete TralbumData.has_discounts
  delete TralbumData.is_bonus
  delete TralbumData.play_cap_data
  delete TralbumData.client_id_sig
  delete TralbumData.is_purchased
  delete TralbumData.items_purchased
  delete TralbumData.is_private_stream
  delete TralbumData.is_band_member
  delete TralbumData.licensed_version_ids
  delete TralbumData.package_associated_license_id
  for (let i = 0; i < TralbumData.trackinfo.length; i++) {
    delete TralbumData.trackinfo[i].is_draft
    delete TralbumData.trackinfo[i].album_preorder
    delete TralbumData.trackinfo[i].unreleased_track
    for (const attr in TralbumData.trackinfo[i]) {
      if (TralbumData.trackinfo[i][attr] === null) {
        delete TralbumData.trackinfo[i][attr]
      }
    }
  }
  for (const attr in TralbumData) {
    if (TralbumData[attr] === null) {
      delete TralbumData[attr]
    }
  }
  return TralbumData
}

function albumKey (url) {
  if (url.startsWith('/')) {
    url = document.location.hostname + url
  }
  if (url.indexOf('://') !== -1) {
    url = url.split('://')[1]
  }
  if (url.indexOf('#') !== -1) {
    url = url.split('#')[0]
  }
  if (url.indexOf('?') !== -1) {
    url = url.split('?')[0]
  }
  return url
}

function albumPath (url) {
  if (url.startsWith('/')) {
    return albumKey(url)
  }
  const a = document.createElement('a')
  a.href = url
  return a.pathname
}

async function storeTralbumData (TralbumData) {
  const expires = TRALBUM_CACHE_HOURS * 3600000
  const cache = JSON.parse(await GM.getValue('tralbumdata', '{}'))
  for (const prop in cache) {
    // Delete cached values, that are older than 2 hours
    if ((new Date()).getTime() - (new Date(cache[prop].time)).getTime() > expires) {
      delete cache[prop]
    }
  }
  TralbumData.time = (new Date()).toJSON()
  cache[albumKey(TralbumData.url)] = TralbumData
  await GM.setValue('tralbumdata', JSON.stringify(cache))
  storeTralbumDataPermanently(TralbumData)
}

async function cachedTralbumData (url) {
  const expires = TRALBUM_CACHE_HOURS * 3600000
  const key = albumKey(url)
  const cache = JSON.parse(await GM.getValue('tralbumdata', '{}'))
  for (const prop in cache) {
    // Delete cached values, that are older than 2 hours
    if ((new Date()).getTime() - (new Date(cache[prop].time)).getTime() > expires) {
      delete cache[prop]
      continue
    }
    if (prop === key) {
      return cache[prop]
    }
  }
  return false
}

async function storeTralbumDataPermanently (TralbumData) {
  const library = JSON.parse(await GM.getValue('tralbumlibrary', '{}'))
  const key = albumKey(TralbumData.url)
  if (key in library) {
    library[key] = Object.assign(library[key], TralbumData)
  } else {
    library[key] = TralbumData
  }
  await GM.setValue('tralbumlibrary', JSON.stringify(library))
}

function playAlbumFromCover (ev) {
  let parent = this
  for (let j = 0; parent.tagName !== 'A' && j < 20; j++) {
    parent = parent.parentNode
  }
  const url = parent.href
  parent.querySelector('img')
  parent.classList.add('discographyplayer_currentalbum')

  // Check if already in playlist
  if (player) {
    musicPlayerCreate()
    const lis = player.querySelectorAll('.playlist .playlistentry')
    for (let i = 0; i < lis.length; i++) {
      if (albumKey(lis[i].dataset.albumUrl) === albumKey(url)) {
        lis[i].click()
        return
      }
    }
  }

  // Load data
  cachedTralbumData(url).then(function onCachedTralbumDataLoaded (TralbumData) {
    if (TralbumData) {
      addAlbumToPlaylist(TralbumData, 0)
    } else {
      playAlbumFromUrl(url)
    }
  })
}

function playAlbumFromUrl (url) {
  getTralbumData(url).then(function onGetTralbumDataLoaded (TralbumData) {
    storeTralbumData(TralbumData)
    addAlbumToPlaylist(TralbumData, 0)
  }).catch(function onGetTralbumDataError (e) {
    window.alert('Could not load album data from url:\n' + url + '\n' + ('error' in e ? e.error : ''))
    console.log(e)
  })
}

async function myAlbumsGetAlbum (url) {
  const key = albumKey(url)
  const data = JSON.parse(await GM.getValue('myalbums', '{}'))

  if (key in data) {
    return data[key]
  } else {
    return false
  }
}

async function myAlbumsUpdateAlbum (albumData) {
  const key = albumKey(albumData.url)
  const data = JSON.parse(await GM.getValue('myalbums', '{}'))

  if (key in data) {
    data[key] = Object.assign(data[key], albumData)
  } else {
    data[key] = albumData
  }

  await GM.setValue('myalbums', JSON.stringify(data))
}

async function myAlbumsNewFromUrl (url, fallback) {
  // Get data from cache or load from url
  url = albumKey(url)
  const albumData = fallback || {}
  let TralbumData = await cachedTralbumData(url)
  if (!TralbumData) {
    try {
      TralbumData = await getTralbumData(document.location.protocol + '//' + url)
    } catch (e) {
      console.log('myAlbumsNewFromUrl() Could not load album data from url:\n' + url)
    }
    if (TralbumData) {
      storeTralbumData(TralbumData)
    }
  }
  if (TralbumData) {
    albumData.artist = TralbumData.artist
    albumData.title = TralbumData.current.title
    albumData.albumCover = `https://f4.bcbits.com/img/a${TralbumData.art_id}_2.jpg`
    albumData.releaseDate = TralbumData.current.release_date
  }
  albumData.url = url
  albumData.listened = false
  return albumData
}

function makeAlbumCoversGreat () {
  if (!('makeAlbumCoversGreat' in document.head.dataset)) {
    document.head.dataset.makeAlbumCoversGreat = true
    const campExplorerCSS = `
.music-grid-item {
  position: relative
}
.music-grid-item .art-play {
  margin-top: -50px;
}
`
    addStyle(`
.music-grid-item .art-play {
  position: absolute;
  width: 74px;
  height: 54px;
  left: 50%;
  top: 50%;
  margin-left: -36px;
  margin-top: -27px;
  opacity: 0;
  transition: opacity 0.2s;
}
.music-grid-item .art-play-bg {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  background: #000;
  border-radius: 4px;
}
.music-grid-item .art-play-icon {
  position: absolute;
  width: 0;
  height: 0;
  left: 28px;
  top: 17px;
  border-width: 10px 0 10px 17px;
  border-color: transparent transparent transparent #fff;
  border-style: dashed dashed dashed solid;
}
.music-grid-item:hover .art-play {
  opacity: 0.6;
}

${CAMPEXPLORER ? campExplorerCSS : ''}
`)
  }
  const onclick = function onclick (ev) {
    ev.preventDefault()
    playAlbumFromCover.apply(this, ev)
  }
  const artPlay = document.createElement('div')
  artPlay.className = 'art-play'
  artPlay.innerHTML = '<div class="art-play-bg"></div><div class="art-play-icon"></div>'

  if (CAMPEXPLORER) {
    document.querySelectorAll('ul.albums').forEach(e => e.classList.add('music-grid'))
    document.querySelectorAll('ul.albums li.album').forEach(e => e.classList.add('music-grid-item'))
  }

  // Albums and single tracks
  const imgs = document.querySelectorAll('.music-grid .music-grid-item a[href*="/album/"] img,.music-grid .music-grid-item a[href*="/track/"] img')
  for (let i = 0; i < imgs.length; i++) {
    if (imgs[i].parentNode.getElementsByClassName('art-play').length) {
      continue
    }
    imgs[i].addEventListener('click', onclick)

    // Add play overlay
    const clone = artPlay.cloneNode(true)
    clone.addEventListener('click', onclick)
    imgs[i].parentNode.appendChild(clone)
  }
}

async function makeAlbumLinksGreat (parentElement) {
  const doc = parentElement || document
  const myalbums = JSON.parse(await GM.getValue('myalbums', '{}'))

  if (!('makeAlbumLinksGreat' in document.head.dataset)) {
    document.head.dataset.makeAlbumLinksGreat = true
    addStyle(`
    .bdp_check_onlinkhover_container { z-index:1002; position:absolute; display:none }
    .bdp_check_onlinkhover_container_shown { display:block; background-color:rgba(255,255,255,0.9); padding:0px 2px 0px 0px; border-radius:5px  }
    .bdp_check_onlinkhover_container:hover { position:absolute; transition: all 300ms linear; background-color:rgba(255,255,255,0.9); padding:0px 10px 0px 7px; border-radius:5px }
    .bdp_check_onchecked_container { z-index:-1; position:absolute; opacity:0.0; margin-top:-2px}
    a:hover .bdp_check_onchecked_container { z-index:1002; position:absolute; transition: opacity 300ms linear; opacity:1.0}

    .bdp_check_onlinkhover_symbol {color:rgba(0,0,50,0.7)}
    .bdp_check_onlinkhover_text {color:rgba(0,0,50,0.7)}
    .bdp_check_onlinkhover_container:hover .bdp_check_onlinkhover_symbol { color:rgba(0,0,100,1.0) }
    .bdp_check_onlinkhover_container:hover .bdp_check_onlinkhover_text { color:rgba(0,100,0,1.0)}
    .bdp_check_onchecked_symbol { color:rgba(0,100,0,0.8) }
    .bdp_check_onchecked_text { color:rgba(150,200,150,0.8) }

    a:hover .bdp_check_onchecked_symbol { text-shadow: 1px 1px #fff; color:rgba(0,50,0,1.0); transition: all 300ms linear }
    a:hover .bdp_check_onchecked_text { text-shadow: 1px 1px #000; color:rgba(200,255,200,0.8); transition: all 300ms linear }

    `)
  }

  const excluded = [...document.querySelectorAll('#carousel-player .now-playing a')]
  excluded.push(...document.querySelectorAll('#discographyplayer a'))
  excluded.push(...document.querySelectorAll('#pastreleases a'))

  /*
  <div class="bdp_check_container bdp_check_onlinkhover_container"><span class="bdp_check_onlinkhover_symbol">\u2610</span> <span class="bdp_check_onlinkhover_text">Check</span></div>
  <div class="bdp_check_container bdp_check_onlinkhover_container"><span class="bdp_check_onlinkhover_symbol">\u1f5f9</span> <span class="bdp_check_onlinkhover_text">Check</span></div>
  <span class="bdp_check_onchecked_symbol">\u2611</span> TITLE <div class="bdp_check_container bdp_check_onchecked_container"><span class="bdp_check_onchecked_text">Played</span></div>
  */

  const onClickSetListened = async function onClickSetListenedAsync (ev) {
    ev.preventDefault()

    let parentA = this
    for (let j = 0; parentA.tagName !== 'A' && j < 20; j++) {
      parentA = parentA.parentNode
    }
    setTimeout(function showSavingLabel () {
      parentA.style.cursor = 'wait'
      parentA.querySelector('.bdp_check_container').innerHTML = 'Saving...'
    }, 0)

    const url = parentA.href
    let albumData = await myAlbumsGetAlbum(url)
    if (!albumData) {
      albumData = await myAlbumsNewFromUrl(url, { title: this.dataset.textContent })
    }
    albumData.listened = (new Date()).toJSON()

    await myAlbumsUpdateAlbum(albumData)

    setTimeout(function hideSavingLabel () {
      parentA.style.cursor = ''
      makeAlbumLinksGreat()
    }, 100)
  }
  const onClickRemoveListened = async function onClickRemoveListenedAsync (ev) {
    ev.preventDefault()

    let parentA = this
    for (let j = 0; parentA.tagName !== 'A' && j < 20; j++) {
      parentA = parentA.parentNode
    }
    setTimeout(function showSavingLabel () {
      parentA.style.cursor = 'wait'
      parentA.querySelector('.bdp_check_container').innerHTML = 'Saving...'
    }, 0)

    const url = parentA.href
    const albumData = await myAlbumsGetAlbum(url)
    if (albumData) {
      albumData.listened = false
      await myAlbumsUpdateAlbum(albumData)
    }

    setTimeout(function hideSavingLabel () {
      parentA.style.cursor = ''
      makeAlbumLinksGreat()
    }, 100)
  }
  const mouseOverLink = function onMouseOverLink (ev) {
    const bdpCheckOnlinkhoverContainer = this.querySelector('.bdp_check_onlinkhover_container')
    if (bdpCheckOnlinkhoverContainer) {
      bdpCheckOnlinkhoverContainer.classList.add('bdp_check_onlinkhover_container_shown')
    }
  }
  const mouseOutLink = function onMouseOutLink (ev) {
    const a = this
    a.dataset.iv = setTimeout(function mouseOutLinkTimeout () {
      const div = a.querySelector('.bdp_check_onlinkhover_container')
      if (div) {
        div.classList.remove('bdp_check_onlinkhover_container_shown')
        div.dataset.iv = a.dataset.iv
      }
    }, 1000)
  }
  const mouseMoveLink = function onMouseLoveLink (ev) {
    if ('iv' in this.dataset) {
      window.clearTimeout(this.dataset.iv)
    }
  }
  const mouseOverDivCheck = function onMouseOverDivCheck (ev) {
    const bdpCheckOnlinkhoverSymbol = this.querySelector('.bdp_check_onlinkhover_symbol')
    if (bdpCheckOnlinkhoverSymbol) {
      bdpCheckOnlinkhoverSymbol.innerText = NOEMOJI ? '\u2611' : '\uD83D\uDDF9'
    }
    if ('iv' in this.dataset) {
      window.clearTimeout(this.dataset.iv)
    }
  }
  const mouseOutDivCheck = function onMouseOutDivCheck (ev) {
    const bdpCheckOnlinkhoverSymbol = this.querySelector('.bdp_check_onlinkhover_symbol')
    if (bdpCheckOnlinkhoverSymbol) {
      bdpCheckOnlinkhoverSymbol.innerText = '\u2610'
    }
  }
  const divCheck = document.createElement('div')
  divCheck.setAttribute('class', 'bdp_check_container bdp_check_onlinkhover_container')
  divCheck.setAttribute('title', 'Mark as played')
  divCheck.innerHTML = '<span class="bdp_check_onlinkhover_symbol">\u2610</span> <span class="bdp_check_onlinkhover_text">Check</span>'

  const divChecked = document.createElement('div')
  divChecked.setAttribute('class', 'bdp_check_container bdp_check_onchecked_container')
  divChecked.innerHTML = '<span class="bdp_check_onchecked_text">Played</span>'

  const spanChecked = document.createElement('span')
  spanChecked.appendChild(document.createTextNode('\u2611 '))
  spanChecked.setAttribute('class', 'bdp_check_onchecked_symbol')

  const a = doc.querySelectorAll('a[href*="/album/"],.music-grid .music-grid-item a[href*="/track/"]')
  let lastKey = ''
  for (let i = 0; i < a.length; i++) {
    if (excluded.indexOf(a[i]) !== -1) {
      continue
    }

    const key = albumKey(a[i].href)
    if (key === lastKey) {
      // Skip multiple consequent links to same album
      continue
    }
    const textContent = a[i].textContent.trim()
    if (!textContent) {
      // Skip album covers only
      continue
    }
    let div
    if (a[i].dataset.textContent) {
      removeViaQuerySelector(a[i], '.bdp_check_onlinkhover_container')
      removeViaQuerySelector(a[i], '.bdp_check_onchecked_container')
      removeViaQuerySelector(a[i], '.bdp_check_onchecked_symbol')
    } else {
      a[i].dataset.textContent = textContent
      a[i].addEventListener('mouseover', mouseOverLink)
      a[i].addEventListener('mousemove', mouseMoveLink)
      a[i].addEventListener('mouseout', mouseOutLink)
    }
    if (key in myalbums && 'listened' in myalbums[key] && myalbums[key].listened) {
      div = divChecked.cloneNode(true)
      div.addEventListener('click', onClickRemoveListened)
      const date = new Date(myalbums[key].listened)
      const since = timeSince(date)
      const dateStr = dateFormater(date)
      div.title = since + ' ago\nClick to mark as NOT played'
      div.querySelector('.bdp_check_onchecked_text').appendChild(document.createTextNode(' ' + dateStr))
      const span = spanChecked.cloneNode(true)
      span.title = since + ' ago\nClick to mark as NOT played'
      span.addEventListener('click', onClickRemoveListened)

      const firstText = firstChildWithText(a[i]) || a[i].firstChild
      firstText.parentNode.insertBefore(span, firstText)
    } else {
      div = divCheck.cloneNode(true)
      div.addEventListener('mouseover', mouseOverDivCheck)
      div.addEventListener('mouseout', mouseOutDivCheck)
      div.addEventListener('click', onClickSetListened)
    }
    a[i].appendChild(div)
    lastKey = key
  }
}
function removeTheTimeHasComeToOpenThyHeartWallet () {
  if ('theTimeHasComeToOpenThyHeartWallet' in document.head.dataset) {
    return
  }
  document.head.dataset.theTimeHasComeToOpenThyHeartWallet = true
  document.head.appendChild(document.createElement('script')).innerHTML = `
    Log.debug("theTimeHasComeToOpenThyHeartWallet: start...")
    function removeViaQuerySelector (parent, selector) {
      if (typeof selector === 'undefined') {
        selector = parent
        parent = document
      }
      for (let el = parent.querySelector(selector); el; el = parent.querySelector(selector)) {
        el.remove()
      }
    }

    if (TralbumData.play_cap_data) {
      TralbumData.play_cap_data.streaming_limit = 100
      TralbumData.play_cap_data.streaming_limits_enabled = false
    }
    for(let i = 0; i < TralbumData.trackinfo.length; i++) {
      TralbumData.trackinfo[i].is_capped = false
      TralbumData.trackinfo[i].play_count = 1
    }

    /* // Alternative would be create new player
    TralbumLimits.onPlayerInit = () => true
    TralbumLimits.updatePlayCounts = () => true
    Player.init(TralbumData, AlbumPage.onPlayerInit);
    */

    // Update player with modified TralbumData
    Player.update(TralbumData)
    Log.debug("theTimeHasComeToOpenThyHeartWallet: player updated")

    // Restore lyrics onClick
    function parentByClassName(node, className) {
      while(!node.parentNode.classList.contains(className)) {
        node = node.parentNode
        if (node.parentNode === document.documentElement) {
          return null
        }
      }
      return node.parentNode
    }
    function onLyricsClick (ev) {
      ev.preventDefault()
      const tr = parentByClassName(this, 'track_row_view')
      if (tr.classList.contains('current_track')) {
        parentByClassName(tr, 'track_list').classList.toggle('auto_lyrics')
      } else {
        tr.classList.toggle('showlyrics')
      }
    }
    document.querySelectorAll('#track_table .track_row_view .info_link a').forEach(function (a) {
      a.addEventListener('click', onLyricsClick)
    })

    // Hide popup (not really needed, but won't hurt)
    window.setInterval(function() {
      if(document.getElementById('play-limits-dialog-cancel-btn')) {
        document.getElementById('play-limits-dialog-cancel-btn').click()
        window.setTimeout(function() {
          removeViaQuerySelector(document, '.ui-dialog.ui-widget')
          removeViaQuerySelector(document, '.ui-widget-overlay')
        }, 100)
      }
    }, 3000)
    Log.debug("theTimeHasComeToOpenThyHeartWallet: done!")
  `
}

function makeCarouselPlayerGreatAgain () {
  if (player) {
    // Hide/minimize discography player
    const closePlayerOnCarouselIv = window.setInterval(function closePlayerOnCarouselInterval () {
      if (!document.getElementById('carousel-player') || document.getElementById('carousel-player').getClientRects()[0].bottom - window.innerHeight > 0) {
        return
      }
      if (player.style.display === 'none') {
        // Put carousel player back down in normal position, because discography player is hidden forever
        document.getElementById('carousel-player').style.bottom = '0px'
        window.clearInterval(closePlayerOnCarouselIv)
      } else if (!player.style.bottom) {
        // Minimize discography player and push carousel player up above the minimized player
        musicPlayerToggleMinimize.call(player.querySelector('.minimizebutton'), null, true)
        document.getElementById('carousel-player').style.bottom = (player.clientHeight - 57) + 'px'
      }
    }, 5000)
  }

  let addListenedButtonToCarouselPlayerLast = null
  const addListenedButtonToCarouselPlayer = function listenedButtonOnCarouselPlayer () {
    const url = document.querySelector('#carousel-player a[href]') ? albumKey(document.querySelector('#carousel-player a[href]').href) : null
    if (url && addListenedButtonToCarouselPlayerLast === url) {
      return
    }
    if (!url) {
      console.log('No url found in carousel player: `#carousel-player a[href]`')
      return
    }
    addListenedButtonToCarouselPlayerLast = url

    removeViaQuerySelector('#carousel-player .carousellistenedstatus')

    const a = document.createElement('a')
    a.className = 'carousellistenedstatus'
    a.addEventListener('click', ev => ev.preventDefault())
    document.querySelector('#carousel-player .controls-extra').insertBefore(a, document.querySelector('#carousel-player .controls-extra').firstChild)
    a.innerHTML = '<span class="listenedstatus">Loading...</span>'
    a.href = 'https://' + url
    makeAlbumLinksGreat(a.parentNode).then(function () {
      removeViaQuerySelector(a, '.listenedstatus')
      const span = document.createElement('span')
      span.addEventListener('click', function () {
        const span = this
        span.parentNode.querySelector('.bdp_check_container').click()
        window.setTimeout(function () {
          if (span.parentNode.querySelector('.bdp_check_container').textContent.indexOf('Played') !== -1) {
            span.parentNode.innerHTML = 'Listened'
          } else {
            span.parentNode.innerHTML = 'Unplayed'
          }
        }, 3000)
      })
      if (a.querySelector('.bdp_check_onchecked_text')) {
        span.className = 'listenedstatus listened'
        span.innerHTML = '<span class="listened-symbol">✓</span> <span class="listened-label">Played</span>'
      } else {
        span.className = 'listenedstatus mark-listened'
        span.innerHTML = '<span class="mark-listened-symbol">✓</span> <span class="mark-listened-label">Mark as played</span>'
      }
      a.insertBefore(span, a.firstChild)
      a.dataset.textContent = document.querySelector('#carousel-player .now-playing .info a .artist span').textContent + ' - ' + document.querySelector('#carousel-player .now-playing .info a .title').textContent
    })
  }

  let lastMediaHubMeta = [null, null]
  const updateChromePositionState = function () {
    const audio = document.querySelector('body>audio')
    if (audio && 'mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
      navigator.mediaSession.setPositionState({
        duration: audio.duration || 180,
        playbackRate: audio.playbackRate,
        position: audio.currentTime
      })
    }
  }
  const addChromeMediaHubToCarouselPlayer = function chromeMediaHubToCarouselPlayer () {
    // Media hub
    if ('mediaSession' in navigator) {
      const audio = document.querySelector('body>audio')
      if (audio) {
        navigator.mediaSession.playbackState = !audio.paused ? 'playing' : 'paused'
        updateChromePositionState()
      }

      const title = document.querySelector('#carousel-player .info-progress span[data-bind*="trackTitle"]').textContent.trim()
      const artwork = document.querySelector('#carousel-player .now-playing img').src
      if (lastMediaHubMeta[0] === title && lastMediaHubMeta[1] === artwork) {
        return
      }
      lastMediaHubMeta = [title, artwork]
      navigator.mediaSession.metadata = new MediaMetadata({
        title: title,
        artist: document.querySelector('#carousel-player .now-playing .artist span').textContent.trim(),
        album: document.querySelector('#carousel-player .now-playing .title').textContent.trim(),
        artwork: [{
          src: artwork,
          sizes: '350x350',
          type: 'image/jpeg'
        }]
      })
      if (!document.querySelector('#carousel-player .transport .prev-icon').classList.contains('disabled')) {
        navigator.mediaSession.setActionHandler('previoustrack', () => document.querySelector('#carousel-player .transport .prev-icon').click())
      } else {
        navigator.mediaSession.setActionHandler('previoustrack', null)
      }
      if (!document.querySelector('#carousel-player .transport .next-icon').classList.contains('disabled')) {
        navigator.mediaSession.setActionHandler('nexttrack', () => document.querySelector('#carousel-player .transport .next-icon').click())
      } else {
        navigator.mediaSession.setActionHandler('nexttrack', null)
      }
      const playButton = document.querySelector('#carousel-player .playpause .play')
      if (playButton && playButton.style.display === 'none') {
        navigator.mediaSession.setActionHandler('play', null)
        navigator.mediaSession.setActionHandler('pause', function () {
          document.querySelector('#carousel-player .playpause').click()
          navigator.mediaSession.playbackState = 'paused'
        })
      } else {
        navigator.mediaSession.setActionHandler('play', function () {
          document.querySelector('#carousel-player .playpause').click()
          navigator.mediaSession.playbackState = 'playing'
        })
        navigator.mediaSession.setActionHandler('pause', null)
      }

      if (audio) {
        navigator.mediaSession.setActionHandler('seekbackward', function (event) {
          const skipTime = event.seekOffset || DEFAULTSKIPTIME
          audio.currentTime = Math.max(audio.currentTime - skipTime, 0)
          updateChromePositionState()
        })
        navigator.mediaSession.setActionHandler('seekforward', function (event) {
          const skipTime = event.seekOffset || DEFAULTSKIPTIME
          audio.currentTime = Math.min(audio.currentTime + skipTime, audio.duration)
          updateChromePositionState()
        })

        try {
          navigator.mediaSession.setActionHandler('stop', function () {
            audio.pause()
            audio.currentTime = 0
            navigator.mediaSession.playbackState = 'paused'
          })
        } catch (error) {
          console.log('Warning! The "stop" media session action is not supported.')
        }

        try {
          navigator.mediaSession.setActionHandler('seekto', function (event) {
            if (event.fastSeek && ('fastSeek' in audio)) {
              audio.fastSeek(event.seekTime)
              return
            }
            audio.currentTime = event.seekTime
            updateChromePositionState()
          })
        } catch (error) {
          console.log('Warning! The "seekto" media session action is not supported.')
        }
      }
    }
  }

  window.setInterval(function addListenedButtonToCarouselPlayerInterval () {
    if (!document.getElementById('carousel-player') || document.getElementById('carousel-player').getClientRects()[0].bottom - window.innerHeight > 0) {
      return
    }
    addListenedButtonToCarouselPlayer()
    addChromeMediaHubToCarouselPlayer()
  }, 2000)

  addStyle(`
  #carousel-player a.carousellistenedstatus:link,#carousel-player a.carousellistenedstatus:visited,#carousel-player a.carousellistenedstatus:hover{
    text-decoration:none;
    cursor:default
  }
  #carousel-player .listened .listened-symbol{
    color:rgb(0,220,50);
    text-shadow:1px 0px #DDD,-1px 0px #DDD,0px -1px #DDD,0px 1px #DDD
  }
  #carousel-player .mark-listened .mark-listened-symbol{
    color:#FFF;
    text-shadow:1px 0px #959595,-1px 0px #959595,0px -1px #959595,0px 1px #959595
  }
  #carousel-player .mark-listened:hover .mark-listened-symbol{
    text-shadow:1px 0px #0AF,-1px 0px #0AF,0px -1px #0AF,0px 1px #0AF
  }
  `)
}

async function addListenedButtonToCollectControls () {
  const lastLi = document.querySelector('.share-panel-wrapper-desktop ul li')
  if (!lastLi) {
    window.setTimeout(addListenedButtonToCollectControls, 300)
    return
  }

  const checkSymbol = NOEMOJI ? '✓' : '✔'

  const myalbums = JSON.parse(await GM.getValue('myalbums', '{}'))

  const key = albumKey(document.location.href)
  const listened = key in myalbums && 'listened' in myalbums[key] && myalbums[key].listened

  const onClickSetListened = async function onClickSetListenedAsync (ev) {
    ev.preventDefault()

    let parent = this
    for (let j = 0; parent.tagName !== 'LI' && j < 20; j++) {
      parent = parent.parentNode
    }
    setTimeout(function showSavingLabel () {
      parent.style.cursor = 'wait'
      parent.innerHTML = 'Saving...'
    }, 0)

    const url = document.location.href
    let albumData = await myAlbumsGetAlbum(url)
    if (!albumData) {
      albumData = await myAlbumsNewFromUrl(url, { title: this.dataset.textContent })
    }
    albumData.listened = (new Date()).toJSON()

    await myAlbumsUpdateAlbum(albumData)

    window.setTimeout(addListenedButtonToCollectControls, 100)
  }
  const onClickRemoveListened = async function onClickRemoveListenedAsync (ev) {
    ev.preventDefault()

    let parent = this
    for (let j = 0; parent.tagName !== 'LI' && j < 20; j++) {
      parent = parent.parentNode
    }
    setTimeout(function showSavingLabel () {
      parent.style.cursor = 'wait'
      parent.innerHTML = 'Saving...'
    }, 0)

    const url = document.location.href
    const albumData = await myAlbumsGetAlbum(url)
    if (albumData) {
      albumData.listened = false
      await myAlbumsUpdateAlbum(albumData)
    }

    window.setTimeout(addListenedButtonToCollectControls, 100)
  }

  removeViaQuerySelector('#discographyplayer_sharepanel')

  const li = lastLi.parentNode.appendChild(document.createElement('li'))
  const button = li.appendChild(document.createElement('span'))
  const icon = button.appendChild(document.createElement('span'))
  const a = button.appendChild(document.createElement('a'))

  li.setAttribute('id', 'discographyplayer_sharepanel')
  a.addEventListener('click', (ev) => ev.preventDefault())
  icon.className = 'sharepanelchecksymbol'

  if (listened) {
    const date = new Date(listened)
    const since = timeSince(date)

    button.title = since + '\nClick to mark as NOT played'
    button.addEventListener('click', onClickRemoveListened)

    icon.style.color = 'rgb(0,220,50)'
    icon.style.textShadow = '1px 0px #DDD,-1px 0px #DDD,0px -1px #DDD,0px 1px #DDD'
    icon.style.paddingRight = '5px'
    icon.appendChild(document.createTextNode(checkSymbol))

    a.appendChild(document.createTextNode('Played'))

    li.appendChild(document.createTextNode(' - '))

    const link = li.appendChild(document.createElement('span'))
    const viewLink = link.appendChild(document.createElement('a'))
    viewLink.href = findUserProfileUrl() + '#listened-tab'
    viewLink.title = 'View list of played albums'
    viewLink.appendChild(document.createTextNode('view'))
  } else {
    button.title = 'Click to mark as played'
    button.addEventListener('click', onClickSetListened)
    try {
      icon.style.color = window.getComputedStyle(document.getElementById('pgBd')).backgroundColor
      icon.style.textShadow = '1px 0px #959595,-1px 0px #959595,0px -1px #959595,0px 1px #959595'
      icon.style.paddingRight = '5px'
    } catch (e) {
      icon.style.color = '#959595'
      icon.style.fontWeight = 700
    }
    icon.appendChild(document.createTextNode(checkSymbol))

    a.appendChild(document.createTextNode('Unplayed'))
  }
}

function makeListenedListTabLink () {
  const grid = document.getElementById('grids').appendChild(document.createElement('div'))
  grid.className = 'grid'
  grid.id = 'listened-grid'

  const inner = grid.appendChild(document.createElement('div'))
  inner.className = 'inner'
  inner.innerHTML = 'Loading...'

  const li = document.querySelector('ol#grid-tabs').appendChild(document.createElement('li'))
  li.id = 'listenedlisttablink'
  li.dataset.tab = 'listened'
  li.setAttribute('data-grid-id', 'listened-grid')
  const span = li.appendChild(document.createElement('span'))
  span.className = 'tab-title'
  span.appendChild(document.createTextNode('played'))

  const count = span.appendChild(document.createElement('span'))
  count.className = 'count'
  GM.getValue('myalbums', '{}').then(function myalbumsLoaded (str) {
    let n = 0
    const myalbums = JSON.parse(str)
    for (const key in myalbums) {
      if (myalbums[key].listened) {
        n++
      }
    }
    count.appendChild(document.createTextNode(n))
  })
  li.addEventListener('click', showListenedListTab)

  return li
}

async function showListenedListTab () {
  if (document.getElementById('owner-controls')) document.getElementById('owner-controls').style.display = 'none'
  if (document.getElementById('wishlist-controls')) document.getElementById('wishlist-controls').style.display = 'none'

  const grid = document.getElementById('listened-grid')
  const gridActive = document.querySelector('#grids .grid.active')
  if (gridActive && gridActive !== grid) {
    gridActive.classList.remove('active')
  }
  grid.classList.add('active')

  const tabLink = document.getElementById('listenedlisttablink')
  const tabLinkActive = document.querySelector('#grid-tab li.active')
  if (tabLinkActive && tabLinkActive !== tabLink) {
    tabLinkActive.classList.remove('active')
  }
  tabLink.classList.add('active')

  if (grid.querySelector('.collection-items')) {
    return
  }

  grid.innerHTML = ''

  const collectionItems = grid.appendChild(document.createElement('div'))
  collectionItems.className = 'collection-items'

  const collectionGrid = collectionItems.appendChild(document.createElement('ol'))
  collectionGrid.className = 'collection-grid'

  const myalbums = JSON.parse(await GM.getValue('myalbums', '{}'))

  for (const key in myalbums) {
    const albumData = myalbums[key]

    if (!albumData.listened) {
      continue
    }

    const artist = albumData.artist || 'Unkown artist'
    const title = albumData.title || 'Unkown title'
    const albumCover = albumData.albumCover || 'https://bandcamp.com/img/0.gif'
    const url = key
    const date = new Date(albumData.listened)
    const since = timeSince(date)
    const dateStr = dateFormater(date)
    let releaseDate
    if ('releaseDate' in albumData) {
      releaseDate = dateFormaterRelease(new Date(albumData.releaseDate))
    } else {
      releaseDate = 'Unknown'
    }

    const li = collectionGrid.appendChild(document.createElement('li'))
    li.className = 'collection-item-container'
    li.innerHTML = `
      <div class="collection-item-gallery-container">
        <span class="bc-ui2 collect-item-icon-alt"></span>
        <div class="collection-item-art-container">
          <img class="collection-item-art" alt="" src="${albumCover}">
        </div>
        <div class="collection-title-details">
          <a target="_blank" href="https://${url}" class="item-link">
            <div class="collection-item-title">${title}</div>
            <div class="collection-item-artist">by ${artist}</div>
          </a>
        </div>
        <div class="collection-item-fav-track">
          <span title="${since} ago" class="favoriteTrackLabel">played</span>
          <div title="${since} ago">
            <span class="fav-track-link">${dateStr}</span>
          </div>
          <span class="favoriteTrackLabel">released</span>
          <div>
            <span class="fav-track-link">${releaseDate}</span>
          </div>
        </div>
      </div>
    `
  }
}

function addVolumeBarToAlbumPage () {
  // Do not add if one of these scripts already added a volume bar
  // https://openuserjs.org/scripts/cuzi/Bandcamp_Volume_Bar
  // https://openuserjs.org/scripts/Mranth0ny62/Bandcamp_Volume_Bar
  // https://openuserjs.org/scripts/ArtificialInput/Bandcamp_Volume_Bar
  // http://greasyfork.icu/en/scripts/11047-bandcamp-volume-bar/
  // http://greasyfork.icu/en/scripts/38012-bandcamp-volume-bar/
  if (document.querySelector('.volumeControl')) {
    return false
  }

  if (!document.querySelector('#trackInfoInner .playbutton')) {
    return
  }

  addStyle(`
    /* Hide if inline_player is hidden */
    .hidden .volumeButton,.hidden .volumeControl,.hidden .volumeLabel{
      display:none
    }

    .volumeButton {
      display: inline-block;
      user-select:none;
      background: #fff;
      border: 1px solid #d9d9d9;
      border-radius: 2px;
      cursor: pointer;
      min-height: 50px;
      min-width: 54px;
      text-align:center;
      margin-top:5px;
    }

    .volumeSymbol {
      margin-top: 16px;
      font-size: 30px;
      color:#222;
      font-weight:bolder;
      transform: rotate(-90deg);
      text-shadow: rgb(255, 255, 255) 0px 0px 0px;
      transition: text-shadow linear 300ms;
    }
    .volumeControl {
      display:inline-block;
      user-select:none;
      top:5px;
    }
    .volumeLabel {
      display:inline-block;
    }

    .nextsongcontrolbutton {
      background:#fff;
      border:1px solid #d9d9d9;
      border-radius:2px;
      cursor:pointer;
      height:24px;
      width:35px;
      margin-top:2px;
      margin-left:80px;
      float:left;
      text-align:center
    }

    .nextsongcontrolicon {
      background-size:cover;
      background-image:${spriteRepeatShuffle};
      width:31px;
      height:20px;
      filter:drop-shadow(#FFF 1px 1px 2px);
      display:inline-block;
      margin-top:1px;
      transition: filter 500ms;
    }
    .nextsongcontrolbutton.active .nextsongcontrolicon {
      filter:drop-shadow(#0060F2 1px 1px 2px);
    }

  `)

  const playbutton = document.querySelector('#trackInfoInner .playbutton')
  const volumeButton = playbutton.cloneNode(true)
  document.querySelector('#trackInfoInner .inline_player').appendChild(volumeButton)
  volumeButton.classList.replace('playbutton', 'volumeButton')
  volumeButton.style.width = playbutton.clientWidth + 'px'
  const volumeSymbol = volumeButton.appendChild(document.createElement('div'))
  volumeSymbol.className = 'volumeSymbol'
  volumeSymbol.appendChild(document.createTextNode(CHROME ? '\uD83D\uDD5B' : '\u23F2'))

  const progbar = document.querySelector('#trackInfoInner .progbar_cell .progbar')
  const volumeBar = progbar.cloneNode(true)
  document.querySelector('#trackInfoInner .inline_player').appendChild(volumeBar)
  volumeBar.classList.add('volumeControl')
  volumeBar.style.width = Math.max(200, progbar.clientWidth) + 'px'
  const thumb = volumeBar.querySelector('.thumb')
  thumb.setAttribute('id', 'deluxe_thumb')
  const progbarFill = volumeBar.querySelector('.progbar_fill')

  const volumeLabel = document.createElement('div')
  document.querySelector('#trackInfoInner .inline_player').appendChild(volumeLabel)
  volumeLabel.classList.add('volumeLabel')

  let dragging = false
  let dragPos
  const width100 = volumeBar.clientWidth - (thumb.clientWidth + 2) // 2px border
  const rot0 = CHROME ? -180 : -90
  const rot100 = CHROME ? 350 : 265 - rot0
  const blue0 = 180
  const blue100 = 75
  const green0 = 90
  const green100 = 100
  const audioAlbumPage = document.querySelector('body>audio')
  addLogVolume(audioAlbumPage)
  const volumeBarPos = volumeBar.getBoundingClientRect().left

  const displayVolume = function updateDisplayVolume () {
    const level = audioAlbumPage.logVolume
    volumeLabel.innerHTML = parseInt(level * 100.0) + '%'
    thumb.style.left = (width100 * level) + 'px'
    progbarFill.style.width = parseInt(level * 100.0) + '%'
    volumeSymbol.style.transform = 'rotate(' + ((level * rot100) + rot0) + 'deg)'
    if (level > 0.005) {
      volumeSymbol.style.textShadow = 'rgb(0, ' + ((level * green100) + green0) + ', ' + ((level * blue100) + blue0) + ') 0px 0px 4px'
      volumeSymbol.style.color = '#03a'
    } else {
      volumeSymbol.style.textShadow = 'rgb(255, 255, 255) 0px 0px 0px'
      volumeSymbol.style.color = '#222'
    }
  }

  thumb.addEventListener('mousedown', function thumbMouseDown (ev) {
    if (ev.button === 0) {
      dragging = true
      dragPos = ev.offsetX
    }
  })
  volumeBar.addEventListener('mouseup', function thumbMouseUp (ev) {
    if (ev.button !== 0) {
      return
    }
    ev.preventDefault()
    ev.stopPropagation()

    if (!dragging) {
      // Click on volume bar without dragging:
      audioAlbumPage.muted = false
      audioAlbumPage.logVolume = Math.max(0.0, Math.min(1.0, (ev.pageX - volumeBarPos) / width100))
      displayVolume()
    }
    dragging = false
    GM.setValue('volume', audioAlbumPage.logVolume)
  })
  document.addEventListener('mouseup', function documentMouseUp (ev) {
    if (ev.button === 0 && dragging) {
      dragging = false
      ev.preventDefault()
      ev.stopPropagation()
      GM.setValue('volume', audioAlbumPage.logVolume)
    }
  })
  document.addEventListener('mousemove', function documentMouseMove (ev) {
    if (ev.button === 0 && dragging) {
      ev.preventDefault()
      ev.stopPropagation()
      audioAlbumPage.muted = false
      audioAlbumPage.logVolume = Math.max(0.0, Math.min(1.0, ((ev.pageX - volumeBarPos) - dragPos) / width100))
      displayVolume()
    }
  })
  const onWheel = function onMouseWheel (ev) {
    ev.preventDefault()
    const direction = Math.min(Math.max(-1.0, ev.deltaY), 1.0)
    audioAlbumPage.logVolume = Math.min(Math.max(0.0, audioAlbumPage.logVolume - 0.05 * direction), 1.0)
    displayVolume()
    GM.setValue('volume', audioAlbumPage.logVolume)
  }
  volumeButton.addEventListener('wheel', onWheel, false)
  volumeBar.addEventListener('wheel', onWheel, false)
  volumeButton.addEventListener('click', function onVolumeButtonClick (ev) {
    if (audioAlbumPage.logVolume < 0.01) {
      if ('lastvolume' in audioAlbumPage.dataset && audioAlbumPage.dataset.lastvolume) {
        audioAlbumPage.logVolume = audioAlbumPage.dataset.lastvolume
        GM.setValue('volume', audioAlbumPage.logVolume)
      } else {
        audioAlbumPage.logVolume = 1.0
      }
    } else {
      audioAlbumPage.dataset.lastvolume = audioAlbumPage.logVolume
      audioAlbumPage.logVolume = 0.0
    }
    displayVolume()
  })

  displayVolume()

  window.clearInterval(ivRestoreVolume)

  // Repeat/shuffle buttons
  const playnextcontrols = document.querySelector('#trackInfoInner .inline_player').appendChild(document.createElement('div'))

  // Show repeat button
  const repeatButton = playnextcontrols.appendChild(document.createElement('div'))
  repeatButton.classList.add('nextsongcontrolbutton', 'repeat')
  repeatButton.setAttribute('title', 'Repeat')
  const repeatButtonIcon = repeatButton.appendChild(document.createElement('div'))
  repeatButtonIcon.classList.add('nextsongcontrolicon')

  repeatButton.dataset.repeat = 'none'
  repeatButtonIcon.style.backgroundPositionY = '-20px'

  repeatButton.addEventListener('click', function () {
    const posY = this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY
    if (posY === '-20px') {
      this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY = '-40px'
      this.classList.toggle('active')
      this.dataset.repeat = 'one'
    } else if (posY === '-40px') {
      this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY = '-60px'
      this.dataset.repeat = 'all'
    } else {
      this.getElementsByClassName('nextsongcontrolicon')[0].style.backgroundPositionY = '-20px'
      this.classList.toggle('active')
      this.dataset.repeat = 'none'
    }
  })
  if (allFeatures.albumPageAutoRepeatAll.enabled) {
    repeatButton.click()
    repeatButton.click()
  }

  // Show shuffle button
  const shuffleButton = playnextcontrols.appendChild(document.createElement('div'))
  if (document.querySelectorAll('#track_table a div').length > 2) {
    shuffleButton.classList.add('nextsongcontrolbutton', 'shuffle')
    shuffleButton.setAttribute('title', 'Shuffle')
    const shuffleButtonIcon = shuffleButton.appendChild(document.createElement('div'))
    shuffleButtonIcon.classList.add('nextsongcontrolicon')
    shuffleButtonIcon.style.backgroundPositionY = '0px'

    shuffleButton.addEventListener('click', function () {
      this.classList.toggle('active')
    })
  }

  const findLastSongIndex = function () {
    const allDiv = document.querySelectorAll('#track_table a div')
    const nextDiv = document.querySelector('#track_table a div.playing')
    if (!nextDiv) {
      return allDiv.length - 1
    }
    for (let i = 1; i < allDiv.length; i++) {
      if (allDiv[i] === nextDiv) {
        return i - 1
      }
    }
    return -1
  }

  const albumPageAudioOnEnded = function (ev) {
    const allDiv = document.querySelectorAll('#track_table a div')

    if (repeatButton.dataset.repeat === 'one') {
      // Click on last song again
      if (allDiv.length > 0) {
        allDiv[findLastSongIndex()].click()
      } else {
        // No tracklist, click on play button
        document.querySelector('#trackInfoInner .inline_player .playbutton').click()
      }
    } else if (shuffleButton.classList.contains('active') && allDiv.length > 1) {
      // Find last song
      const lastSongIndex = findLastSongIndex()
      // Set a random song (that is not the last song)
      let index = lastSongIndex
      while (index === lastSongIndex) {
        index = randomIndex(allDiv.length)
      }
      if (index !== lastSongIndex + 1) {
        allDiv[index].click()
      }
    } else if (repeatButton.dataset.repeat === 'all') {
      if (findLastSongIndex() === allDiv.length - 1) {
        if (allDiv[0]) {
          allDiv[0].click() // Click on first song's play button
        } else {
          // No tracklist, click on play button
          document.querySelector('#trackInfoInner .inline_player .playbutton').click()
        }
      }
    }
  }

  let lastMediaHubTitle = null
  const updateChromePositionState = function () {
    if (audioAlbumPage && 'mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {
      navigator.mediaSession.setPositionState({
        duration: audioAlbumPage.duration || 180,
        playbackRate: audioAlbumPage.playbackRate,
        position: audioAlbumPage.currentTime
      })
    }
  }
  const albumPageUpdateMediaHubListener = function albumPageUpdateMediaHub () {
    // Media hub
    if ('mediaSession' in navigator) {
      if (audioAlbumPage) {
        navigator.mediaSession.playbackState = !audioAlbumPage.paused ? 'playing' : 'paused'
        updateChromePositionState()
      }
      const title = document.querySelector('#trackInfoInner .inline_player .title').textContent.trim()
      if (lastMediaHubTitle === title) {
        return
      }
      lastMediaHubTitle = title
      const TralbumData = unsafeWindow.TralbumData
      // Pre load image to get dimension
      const cover = document.createElement('img')
      cover.onload = function onCoverLoaded () {
        navigator.mediaSession.metadata = new MediaMetadata({
          title: title,
          artist: TralbumData.artist,
          album: TralbumData.current.title,
          artwork: [{
            src: cover.src,
            sizes: `${cover.width}x${cover.height}`,
            type: 'image/jpeg'
          }]
        })
      }
      cover.src = `https://f4.bcbits.com/img/a${TralbumData.current.art_id}_2.jpg`
      if (!document.querySelector('#trackInfoInner .inline_player .prevbutton').classList.contains('hiddenelem')) {
        navigator.mediaSession.setActionHandler('previoustrack', () => document.querySelector('#trackInfoInner .inline_player .prevbutton').click())
      } else {
        navigator.mediaSession.setActionHandler('previoustrack', null)
      }
      if (!document.querySelector('#trackInfoInner .inline_player .nextbutton').classList.contains('hiddenelem')) {
        navigator.mediaSession.setActionHandler('nexttrack', () => document.querySelector('#trackInfoInner .inline_player .nextbutton').click())
      } else {
        navigator.mediaSession.setActionHandler('nexttrack', null)
      }
      if (audioAlbumPage) {
        navigator.mediaSession.setActionHandler('play', function () {
          audioAlbumPage.play()
          navigator.mediaSession.playbackState = 'playing'
        })
        navigator.mediaSession.setActionHandler('pause', function () {
          audioAlbumPage.play()
          navigator.mediaSession.playbackState = 'paused'
        })

        navigator.mediaSession.setActionHandler('seekbackward', function (event) {
          const skipTime = event.seekOffset || DEFAULTSKIPTIME
          audioAlbumPage.currentTime = Math.max(audioAlbumPage.currentTime - skipTime, 0)
          updateChromePositionState()
        })
        navigator.mediaSession.setActionHandler('seekforward', function (event) {
          const skipTime = event.seekOffset || DEFAULTSKIPTIME
          audioAlbumPage.currentTime = Math.min(audioAlbumPage.currentTime + skipTime, audioAlbumPage.duration)
          updateChromePositionState()
        })

        try {
          navigator.mediaSession.setActionHandler('stop', function () {
            audioAlbumPage.pause()
            audioAlbumPage.currentTime = 0
            navigator.mediaSession.playbackState = 'paused'
          })
        } catch (error) {
          console.log('Warning! The "stop" media session action is not supported.')
        }

        try {
          navigator.mediaSession.setActionHandler('seekto', function (event) {
            if (event.fastSeek && ('fastSeek' in audioAlbumPage)) {
              audioAlbumPage.fastSeek(event.seekTime)
              return
            }
            audioAlbumPage.currentTime = event.seekTime
            updateChromePositionState()
          })
        } catch (error) {
          console.log('Warning! The "seekto" media session action is not supported.')
        }
      }
    }
  }

  audioAlbumPage.addEventListener('ended', albumPageAudioOnEnded)
  audioAlbumPage.addEventListener('play', albumPageUpdateMediaHubListener)
  audioAlbumPage.addEventListener('ended', albumPageUpdateMediaHubListener)
}

function clickAddToWishlist () {
  const wishButton = document.querySelector('#collect-item>*')
  if (!wishButton) {
    window.setTimeout(clickAddToWishlist, 300)
    return
  }
  wishButton.click()
  if (document.querySelector('#collection-main a')) {
    // if logged in, the click should be successful, so try to close the window
    window.setTimeout(window.close, 1000)
  }
}

function addReleaseDateButton () {
  const meta = document.querySelector('*[itemprop="datePublished"]')
  if (!meta || !meta.content) {
    return // no release date found
  }
  const TralbumData = unsafeWindow.TralbumData
  const now = new Date()
  const releaseDate = new Date(TralbumData.current.release_date)
  const days = parseInt(Math.ceil((releaseDate - now) / (1000 * 60 * 60 * 24)))
  if (releaseDate < now) {
    return // Release date is in the past
  }
  const key = albumKey(TralbumData.url)

  addStyle(`
  .releaseReminderButton {
    font-size:13px;
    font-weight:700;
    cursor:pointer;
    transition: border 500ms, padding 500ms
  }
  .releaseReminderButton.active {
    border-radius:5px;
    padding:0px 5px;
    border:#3fb32f66 solid 2px
  }
  .releaseReminderButton:hover .releaseLabel {
    text-decoration:underline
  }
  `)

  const div = document.querySelector('.share-collect-controls').appendChild(document.createElement('div'))
  div.style = 'margin-top:4px'
  const span = div.appendChild(document.createElement('span'))
  span.className = 'custom-link-color releaseReminderButton'
  span.title = 'Releases ' + dateFormaterRelease(releaseDate)
  const daysStr = days === 1 ? 'tomorrow' : (`in ${days} days`)
  span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Notify <time datetime="${releaseDate.toISOString()}">${daysStr}</time></span>`
  span.addEventListener('click', (ev) => toggleReleaseReminder(ev, span))

  GM.getValue('releasereminder', '{}').then(function (str) {
    const releaseReminderData = JSON.parse(str)
    if (key in releaseReminderData) {
      span.classList.add('active')
      span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Reminder set (<time datetime="${releaseDate.toISOString()}">${daysStr}</time>)</span>`
    }
  })
}

async function toggleReleaseReminder (ev, span) {
  const TralbumData = unsafeWindow.TralbumData
  const key = albumKey(TralbumData.url)
  const releaseReminderData = JSON.parse(await GM.getValue('releasereminder', '{}'))
  if (key in releaseReminderData) {
    delete releaseReminderData[key]
  } else {
    releaseReminderData[key] = {
      albumCover: `https://f4.bcbits.com/img/a${TralbumData.art_id}_2.jpg`,
      releaseDate: TralbumData.current.release_date,
      artist: TralbumData.artist,
      title: TralbumData.current.title
    }
  }
  await GM.setValue('releasereminder', JSON.stringify(releaseReminderData))

  if (span) {
    const releaseDate = new Date(TralbumData.current.release_date)
    const now = new Date()
    const days = parseInt(Math.ceil((releaseDate - now) / (1000 * 60 * 60 * 24)))
    const daysStr = days === 1 ? 'tomorrow' : (`in ${days} days`)
    if (key in releaseReminderData) {
      span.classList.add('active')
      span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Reminder set (<time datetime="${releaseDate.toISOString()}">${daysStr}</time>)</span>`
    } else {
      span.classList.remove('active')
      span.innerHTML = `<span>\u23F0</span> <span class="releaseLabel">Notify <time datetime="${releaseDate.toISOString()}">${daysStr}</time></span>`
    }
  }
}
async function removeReleaseReminder (ev) {
  ev.preventDefault()
  const key = this.parentNode.dataset.key
  const releaseReminderData = JSON.parse(await GM.getValue('releasereminder', '{}'))
  if (key in releaseReminderData) {
    delete releaseReminderData[key]
    await GM.setValue('releasereminder', JSON.stringify(releaseReminderData))
  }
  this.parentNode.remove()
}
function maximizePastReleases () {
  document.getElementById('pastreleases').style.opacity = 0.0
  window.setTimeout(() => showPastReleases(null, true), 500)
  document.getElementById('pastreleases').removeEventListener('click', maximizePastReleases)
}
async function showPastReleases (ev, forceShow) {
  let hideDate = await GM.getValue('pastreleaseshidden', false)
  const releaseReminderData = JSON.parse(await GM.getValue('releasereminder', '{}'))
  const releases = []
  let pastReleasesCounter = 0
  const now = new Date()
  now.setHours(23)
  now.setMinutes(59)
  for (const key in releaseReminderData) {
    releaseReminderData[key].key = key
    releaseReminderData[key].date = new Date(releaseReminderData[key].releaseDate)
    releaseReminderData[key].past = now >= releaseReminderData[key].date
    if (releaseReminderData[key].past) {
      pastReleasesCounter++
    }
    releases.push(releaseReminderData[key])
  }
  releases.sort((a, b) => b.date - a.date)

  if (releases.length === 0 || pastReleasesCounter === 0) {
    return
  }

  if (!document.getElementById('pastreleases')) {
    addStyle(`
    #pastreleases {
      position:fixed;
      bottom:1%;
      left:10px;
      background:#d5dce4;
      color:#033162;
      font-size:10pt;
      border:1px solid #033162;
      z-index:200;
      opacity:0.0;
      transition: opacity 700ms;
      overflow:auto
    }
    #pastreleases .tablediv {
      display: table;
      position:relative;
    }
    #pastreleases .entry,#pastreleases .header {
      display:table-row
    }
    #pastreleases .entry > *,#pastreleases .header > * {
      display:table-cell;
      line-height:21pt
    }
    #pastreleases .upcoming {
      cursor:pointer;
      font-size:x-small
    }
    #pastreleases .controls {
      cursor:pointer;
      position:absolute;
      top:0px;
      right:1px;
      line-height:11pt
    }
    #pastreleases .entry:link {
      position:relative;
      border-top:1px solid #033162;
      color:#033162;
      text-decoration:none
    }
    #pastreleases .entry:nth-child(odd) {
      background:#c5ccd4
    }
    #pastreleases .entry:hover,#pastreleases .entry:visited {
      color:#033162;
      text-decoration:none
    }
    #pastreleases .entry.future {
      display:none;
      background:#9fc2ea;
    }
    #pastreleases .entry.future:nth-child(odd) {
      background:#8fc2e1;
    }
    #pastreleases .entry .image {
      background-size:contain;
      width:21pt;
      height:21pt
    }
    #pastreleases .entry:hover .image {
      display:block;
      position:fixed;
      bottom:10px;
      top:50%;
      left:50%;
      margin-right:-50%;
      transform:translate(-50%, -50%);
      width:350px;
      height:350px;
      background:black;
      border:5px solid white;
    }
    #pastreleases .entry time {
      padding-right: 2px
    }
    #pastreleases .entry .title {
      padding-left: 2px;
      border-left: 1px solid #47a2bd
    }
    #pastreleases .remove {
      font-family:sans-serif;
      color:#97174e;
      font-size: small;
      padding-right:3px
    }
    `)
  }
  const div = document.body.appendChild(document.getElementById('pastreleases') || document.createElement('div'))
  div.setAttribute('id', 'pastreleases')
  div.style.maxHeight = (document.documentElement.clientHeight - 50) + 'px'
  div.style.maxWidth = (document.documentElement.clientWidth - 100) + 'px'
  window.setTimeout(function () {
    div.style.opacity = 1.0
  }, 200)
  div.innerHTML = ''

  const table = div.appendChild(document.createElement('div'))
  table.classList.add('tablediv')

  const firstRow = table.appendChild(document.createElement('div'))
  firstRow.classList.add('header')
  firstRow.appendChild(document.createTextNode('\u23F0'))
  firstRow.appendChild(document.createElement('span'))

  if (!forceShow && hideDate && !isNaN(hideDate = new Date(hideDate)) && (new Date() - hideDate) < 1000 * 60 * 60) {
    firstRow.appendChild(document.createTextNode(`${pastReleasesCounter} release` + (pastReleasesCounter === 1 ? '' : 's')))
    table.addEventListener('click', maximizePastReleases)
    return
  } else {
    GM.setValue('pastreleaseshidden', '')
  }

  const upcoming = firstRow.appendChild(document.createElement('span'))
  if (releases.length !== pastReleasesCounter) {
    upcoming.appendChild(document.createTextNode(' Show upcoming'))
    upcoming.classList.add('upcoming')
    upcoming.addEventListener('click', function () {
      document.querySelectorAll('#pastreleases .future').forEach(function (el) {
        el.style.display = 'table-row'
      })
      this.remove()
    })
  }

  const controls = firstRow.appendChild(document.createElement('span'))
  controls.classList.add('controls')

  const refresh = controls.appendChild(document.createElement('span'))
  refresh.setAttribute('title', 'Update')
  refresh.addEventListener('click', function () {
    document.getElementById('pastreleases').style.opacity = 0.0
    window.setTimeout(() => showPastReleases(null, true), 1200)
  })
  refresh.appendChild(document.createTextNode(NOEMOJI ? 'Refresh' : '⟳'))

  const close = controls.appendChild(document.createElement('span'))
  close.setAttribute('title', 'Hide')
  close.addEventListener('click', function () {
    GM.setValue('pastreleaseshidden', new Date().toJSON())
    document.getElementById('pastreleases').style.opacity = 0.0
    window.setTimeout(function () {
      document.getElementById('pastreleases').remove()
    }, 700)
  })
  close.appendChild(document.createTextNode('X'))

  releases.forEach(function (release) {
    const days = parseInt(Math.ceil((release.date - now) / (1000 * 60 * 60 * 24)))
    const daysStr = days === 1 ? 'tomorrow' : (`in ${days} days`)
    let title = `${release.artist} - ${release.title}`

    const entry = table.appendChild(document.createElement('a'))
    entry.setAttribute('title', title)
    entry.dataset.key = release.key
    entry.classList.add('entry')
    entry.classList.add(release.past ? 'past' : 'future')
    entry.setAttribute('href', document.location.protocol + '//' + release.key)
    entry.setAttribute('target', '_blank')

    const removeButton = entry.appendChild(document.createElement('span'))
    removeButton.setAttribute('title', 'Remove album')
    removeButton.classList.add('remove')
    removeButton.appendChild(document.createTextNode(NOEMOJI ? 'X' : '╳'))
    removeButton.addEventListener('click', removeReleaseReminder)

    const time = entry.appendChild(document.createElement('time'))
    time.setAttribute('datetime', release.date.toISOString())
    time.setAttribute('title', 'Releases ' + dateFormaterRelease(release.date))
    if (release.past) {
      time.appendChild(document.createTextNode(dateFormaterNumeric(release.date)))
    } else {
      time.appendChild(document.createTextNode(daysStr))
    }

    const span = entry.appendChild(document.createElement('span'))
    span.classList.add('title')
    title = title.length < 60 ? title : (title.substr(0, 57) + '…')
    span.appendChild(document.createTextNode(' ' + title))

    const image = entry.appendChild(document.createElement('div'))
    image.classList.add('image')
    image.style.backgroundRepeat = 'no-repeat'
    image.style.backgroundSize = 'contain'
    image.style.backgroundImage = `url(${release.albumCover})`
  })
}

function mainMenu (startBackup) {
  addStyle(`
    .deluxemenu {
      position:fixed;
      height:auto;
      overflow:auto;
      top:20px;
      left:20px;
      z-index:200;
      padding:5px;
      transition: left 1s;
      border:2px solid black;
      border-radius:10px;
      color:black;
      background:white;
    }
    .deluxemenu input{
      box-shadow: 2px 2px 5px #5555;
      transition: box-shadow 500ms;
    }
    .deluxemenu fieldset{
      border: 1px solid #000a;
      border-radius: 4px;
      box-shadow: 1px 1px 3px #0005;
    }
    .deluxemenu fieldset legend{
      margin-left: 10px;
      color: #000a
    }
    .breathe {
      animation: breathe 1.5s linear infinite
    }
    @keyframes breathe {
      50% { opacity: 0.3 }
    }
    .errorblink {
      animation: errorblink 1.5s linear infinite;
      border: 2px solid red;
    }
    @keyframes errorblink {
      50% { border-color:#6a0c41 }
    }

  `)

  if (startBackup === true) {
    exportMenu()
    return
  }

  if (document.querySelector('.deluxemenu')) {
    return
  }

  // Blur background
  if (document.getElementById('centerWrapper')) { document.getElementById('centerWrapper').style.filter = 'blur(4px)' }

  const main = document.body.appendChild(document.createElement('div'))
  main.className = 'deluxemenu'
  main.innerHTML = `<h2>${SCRIPT_NAME}</h2>
  Source code license: <a target="_blank" href="https://github.com/cvzi/Bandcamp-script-deluxe-edition/blob/master/LICENSE">MIT</a><br>
  Support: <a target="_blank" href="https://github.com/cvzi/Bandcamp-script-deluxe-edition">github.com/cvzi/Bandcamp-script-deluxe-edition</a><br>
  OUJS.org: <a target="_blank" href="https://openuserjs.org/scripts/cuzi/Bandcamp_script_(Deluxe_Edition)">openuserjs.org/scripts/cuzi/Bandcamp_script_(Deluxe_Edition)</a><br>
  Dark theme based on: <a target="_blank" href="https://userstyles.org/styles/171538/bandcamp-in-dark">"Bandcamp In Dark"</a> by <a target="_blank" href="https://userstyles.org/users/563391">Simonus</a><br>
  Libraries used:<br>
   * <a target="_blank" href="https://json5.org/">JSON5 - JSON for Humans</a> (MIT license)
   <h3>Options</h3>
  `

  window.setTimeout(function moveMenuIntoView () {
    main.style.maxHeight = (document.documentElement.clientHeight - 150) + 'px'
    main.style.maxWidth = (document.documentElement.clientWidth - 40) + 'px'
    main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px'
  }, 0)

  Promise.all([
    GM.getValue('volume', '0.7'),
    GM.getValue('myalbums', '{}'),
    GM.getValue('tralbumdata', '{}'),
    GM.getValue('enabledFeatures', false),
    GM.getValue('markasplayedThreshold', '10s')
  ]).then(function allPromisesLoaded (values) {
    // let volume = parseFloat(values[0])
    // volume = Number.isNaN(volume) ? 0.7 : volume
    const myalbums = JSON.parse(values[1])
    const tralbumdata = JSON.parse(values[2])
    getEnabledFeatures(values[3])
    const markasplayedThreshold = values[4]

    const checkboxOnChange = async function onCheckboxChange () {
      const input = this
      getEnabledFeatures(await GM.getValue('enabledFeatures', false))
      allFeatures[input.name].enabled = input.checked
      await GM.setValue('enabledFeatures', JSON.stringify(allFeatures))
      input.style.boxShadow = '2px 2px 5px #0a0f'
      window.setTimeout(function resetBoxShadowTimeout () {
        input.style.boxShadow = ''
      }, 3000)
      updateMoreVisibility()
    }

    const thresholdOnChange = async function onThresholdChange () {
      const input = this
      let value = input.value.trim()
      const m = value.match(/^(\d+)(s|%)$/)
      if (m && parseInt(m[1]) >= 0 && (m[2] === 's' || parseInt(m[1]) <= 100)) {
        value = m[1] + m[2]
      } else if (value.match(/^\d+$/) && parseInt(value.split('\n')[0]) >= 0) {
        value = value.split('\n')[0] + 's'
      } else {
        window.alert('Format does not match!\nChoose either a time in seconds e.g. 10s or a percentage e.g. 50%')
        return
      }

      await GM.setValue('markasplayedThreshold', value)
      input.value = value
      input.style.boxShadow = '2px 2px 5px #0a0f'
      window.setTimeout(function resetBoxShadowTimeout () {
        input.style.boxShadow = ''
      }, 3000)
    }
    const updateMoreVisibility = function () {
      for (const feature in allFeatures) {
        if (document.getElementById('feature_' + feature + '_more_on')) {
          document.getElementById('feature_' + feature + '_more_on').style.display = allFeatures[feature].enabled ? 'block' : 'none'
        }
        if (document.getElementById('feature_' + feature + '_more_off')) {
          document.getElementById('feature_' + feature + '_more_off').style.display = allFeatures[feature].enabled ? 'none' : 'block'
        }
      }
    }

    for (const feature in allFeatures) {
      const div = main.appendChild(document.createElement('div'))
      const checkbox = div.appendChild(document.createElement('input'))
      checkbox.type = 'checkbox'
      checkbox.id = 'feature_' + feature
      checkbox.name = feature
      checkbox.checked = allFeatures[feature].enabled
      const label = div.appendChild(document.createElement('label'))
      label.setAttribute('for', 'feature_' + feature)
      label.innerHTML = allFeatures[feature].name
      checkbox.addEventListener('change', checkboxOnChange)

      if (feature === 'markasplayedAuto') {
        main.appendChild(document.createTextNode(' '))
        const inputThreshold = div.appendChild(document.createElement('input'))
        inputThreshold.type = 'text'
        inputThreshold.value = markasplayedThreshold
        inputThreshold.size = 3
        inputThreshold.title = 'For example: 10s or 50%'
        inputThreshold.id = 'feature_' + feature + '_threshold'
        div.appendChild(document.createTextNode(' '))
        const label = div.appendChild(document.createElement('label'))
        label.setAttribute('for', 'feature_' + feature + '_threshold')
        label.innerHTML = 'seconds or percentage.'
        inputThreshold.addEventListener('change', thresholdOnChange)
      }

      if (feature in moreSettings) {
        if (typeof (moreSettings[feature]) === 'function') {
          const moreSettinsContainer = main.appendChild(document.createElement('fieldset'))
          moreSettings[feature](moreSettinsContainer).then(function (v) {
            if (v) {
              moreSettinsContainer.appendChild(document.createElement('legend')).appendChild(document.createTextNode(v))
            }
          })
        } else {
          if ('true' in moreSettings[feature]) {
            const moreSettinsContainerOn = main.appendChild(document.createElement('fieldset'))
            moreSettinsContainerOn.setAttribute('id', 'feature_' + feature + '_more_on')
            moreSettinsContainerOn.style.display = allFeatures[feature].enabled ? 'block' : 'none'
            moreSettings[feature].true(moreSettinsContainerOn).then(function (v) {
              if (v) {
                moreSettinsContainerOn.appendChild(document.createElement('legend')).appendChild(document.createTextNode(v))
              }
            })
          }
          if ('false' in moreSettings[feature]) {
            const moreSettinsContainerOff = main.appendChild(document.createElement('fieldset'))
            moreSettinsContainerOff.setAttribute('id', 'feature_' + feature + '_more_off')
            moreSettinsContainerOff.style.display = allFeatures[feature].enabled ? 'none' : 'block'
            moreSettings[feature].false(moreSettinsContainerOff).then(function (v) {
              if (v) {
                moreSettinsContainerOff.appendChild(document.createElement('legend')).appendChild(document.createTextNode(v))
              }
            })
          }
        }
      }
    }

    // Hint
    main.appendChild(document.createElement('br'))
    const p = main.appendChild(document.createElement('p'))
    p.appendChild(document.createTextNode('Changes may require a page reload (F5)'))

    // Bottom buttons
    main.appendChild(document.createElement('br'))
    const buttons = main.appendChild(document.createElement('div'))

    const closeButton = buttons.appendChild(document.createElement('button'))
    closeButton.appendChild(document.createTextNode('Close'))
    closeButton.style.color = 'black'
    closeButton.addEventListener('click', function onCloseButtonClick () {
      document.querySelector('.deluxemenu').remove()
      // Un-blur background
      if (document.getElementById('centerWrapper')) {
        document.getElementById('centerWrapper').style.filter = ''
      }
    })

    const clearCacheButton = buttons.appendChild(document.createElement('button'))
    clearCacheButton.appendChild(document.createTextNode('Clear cache'))
    clearCacheButton.style.color = 'black'
    clearCacheButton.addEventListener('click', function onClearCacheButtonClick () {
      Promise.all([
        GM.setValue('genius_selectioncache', '{}'),
        GM.setValue('genius_requestcache', '{}'),
        GM.setValue('tralbumdata', '{}')
      ]).then(function showClearedLabel () {
        clearCacheButton.innerHTML = 'Cleared'
      })
    })
    Promise.all([
      GM.getValue('genius_selectioncache', '{}'),
      GM.getValue('genius_requestcache', '{}')
    ]).then(function (values) {
      JSON.stringify(tralbumdata)
      const bytesN = values[0].length - 2 + values[1].length - 2 + JSON.stringify(tralbumdata).length - 2
      const bytes = metricPrefix(bytesN, 1, 1024) + 'Bytes'
      clearCacheButton.replaceChild(document.createTextNode('Clear cache (' + bytes + ')'), clearCacheButton.firstChild)
    })

    let myalbumsLength = 0
    for (const key in myalbums) {
      if (myalbums[key].listened) {
        myalbumsLength++
      }
    }
    const exportButton = buttons.appendChild(document.createElement('button'))
    exportButton.appendChild(document.createTextNode('Export played albums (' + myalbumsLength + ')'))
    exportButton.style.color = 'black'
    exportButton.addEventListener('click', function onExportButtonClick () {
      document.querySelector('.deluxemenu').remove()
      exportMenu()
    })

    main.appendChild(document.createElement('br'))
    main.appendChild(document.createElement('br'))

    const donateLink = main.appendChild(document.createElement('a'))
    const donateButton = donateLink.appendChild(document.createElement('button'))
    donateButton.appendChild(document.createTextNode('\u2764\uFE0F Donate & Support'))
    donateButton.style.color = '#e81224'
    donateLink.setAttribute('href', 'https://github.com/cvzi/Bandcamp-script-deluxe-edition#donate')
    donateLink.setAttribute('target', '_blank')

    main.appendChild(document.createElement('br'))
    main.appendChild(document.createElement('br'))
  })
  window.setTimeout(function moveMenuIntoView () {
    let moveLeft = 0
    main.style.maxHeight = (document.documentElement.clientHeight - 40) + 'px'
    main.style.maxWidth = (document.documentElement.clientWidth - 40) + 'px'
    if (document.querySelector('#discographyplayer')) {
      if (document.querySelector('#discographyplayer').clientHeight < 100) {
        main.style.maxHeight = (document.documentElement.clientHeight - 150) + 'px'
        main.style.maxWidth = (document.documentElement.clientWidth - 40) + 'px'
      } else if (document.querySelector('#discographyplayer').clientHeight > 300) {
        main.style.maxHeight = (document.documentElement.clientHeight - 40) + 'px'
        main.style.maxWidth = (document.documentElement.clientWidth - 40 - document.querySelector('#discographyplayer').clientWidth) + 'px'
        moveLeft = document.querySelector('#discographyplayer').clientWidth + 20
      }
    }
    window.setTimeout(function () {
      main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth) - moveLeft) + 'px'
    }, 10)
  }, 10)
}

function exportMenu (showClearButton) {
  addStyle(`
    .deluxeexportmenu table {
    }

    .deluxeexportmenu table tr>td {
      color:black
    }
    .deluxeexportmenu table tr>td:nth-child(3) {
      color:silver
    }
    .deluxeexportmenu textarea.animated{
      box-shadow: 2px 2px 5px #5555;
      transition: box-shadow 500ms;
    }
    .deluxeexportmenu .drophint {
      position:absolute;
      top:10%;
      left:30%;
      color:#0097ff;
      font-size:3em;
      display:none;
    }
  `)

  // Blur background
  if (document.getElementById('centerWrapper')) { document.getElementById('centerWrapper').style.filter = 'blur(4px)' }

  const main = document.body.appendChild(document.createElement('div'))
  main.className = 'deluxeexportmenu deluxemenu'
  main.innerHTML = `<h2>Export played albums</h2>
  <h1 class="drophint">Drop to restore from backup</h1>
  Available fields per album:<br>
  <table>
    <tr>
      <td>%artist%</td>
      <td>Artist name</td>
      <td>Jay-X</td>
    </tr>
    <tr>
      <td>%title%</td>
      <td>Song title</td>
      <td>Classic song</td>
    </tr>
    <tr>
      <td>%cover%</td>
      <td>Cover image url</td>
      <td>https://f4.bcbits.com/img/a2588527047_2.jpg</td>
    </tr>
    <tr>
      <td>%url%</td>
      <td>Album url</td>
      <td>petrolgirls.bandcamp.com/album/cut-stitch</td>
    </tr>
    <tr>
      <td>%releaseDate% / %releaseUnix% / %releaseTimestamp%</td>
      <td>Release date</td>
      <td>2019-02-07T14:01:59.100Z / 1549548119 / 1549548119100</td>
    </tr>
    <tr>
      <td>%listenedDate% / %listenedUnix% / %listenedTimestamp%</td>
      <td>Played/Listened date</td>
      <td>2019-02-07T02:17:21.315Z / 1549505841 / 1549505841315</td>
    </tr>
    <tr>
      <td>%releaseY% / %releaseYYYY%</td>
      <td>Release: Year</td>
      <td>19 / 2019</td>
    </tr>
    <tr>
      <td>%releaseM% / %releaseMM% / %releaseMon% / %releaseMonth%</td>
      <td>Release: Month</td>
      <td>2 / 02 / Feb / February</td>
    </tr>
    <tr>
      <td>%releaseD% / %releaseDD%</td>
      <td>Release: Day of month</td>
      <td>7 / 07</td>
    </tr>
    <tr>
      <td>%releaseDay%</td>
      <td>Release: Day of week</td>
      <td>Friday</td>
    </tr>
    <tr>
      <td>%listenedY% / %listenedYYYY%</td>
      <td>Played: Year</td>
      <td>19 / 2019</td>
    </tr>
    <tr>
      <td>%listenedM% / %listenedMM% / %listenedMon% / %listenedMonth%</td>
      <td>Played: Month</td>
      <td>2 / 02 / Feb / February</td>
    </tr>
    <tr>
      <td>%listenedD% / %listenedDD%</td>
      <td>Played: Day of month</td>
      <td>7 / 07</td>
    </tr>
    <tr>
      <td>%listenedDay%</td>
      <td>Played: Day of week</td>
      <td>Friday</td>
    </tr>

  </table>
  `
  const drophint = main.querySelector('.drophint')

  window.setTimeout(function moveMenuIntoView () {
    main.style.maxHeight = (document.documentElement.clientHeight - 40) + 'px'
    main.style.maxWidth = (document.documentElement.clientWidth - 40) + 'px'
    main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px'
  }, 0)

  GM.getValue('myalbums', '{}').then(function myalbumsLoaded (myalbumsStr) {
    const myalbums = JSON.parse(myalbumsStr)
    const listenedAlbums = []
    for (const key in myalbums) {
      if (myalbums[key].listened) {
        listenedAlbums.push(myalbums[key])
      }
    }
    main.querySelector('h2').appendChild(document.createTextNode(' (' + listenedAlbums.length + ' records)'))

    let format = '%artist% - %title%'

    const formatAlbum = function formatAlbumStr (format, myAlbum) {
      const releaseDate = new Date(myAlbum.releaseDate)
      const listenedDate = new Date(myAlbum.listened)
      const fields = {
        '%artist%': () => myAlbum.artist,
        '%title%': () => myAlbum.title,
        '%cover%': () => myAlbum.albumCover,
        '%url%': () => myAlbum.url,
        '%releaseDate%': () => releaseDate.toISOString(),
        '%listenedDate%': () => listenedDate.toISOString(),
        '%releaseUnix%': () => parseInt(releaseDate.getTime() / 1000),
        '%listenedUnix%': () => parseInt(listenedDate.getTime() / 1000),
        '%releaseTimestamp%': () => releaseDate.getTime(),
        '%listenedTimestamp%': () => listenedDate.getTime(),
        '%releaseY%': () => releaseDate.getFullYear().toString().substring(2),
        '%releaseYYYY%': () => releaseDate.getFullYear(),
        '%releaseM%': () => releaseDate.getMonth() + 1,
        '%releaseMM%': () => padd(releaseDate.getMonth() + 1, 2, '0'),
        '%releaseMon%': () => releaseDate.toLocaleString(undefined, { month: 'short' }),
        '%releaseMonth%': () => releaseDate.toLocaleString(undefined, { month: 'long' }),
        '%releaseD%': () => releaseDate.getDate(),
        '%releaseDD%': () => padd(releaseDate.getDate(), 2, '0'),
        '%releaseDay%': () => releaseDate.toLocaleString(undefined, { weekday: 'long' }),
        '%listenedY%': () => listenedDate.getFullYear().toString().substring(2),
        '%listenedYYYY%': () => listenedDate.getFullYear(),
        '%listenedM%': () => listenedDate.getMonth() + 1,
        '%listenedMM%': () => padd(listenedDate.getMonth() + 1, 2, '0'),
        '%listenedMon%': () => listenedDate.toLocaleString(undefined, { month: 'short' }),
        '%listenedMonth%': () => listenedDate.toLocaleString(undefined, { month: 'long' }),
        '%listenedD%': () => listenedDate.getDate(),
        '%listenedDD%': () => padd(listenedDate.getDate(), 2, '0'),
        '%listenedDay%': () => listenedDate.toLocaleString(undefined, { weekday: 'long' }),
        '%json%': () => JSON.stringify(myAlbum),
        '%json5%': () => JSON5.stringify(myAlbum)
      }

      for (const field in fields) {
        if (format.includes(field)) {
          try {
            format = format.replace(field, fields[field]())
          } catch (e) {
            console.log('Could not format replace "' + field + '": ' + e)
          }
        }
      }
      return format
    }

    const sortBy = function sortByCmp (sortKey) {
      const cmps = {
        playedAsc: function playedAsc (a, b) {
          return -cmps.playedDesc(a, b)
        },
        playedDesc: function playedDesc (a, b) {
          try {
            return new Date(b.listened) - new Date(a.listened)
          } catch (e) {
            return 0
          }
        },
        releasedAsc: function releasedAsc (a, b) {
          return -cmps.releasedDesc(a, b)
        },
        releasedDesc: function releasedDesc (a, b) {
          try {
            return new Date(b.releaseDate) - new Date(a.releaseDate)
          } catch (e) {
            return 0
          }
        },
        artist: function artist (a, b, fallbackToTitle) {
          const d = a.artist.localeCompare(b.artist)
          if (d === 0 && fallbackToTitle) {
            return cmps.title(a, b, false)
          } else {
            return d
          }
        },
        title: function title (a, b, fallbackToArtist) {
          const d = a.title.localeCompare(b.title)
          if (d === 0 && fallbackToArtist) {
            return cmps.artist(a, b, false)
          } else {
            return d
          }
        }
      }

      listenedAlbums.sort(cmps[sortKey])
    }

    const generate = function generateStr () {
      const textarea = document.getElementById('export_output')
      window.setTimeout(function generateStrAnimation () {
        textarea.classList.remove('animated')
        textarea.style.boxShadow = '2px 2px 5px #00af'
      }, 0)

      let str
      if (format === '%backup%') {
        str = myalbumsStr
      } else {
        const sortSelect = document.getElementById('sort_select')
        sortBy(sortSelect.options[sortSelect.selectedIndex].value)

        str = []
        for (let i = 0; i < listenedAlbums.length; i++) {
          str.push(formatAlbum(format, listenedAlbums[i]))
        }
        str = str.join(navigator.platform.startsWith('Win') ? '\r\n' : '\n')
      }
      window.setTimeout(function generateStrAnimationSuccess () {
        textarea.value = str
        textarea.classList.add('animated')
        textarea.style.boxShadow = '2px 2px 5px #0a0f'
      }, 50)

      window.setTimeout(function generateStrResetAnimation () {
        textarea.style.boxShadow = ''
      }, 3000)
      return str
    }

    const inputFormatOnChange = async function onInputFormatChange () {
      const input = this
      const formatExample = document.getElementById('format_example')
      format = input.value

      formatExample.value = listenedAlbums.length > 0 ? formatAlbum(format, listenedAlbums[0]) : ''
      formatExample.style.boxShadow = '2px 2px 5px #0a0f'

      window.setTimeout(function resetBoxShadow () {
        formatExample.style.boxShadow = ''
      }, 3000)
    }

    const importData = function importDate (data) {
      GM.getValue('myalbums', '{}').then(function myalbumsLoaded (myalbumsStr) {
        let myalbums = JSON.parse(myalbumsStr)
        myalbums = Object.assign(myalbums, data)
        return GM.setValue('myalbums', JSON.stringify(myalbums))
      }).then(function myalbumsSaved () {
        document.getElementById('exportmenu_close').click()
        window.setTimeout(() => exportMenu(true), 50)
      })
    }
    const handleFiles = async function handleFilesAsync (fileList) {
      if (fileList.length === 0) {
        console.log('fileList is empty')
        return
      }

      let data
      try {
        data = await (new Response(fileList[0])).json()
      } catch (e) {
        window.alert('Could not load file:\n' + e)
        return
      }

      const n = Object.keys(data).length
      if (window.confirm('Found ' + n + ' albums. Continue import and overwrite existing albums?')) {
        importData(data)
      }
    }

    const inputTable = main.appendChild(document.createElement('table'))
    let tr
    let td

    tr = inputTable.appendChild(document.createElement('tr'))
    td = tr.appendChild(document.createElement('td'))
    const label = td.appendChild(document.createElement('label'))
    label.setAttribute('for', 'export_format')
    label.appendChild(document.createTextNode('Format:'))

    td = tr.appendChild(document.createElement('td'))
    const inputFormat = td.appendChild(document.createElement('input'))
    inputFormat.type = 'text'
    inputFormat.value = format
    inputFormat.id = 'export_format'
    inputFormat.style.width = '600px'
    inputFormat.addEventListener('change', inputFormatOnChange)
    inputFormat.addEventListener('keyup', inputFormatOnChange)

    tr = inputTable.appendChild(document.createElement('tr'))

    td = tr.appendChild(document.createElement('td'))
    td.appendChild(document.createTextNode('Example:'))

    td = tr.appendChild(document.createElement('td'))
    const inputExample = td.appendChild(document.createElement('input'))
    inputExample.type = 'text'
    inputExample.value = listenedAlbums.length > 0 ? formatAlbum(format, listenedAlbums[0]) : ''
    inputExample.readonly = true
    inputExample.id = 'format_example'
    inputExample.style.width = '600px'

    td = tr.appendChild(document.createElement('td'))
    td.appendChild(document.createTextNode('Sort by:'))

    td = tr.appendChild(document.createElement('td'))
    const sortSelect = td.appendChild(document.createElement('select'))
    sortSelect.id = 'sort_select'
    sortSelect.innerHTML = `
      <option value="playedDesc">Recent play first</option>
      <option value="playedAsc">Recent play last</option>
      <option value="releasedDesc">Recent release first</option>
      <option value="releasedAsc">Recent release last</option>
      <option value="artist">Artist A-Z</option>
      <option value="title">Title A-Z</option>
    `

    tr = inputTable.appendChild(document.createElement('tr'))
    td = tr.appendChild(document.createElement('td'))
    td.setAttribute('colspan', '2')
    const generateButton = td.appendChild(document.createElement('button'))
    generateButton.appendChild(document.createTextNode('Generate'))
    generateButton.addEventListener('click', (ev) => generate())
    const exportButton = td.appendChild(document.createElement('button'))
    exportButton.appendChild(document.createTextNode('Export to file'))
    exportButton.title = 'Download as a text file'
    exportButton.addEventListener('click', function onExportFileButtonClick () {
      const dateSuffix = (new Date()).toISOString().split('T')[0]
      document.getElementById('export_download_link').download = 'bandcampPlayedAlbums_' + dateSuffix + '.txt'
      document.getElementById('export_download_link').href = 'data:text/plain,' + encodeURIComponent(generate())
      window.setTimeout(() => document.getElementById('export_download_link').click(), 50)
    })
    const backupButton = td.appendChild(document.createElement('button'))
    backupButton.title = 'Backup to JSON file. Can be restored on another browser'
    backupButton.appendChild(document.createTextNode('Backup'))
    backupButton.addEventListener('click', function onBackupButtonClick () {
      format = '%backup%'
      document.getElementById('export_format').value = format
      document.getElementById('format_example').value = 'JSON dictionary'
      const dateSuffix = (new Date()).toISOString().split('T')[0]
      document.getElementById('export_download_link').download = 'bandcampPlayedAlbums_' + dateSuffix + '.json'
      document.getElementById('export_download_link').href = 'data:application/json,' + encodeURIComponent(generate())
      document.getElementById('export_clear_button').style.display = ''
      GM.setValue('myalbums_lastbackup', Object.keys(myalbums).length + '#####' + (new Date()).toJSON())
      window.setTimeout(() => document.getElementById('export_download_link').click(), 50)
    })
    const restoreButton = td.appendChild(document.createElement('button'))
    restoreButton.title = 'Restore from JSON file backup'
    restoreButton.appendChild(document.createTextNode('Restore'))
    restoreButton.addEventListener('click', function onBackupButtonClick () {
      inputFile.click()
    })

    const clearButton = td.appendChild(document.createElement('button'))
    clearButton.appendChild(document.createTextNode('Clear played albums'))
    clearButton.id = 'export_clear_button'
    if (showClearButton !== true) {
      clearButton.style.display = 'none'
    }
    clearButton.addEventListener('click', function onClearButtonClick () {
      if (window.confirm('Remove all played albums?\n\nThis cannot be undone.')) {
        if (window.confirm('Are you sure? Delete all played albums?')) {
          GM.setValue('myalbums', '{}').then(function myalbumsSaved () {
            document.getElementById('exportmenu_close').click()
            window.setTimeout(exportMenu, 50)
          })
        }
      }
    })

    const downloadA = td.appendChild(document.createElement('a'))
    downloadA.id = 'export_download_link'
    downloadA.href = '#'
    downloadA.download = 'bandcamp_played_albums.txt'
    downloadA.target = '_blank'

    const inputFile = td.appendChild(document.createElement('input'))
    inputFile.type = 'file'
    inputFile.id = 'input_file'
    inputFile.accept = '.txt,plain/text,.json,application/json'
    inputFile.style.display = 'none'
    inputFile.addEventListener('change', function onFileChanged (ev) {
      handleFiles(this.files)
    }, false)
    main.addEventListener('dragenter', function dragenter (ev) {
      ev.stopPropagation()
      ev.preventDefault()
      main.style.backgroundColor = '#c6daf9'
      drophint.style.left = (main.clientWidth / 2 - drophint.clientWidth / 2) + 'px'
      drophint.style.display = 'block'
    }, false)
    main.addEventListener('dragleave', function dragleave (ev) {
      main.style.backgroundColor = 'white'
      drophint.style.display = 'none'
    }, false)
    main.addEventListener('dragover', function dragover (ev) {
      ev.stopPropagation()
      ev.preventDefault()
      main.style.backgroundColor = '#c6daf9'
      drophint.style.display = 'block'
    }, false)
    main.addEventListener('drop', function drop (ev) {
      ev.stopPropagation()
      ev.preventDefault()
      main.style.backgroundColor = 'white'
      drophint.style.display = 'none'
      handleFiles(ev.dataTransfer.files)
    }, false)

    tr = inputTable.appendChild(document.createElement('tr'))
    td = tr.appendChild(document.createElement('td'))
    td.setAttribute('colspan', '3')
    const textarea = td.appendChild(document.createElement('textarea'))
    textarea.id = 'export_output'
    textarea.style.width = Math.max(500, main.clientWidth - 50) + 'px'

    // Bottom buttons
    main.appendChild(document.createElement('br'))
    main.appendChild(document.createElement('br'))
    const buttons = main.appendChild(document.createElement('div'))

    const closeButton = buttons.appendChild(document.createElement('button'))
    closeButton.appendChild(document.createTextNode('Close'))
    closeButton.id = 'exportmenu_close'
    closeButton.style.color = 'black'
    closeButton.addEventListener('click', function onCloseButtonClick () {
      document.querySelector('.deluxeexportmenu').remove()
      // Un-blur background
      if (document.getElementById('centerWrapper')) {
        document.getElementById('centerWrapper').style.filter = ''
      }
    })
  })
  window.setTimeout(function moveMenuIntoView () {
    main.style.maxHeight = (document.documentElement.clientHeight - 40) + 'px'
    main.style.maxWidth = (document.documentElement.clientWidth - 40) + 'px'
    main.style.left = Math.max(20, 0.5 * (document.body.clientWidth - main.clientWidth)) + 'px'
  }, 0)
}

function checkBackupStatus () {
  GM.getValue('myalbums_lastbackup', '').then(function myalbumsLastBackupLoaded (value) {
    if (!value || !value.includes('#####')) {
      // Set current date (install date) as initial value
      GM.setValue('myalbums_lastbackup', '0#####' + (new Date()).toJSON())
      return
    }
    const parts = value.split('#####')
    const n0 = parseInt(parts[0])
    const lastBackup = new Date(parts[1])
    if ((new Date()) - lastBackup > BACKUP_REMINDER_DAYS * 86400000) {
      GM.getValue('myalbums', '{}').then(function myalbumsLoaded (str) {
        const n1 = Object.keys(JSON.parse(str)).length
        if (Math.abs(n0 - n1) > 10) {
          showBackupHint(lastBackup, Math.abs(n0 - n1))
        }
      })
    }
  })
}

function showBackupHint (lastBackup, changedRecords) {
  const since = timeSince(lastBackup)

  addStyle(`
    .backupreminder {
      position:fixed;
      height:auto;
      overflow:auto;
      top:110%;
      left:40%;
      z-index:200;
      padding:5px;
      transition: top 1s;
      border:2px solid black;
      border-radius:10px;
      color:black;
      background:white;
    }
  `)

  // Blur background
  if (document.getElementById('centerWrapper')) { document.getElementById('centerWrapper').style.filter = 'blur(4px)' }

  const main = document.body.appendChild(document.createElement('div'))
  main.className = 'backupreminder'
  main.innerHTML = `<h2>${SCRIPT_NAME}</h2>
  <h1>Backup reminder</h1>
  <p>
    Your last backup was ${since} ago. Since then, you played ${changedRecords} albums.
  </p>
  `

  main.appendChild(document.createElement('br'))
  const buttons = main.appendChild(document.createElement('div'))

  const closeButton = buttons.appendChild(document.createElement('button'))
  closeButton.appendChild(document.createTextNode('Close'))
  closeButton.id = 'backupreminder_close'
  closeButton.style.color = 'black'
  closeButton.addEventListener('click', function onCloseButtonClick () {
    document.querySelector('.backupreminder').remove()
    // Un-blur background
    if (document.getElementById('centerWrapper')) {
      document.getElementById('centerWrapper').style.filter = ''
    }
  })

  buttons.appendChild(document.createTextNode(' '))

  const backupButton = buttons.appendChild(document.createElement('button'))
  backupButton.appendChild(document.createTextNode('Start backup'))
  backupButton.style.color = '#0687f5'
  backupButton.addEventListener('click', function backupButtonClick () {
    document.getElementById('backupreminder_close').click()
    mainMenu(true)
  })

  buttons.appendChild(document.createTextNode(' '))

  const ignoreButton = buttons.appendChild(document.createElement('button'))
  ignoreButton.appendChild(document.createTextNode('Disable reminder'))
  ignoreButton.style.color = 'black'
  ignoreButton.addEventListener('click', async function ignoreButtonClick () {
    getEnabledFeatures(await GM.getValue('enabledFeatures', false))
    if (allFeatures.backupReminder.enabled) {
      allFeatures.backupReminder.enabled = false
    }
    await GM.setValue('enabledFeatures', JSON.stringify(allFeatures))
    document.getElementById('backupreminder_close').click()
  })

  window.setTimeout(function moveMenuIntoView () {
    main.style.maxHeight = (document.documentElement.clientHeight - 40) + 'px'
    main.style.maxWidth = (document.documentElement.clientWidth - 40) + 'px'
    main.style.left = Math.max(20, 0.5 * (document.documentElement.clientWidth - main.clientWidth)) + 'px'
    main.style.top = Math.max(20, 0.3 * document.documentElement.clientHeight) + 'px'
  }, 0)
}

function downloadMp3FromLink (ev, a, addSpinner, removeSpinner, noGM) {
  const url = a.href
  if (GM.download && !noGM) {
    // Use Tampermonkey GM.download function
    console.log('Using GM.download function')
    ev.preventDefault()
    addSpinner(a)
    let GMdownloadStatus = 0
    GM.download({
      url: url,
      name: a.download || 'default.mp3',
      onerror: function downloadMp3FromLinkOnError (e) {
        console.log('GM.download onerror:', e)
      },
      ontimeout: function downloadMp3FromLinkOnTimeout () {
        window.alert('Could not download via GM.download. Time out.')
        document.location.href = url
      },
      onload: function downloadMp3FromLinkOnLoad () {
        console.log('Successfully downloaded via GM.download')
        GMdownloadStatus = 1
        window.setTimeout(() => removeSpinner(a), 500)
      }
    }).then(function (o) {
      console.log('GM.download() finished')
      GMdownloadStatus = 1
      window.setTimeout(() => removeSpinner(a), 500)
    }).catch(function (e) {
      GMdownloadStatus = 0
      console.log('GM.download() failed', e)
      window.setTimeout(function () {
        if (GMdownloadStatus !== 1) {
          if (url.startsWith('data')) {
            console.log('GM.download failed with data url')
            document.location.href = url
          } else {
            console.log('Trying again with GM.download disabled')
            downloadMp3FromLink(ev, a, addSpinner, removeSpinner, true)
          }
        }
      }, 1000)
    })
    return
  }

  if (!url.startsWith('http') || navigator.userAgent.indexOf('Chrome') !== -1) {
    // Just open the link normally (no prevent default)
    addSpinner(a)
    window.setTimeout(() => removeSpinner(a), 1000)
    return
  }

  // Use GM.xmlHttpRequest to download and offer data uri
  ev.preventDefault()
  console.log('Using GM.xmlHttpRequest to download and then offer data uri')
  addSpinner(a)
  GM.xmlHttpRequest({
    method: 'GET',
    overrideMimeType: 'text/plain; charset=x-user-defined',
    url: url,
    onload: function onMp3Load (response) {
      console.log('Successfully received data via GM.xmlHttpRequest, starting download')
      a.href = 'data:audio/mpeg;base64,' + base64encode(response.responseText)
      window.setTimeout(() => a.click(), 10)
    },
    onerror: function onMp3LoadError (response) {
      window.alert('Could not download via GM.xmlHttpRequest')
      document.location.href = url
    }
  })
}

function addDownloadLinksToAlbumPage () {
  addStyle(`
  .download-col .downloaddisk:hover {
    text-decoration:none
  }
  /* From http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/ */
  .downspinner {
    height:16px;
    width:16px;
    margin:0px auto;
    position:relative;
    display:inline-block;
    animation: spinnerrotation 3s infinite linear;
    cursor:wait;
  }
  @keyframes spinnerrotation {
    from {transform: rotate(0deg)}
    to {transform: rotate(359deg)}
  }`)

  const addSpiner = function downloadLinksOnAlbumPageAddSpinner (el) {
    el.style = ''
    el.classList.add('downspinner')
  }

  const removeSpinner = function downloadLinksOnAlbumPageRemoveSpinner (el) {
    el.classList.remove('downspinner')
    el.style = 'background:#1cea1c; border-radius:5px; padding:1px; opacity:0.5'
  }

  const TralbumData = unsafeWindow.TralbumData
  if (TralbumData && TralbumData.hasAudio && !TralbumData.freeDownloadPage && TralbumData.trackinfo) {
    var hoverdiv = document.querySelectorAll('.download-col div')
    if (hoverdiv.length > 0) {
      // Album page
      for (let i = 0; i < TralbumData.trackinfo.length; i++) {
        const t = TralbumData.trackinfo[i]
        for (var prop in t.file) {
          const mp3 = t.file[prop].replace(/^\/\//, 'http://')
          const a = document.createElement('a')
          a.className = 'downloaddisk'
          a.href = mp3
          a.download = (t.track_num == null ? '' : ((t.track_num > 9 ? '' : '0') + t.track_num + '. ')) + fixFilename(TralbumData.artist + ' - ' + t.title) + '.mp3'
          a.title = 'Download ' + prop
          a.appendChild(document.createTextNode(NOEMOJI ? '\u2193' : '\uD83D\uDCBE'))
          a.addEventListener('click', function onDownloadLinkClick (ev) {
            downloadMp3FromLink(ev, this, addSpiner, removeSpinner)
          })
          hoverdiv[i].appendChild(a)
          break
        }
      }
    } else if (document.querySelector('#trackInfo .download-link')) {
      // Single track page
      const t = TralbumData.trackinfo[0]
      const prop = Object.keys(t.file)[0]
      const mp3 = t.file[prop].replace(/^\/\//, 'http://')
      const a = document.createElement('a')
      a.className = 'downloaddisk'
      a.href = mp3
      a.download = (t.track_num == null ? '' : ((t.track_num > 9 ? '' : '0') + t.track_num + '. ')) + fixFilename(TralbumData.artist + ' - ' + t.title) + '.mp3'
      a.title = 'Download ' + prop
      a.appendChild(document.createTextNode(NOEMOJI ? '\u2193' : '\uD83D\uDCBE'))
      a.addEventListener('click', function onDownloadLinkClick (ev) {
        downloadMp3FromLink(ev, this, addSpiner, removeSpinner)
      })
      document.querySelector('#trackInfo .download-link').parentNode.appendChild(a)
    }
  }
}

function addLyricsToAlbumPage () {
  // Load lyrics from html into TralbumData
  const TralbumData = unsafeWindow.TralbumData
  function findInTralbumData (url) {
    for (let i = 0; i < TralbumData.trackinfo.length; i++) {
      const t = TralbumData.trackinfo[i]
      if (url.endsWith(t.title_link)) {
        return t
      }
    }
    return null
  }
  const tracks = Array.from(document.querySelectorAll('#track_table .track_row_view .title a')).map(a => findInTralbumData(a.href))
  document.querySelectorAll('#track_table .track_row_view .title a').forEach(function (a) {
    const tr = parentQuery(a, 'tr[rel]')
    const trackNum = tr.getAttribute('rel').split('tracknum=')[1]
    const lyricsRow = document.querySelector('#track_table tr#lyrics_row_' + trackNum)
    const lyricsLink = tr.querySelector('.geniuslink')
    if (lyricsRow) {
      const i = parseInt(lyricsRow.id.split('lyrics_row_')[1]) - 1
      tracks[i].lyrics = lyricsRow.querySelector('div').textContent
    } else if (!lyricsLink) {
      // Add genius link
      const lyricsLink = tr.querySelector('.info_link a')
      lyricsLink.dataset.trackNum = trackNum
      lyricsLink.href = '#geniuslyrics-' + trackNum
      lyricsLink.classList.add('geniuslink')
      lyricsLink.appendChild(document.createTextNode('genius'))
      lyricsLink.addEventListener('click', function () {
        loadGeniusLyrics(parseInt(this.dataset.trackNum))
      })
    }
  })
}

var genius = null
var geniusContainerTr = null
var geniusTrackNum = -1
var geniusArtistsArr = []
var geniusTitle = ''
function geniusGetCleanLyricsContainer () {
  geniusContainerTr.innerHTML = `
                    <td colspan="5">
                      <div></div>
                    </td>
`

  return geniusContainerTr.querySelector('div')
}
function geniusAddLyrics (force, beLessSpecific) {
  genius.f.loadLyrics(force, beLessSpecific, geniusTitle, geniusArtistsArr, true)
}
function geniusHideLyrics () {
  document.querySelectorAll('.loadingspinner').forEach((spinner) => spinner.remove())
  document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'))
}
function geniusSetFrameDimensions (container, iframe) {
  const width = iframe.style.width = '500px'
  const height = iframe.style.height = '650px'

  if (genius.option.themeKey === 'spotify') {
    iframe.style.backgroundColor = 'black'
  } else {
    iframe.style.backgroundColor = ''
  }

  return [width, height]
}
function geniusAddCss () {
  addStyle(`
  #myconfigwin39457845 {
    z-index:2060 !important;
    position:fixed !important;
    background-color:${darkModeModeCurrent === true ? '#a2a2a2' : 'white'} !important;
    color:${darkModeModeCurrent === true ? 'white' : 'black'} !important;
  }
  #myconfigwin39457845 h1 {
    margin:5px;
  }
  #myconfigwin39457845 div {
    background-color:${darkModeModeCurrent === true ? '#3E3E3E' : '#EFEFEF'} !important
  }
  #myconfigwin39457845 .divAutoShow {
    display:none
  }
  #myconfigwin39457845  button {
    background-color: #cacaca !important;
    color: black !important;
    border: 2px outset !important;
    padding: 1px !important;
    font-size: 1.2em !important;
  }
  #lyricsiframe {
    opacity:0.1;
    transition:opacity 2s;
    margin:0px;
    padding:0px;
    position:relative;
  }
  .lyricsnavbar {
    font-size : 0.7em;
    text-align:right;
    padding: 0px 10px 0px 0px !important;
    background:${darkModeModeCurrent === true ? '#7d7c7c' : '#fafafa'} !important;
   }
  .lyricsnavbar span,.lyricsnavbar a:link,.lyricsnavbar a:visited  {
    color:#606060;
    text-decoration:none;
    transition:color 400ms;
   }
  .lyricsnavbar a:hover,.lyricsnavbar span:hover {
    color:#9026e0;
    text-decoration:none;
  }
  .loadingspinner {
      color:black;
      font-size:12px;
      line-height:15px;
      width:15px !important;
      height:15px !important;
      padding: 2px !important;
    }
  .loadingspinnerholder {
    z-index:10;
    cursor:progress;
    position:relative;
    width:20px !important;
    height:20px !important;
  }
  .searchresultlist {
    margin:0px !important;
    padding:0px !important;
    border:1px solid black;
    border-radius: 3px;
    width: 450px !important;
  }
  .searchresultlist ol {
    list-style: none;
    padding: 0px !important;
    margin:0px; !important
  }
  .searchresultlist ol li {
    width: 430px !important;
  }
  .searchresultlist ol li div {
    width: auto !important;
  }
  `)
}
function geniusCreateSpinner (spinnerHolder) {
  geniusContainerTr.querySelector('div').insertBefore(spinnerHolder, geniusContainerTr.querySelector('div').firstChild)

  const spinner = spinnerHolder.appendChild(document.createElement('div'))
  spinner.classList.add('loadingspinner')

  return spinner
}

function geniusShowSearchField (query) {
  const b = geniusGetCleanLyricsContainer()
  console.log(b)

  b.style.border = '1px solid black'
  b.style.borderRadius = '3px'
  b.style.padding = '5px'

  b.appendChild(document.createTextNode('Search genius.com: '))
  b.style.paddingRight = '15px'
  const input = b.appendChild(document.createElement('input'))
  input.className = 'SearchInputBox__input'
  input.placeholder = 'Search genius.com...'
  input.style = 'width: 300px;background-color: #F3F3F3;padding: 10px 30px 10px 10px;font-size: 14px; border: none;color: #333;margin: 6px 0;height: 17px;border-radius: 3px;'

  const span = b.appendChild(document.createElement('span'))
  span.style = 'cursor:pointer; margin-left: -25px;'
  span.appendChild(document.createTextNode(' \uD83D\uDD0D'))

  if (query) {
    input.value = query
  } else if (genius.current.artists) {
    input.value = genius.current.artists
  }
  input.addEventListener('change', function onSearchLyricsButtonClick () {
    if (input.value) {
      genius.f.searchByQuery(input.value, b)
    }
  })
  input.addEventListener('keyup', function onSearchLyricsKeyUp (ev) {
    if (ev.keyCode === 13) {
      ev.preventDefault()
      if (input.value) {
        genius.f.searchByQuery(input.value, b)
      }
    }
  })
  span.addEventListener('click', function onSearchLyricsKeyUp (ev) {
    if (input.value) {
      genius.f.searchByQuery(input.value, b)
    }
  })

  input.focus()
}
function geniusListSongs (hits, container, query) {
  if (!container) {
    container = geniusGetCleanLyricsContainer()
  }

  // Back to search button
  const backToSearchButton = document.createElement('a')
  backToSearchButton.href = '#'
  backToSearchButton.appendChild(document.createTextNode('Back to search'))
  backToSearchButton.addEventListener('click', function backToSearchButtonClick (ev) {
    ev.preventDefault()
    if (query) {
      geniusShowSearchField(query)
    } else if (genius.current.artists) {
      geniusShowSearchField(genius.current.artists + ' ' + genius.current.title)
    } else {
      geniusShowSearchField()
    }
  })

  const separator = document.createElement('span')
  separator.setAttribute('class', 'second-line-separator')
  separator.setAttribute('style', 'padding:0px 3px')
  separator.appendChild(document.createTextNode('•'))

  // Hide button
  const hideButton = document.createElement('a')
  hideButton.href = '#'
  hideButton.appendChild(document.createTextNode('Hide'))
  hideButton.addEventListener('click', function hideButtonClick (ev) {
    ev.preventDefault()
    geniusHideLyrics()
  })

  // List search results
  const trackhtml = '<div style="float:left;"><div class="onhover" style="margin-top:-0.25em;display:none"><span style="color:black;font-size:2.0em">🅖</span></div><div class="onout"><span style="font-size:1.5em">📄</span></div></div>' +
  '<div style="float:left; margin-left:5px">$artist • $title <br><span style="font-size:0.7em">👁 $stats.pageviews $lyrics_state</span></div><div style="clear:left;"></div>'
  container.innerHTML = '<ol class="tracklist" style="font-size:1.15em"></ol>'

  container.classList.add('searchresultlist')
  if (darkModeModeCurrent === true) {
    container.style.backgroundColor = '#262626'
    container.style.position = 'relative'
  }

  container.insertBefore(hideButton, container.firstChild)
  container.insertBefore(separator, container.firstChild)
  container.insertBefore(backToSearchButton, container.firstChild)

  const ol = container.querySelector('ol')
  const searchresultsLengths = hits.length
  const title = genius.current.title
  const artists = genius.current.artists
  const onclick = function onclick () {
    genius.f.rememberLyricsSelection(title, artists, this.dataset.hit)
    genius.f.showLyrics(JSON.parse(this.dataset.hit), searchresultsLengths)
  }
  const mouseover = function onmouseover () {
    this.querySelector('.onhover').style.display = 'block'
    this.querySelector('.onout').style.display = 'none'
    this.style.backgroundColor = darkModeModeCurrent === true ? 'rgb(70, 70, 70)' : 'rgb(200, 200, 200)'
  }
  const mouseout = function onmouseout () {
    this.querySelector('.onhover').style.display = 'none'
    this.querySelector('.onout').style.display = 'block'
    this.style.backgroundColor = darkModeModeCurrent === true ? '#262626' : 'rgb(255, 255, 255)'
  }

  hits.forEach(function forEachHit (hit) {
    const li = document.createElement('li')
    if (darkModeModeCurrent === true) {
      li.style.backgroundColor = '#262626'
    }
    li.style.cursor = 'pointer'
    li.style.transition = 'background-color 0.2s'
    li.style.padding = '3px'
    li.style.margin = '2px'
    li.style.borderRadius = '3px'
    li.innerHTML = trackhtml.replace(/\$title/g, hit.result.title_with_featured).replace(/\$artist/g, hit.result.primary_artist.name).replace(/\$lyrics_state/g, hit.result.lyrics_state).replace(/\$stats\.pageviews/g, genius.f.metricPrefix(hit.result.stats.pageviews, 1))
    li.dataset.hit = JSON.stringify(hit)

    li.addEventListener('click', onclick)
    li.addEventListener('mouseover', mouseover)
    li.addEventListener('mouseout', mouseout)
    ol.appendChild(li)
  })
}
function geniusOnLyricsReady (song, container) {
  container.parentNode.parentNode.dataset.loaded = 'loaded'
}
function geniusOnNoResults (songTitle, songArtistsArr) {
  geniusContainerTr.dataset.loaded = 'loaded'
  document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'))
  document.querySelector(`#track_table tr[rel="tracknum=${geniusTrackNum}"]`).classList.add('showlyrics')
  geniusShowSearchField(songArtistsArr.join(' ') + ' ' + songTitle)
}
function initGenius () {
  if (!genius) {
    genius = geniusLyrics({
      GM: {
        xmlHttpRequest: GM.xmlHttpRequest,
        getValue: (name, defaultValue) => GM.getValue('genius_' + name, defaultValue),
        setValue: (name, value) => GM.setValue('genius_' + name, value)
      },
      scriptName: SCRIPT_NAME,
      scriptIssuesURL: 'https://github.com/cvzi/Bandcamp-script-deluxe-edition/issues',
      scriptIssuesTitle: 'Report problem: github.com/cvzi/Bandcamp-script-deluxe-edition/issues',
      domain: document.location.origin + '/',
      emptyURL: document.location.origin + '/robots.txt',
      addCss: geniusAddCss,
      listSongs: geniusListSongs,
      showSearchField: geniusShowSearchField,
      addLyrics: geniusAddLyrics,
      hideLyrics: geniusHideLyrics,
      getCleanLyricsContainer: geniusGetCleanLyricsContainer,
      setFrameDimensions: geniusSetFrameDimensions,
      // onResize: onResize,
      createSpinner: geniusCreateSpinner,
      onLyricsReady: geniusOnLyricsReady,
      onNoResults: geniusOnNoResults
    })
  }
}

function loadGeniusLyrics (trackNum) {
  // Toggle lyrics
  geniusContainerTr = document.getElementById('lyrics_row_' + trackNum)
  let tr
  if (geniusContainerTr) {
    tr = document.querySelector(`#track_table tr[rel="tracknum=${trackNum}"]`)
    if ('loaded' in geniusContainerTr.dataset && geniusContainerTr.dataset.loaded === 'loaded') {
      if (tr.classList.contains('showlyrics')) {
        // Hide lyrics if already loaded
        document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'))
      } else {
        // Show lyrics again
        document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'))
        tr.classList.add('showlyrics')
      }
      return
    } else if (geniusTrackNum === trackNum) {
      // Lyrics currently loading
      console.log('loadGeniusLyrics already loading trackNum=' + trackNum)
      return
    }
  }

  geniusTrackNum = trackNum
  if (!geniusContainerTr) {
    geniusContainerTr = document.createElement('tr')
    geniusContainerTr.className = 'lyricsRow'
    geniusContainerTr.setAttribute('id', 'lyrics_row_' + trackNum)
    tr = document.querySelector(`#track_table tr[rel="tracknum=${trackNum}"]`)
    if (tr.nextElementSibling) {
      tr.parentNode.insertBefore(geniusContainerTr, tr.nextElementSibling)
    } else {
      tr.parentNode.appendChild(geniusContainerTr)
    }
    document.querySelectorAll('#track_table tr.showlyrics').forEach(e => e.classList.remove('showlyrics'))
    tr.classList.add('showlyrics')

    const spinnerHolder = geniusContainerTr.appendChild(document.createElement('div'))
    spinnerHolder.classList.add('loadingspinnerholder')
    const spinner = spinnerHolder.appendChild(document.createElement('div'))
    spinner.classList.add('loadingspinner')
  }

  initGenius()

  const track = unsafeWindow.TralbumData.trackinfo.find((t) => t.track_num === trackNum)
  geniusTitle = track.title
  geniusArtistsArr = unsafeWindow.TralbumData.artist.split(/&|,|ft\.?|feat\.?/).map(s => s.trim())

  geniusAddLyrics()
}

function appendMainMenuButtonTo (ul) {
  const li = ul.insertBefore(document.createElement('li'), ul.firstChild)
  li.className = 'menubar-item hoverable'
  li.title = 'userscript settings - ' + SCRIPT_NAME
  const a = li.appendChild(document.createElement('a'))
  a.className = 'settingssymbol'
  a.style.fontSize = '24px'
  a.style.transition = 'transform 2s ease-out'
  if (NOEMOJI) {
    a.appendChild(document.createTextNode('\u26ED'))
  } else {
    a.appendChild(document.createTextNode('\u2699\uFE0F'))
  }
  a.addEventListener('mouseover', function () {
    this.style.transform = 'rotate(360deg)'
  })
  li.addEventListener('click', () => mainMenu())
}

function appendMainMenuButtonLeftTo (leftOf) {
  const rect = leftOf.getBoundingClientRect()
  const ul = document.createElement('ul')
  ul.className = 'bcsde_settingsbar'
  appendMainMenuButtonTo(document.body.appendChild(ul))
  addStyle(`
  .bcsde_settingsbar {position:absolute; top:-15px; left:${rect.right}px; list-style-type: none; padding:0; margin:0; opacity:0.6; transition:top 300ms}
  .bcsde_settingsbar:hover {top:${rect.top}px}
  .bcsde_settingsbar a:hover {text-decoration:none}
  .bcsde_settingsbar li {float:left; padding:0; margin:0}`)
  window.addEventListener('resize', function () {
    ul.style.left = leftOf.getBoundingClientRect().right + 'px'
  })
}

function humour () {
  if (document.getElementById('salesfeed')) {
    const salesfeedHumour = {}
    salesfeedHumour.all = [
      `${SCRIPT_NAME} by cuzi, Dark theme by Simonus`,
      `Provide feedback for ${SCRIPT_NAME} on openuser.js or github.com`,
      `${SCRIPT_NAME} - “nobody pays for software anymore” 🙌🏽`
    ]
    salesfeedHumour.chosen = salesfeedHumour.all[0]
    unsafeWindow.$('#pagedata').data('blob').salesfeed_humour = salesfeedHumour
  }
}

function darkMode () {
  // CSS taken from https://userstyles.org/styles/171538/bandcamp-in-dark by Simonus (Version from January 24, 2020)
  // https://userstyles.org/api/v1/styles/css/171538

  let propOpenWrapperBackgroundColor = '#2626268f'
  try {
    const brightnessStr = window.localStorage.getItem('bcsde_bgimage_brightness')
    if (brightnessStr !== null && brightnessStr !== 'null') {
      const brightness = parseFloat(brightnessStr)
      const alpha = (brightness - 50) / 255
      propOpenWrapperBackgroundColor = `rgba(0, 0, 0, ${alpha})`
    }
  } catch (e) {
    console.log('Could not access window.localStorage: ' + e)
  }

  const css = `
:root {
  --pgBdColor: #262626;
  --propOpenWrapperBackgroundColor: ${propOpenWrapperBackgroundColor}
}


/* Bandcamp: Stick Track List to Player https://userstyles.org/styles/123397/ */

/* move merchandising down, so playlist or track description moves up below player */
#centerWrapper #pgBd #trackInfoInner {
    display: flex;
    flex-direction: column;
}
#centerWrapper #pgBd #trackInfoInner > .tralbumCommands {
    order: 1;
}
/* move upcoming shows down, so discography moves up below band info */
#centerWrapper #pgBd #rightColumn {
    display: flex;
    flex-direction: column;
}
#centerWrapper #pgBd #rightColumn > #showography {
    order: 1;
}
/* make modals less modal */
/*OFF for now */
.ui-widget-overlay {
    display: none;
}
.ui-dialog.ui-widget.ui-widget-content.ui-corner-all.nu-dialog.no-title {
    position: fixed !important;
    top: 0 !important;
    right: 0 !important;
    bottom: auto !important;
    left: auto !important;
}
.inline_player .nextbutton,
.inline_player .prevbutton,
svg {
    filter: invert(100%);
}
a {
    color: #da5 !important;
}
.trackYear,
button {
    color: #ac6 !important;
}
div#collection-container.collection-container,
div.home {
    background: #000 !important;
}
div.area_text,
div.sort_controls,
div.text,
span {
    color: #ccc !important;
}
div#dlg0_h.hd,
div#pgBd.yui-skin-sam,
div.blogunit-details-section,
div.collection-item-details-container {
    background: var(--pgBdColor) !important;
}
div.collection-item-artist,
h1 {
    color: #ccc !important;
}
DIV.track_number.secondaryText,
div.collection-item-title,
div.message,
h2 {
    color: #FFF !important;
}
h3 {
    color: #FFED80 !important;
}
DIV.tralbumData.tralbum-credits {
    color: #ccc !important;
}
DIV#license.info,
DIV.tralbumData.tralbum-about,
DIV.tralbumData.tralbum-feed,
li {
    color: #806300 !important;
}
button.sc-button.sc-button-small.sc-button-responsive.sc-button-addtoset {
    color: black !important;
}
div#fan-suggestions.dotted-section.mine,
div.bcweekly-bd,
div.collection-item-gallery-container,
div.collection-stats.dotted-section.mine {
    background: #222222 !important;
}
p {
    color: #aaa !important;
}
div.sound__soundActions {
    background: transparent !important;
}
button.sc-button.sc-button-small.sc-button-responsive.sc-button-addtoset {
    color: #111111 !important;
}
div.ft.fakeFt {
    background: #555555 !important;
}
div.bd.footerless {
    background: #999999 !important;
}
.walkthrough ol {
    background-color: #373737;
}
.walkthrough .button {
    background: #262626;
    border: #262626;
}
.fan-banner.empty.owner {
    background-color: #373737;
}
#menubar,
#pgFt,
.menubar-outer {
    background-color: #26423b !important;
    border-bottom: dotted #000 1px !important;
}
#menubar-wrapper {
    background-color: #000;
    border-bottom: dotted #000 1px !important;
}
#menubar input#search-field {
    margin: 0;
    height: 21px;
    line-height: 21px;
    width: 222px;
    font-family: "Helvetica Neue", Arial, sans-serif;
    color: #fff;
    font-size: 13px;
    padding: 0 21px 0 3px;
    -webkit-user-select: text;
    text-align: center;
    background-color: #282828;
    border: 1px solid #282828;
    outline: none;
    border-radius: 3px;
}
#menubar input#search-field.focused {
    background-color: #282828;
    border: 1px solid #282828;
}
.fan-bio .edit-profile a {
    border: 1px solid #373737;
    border-radius: 5px;
    outline: none;
    background: #373737;
    color: #aaa;
    font-weight: 500;
    padding: 5px 9px;
    font-size: 11px;
    line-height: 15px;
    text-transform: uppercase;
    display: inline-block;
}
.grids {
    color: #fff;
    margin: 0 0 100px;
}
.recommendations-container {
    background-color: #373737;
    border-top: dotted #373737 1px;
}
.fan-container .top.editing {
    border-bottom: 1px solid #2a2a2a;
    background-color: rgb(25, 25, 25);
}
.ui-dialog.nu-dialog .ui-dialog-titlebar {
    padding: 15px 20px 12px;
    background-color: #282828;
    border-bottom: 1px solid #282828;
}
.ui-widget-content {
    border: 1px solid #373;
    background: #373737;
}
.app-promo-desktop,
.bcdaily,
.discover,
.email-intake,
.notable {
    background-color: #262626;
}
.bcdaily .bcdaily-story {
    min-height: 280px;
    background: #373737;
}
.notable-item {
    background-color: #373737;
}
.item-page {
    background: #373737;
    border: 1px solid #373737;
}
.follow-fan-btn {
    background-color: #373737;
    border: 1px solid #373737;
}
.spotlight-bio,
.spotlight-button,
.spotlight-link,
.spotlight-location,
.spotlight-name {
    color: #fff;
}
.aotd-large {
    background: #373737;
}
.factoid-title {
    color: #46C5D5;
}
#autocomplete-results.autocompleted {
    background: #262626;
    border: 1px solid #262626;
    color: white;
}
.searchwidget.keyboard-focus input[type=text]:focus {
    background: #262626;
    box-shadow: 0 0;
}
.discover-detail-inner {
    background-color: #373737;
}
body.wordpress {
    background: #262626;
}
.wordpress .sidebar .textwidget {
    color: #fff;
}
.wordpress h1 a {
    display: block;
    height: 60px;
    background-size: 242px 28px;
    background-position: 24.6% 50%;
}
p {
    color: #ffffff !important;
}
.wordpress #content {
    color: #ffffff;
}
#dash-container .follow-band,
#dash-container .follow-discover,
#dash-container .follow-fan {
    border: 1px solid #373737;
    background: linear-gradient(to bottom, #373737 0%, #373737 100%);
}
html {
    background: #1E1E1E !important;
}
#stories-vm .story-innards {
    background-color: #373737;
}
.pane {
    color: #c7c7c7;
}
#settings-menubar {
    border-right: 1px solid #383838;
}
#settings-menubar li {
    border-left: 1px solid #383838;
    border-bottom: 1px solid #383838;
    border-top: 1px solid #383838;
}
.share_dialog.ui-dialog .ui-dialog-content {
    background-color: #262626;
}
.share_dialog .section_head {
    color: #fff;
}
.buy-dlg {
    color: #ffffff;
}
#menubar > ul > li .logo {
    background: url('https://www.dropbox.com/s/8s7km8r329l7qy7/bandcamp-logo-gray.png?dl=1') 0 0 no-repeat;
    background-size: contain;
    height: 20px;
    margin-top: 15px;
    width: 85px;
}
.hd-logo {
    background: transparent url('https://www.dropbox.com/s/8s7km8r329l7qy7/bandcamp-logo-gray.png?dl=1') no-repeat;
    background-size: 100%;
    margin-top: 24px;
    height: 25px;
    width: 156px;
}
.wordpress h1 a {
    display: block;
    text-indent: -999em;
    background: url('https://www.dropbox.com/s/mx80o2eenp43l0o/bandcamp-daily-retina-dark-theme.png?dl=1') no-repeat;
    height: 60px;
    background-size: 242px 28px;
    background-position: 24.6% 50%;
}
#pgBd {
    color: #fff;
}
.download-bottom-area {
    border-top: none;
    background: none;
}
.download .formats-container {
    border: 1px solid #373737;
    background-color: #373737;
}
.download .formats {
    list-style: none;
    color: #888;
    padding: 0;
    background-color: #373737;
    width: 170px;
    z-index: 2;
    cursor: default;
}
.download .formats li:hover {
    background-color: #262626;
}
/* ####################################### */
html {
  scrollbar-color: #222 #26423b;
}

::-webkit-scrollbar {
  height: 13px;
}
::-webkit-scrollbar-thumb {
  background: #26423b;
  border:1px solid #4a4a4a;
}
::-webkit-scrollbar-thumb:hover {
  background: #316d4b;
}
::-webkit-scrollbar-thumb:active {
  background: #316d4b;
}
::-webkit-scrollbar-track {
  background: #4a4a4a;
}
::-webkit-scrollbar-track:hover {
  background: #4a4a4a;
}
::-webkit-scrollbar-track:active {
  background: #4a4a4a;
}
::-webkit-scrollbar-corner {
  background: #4a4a4a;
}

body {
  background-color:#000 !important;
  color:#fff !important
}

#propOpenWrapper {
  background-color: var(--propOpenWrapperBackgroundColor) !important;
  transition:background-color 500ms
}

img,.bcdaily-thumb-img {
    filter:brightness(70%)
}
img:hover,.bcdaily-thumb-img:hover {
    filter:none;
}
img.imageviewer_image {
    filter:none
}

.bclogo svg {
  filter:brightness(60%)
}

.inline_player .playbutton.busy::after {
  opacity:0.3;
  background-image:url('https://bandcamp.com/img/loading-dark.gif')
}

.inline_player .playbutton,
.inline_player .volumeButton,
.inline_player .nextsongcontrolbutton,
.track_list .play_status {
  background-color:#686868;
  border-color:#595959;
}

.nextsongcontrolbutton .nextsongcontrolicon {
  filter:drop-shadow(#090909b3 1px 1px 2px)
}
.nextsongcontrolbutton.active .nextsongcontrolicon {
  filter:drop-shadow(#a3f204 1px 1px 2px) !important
}

.inline_player .progbar .thumb {
  background-color:#000;
  border-color:#ccc

}
.inline_player .nextbutton, .inline_player .prevbutton {
  opacity:0.7
}
.track_list tr.lyricsRow td[colspan] div{
  color: #f8f8f8;
}

input[type=text],input[type=password],textarea {
  background-color:#121f12 !important;
  color:rgb(64, 179, 51) !important
}

#autocomplete-results .see-all {
  background-color: #f3f3f345 !important;
}

.deluxemenu {
  color: #c9ebfb !important;
  background: #00042f !important;
}
.deluxemenu button {
  background: #1c1494;
}
.deluxeexportmenu table tr>td {
  color: rgb(0,161,198) !important;
}
.deluxeexportmenu table tr>td:nth-child(3) {
  color:rgb(0, 107, 198) !important
}
.deluxemenu fieldset{
  border: 1px solid #fffa !important;
  box-shadow: 1px 1px 3px #fff5 !important;
}
.deluxemenu fieldset legend{
  color: #fffa !important
}

#discographyplayer {
  background-color:#26423b !important;
  color:#869593 !important;
}
#discographyplayer .playlist .playing {
  background: #619aa9db !important;
}
#timeline {
    background: rgba(34, 57, 42, 0.69) !important;
}
#bufferbar {
  background: rgba(77, 79, 76, 0.59) !important;
}
#playhead {
  background: rgb(42, 108, 33) !important;
}
#discographyplayer .playlist {
  scrollbar-color: #222 #26423b !important;
}

#band-navbar {
    background-color: #333 !important;
}

.hd.corp-home {
  background-color:#26423b
}
#hub .bd-section.top-section {
  opacity:0.8
}

#s-daily {
    background: #262626 !important;
}
.franchise-description {
  color: #d7d072
}
.footer-gradient {
  background-image:linear-gradient(to bottom, #262626, #5e5e5e)
}
#s-daily dailyfooter {
  background-color:#5e5e5e
}
#s-daily dailyfooter h2 {
  -webkit-text-stroke: 2px #257110 !important;
}
#s-daily a.pagination-link {
  -webkit-text-stroke: 2px #257110 !important;
}
#s-daily a.pagination-link .back-text {
  -webkit-text-stroke: 2px #1c6c3f !important;
}
article-title {
  color: #e3e3e3;
}
.mpmerchformats {
  color:#909090;
}
article-footer {
  color:#909090;
}
article > article-end {
  filter:invert(75%)
}
article .icon {
    filter: invert(50%);
}

.salesfeed .item-inner:hover {
    background-color: #0e738c !important;
}

.hd.header-rework-2018 .hd-sub-head .blue-gradient {
  background: -webkit-linear-gradient(left, #da5, #daf) !important;
}
.factoid .dots {
  filter:brightness(300%);
}

.bdp_check_onlinkhover_container_shown {
  background-color:#26423ba8 !important;
}
.bdp_check_onlinkhover_container:hover {
  background-color:#2d7d39a8 !important;
  box-shadow: #2db91f7a 0px 0px 5px;) !important;
}

.lyricsText {
  color:#9b9b9b;
}

/* Upcoming releases reminder */
#pastreleases {
  background-color:#154a86!important
 }
#pastreleases .entry:nth-child(odd) {
  background-color:#3e6c9f!important
}
#pastreleases .entry.future {
  background-color:#4783c8!important;
}
#pastreleases .entry.future:nth-child(odd) {
  background-color:#11447d!important;
}
  `
  addStyle(css)

  window.setTimeout(humour, 3000)
  darkModeInjected = true
}

async function darkModeOnLoad () {
  const yes = await darkModeMode()
  if (!yes) {
    return
  }

  // Load body's background image and detect if it is light or dark and adapt it's transparency
  const backgroudImageCSS = window.getComputedStyle(document.body).backgroundImage
  let imageURL = backgroudImageCSS.match(/["'](.*)["']/)
  let shouldUpdate = false
  let hasBackgroundImage = false
  if (imageURL && imageURL[1]) {
    imageURL = imageURL[1]
    shouldUpdate = true
    hasBackgroundImage = true
    try {
      const editTime = parseInt(window.localStorage.getItem('bcsde_bgimage_brightness_time'))
      if (Date.now() - editTime < 604800000) {
        shouldUpdate = false
      }
    } catch (e) {
      console.log('Could not read from window.localStorage: ' + e)
    }
  }
  if (shouldUpdate) {
    const canvas = await loadCrossSiteImage(imageURL)
    const ctx = canvas.getContext('2d')
    const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data
    let sum = 0.0
    let div = 0
    const stepSize = canvas.width * canvas.height / 1000
    const len = data.length - 4
    for (let i = 0; i < len; i += 4 * parseInt(stepSize * Math.random())) {
      const v = Math.max(Math.max(data[i], data[i + 1]), data[i + 2])
      sum += v
      div++
    }
    const brightness = sum / div
    const alpha = (brightness - 50) / 255
    document.querySelector('#propOpenWrapper').style.backgroundColor = `rgba(0, 0, 0, ${alpha})`
    console.log(`Brightness updated: ${brightness}, alpha: ${alpha}`)
    try {
      window.localStorage.setItem('bcsde_bgimage_brightness', brightness)
      window.localStorage.setItem('bcsde_bgimage_brightness_time', Date.now())
    } catch (e) {
      console.log('Could not write to window.localStorage: ' + e)
    }
  }

  if (!hasBackgroundImage) {
    // No background image, check background color
    const color = window.getComputedStyle(document.body).backgroundColor
    if (color) {
      const m = color.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/)
      if (m) {
        const [, r, g, b] = m
        if (r < 70 && g < 70 && b < 70) {
          addStyle(`
            :root {
              --propOpenWrapperBackgroundColor: rgb(${r}, ${g}, ${b})
            }
          `)
        }
      }
    }
  }
  // pgBd background color
  if (document.getElementById('custom-design-rules-style')) {
    const customCss = document.getElementById('custom-design-rules-style').textContent
    if (customCss.indexOf('#pgBd') !== -1) {
      const pgBdStyle = customCss.split('#pgBd')[1].split('}')[0]
      const m = pgBdStyle.match(/background(-color)?\s*:\s*(.+?)[;\s]/m)
      if (m && m.length > 2 && m[2]) {
        const color = css2rgb(m[2])
        if (color) {
          const [r, g, b] = color
          if (r < 70 && g < 70 && b < 70) {
            addStyle(`
              :root {
                --pgBdColor: rgb(${r}, ${g}, ${b});
              }
            `)
          }
        }
      }
    }
  }
}

async function updateSuntimes () {
  const value = await GM.getValue('darkmode', '1')
  if (value.startsWith('3#')) {
    const data = JSON.parse(value.substring(2))
    const sunData = suntimes(new Date(), data.latitude, data.longitude)
    const newValue = '3#' + JSON.stringify(Object.assign(data, sunData))
    if (newValue !== value) {
      await GM.setValue('darkmode', newValue)
    }
  }
}

function confirmDomain () {
  return new Promise(function confirmDomainPromise (resolve) {
    GM.getValue('domains', '{}').then(function (v) {
      const domains = JSON.parse(v)
      if (document.location.hostname in domains) {
        const isBandcamp = domains[document.location.hostname]
        return resolve(isBandcamp)
      } else {
        window.setTimeout(function () {
          const isBandcamp = window.confirm(`${SCRIPT_NAME}

This page looks like a bandcamp page, but the URL ${document.location.hostname} is not a bandcamp URL.

Do you want to run the userscript on this page?

If this is a malicious website, running the userscript may leak personal data (e.g. played albums) to the website`)
          domains[document.location.hostname] = isBandcamp
          GM.setValue('domains', JSON.stringify(domains)).then(() => resolve(isBandcamp))
        }, 3000)
      }
    })
  })
}

async function setDomain (enabled) {
  const domains = JSON.parse(await GM.getValue('domains', '{}'))
  domains[document.location.hostname] = enabled
  await GM.setValue('domains', JSON.stringify(domains))
}

var darkModeModeCurrent = null
async function darkModeMode () {
  if (darkModeModeCurrent != null) {
    return darkModeModeCurrent
  }
  const value = await GM.getValue('darkmode', '1')
  darkModeModeCurrent = false
  if (value.startsWith('1')) {
    darkModeModeCurrent = true
  } else if (value.startsWith('2#')) {
    darkModeModeCurrent = nowInTimeRange(value.substring(2))
  } else if (value.startsWith('3#')) {
    const data = JSON.parse(value.substring(2))
    window.setTimeout(updateSuntimes, Math.random() * 10000)
    darkModeModeCurrent = nowInBetween(new Date(data.sunset), new Date(data.sunrise))
  }
  return darkModeModeCurrent
}

function start () {
  // Load settings and enable darkmode
  GM.getValue('enabledFeatures', false).then((value) => getEnabledFeatures(value)).then(function () {
    if (BANDCAMP && allFeatures.darkMode.enabled) {
      darkModeMode().then(function (yes) {
        if (yes) {
          darkMode()
        }
      })
    }
  })
}

function onLoaded () {
  if (!BANDCAMP && document.querySelector('#legal.horizNav li.view-switcher.desktop a')) {
    // Page is a bandcamp page but does not have a bandcamp domain
    confirmDomain().then(function (isBandcamp) {
      BANDCAMP = isBandcamp
      if (isBandcamp) {
        onLoaded()
        GM.registerMenuCommand(SCRIPT_NAME + ' - disable on this page', () => setDomain(false).then(() => document.location.reload()))
      } else {
        GM.registerMenuCommand(SCRIPT_NAME + ' - enable on this page', () => setDomain(true).then(() => document.location.reload()))
      }
    })
    return
  } else if (!BANDCAMP && !CAMPEXPLORER) {
    // Not a bandcamp page -> quit
    return
  }

  if (allFeatures.darkMode.enabled) {
    // Darkmode in start() is only run on bandcamp domains
    if (!darkModeInjected) {
      darkModeMode().then(function (yes) {
        if (yes) {
          darkMode()
        }
      })
    }
    window.setTimeout(darkModeOnLoad, 0)
  }

  const maintenanceContent = document.querySelector('.content')
  if (maintenanceContent && maintenanceContent.textContent.indexOf('are offline') !== -1) {
    console.log('Maintenance detected')
  } else {
    if (NOEMOJI) {
      addStyle('@font-face{font-family:Symbola;src:local("Symbola Regular"),local("Symbola"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.woff2) format("woff2"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.woff) format("woff"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.ttf) format("truetype"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.otf) format("opentype"),url(https://cdnjs.cloudflare.com/ajax/libs/mathquill/0.10.1/font/Symbola.svg#Symbola) format("svg")}' +
        '.sharepanelchecksymbol,.bdp_check_onlinkhover_symbol,.bdp_check_onchecked_symbol,.volumeSymbol,.downloaddisk,.downloadlink,#user-nav .settingssymbol,.listened-symbol,.mark-listened-symbol,.minimizebutton{font-family:Symbola,Quivira,"Segoe UI Symbol","Segoe UI Emoji",Arial,sans-serif}' +
        '.downloaddisk,.downloadlink{font-weight: bolder}')
    }

    if (allFeatures.releaseReminder.enabled) {
      showPastReleases()
    }

    if (document.querySelector('#indexpage .indexpage_list_cell a[href^="/album/"] img')) {
      // Index pages are almost like discography page. To make them compatible, let's add the class names from the discography page
      document.querySelector('#indexpage').classList.add('music-grid')
      document.querySelectorAll('#indexpage .indexpage_list_cell').forEach(cell => cell.classList.add('music-grid-item'))
      addStyle('#indexpage .ipCellImage { position:relative }')
    }

    if (allFeatures.discographyplayer.enabled && document.querySelector('.music-grid .music-grid-item a[href^="/album/"] img')) {
      // Discography page
      makeAlbumCoversGreat()
    }

    if (document.querySelector('.inline_player')) {
      // Album page with player
      if (allFeatures.thetimehascome.enabled) {
        removeTheTimeHasComeToOpenThyHeartWallet()
      }
      if (allFeatures.albumPageVolumeBar.enabled) {
        window.setTimeout(addVolumeBarToAlbumPage, 3000)
      }
      if (allFeatures.albumPageDownloadLinks.enabled) {
        window.setTimeout(addDownloadLinksToAlbumPage, 500)
      }
      if (allFeatures.albumPageLyrics.enabled) {
        window.setTimeout(addLyricsToAlbumPage, 500)
      }
      if (unsafeWindow.TralbumData && unsafeWindow.TralbumData.current && unsafeWindow.TralbumData.trackinfo) {
        const TralbumData = correctTralbumData(JSON.parse(JSON.stringify(unsafeWindow.TralbumData)), document.body.innerHTML)
        storeTralbumDataPermanently(TralbumData)
      }
    }

    if (document.querySelector('.share-panel-wrapper-desktop')) {
      // Album page with Share,Embed,Wishlist links

      if (allFeatures.markasplayedEverywhere.enabled) {
        addListenedButtonToCollectControls()
      }

      if (document.location.hash === '#collect-wishlist') {
        clickAddToWishlist()
      }

      if (document.querySelector('*[itemprop="datePublished"]')) {
        addReleaseDateButton()
      }
    }

    GM.registerMenuCommand(SCRIPT_NAME + ' - Settings', mainMenu)
    if (document.getElementById('user-nav')) {
      appendMainMenuButtonTo(document.getElementById('user-nav'))
    } else if (document.getElementById('customHeaderWrapper')) {
      appendMainMenuButtonLeftTo(document.getElementById('customHeaderWrapper'))
    }

    if (document.getElementById('carousel-player') || document.querySelector('.play-carousel')) {
      window.setTimeout(makeCarouselPlayerGreatAgain, 5000)
    }

    if (document.querySelector('ol#grid-tabs li') && document.querySelector('.fan-bio-pic-upload-container')) {
      const listenedTabLink = makeListenedListTabLink()
      if (document.location.hash === '#listened-tab') {
        window.setTimeout(function resetGridTabs () {
          document.querySelector('#grid-tabs .active').classList.remove('active')
          document.querySelector('#grids .grid.active').classList.remove('active')
          listenedTabLink.classList.add('active')
          listenedTabLink.click()
        }, 500)
      }
    }

    if (allFeatures.albumPageVolumeBar.enabled) {
      restoreVolume()
    }

    if (allFeatures.markasplayedEverywhere.enabled) {
      makeAlbumLinksGreat()
    }

    if (allFeatures.backupReminder.enabled) {
      checkBackupStatus()
    }

    if (CAMPEXPLORER) {
      let lastTagsText = document.querySelector('.tags') ? document.querySelector('.tags').textContent : ''
      window.setInterval(function () {
        const tagsText = document.querySelector('.tags') ? document.querySelector('.tags').textContent : ''
        if (lastTagsText !== tagsText) {
          lastTagsText = tagsText
          if (allFeatures.discographyplayer.enabled) {
            makeAlbumCoversGreat()
          }
          if (allFeatures.markasplayedEverywhere.enabled) {
            makeAlbumLinksGreat()
          }
        }
      }, 3000)
    }

    if (document.location.pathname === '/robots.txt') {
      initGenius()
    }

    GM.getValue('musicPlayerState', '{}').then(function restoreState (s) {
      if (s !== '{}') {
        GM.setValue('musicPlayerState', '{}')
        musicPlayerRestoreState(JSON.parse(s))
      }
    })
  }
}

start()
if (document.readyState !== 'complete' || document.readyState !== 'loaded') {
  document.addEventListener('DOMContentLoaded', onLoaded)
} else {
  onLoaded()
}