cuzi / Multi-OCH Helper

// ==UserScript==
// @name             Multi-OCH Helper
// @namespace        cuzi
// @license          MIT
// @description      nopremium.pl and premiumize.me. Inserts a direct download link on several one-click-hosters and some container/folder providers.
// @homepageURL      https://openuserjs.org/scripts/cuzi/Multi-OCH_Helper
// @updateURL        https://openuserjs.org/meta/cuzi/Multi-OCH_Helper.meta.js
// @contributionURL  https://buymeacoff.ee/cuzi
// @contributionURL  https://ko-fi.com/cuzicvzi
// @icon             https://raw.githubusercontent.com/cvzi/Userscripts/master/Multi-OCH/icons/helper.png
// @version          17.1.3

// @match            https://cvzi.github.io/Userscripts/index.html?link=*
// @match            https://www.nopremium.pl/files*
// @match            https://www.premiumize.me/hosters/*
// @match            https://www.premiumize.me/services/*
// @match            https://www.premiumize.me/downloader*

// @match            https://*.filecrypt.cc/Container/*
// @match            https://*.filecrypt.cc/helper.html*
// @match            https://protected.to/*
// @match            https://rapidgator.net/folder/*
// @match            https://safelinking.net/p/*
// @match            https://multiup.org/*

// @match            https://1fichier.com/*
// @match            https://*.1fichier.com/*
// @match            https://www.4shared.com/*
// @match            https://alfafile.net/*
// @match            https://*.alfafile.net/*
// @match            https://anonfiles.com/*
// @match            https://bayfiles.com/*
// @match            https://*.bayfiles.com/*
// @match            http://clicknupload.link/*
// @match            https://clicknupload.to/*
// @match            https://clicknupload.org/*
// @match            https://clicknupload.co/*
// @match            https://clicknupload.cc/*
// @match            https://clicknupload.to/*
// @match            https://clicknupload.club/*
// @match            https://clicknupload.click/*
// @match            https://dailyuploads.net/*
// @match            https://ddl.to/*
// @match            https://ddownload.com/*
// @match            https://*.dropapk.com/*
// @match            https://dropapk.com/*
// @match            https://*.drop.download.com/*
// @match            https://drop.download.com/*
// @match            https://fastclick.to/*
// @match            https://fastshare.cz/*
// @match            https://fikper.com/*
// @match            https://file.al/*
// @match            https://www.file.al/*
// @match            https://filefactory.com/*
// @match            https://www.filefactory.com/*
// @match            https://filenext.com/*
// @match            https://www.filenext.com/*
// @match            https://filer.net/*
// @match            https://filerice.com/*
// @match            https://filespace.com/*
// @match            https://filestore.to/*
// @match            http://fireget.com/*
// @match            https://fireget.com/*
// @match            https://hitfile.net/*
// @match            https://hil.to/*
// @match            https://isra.cloud/*
// @match            https://katfile.com/*
// @match            https://www.mediafire.com/*
// @match            https://mediafire.com/*
// @match            https://mega.nz/*
// @match            https://megaup.net/*
// @match            https://mixdrop.co/*
// @match            https://modsbase.com/*
// @match            https://nitroflare.com/*
// @match            https://rapidgator.net/file/*
// @match            https://rg.to/file/*
// @match            https://spicyfile.com/*
// @match            https://www.spicyfile.com/*
// @match            https://turbobit.net/*
// @match            https://turb.to/*
// @match            https://tusfiles.net/*
// @match            https://ubiqfile.com/*
// @match            https://uploadboy.com/*
// @match            https://uploadgig.com/*
// @match            https://uptobox.com/*
// @match            https://userscloud.com/*
// @match            https://vidoza.org/*
// @match            https://worldbytez.com/*
// @match            https://wrzucajpliki.pl/*
// @match            https://xubster.com/*
// @match            https://*.zippyshare.com/*

// @require          https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js
// @require          https://greasyfork.org/scripts/13883-aes-js/code/aesjs.js
// @grant            GM.registerMenuCommand
// @grant            unsafeWindow
// @grant            GM_setClipboard
// @grant            GM.xmlHttpRequest
// @grant            GM.openInTab
// @grant            GM.setClipboard
// @grant            GM.setValue
// @grant            GM.getValue
// @grant            GM.deleteValue
// @grant            GM.listValues
// ==/UserScript==

/* globals confirm, alert, GM, GM_setClipboard, unsafeWindow, $, atob, slowAES, cloneInto */
/* eslint n/no-callback-literal: 0 */
/* jshint asi: true, esversion: 8 */

(async function () {
  'use strict'

  // And to keep for myself whatever I may find? - Certainly. For yourself, and any friends you want to share with you.

  // This program inserts a download link on One-Click-Hosters and a few folder services.
  // If you click on the button, the current website address (or the links on the relink website) will be sent to nopremium.pl and you'll receive a nopremium.pl download link.
  //
  // Standard actions for the button are
  // * left mouse click:                      copy the link to the clipboard
  // * middle/wheel click:                    start download of the link
  // * right mouse click:                     open the nopremium.pl website and insert the link in the text box
  // * hovering the mouse over the button:    open a menu with all the above option
  //

  const scriptName = 'Multi-OCH Helper'
  const scriptReferer = 'multiochhelper'
  const scriptHightligherName = 'Multi-OCH Helper Highlight links'

  const chrome = ~navigator.userAgent.indexOf('Chrome')
  const greasemonkey = 'info' in GM && 'scriptHandler' in GM.info && GM.info.scriptHandler === 'Greasemonkey'

  const config = {
    position: [['bottom', 'top'], ['left', 'right']],
    position_desc: ['vertical', 'horizontal'],
    position_quest: 'Position of the Button. If you use "' + scriptHightligherName + '" this has to be set to bottom left',
    leftClick: ['clipboard', 'download', 'showLinks', 'openWebsite', 'menu', 'sendToJD', 'none'],
    leftClick_desc: ['Copy link to clipboard', 'Direct download', 'Show links like on website', 'Open the multihoster website', 'Show the extended menu', 'Send links to JDownloader', 'Do nothing'],
    leftClick_quest: 'Action on left mouse click on button',
    middleClick: ['download', 'clipboard', 'showLinks', 'openWebsite', 'menu', 'sendToJD', 'none'],
    middleClick_desc: ['Direct download', 'Copy link to clipboard', 'Show links like on website', 'Open the multihoster website', 'Show the extended menu', 'Send links to JDownloader', 'Do nothing'],
    middleClick_quest: 'Action on middle mouse/wheel click on button',
    rightClick: ['openWebsite', 'clipboard', 'showLinks', 'download', 'menu', 'sendToJD', 'none'],
    rightClick_desc: ['Show links like on website', 'Copy link to clipboard', 'Direct download', 'Open the multihoster website', 'Show the extended menu', 'Send links to JDownloader', 'Do nothing'],
    rightClick_quest: 'Action on right mouse click on button',
    mouseOver: ['menu', 'clipboard', 'download', 'showLinks', 'openWebsite', 'sendToJD', 'none'],
    mouseOver_desc: ['Show the extended menu', 'Copy link to clipboard', 'Direct download', 'Show links like on website', 'Open the multihoster website', 'Send links to JDownloader', 'Do nothing'],
    mouseOver_quest: 'Action on mouse hover over button',
    mouseOverDelay: 'int',
    mouseOverDelay_range: [0, 700, 3000],
    mouseOverDelay_quest: 'Mouse hover time before action is executed.',
    mouseOverDelay_suffix: 'milliseconds',
    newTab: 'bool',
    newTab_desc: ['Open in a new tab', 'Open in the same window'],
    newTab_quest: 'Should websites be opened in a new tab?',
    updateHosterStatusInterval: 'int',
    updateHosterStatusInterval_range: [1, 168, 9999],
    updateHosterStatusInterval_quest: 'How often should the status of the hosters be updated?',
    updateHosterStatusInterval_prefix: 'Every',
    updateHosterStatusInterval_suffix: 'hours',
    jDownloaderSupport: 'bool',
    jDownloaderSupport_desc: ['Show JDownloader button if JDownloader is runnning', 'Never show JDownloader button'],
    jDownloaderSupport_quest: ['Show a JDownloader button in the menu?']

  }
  const settings = {}
  // Load settings
  const savedsettings = JSON.parse(await GM.getValue('settings', '{}')) // e.g. {  position : [0,1], newTab : 1  }
  for (const key in config) {
    if (key in savedsettings) { // Saved
      if (config[key] === 'int') { // Int
        settings[key] = parseInt(savedsettings[key], 10)
      } else if (config[key] === 'string') { // String
        settings[key] = savedsettings[key].toString()
      } else if (config[key] === 'bool') { // Bool
        settings[key] = (savedsettings[key] === 'true' || savedsettings[key] === true)
      } else if (Array.isArray(config[key][0])) { // Nested array
        if (!Array.isArray(savedsettings[key])) {
          try {
            const tmp = JSON.parse(savedsettings[key])
            if (Array.isArray(tmp)) {
              savedsettings[key] = tmp
            }
          } catch (e) {}
        }
        settings[key] = []
        for (let i = 0; i < savedsettings[key].length; i++) {
          settings[key].push(savedsettings[key][i])
        }
      } else { // Array
        settings[key] = savedsettings[key]
      }
    } else { // Default
      if (config[key] === 'int') { // Int
        settings[key] = config[key + '_range'][1]
      } else if (config[key] === 'string') { // String
        settings[key] = '' // String defaults to empty string
      } else if (config[key] === 'bool') { // Bool
        settings[key] = true
      } else if (Array.isArray(config[key][0])) { // Nested array defaults to first value for each array
        settings[key] = []
        for (let i = 0; i < config[key].length; i++) {
          settings[key].push(config[key][i][0])
        }
      } else {
        settings[key] = config[key][0] // Array defaults to first value
      }
    }
  }

  const JDOWNLOADER = 'http://127.0.0.1:9666/'
  const SPINNERCSS = `/* http://www.designcouch.com/home/why/2013/05/23/dead-simple-pure-css-loading-spinner/ */
  .ochspinner {
    height:16px;
    width:16px;
    margin:0px auto;
    position:relative;
    animation: rotation .6s infinite linear;
    border-left:6px solid rgba(0,174,239,.15);
    border-right:6px solid rgba(0,174,239,.15);
    border-bottom:6px solid rgba(0,174,239,.15);
    border-top:6px solid rgba(0,174,239,.8);
    border-radius:100%;
  }
  @keyframes rotation {
    from {transform: rotate(0deg)}
    to {transform: rotate(359deg)}
  }
  `
  // const LOADINGBARBG = 'background: #b4e391;background: linear-gradient(to bottom, #b4e391 0%,#61c419 50%,#b4e391 100%);'

  let showOneclickButton = false
  let showOneclickLink = ''
  let showOneclickFromHighlighScriptAllLinks = document.location.host === 'cvzi.github.io'
  let showOneclickFromHighlighScriptAllLinksLoc = false
  let showOneclickFromHighlighScriptAllLinksLinks = ''
  let showOneclickFromHighlighScriptSelectedLinks = false
  let showOneclickFromHighlighScriptSelectedLinksLoc = false
  let showOneclickFromHighlighScriptSelectedLinksLinks = ''

  let linksBeforeSelection = false

  const multi = {
    'premiumize.me': new function () {
      const self = this
      this.config = {
        apikey: 'string',
        apikey_hidden: true,
        apikey_quest: 'Enter your premiumize.me API key',
        apikey_prefix: 'API key: ',
        apikey_suffix: ' find it under <a target="_blank" href="https://www.premiumize.me/account">https://www.premiumize.me/account</a>'
      }
      this.key = 'premiumize.me'
      this.name = 'premiumize'
      this.homepage = 'https://www.premiumize.me/'
      // this.updateStatusURL = 'https://www.premiumize.me/services';
      this.updateStatusURLpattern = /https:\/\/www\.premiumize\.me\/services\/?/
      this.updateDownloadProgressInterval = 5000
      this.updateDownloadProgressInterfaceInterval = 500

      this.status = {}

      this.init = async function () {
        self.status = JSON.parse(await GM.getValue(self.key + '_status', '{}'))
        self.lastUpdate = new Date(await GM.getValue(self.key + '_status_time', 0))
      }

      this.settings = {}
      this.loadSettings = async function (silent) {
      // Load settings, use first value as default
        const savedsettings = JSON.parse(await GM.getValue(self.key + '_settings', '{}'))

        for (const key in self.config) {
          if (key.endsWith('desc') || key.endsWith('range') || key.endsWith('quest') || key.endsWith('prefix') || key.endsWith('suffix')) {
            continue
          }
          if (key in savedsettings) { // Saved
            if (self.config[key] === 'int') { // Int
              self.settings[key] = parseInt(savedsettings[key], 10)
            } else if (self.config[key] === 'string') { // String
              self.settings[key] = savedsettings[key].toString()
            } else if (config[key] === 'bool') { // Bool
              self.settings[key] = savedsettings[key] === 'true' || savedsettings[key] === true
            } else if (Array.isArray(savedsettings[key])) { // Nested array
              self.settings[key] = []
              for (let i = 0; i < savedsettings[key].length; i++) {
                self.settings[key].push(savedsettings[key][i])
              }
            } else { // Array
              self.settings[key] = savedsettings[key]
            }
          } else { // Default
            if (self.config[key] === 'int') { // Int
              self.settings[key] = self.config[key + '_range'][1]
            } else if (self.config[key] === 'string') { // String
              self.settings[key] = '' // String defaults to empty string
            } else if (config[key] === 'bool') { // Bool
              self.settings[key] = true
            } else if (Array.isArray(self.config[key][0])) { // Nested array defaults to first value for each array
              self.settings[key] = []
              for (let i = 0; i < self.config[key].length; i++) {
                self.settings[key].push(self.config[key][i][0])
              }
            } else {
              self.settings[key] = self.config[key][0] // Array defaults to first value
            }
          }
        }

        if (!self.settings.apikey && !silent) {
        // Try to get the apikey from the website
          GM.xmlHttpRequest({
            method: 'GET',
            url: self.homepage + 'account',
            onerror: function (response) {
              console.log(scriptName + ': premiumize.me API Key could not be loaded')
              setStatus('You have not set you premiumize.me Api key ')
            },
            onload: function (response) {
              let s = ''
              try {
                s = response.responseText.split('class="apipass"')[1].split('</')[0].split('>')[1]
              } catch (e) {
              }
              if (s) {
                self.settings.apikey = s
                GM.setValue(self.key + '_settings', JSON.stringify(self.settings))

                console.log(scriptName + ': premiumize.me API Key was loaded from account and saved!')
              } else {
                setStatus('You need to set you premiumize.me Api key')
              }
            }
          })
        }
      }

      this.updateStatus = async function () { // Update list of online hosters
        await self.loadSettings()
        if (document.location.href.match(self.updateStatusURL)) {
        // Read and save current status of all hosters
          if ($('table.table tr>td:first-child').length) {
            self.status = {}
            await GM.setValue(self.key + '_status_time', '' + (new Date()))
            $('table.table tr>td:first-child').each(function () {
              const text = $(this).text()
              if (text.match(/^\s*[0-9a-z-]+\.\w{0,6}\s*$/i)) {
                const name = text.match(/^\s*([0-9a-z-]+)\.\w{0,6}\s*$/i)[1]
                self.status[name.toLowerCase()] = true
              }
            })
            await GM.setValue(self.key + '_status', JSON.stringify(self.status))
            console.log(scriptName + ': ' + self.name + ': Hosters (' + Object.keys(self.status).length + ') updated')
          } else if (self.settings.apikey) {
            GM.xmlHttpRequest({
              method: 'GET',
              url: self.homepage + 'api/services/list?apikey=' + encodeURIComponent(self.settings.apikey),
              onerror: function (response) {
                console.log(scriptName + ': GM.xmlHttpRequest error: ' + self.homepage + 'api/services/list')
                console.log(response)
              },
              onload: async function (response) {
                const result = JSON.parse(response.responseText)
                /*
              { "cache": [ "uploaded.to", "filefactory.com", ... ], "directdl": [ "uploaded.to", "filefactory.com", ... ] }
              */
                if ('cache' in result && 'directdl' in result) {
                  self.status = {}
                  await GM.setValue(self.key + '_status_time', '' + (new Date()))
                  result.cache.forEach(function (host) {
                    const name = host.match(/^\s*([0-9a-z-]+)\.\w{0,6}\s*$/i)[1]
                    self.status[name.toLowerCase()] = result.directdl.indexOf(host) !== -1
                  })

                  await GM.setValue(self.key + '_status', JSON.stringify(self.status))
                  console.log(scriptName + ': ' + self.name + ': Hosters (' + Object.keys(self.status).length + ') updated')
                } else {
                  console.log(scriptName + ': GM.xmlHttpRequest error: ' + self.homepage + 'api/services/list')
                  console.log(response)
                }
              }
            })
          } else {
            console.log(scriptName + ': Cannot update hosters, no html and no api key found')
          }
        } else {
          alert(scriptName + '\n\nError: wrong update URL')
        }
      }
      this.isOnline = hostername => hostername in self.status && self.status[hostername]

      this.getOpenWebsiteURL = function (urls) {
      // Return a link to the premiumize.me website that will insert the links
        const url = this.homepage + 'downloader?link:' + encodeURIComponent(urls.join('\n'))
        return url
      }

      this.checkLink = function (url, cb) { // check whether the link is supported and online
        const host = url.match(/https?:\/\/(.+?)\//)[1]
        let hoster = host.split('.')
        hoster.pop()
        hoster = hoster.pop().replace('-', '')
        cb(this.isOnline(hoster))
      }

      this.getResults = function (urls, cb) {
      // cb($node,linkNumber) -- $node contains the result, linkNumber is the number of links that should be online i.e. number of hashes
        alert('This function does not work for ' + this.name)
      }

      this._notLoggedIn = false

      this.getLinks = async function (urls, cb) {
        await showConfirm('fairPointsWarning', 'You will be charged premiumize fair points for generating ' + (urls.length > 1 ? ('<b>' + urls.length + '</b> files') : ('<b>one</b> file')) + '!<br><br>Generate links?', function () { self._getLinks(urls, cb) }, function () { setStatus('Operation canceled!', 0); cb([], -1) }, self)
      }

      this._getLinks = function (urls, cb) {
        setTitle('✈️' + urls.length + '🔗 ')
        const N = urls.length
        const downloadLinks = []
        const errors = []
        for (let i = 0; i < urls.length; i++) {
          this._addSingleTransfer(urls[i], function (downloadlink, originallink, message) {
            if (downloadlink) {
              downloadLinks.push(downloadlink)
            } else {
              errors.push([originallink, message])
            }
          })
        }

        const checkprogress = function () {
          if (self._notLoggedIn) {
            // Stop checking and open premiumize homepage
            setTitle('🔑 ')
            setStatus(self.name + ' error: Not logged in!\nMaybe update your API key?', 0)
            GM.openInTab(self.homepage)
            cb([], -2)
            return
          }

          if (N === errors.length) { // All errors
            setTitle('❌ ')
            cb(false, -1)
            if (errors.length === 1 && errors[0][1]) {
              setStatus(errors[0][1], 0)
            } else {
              alert('Errors occured\n' + errors.length + ' links failed:\n\n' + errors.join('\n'))
            }
          } else if (N === downloadLinks.length + errors.length) { // All finished
            setTitle(downloadLinks.length + '/' + errors.length + '✅ ')
            cb(downloadLinks)
            if (errors.length > 0) { // Errors occured
              alert('Errors occured\n' + errors.length + ' links failed:\n\n' + errors.join('\n'))
            }
          } else { // not finished yet
            setTitle(downloadLinks.length + '/' + N + '⏳ ')
            window.setTimeout(checkprogress, self.updateDownloadProgressInterfaceInterval)
          }
        }
        window.setTimeout(checkprogress, self.updateDownloadProgressInterfaceInterval * Math.max(5, N))
      }

      this._addSingleTransfer = function (url, cb) {
        GM.xmlHttpRequest({
          method: 'POST',
          url: self.homepage + 'api/transfer/create',
          data: 'apikey=' + encodeURIComponent(self.settings.apikey) + '&src=' + encodeURIComponent(url),
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cache-Control': 'no-cache'
          },
          onerror: function (response) {
            console.log(scriptName + ': GM.xmlHttpRequest error: ' + self.homepage + 'api/transfer/create')
            console.log(response)
            cb(false, url, 'GM.xmlHttpRequest error: api/transfer/create')
          },
          onload: function (response) {
            const result = JSON.parse(response.responseText)
            /*
          {"status":"success","type":"savetocloud","id":"gfwRtdgd5fgdfgfhgfhf","name":"test.zip"}
          {"status":"error","error":"duplicate","id":"gfdgd5fgFddfgfhgfhf","message":"You already have this job added."}
          {"status":"error","message":"This link is not available on the file hoster website"}
          */
            if ('id' in result && result.id) {
              window.setTimeout(function () {
                self._getFileFromTransfer(url, result.id, cb)
              }, 1000)
              if ('message' in result) {
                addStatus(result.message, -1)
              }
            } else {
              if ('message' in result && !self._notLoggedIn) {
                addStatus(result.message, -1)
                if (~result.message.indexOf('log')) {
                  self._notLoggedIn = true
                }
              }
              cb(false, url, 'message' in result ? result.message : response.responseText)
            }
          }
        })
      }

      this._getFileFromTransfer = function (url, transferId, cb) {
        GM.xmlHttpRequest({
          method: 'GET',
          url: self.homepage + 'api/transfer/list?apikey=' + encodeURIComponent(self.settings.apikey),
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cache-Control': 'no-cache'
          },
          onerror: function (response) {
            console.log(scriptName + ': GM.xmlHttpRequest error: ' + self.homepage + 'api/transfer/list')
            console.log(response)
            cb(false, url, 'GM.xmlHttpRequest error: /api/transfer/list')
          },
          onload: function (response) {
            const result = JSON.parse(response.responseText)
            /*
          {
            "status": "success",
            "transfers": [
              {
                "id": "xXFDSXXFDSGD",
                "name": "test.zip",
                "message": null,
                "status": "finished",
                "progress": 0,
                "folder_id": "gfjdfsuigjfdoikfsadf",
                "file_id": "trhgf982u30fjklfsdag"
              }
            ]
          }
          {
            "status": "success",
            "transfers": [
              {
                "id":"xXFDSXXFDSGD",
                "name":"test.zip",
                "message":"Initializing Download...",
                "status":"running",
                "progress":0,
                "folder_id":"gfjdfsuigjfdoikfsadf",
                "file_id":null
              }
            ]
          }
          */
            if (result.status === 'success' && 'transfers' in result) {
              for (let i = 0; i < result.transfers.length; i++) {
                if (result.transfers[i].id === transferId) {
                  if (result.transfers[i].file_id) {
                  // Finished
                    window.setTimeout(function () {
                      self._getSingleLink(url, result.transfers[i].file_id, cb)
                    }, result.transfers[i].status === 'finished' ? 10 : self.updateDownloadProgressInterval)
                  } else {
                  // Downloading
                    if ('message' in result.transfers[i] && result.transfers[i].message) {
                      setStatus(result.transfers[i].message, -1)
                    }
                    window.setTimeout(function () {
                      self._getFileFromTransfer(url, transferId, cb)
                    }, self.updateDownloadProgressInterval)
                  }

                  return
                }
              }
            }
            if ('message' in result && result.message) {
              alert(scriptName + '\n\nCould not get /api/transfer/list\nError:\n' + result.message)
            }
            cb(false, url, 'Could not find url in transfer list')
          }
        })
      }

      this._getSingleLink = function (url, fileId, cb) {
        GM.xmlHttpRequest({
          method: 'POST',
          url: self.homepage + 'api/item/details',
          data: 'apikey=' + encodeURIComponent(self.settings.apikey) + '&id=' + encodeURIComponent(fileId),
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cache-Control': 'no-cache'
          },
          onerror: function (response) {
            console.log(scriptName + ': GM.xmlHttpRequest error: ' + self.homepage + 'api/item/details')
            console.log(response)
            cb(false, url, 'GM.xmlHttpRequest error: /api/item/details')
          },
          onload: function (response) {
            const result = JSON.parse(response.responseText)
            /*
          {
            "id": "xxXxXxxxXxxx",
            "name": "test.zip",
            "size": 156,
            "created_at": 1572458477,
            "transcode_status": "not_applicable",
            "folder_id": "XxXXXxxxxxx",
            "ip": "1.1.1.1",
            "acodec": "",
            "vcodec": "",
            "mime_type": "application/zip",
            "opensubtitles_hash": "",
            "resx": "",
            "resy": "",
            "duration": "",
            "virus_scan": "ok",
            "type": "file",
            "link": "https://down.host.example.com/dl/abcdefg/test.zip",
            "stream_link": null
          }
          */
            if ('link' in result && result.link) {
              cb(result.link, url)
            } else {
              window.setTimeout(function () {
                self._getSingleLink(url, fileId, cb)
              }, self.updateDownloadProgressInterval)
            }
          }
        })
      }
    }(),

    'nopremium.pl': new function () {
      const self = this
      this.config = {
        mode: ['transfer', 'premium', 'none'],
        mode_desc: ['Transfer User (Pakiety Transferowe)', 'Premium User (Konta Premium)', 'No account'],
        mode_quest: 'What kind of account do you have at nopremium.pl',
        downloadmode: ['direct', 'server'],
        downloadmode_desc: ['Direct download (TRYB SZYBKIEGO POBIERANIA)', 'Downloading via NoPremium.pl server (TRYB POBIERANIA NA SERWERY)'],
        downloadmode_quest: ['Which download mode do you want to use?']
      }
      this.key = 'nopremium.pl'
      this.name = 'NoPremium.pl'
      this.homepage = 'https://www.nopremium.pl/'
      this.updateStatusURL = 'https://www.nopremium.pl/files'
      this.updateStatusURLpattern = /https?:\/\/www\.nopremium\.pl\/files\/?/
      this.updateDownloadProgressInterval = 5000
      const mapHosterName = name => name.replace('-', '')
      this.status = {}

      this.init = async function () {
        self.status = JSON.parse(await GM.getValue(self.key + '_status', '{}'))

        self.lastUpdate = new Date(await GM.getValue(self.key + '_status_time', 0))
      }

      this.settings = {}
      this.loadSettings = async function (silent) {
      // Load settings, use first value as default
        const savedsettings = JSON.parse(await GM.getValue(self.key + '_settings', '{}'))

        for (const key in self.config) {
          if (key.endsWith('desc') || key.endsWith('range') || key.endsWith('quest') || key.endsWith('prefix') || key.endsWith('suffix')) {
            continue
          }
          if (key in savedsettings) { // Saved
            if (self.config[key] === 'int') { // Int
              self.settings[key] = parseInt(savedsettings[key], 10)
            } else if (self.config[key] === 'string') { // String
              self.settings[key] = savedsettings[key].toString()
            } else if (config[key] === 'bool') { // Bool
              self.settings[key] = savedsettings[key] === 'true' || savedsettings[key] === true
            } else if (Array.isArray(savedsettings[key])) { // Nested array
              self.settings[key] = []
              for (let i = 0; i < savedsettings[key].length; i++) {
                self.settings[key].push(savedsettings[key][i])
              }
            } else { // Array
              self.settings[key] = savedsettings[key]
            }
          } else { // Default
            if (self.config[key] === 'int') { // Int
              self.settings[key] = self.config[key + '_range'][1]
            } else if (self.config[key] === 'string') { // String
              self.settings[key] = '' // String defaults to empty string
            } else if (config[key] === 'bool') { // Bool
              self.settings[key] = true
            } else if (Array.isArray(self.config[key][0])) { // Nested array defaults to first value for each array
              self.settings[key] = []
              for (let i = 0; i < self.config[key].length; i++) {
                self.settings[key].push(self.config[key][i][0])
              }
            } else {
              self.settings[key] = self.config[key][0] // Array defaults to first value
            }
          }
        }
      }

      this.updateStatus = async function () { // Update list of online hosters
        if (document.location.href.match(self.updateStatusURL)) {
        // Read and save current status of all hosters
          await GM.setValue(self.key + '_status_time', '' + (new Date()))
          self.status = {}
          $('#servers a[title]').each(function () {
            const name = mapHosterName(this.title)
            self.status[name] = true
          })
          await GM.setValue(self.key + '_status', JSON.stringify(self.status))
          console.log(scriptName + ': ' + self.name + ': Hosters (' + Object.keys(self.status).length + ') updated')
        } else {
          alert(scriptName + '\n\nError: wrong update URL')
        }
      }
      this.isOnline = hostername => hostername in self.status && self.status[hostername]

      this.getOpenWebsiteURL = function (urls) {
      // Return a link to the nopremium.pl website that will insert the links
        const url = this.homepage + 'files?link:' + encodeURIComponent(urls.join('\n'))
        return url
      }

      const getHashs = function (urls, cb, silent) {
      // cb(hashes,sizestring)
        setTitle('✈️ ')
        setStatus('Sending ' + (urls.length === 1 ? 'one link' : (urls.length + ' links')), -1)
        GM.xmlHttpRequest({
          method: 'POST',
          url: self.homepage + 'files',
          data: 'watchonline=&session=' + (Math.round(Math.random() * 1234567)) + '&links=' + encodeURIComponent(urls.join('\n')),
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cache-Control': 'no-cache'
          // "Referer" : "https://www.nopremium.pl/files" // FIREFOX57
          },
          onload: function (response) {
            if (response.responseText.indexOf('<input type="text" name="login" placeholder="Login"/>') !== -1) {
              setTitle('🔑 ')
              setStatus(self.name + ' error: Not logged in!', 0)
              GM.openInTab(self.homepage)
              return cb([], -1)
            }

            const hashes = []
            // Find hashes
            const re = /name="hash(\d+)" value="(\w+)"/g // <input type="checkbox" id="hash0" name="hash0" value="fab3c41988" onclick="UpdateCounter();" c
            let ma = re.exec(response.responseText)
            while (ma) {
              hashes.push(ma[2])
              ma = re.exec(response.responseText)
            }
            // Find errors
            ma = response.responseText.match(/Pliki nieprzetworzone \((\d+)\)/)
            if (ma && !silent) {
              addStatus('Error: ' + (parseInt(ma[1], 10) === 1 ? ('One file is offline or unsupported') : (ma[1] + ' files are offline or unsupported')), 0)
            }

            // Find size
            let size = '0 Byte'
            if (response.responseText.indexOf('id="countSize"') !== -1) {
              ma = response.responseText.split('id="countSize"')[1].match(/value="(\d+.?\d*) (\w+)"/) // <input type="text" name="countSize" id="countSize" style="width:80px;" readonly="readonly" value="1.38 GB">
              size = ma[1] + ' ' + ma[2]
            }

            setStatus(self.name + ' identified ' + (hashes.length === 1 ? 'one online file' : (hashes.length + ' online files')), -1)
            setTitle(hashes.length + '🔗 ')
            cb(hashes, size)
          }
        })
      }

      this.checkLink = function (url, cb) { // check whether the link is supported and online
      // cb(boolresult)
        return getHashs([url], function (hashes, size) {
          cb(hashes.length === 1)
        }, true)
      }

      this.getResults = function (urls, cb, hashes) {
      // cb($node,linkNumber) -- $node contains the result, linkNumber is the number of links that should be online i.e. number of hashes
      // Get download links from nopremium.pl and show the usual info about the file, that is normally shown on nopremium.pl
        if (typeof hashes === 'undefined') {
        // 1. Get hashes and show transfer warning
          getHashs(urls, async function (hashes, size) {
            if (settings.mode === 'transfer') {
              await showConfirm('transferWarning', 'You will be charged <b>' + size + "</b> 'Transfer' for generating " + (hashes.length > 1 ? ('<b>' + hashes.length + '</b> files') : ('<b>one</b> file')) + '!<br><br>Generate links?', function () { this.getResults(urls, cb, hashes) }, null, self)
            } else if (hashes.length > 0) {
              self.getResults(urls, cb, hashes)
            } else if (size === -1) { // Error was already handled (probably not logged in)
              console.log('getHashs->cb: Error was already handled (probably not logged in)')
              cb(false, -2)
            } else { // No files found
              setStatus('No online/available files', 0)
              cb(false, 0)
            }
          })
          return
        }

        // 2. Work with hashes
        const $resultContainer = $('<div></div>').attr('id', 'generated-links')
        const mode = self.settings.downloadmode === 'direct' ? 0 : 1 // 0 -> direct , 1  ->  via server
        GM.xmlHttpRequest({
          method: 'POST',
          url: self.homepage + 'files',
          data: 'insert=1&mode=' + mode + '&hh=0&hash[]=' + hashes.join('&hash[]=') + '&',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cache-Control': 'no-cache'
          // "Referer" : "https://www.nopremium.pl/files"  // FIREFOX57
          },
          onload: function (response) {
            GM.xmlHttpRequest({
              method: 'POST',
              url: self.homepage + 'files',
              data: 'loadfiles=1',
              headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Cache-Control': 'no-cache'
              // "Referer" : "https://www.nopremium.pl/files"  // FIREFOX57
              },
              onload: function (response) {
                if (mode === 0) {
                  $resultContainer.append($('<div></div>').append(response.responseText).find('#fastFilesArea'))
                } else {
                  $resultContainer.append($('<div></div>').append(response.responseText).find('#downloadFilesArea'))
                }
                $resultContainer.find('input[type=checkbox]').remove()
                cb($resultContainer, hashes.length)
              }
            })
          }
        })
      }
      this.getLinks = function (urls, cb) {
      // cb(downloadlinks)

        if (this.settings.downloadmode === 'direct') {
          return this._getDirectLinks(urls, cb)
        } else {
          return this._getServerLinks(urls, cb)
        }
      }

      this._getDirectLinks = function (urls, cb) {
      // Get Direct download links

        this.getResults(urls, async function ($node, N) {
          if (!$node || N < 1) {
            cb(false)
            return
          }

          const text = $node.html()

          /*
        <td>16-08-2014 20:22</td>
        <td class="dlBox"><a href="http://direct.nopremium.pl/9091456/7895ca02bfcb2c2e43806f1079b7ff069129e/result.file"><img src="https://www.nopremium.pl/images/download_ico.png" alt="Sciagnij" title="Sciagnij"></a></td>
        */
          const files = []
          const re = /<td>(\d+)-(\d+)-(\d+) (\d+):(\d+)<\/td>(\s|\n)+<td class="dlBox"><a href="(.*?)"/gm
          let m = re.exec(text) // wholeString, 16,08,2014,20,37,#newline#,http://direct.nopremium.pl/9091456/7895ca02bfcb2c2e43806f1079b7ff069129e/result.file

          while (m) {
            if (m[7].indexOf('//direct.nopremium.pl') === -1) {
              continue // Skip files via server, only use direct download links
            }
            const d = new Date(m[3], m[2], m[1], m[4], m[5], 0, 0)
            files.push([d, m[7]])
            m = re.exec(text)
          }

          if (files.length === 0) {
            alert(scriptName + '\n\nAn error occured.\nCould not find download links in response.')
            cb(false)
            return
          }
          // Find youngest files by comparing their ids
          const pattern = /\.pl\/(\d+)\//
          files.sort(function (a, b) {
            const x = a[1].match(pattern)[1]
            const y = a[1].match(pattern)[1]
            return x > y ? -1 : x < y ? 1 : 0
          })

          const result = []
          for (let i = 0; i < N; i++) {
            result.push(files[i][1])
            await cacheLink([urls[i]], files[i][0], [files[i][1]], self.key) // CACHE single URLs
          }

          await cacheLink(urls, new Date(), result, self.key) // CACHE all URLs

          cb(result)
        })
      }

      this._getServerLinks = function (urls, cb) {
        this.getResults(urls, function ($node, N) {
          if (N === 0) {
            cb(false)
          } else {
            self._getProgress(cb, $node, N)
          }
        })
      }

      this._getProgress = function (cb, $node, N, ids) {
        GM.xmlHttpRequest({
          method: 'POST',
          url: self.homepage + 'files',
          data: 'downloadprogress=1',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cache-Control': 'no-cache'
          // "Referer" : "https://www.nopremium.pl/files" // FIREFOX57
          },
          onerror: function () {
            self._getProgressBlocked = false

            window.setTimeout(function () {
              self._getProgress(cb, $node, N, ids)
            }, self.updateDownloadProgressInterval)
          },
          onload: function (response) {
            self._getProgressBlocked = false

            let data
            try {
              data = JSON.parse(response.responseText)
            } catch (e) {
              console.log(scriptName + ': ' + e)
              console.log(response.responseText)

              if (response.responseText.indexOf('<input type="text" name="login" placeholder="Login"/>') !== -1) {
                setTitle('🔑 ')
                setStatus(self.name + ' error: Not logged in!', 0)
                GM.openInTab(self.homepage)
                cb(false, -2)
              } else {
                window.setTimeout(function () {
                  self._getProgress(cb, $node, N, ids)
                }, self.updateDownloadProgressInterval)
              }
              return
            }

            data.StandardFiles.sort(function (a, b) {
              const x = new Date(a.insert_date.split('-').join('/'))
              const y = new Date(b.insert_date.split('-').join('/'))
              return x > y ? -1 : x < y ? 1 : 0
            })

            const result = []
            const runnning = []
            let percent = 0
            const progess = []

            if (!ids) { // First run: Find the correct files: just use the first N files
              ids = []
              for (let i = 0; i < data.StandardFiles.length && i < N; i++) {
                ids.push(data.StandardFiles[i].id)
                if (data.StandardFiles[i].status === 'finish') {
                  result.push(data.StandardFiles[i].download_url)
                  progess.push(100)
                  percent += 100
                } else {
                  runnning.push(data.StandardFiles[i])
                  if (parseInt(data.StandardFiles[i].status, 10) > 0) {
                    progess.push(parseInt(data.StandardFiles[i].status, 10))
                    percent += parseInt(data.StandardFiles[i].status, 10)
                  }
                }
              }
            } else { // Consecutive runs: Use the ids from first run
              for (let i = 0; i < data.StandardFiles.length; i++) {
                if (ids.indexOf(data.StandardFiles[i].id) === -1) continue
                if (data.StandardFiles[i].status === 'finish') {
                  result.push(data.StandardFiles[i].download_url)
                  progess.push(100)
                  percent += 100
                } else {
                  runnning.push(data.StandardFiles[i])
                  if (parseInt(data.StandardFiles[i].status, 10) > 0) {
                    progess.push(parseInt(data.StandardFiles[i].status, 10))
                    percent += parseInt(data.StandardFiles[i].status, 10)
                  }
                }
              }
            }

            /*
          Regarding caching in server mode:
            If you add a file, that is already on the server (or currently downloading), you will not be charged additional bandwith - therefore caching is not necessary at the moment.
          */

            if (result.length === N) {
              setStatus((result.length === 1 ? 'One file' : (result.length + ' files')) + ' downloaded to server', 1)
              setTitle(result.length + '✅ ')
              cb(result)
            } else {
            // Waiting
              percent = percent / N
              // setStatus('Download '+result.length+'/'+N+' ('+Math.floor(percent)+'%)\n<span title="'+round(percent,2)+'%" style="display:block; width:120px; height:18px; background:white; border:1px solid black; border-radius:5px;"><span style="display:block; border-radius:5px; height:18px; width:'+Math.ceil(percent*1.2)+'px; '+LOADINGBARBG+'"> </span></span>',-1);
              const dotheight = N > 2 ? 2 : 4
              let h = 'Download ' + result.length + '/' + N + ' (' + Math.floor(percent) + '%)\n<div style="display:block; width:130px; height:auto; background:white; border:1px solid black; border-radius:5px; padding:2px; ">'
              for (let i = 0; i < N; i++) {
                if (progess[i]) {
                  h += '<span style="display:block; width:' + Math.ceil(progess[i] * 1.2) + 'px; height:1px; background:white; border-top:' + dotheight + 'px ' + (progess[i] > 99.9 ? 'solid' : 'dotted') + ' green; margin-bottom:1px;"></span>'
                } else {
                  h += '<span style="display:block; width:0x; height:1px; background:white; border-top:' + dotheight + 'px dotted silver; margin-bottom:1px;"></span>'
                }
              }
              h += '</div>'

              setTitle(Math.floor(percent) + '%⏳ ')

              setStatus(h)
              showOnlyStatus()

              window.setTimeout(function () {
                self._getProgress(cb, $node, N, ids)
              }, self.updateDownloadProgressInterval)
            }
          }
        })
      }
    }()

  }

  const debridprovider = Object.keys(multi)
  let currentdebrid = await GM.getValue('currentdebrid', debridprovider[0])

  for (const key in multi) {
    await multi[key].init()
    if (key === currentdebrid) {
      await multi[key].loadSettings()
      continue
    }
    if (!greasemonkey) {
      GM.registerMenuCommand(scriptName + ' - Switch to ' + multi[key].name, (function (key) {
        return async function () {
          if (!confirm(scriptName + '\n\nSet multi-download provider:\n' + multi[key].name)) return

          await GM.setValue('currentdebrid', key)
          currentdebrid = key
          document.location.reload()
        }
      })(key)
      )
    }
  }

  if (!greasemonkey) {
    GM.registerMenuCommand(scriptName + ' - Delete cached links', async function () {
      if (!confirm(scriptName + '\n\nReally delete cached links?')) return

      await GM.setValue('cachedDownloadLinks', '{}')

      alert(scriptName + '\n\nCache is empty!')
    })
    GM.registerMenuCommand(scriptName + ' - Restore dialogs and warnings', async function () {
      if (!confirm(scriptName + '\n\nReally restore all dialogs and warnings?')) return

      await GM.setValue('dialogs', '[]')

      alert(scriptName + '\n\nDialogs and warnings restored')
    })
  }

  /*
  function round (f, p) {
  // Round f to p places after the comma
    return parseFloat(parseFloat(f).toFixed(p))
  }
  */

  const orgDocumentTitle = document.title
  function setTitle (message) {
    if (window.parent.parent !== window) {
      window.parent.parent.postMessage({ iAm: 'Unrestrict.li', type: 'title', str: message }, '*')
    }
    if (message) {
      document.title = message + orgDocumentTitle
    } else {
      document.title = orgDocumentTitle
    }
  }

  function popUp (id, onClose, thisArg, doNotCloseOnOutsideClick) {
  // Remove window scrolling
    $(document.body).css('overflow', 'hidden')
    let zi = getNextZIndex()
    id = id || ('popup' + (new Date()).getTime())
    const $par = $('<div style="position:absolute; top:0px;"></div>').attr('id', id).appendTo(document.body)
    const $background = $('<div style="position:fixed; top:0px; left:0px; right:0px; bottom:0px; background:black; opacity:0.5; z-index:' + (zi++) + '"></div>').appendTo($par)
    const $div = $('<div style="position:fixed; top:50px; left:100px; overflow:auto; z-index:' + (zi++) + '; background:#E6E6E6; color:Black; border:#B555C5 2px solid;border-radius:5px; padding:10px; font-family: "Ubuntu",Arial,Sans-Serif"></div>').css('maxHeight', window.innerHeight - 100).css('maxWidth', window.innerWidth - 200).appendTo($par)

    const close = function () {
      $par.remove()
      if (onClose) onClose.call(thisArg)
      // Restore scrolling
      $(document.body).css('overflow', 'initial')
    }

    if (!doNotCloseOnOutsideClick) {
      $background.click(close)
    }

    return { node: $div, close }
  }

  function configForm ($form, c, s, formid) {
    for (const key in c) {
      if (key.endsWith('desc') || key.endsWith('range') || key.endsWith('quest') || key.endsWith('prefix') || key.endsWith('suffix') || key.endsWith('hidden')) {
        continue
      }

      const $p = $('<p>').appendTo($form)

      if (c[key + '_quest']) {
        $p.append(c[key + '_quest'])
      } else {
        $p.append(key)
      }

      $p.append('<br>')

      if (c[key + '_prefix']) {
        $p.append(c[key + '_prefix'] + ' ')
      }

      const hidden = (key + '_hidden') in c && c[key + '_hidden']
      if (c[key] === 'int') { // Int
        const $input = $('<input type="number">').addClass('form_' + formid).data('key', key).data('parse', 'int').val(s[key]).appendTo($p)
        if (c[key + '_range']) {
          $input.prop('min', c[key + '_range'][0])
          $input.prop('max', c[key + '_range'][2])
          $input.prop('title', c[key + '_range'][0] + ' - ' + c[key + '_range'][2])
        }
      } else if (c[key] === 'string') { // String
        const $inputText = $('<input type="text">').addClass('form_' + formid).data('key', key).data('parse', 'string').appendTo($p)
        if (hidden && s[key]) {
          $inputText.val('## HIDDEN ##')
          $inputText.data('hidden', '1')
        } else {
          $inputText.val(s[key])
        }
      } else if (c[key] === 'bool') { // Bool
        const $select = $('<select></select>').addClass('form_' + formid).data('key', key).data('parse', 'bool').appendTo($p)

        const $optionYes = $('<option></option>').val('true').appendTo($select)
        if (c[key + '_desc']) {
          $optionYes.html(c[key + '_desc'][0])
        } else {
          $optionYes.html('Yes')
        }
        if (s[key]) {
          $optionYes[0].selected = true
        }

        const $optionNo = $('<option></option>').val('false').appendTo($select)
        if (c[key + '_desc']) {
          $optionNo.html(c[key + '_desc'][1])
        } else {
          $optionNo.html('No')
        }
        if (!s[key]) {
          $optionNo[0].selected = true
        }
      } else if (Array.isArray(c[key][0])) { // Nested array
        for (let j = 0; j < c[key].length; j++) {
          if (c[key + '_desc'] && !Array.isArray(c[key + '_desc'][j])) {
            $p.append(c[key + '_desc'][j] + ': ')
          }

          const $select = $('<select></select>').addClass('form_' + formid).data('key', key).data('index', j).appendTo($p)
          for (let i = 0; i < c[key][j].length; i++) {
            const $option = $('<option></option>').val(c[key][j][i]).appendTo($select)
            if (c[key + '_desc'] && Array.isArray(c[key + '_desc'][0])) {
              $option.html(c[key + '_desc'][j][i])
            } else {
              $option.html(c[key][j][i])
            }
            if (s[key][j] === c[key][j][i]) { $option[0].selected = true }
          }
          $p.append('<br>')
        }
      } else { // Array
        const $select = $('<select></select>').addClass('form_' + formid).data('key', key).appendTo($p)
        for (let i = 0; i < c[key].length; i++) {
          const $option = $('<option></option>').val(c[key][i]).appendTo($select)
          if (c[key + '_desc']) {
            $option.html(c[key + '_desc'][i])
          } else {
            $option.html(c[key][i])
          }
          if (s[key] === c[key][i]) { $option[0].selected = true }
        }
      }

      if (c[key + '_suffix']) {
        $p.append(' ' + c[key + '_suffix'])
      }
    }
  }

  async function saveSettings (ev) {
    const $body = ev.data
    const $form = $body.find('.form')

    // Save preferred hoster:
    currentdebrid = $form.find('.debridhoster').val()

    // Save options:
    const newsettings = { general: {} }
    for (const key in multi) {
      newsettings[key] = {}
    }

    $form.find('*[class^=form_]').each(function () {
      const $this = $(this)
      const namespace = $this.prop('class').split('_', 2)[1]
      const key = $this.data('key')
      const index = $this.data('index')
      let value = $this.val()
      const parse = $this.data('parse')
      const hiddenAndUnchanged = $this.data('hidden') && value === '## HIDDEN ##'
      if (typeof index !== 'undefined') { // Nested Array
        if (!(key in newsettings[namespace]) || !Array.isArray(newsettings[namespace][key])) {
          newsettings[namespace][key] = []
        }
        newsettings[namespace][key][index] = value
      } else { // Normal
        if (hiddenAndUnchanged) {
          value = multi[namespace].settings[key]
        } else if (parse === 'int') {
          value = parseInt(value, 10)
        } else if (parse === 'bool') {
          value = (value === 'true')
        }
        newsettings[namespace][key] = value
      }
    })

    await GM.setValue('setup', true)
    await GM.setValue('currentdebrid', currentdebrid)
    await GM.setValue('settings', JSON.stringify(newsettings.general))
    for (const key in multi) {
      await GM.setValue(key + '_settings', JSON.stringify(newsettings[key]))
    }

    alert(scriptName + '\n\nSettings were successfully saved!')
    document.location.reload()
  }

  async function aboutMe () {
    const popup = popUp('multiochhelper_about', null, null, true)
    const $popup = popup.node
    const $frame = $('<iframe width="' + (window.innerWidth - 250) + '" height="' + (window.innerHeight - 150) + '" style="border:0">').appendTo($popup)
    $frame.bind('load', async function (e) {
      // Load settings for all
      for (const key in multi) {
        await multi[key].loadSettings(true)
      }

      const $body = $($frame[0].contentDocument.getElementsByTagName('body')[0])

      $body.css('fontFamily', 'Ubuntu,Arial,Sans-Serif')

      $('<div style="position:fixed; top:0px; right:5px; cursor:pointer; color:White; background:#b555c5; border: 1px solid White; border-radius:3px; padding:0px; font-weight:bold ; " title="Close menu">X</span>').click(function () { if (confirm('Settings will NOT be saved!')) popup.close() }).appendTo($body)

      $body.append('<h2>' + scriptName + '</h2>')
      $('<a>').appendTo($body).attr('target', '_blank').css('fontSize', 'small').html('https://openuserjs.org/scripts/cuzi/Multi-OCH_Helper').attr('href', 'https://openuserjs.org/scripts/cuzi/Multi-OCH_Helper')

      const $form = $('<div class="form">').appendTo($body)

      // General options
      $form.append('<h3>Settings</h3>')
      configForm($form, config, settings, 'general')

      // Preferred multihoster
      const $p = $('<p>').appendTo($form)
      $p.append('Preferred multihoster:<br>')
      const $select = $('<select></select>').addClass('debridhoster').appendTo($p)
      for (const key in multi) {
        const $option = $('<option></option>').val(key).appendTo($select)
        $option.html(multi[key].name)
        $option[0].selected = key === currentdebrid
      }

      // Options for multihosters
      for (const key in multi) {
        $('<h3>').appendTo($form).html(multi[key].name)
        $('<a>').appendTo($form).css('fontSize', 'small').attr('target', '_blank').html(multi[key].homepage).attr('href', multi[key].homepage)
        if (multi[key].config) {
          configForm($form, multi[key].config, multi[key].settings, key)
        } else {
          $('<p>').appendTo($form).text('No settings available for this service.')
        }
      }

      $form.append('<br>')

      $('<input type="button">').val('Cancel').click(function () {
        if (confirm('Settings will NOT be saved!')) {
          popup.close()
        }
      }).appendTo($form)
      $('<input type="button">').val('Save').click($body, saveSettings).appendTo($form)

      $form.append('<h3>Other options</h3>')

      $('<input type="button">').val('Clear cache (' + humanBytes((await GM.getValue('cachedDownloadLinks', '{}')).length - 2) + ')').appendTo($form).click(async function () {
        if (!confirm(scriptName + '\n\nReally delete cached links?')) {
          return
        }

        await GM.setValue('cachedDownloadLinks', '{}')

        this.value = 'Clear cache (' + humanBytes((await GM.getValue('cachedDownloadLinks', '{}')).length - 2) + ')'
        alert(scriptName + '\n\nCache is empty!')
      })

      $form.append('<br>')
      $form.append('<br>')

      $('<input type="button">').val('Restore dialogs and warnings').appendTo($form).click(async function () {
        if (!confirm(scriptName + '\n\nReally restore all dialogs and warnings?')) {
          return
        }

        await GM.setValue('dialogs', '[]')

        alert(scriptName + '\n\nDialogs and warnings restored')
      })

      let greasemonkeyIssue = ''
      if (greasemonkey) {
        greasemonkeyIssue = `<li>In Greasymonkey it is not possible to select multiple links with the mouse and send them at once.<br>
      The reason is this bug: <a href="https://github.com/greasemonkey/greasemonkey/issues/2574">https://github.com/greasemonkey/greasemonkey/issues/2574</a><br>
      If you need this functionality, you can use Tampermonkey instead of Greasemonkey</li>`
      }

      $(`<div>
      <br>
      <br>
      <h3>Known issues:</h3>
      <ul>
      <li>nopremium.pl sometimes omits a few links in folders</li>
      <li>In Firefox the script sometimes does not work if the "Accept thid-parts cookies" policy is set to "Never".<br>
      To resolve this problem open the Firefox options and go to the tab "Privacy". Set the "Accept thid-parts cookies" to "From visited" or "Always"<br>
      Close and re-open Firefox. Log out and then log in your nopremium.pl account. Everything should work fine now.</li>
      ${greasemonkeyIssue}
      </ul>
      </div><br><br><br>`).appendTo($body)

      $('<input type="button">').val('Debug info').appendTo($body).click(inspectGMvalues)
    })
    if (chrome) {
      $frame.attr('src', 'about:blank')
    }
  }

  function inspectGMvalues () {
    let iv
    const popup = popUp('multiochhelper_inspectGM', function () {
      clearInterval(iv)
    })
    const $popup = popup.node
    const $frame = $('<iframe width="' + (window.innerWidth - 250) + '" height="' + (window.innerHeight - 150) + '" style="border:0">').appendTo($popup)
    $frame.bind('load', async function (e) {
      $($frame[0].contentDocument.getElementsByTagName('head')[0]).append('<style type="text/css">' + SPINNERCSS + '</style>')

      const $body = $($frame[0].contentDocument.getElementsByTagName('body')[0])
      $body.append('<h2>' + scriptName + '</h2>')

      let keys = await GM.listValues()
      if (keys.length && typeof keys[0] === 'undefined') { // Firefox 35+ workaround
        keys = cloneInto(await GM.listValues(), window)
      }

      const $table = $('<table>').appendTo($body)
      let $tr
      $tr = $('<tr>').appendTo($table)
      $('<th>').html('Key').appendTo($tr)
      $('<th>').html('Value').appendTo($tr)
      $('<th>').html('Type').appendTo($tr)
      $('<th>').html('').appendTo($tr)

      const deleteValue = async function (ev) {
        const key = $(this).data('key')
        await GM.deleteValue(key)
        $(this).parent().parent().remove()
      }

      let total = 0
      for (let i = 0; i < keys.length; i++) {
        const value = await GM.getValue(keys[i])
        let svalue = '' + value
        let len = 1
        if (typeof value === 'undefined') {
          svalue = 'undefined'
        } else if (typeof value === 'string') {
          len = value.length
        }
        total += len
        $tr = $('<tr>').appendTo($table)
        $('<td>').html(keys[i]).appendTo($tr)
        $('<td>').append($('<input type="text" style="width:600px">').val(svalue)).appendTo($tr)
        $('<td>').append('' + (typeof value) + (typeof value === 'string' ? ('(' + len + ')') : '')).appendTo($tr)
        $('<td>').append($('<input type="button">').val('Delete').data('key', keys[i]).click(deleteValue)).appendTo($tr)
      }

      $tr = $('<tr>').appendTo($table)
      $('<th>').html('Total').appendTo($tr)
      $('<th>').html(keys.length).appendTo($tr)
      $('<th>').html('approx. ' + humanBytes(total)).appendTo($tr)

      const $reload = $('<div>').appendTo($body)
      $('<div style="display:inline-block;width:20px; height:20px;" class="ochspinner"></div>').appendTo($reload)
      $reload.append(' Reload in ')
      const $timer = $('<span style="pointer:cursor;" title="Click to reload now"></span>').html('20 seconds').click(function () { this.innerHTML = 0 }).appendTo($reload)
      iv = window.setInterval(function () {
        let s = parseInt($timer.html(), 10)
        if (s === 0) {
          clearInterval(iv)
          popup.close()
          inspectGMvalues()
        } else {
          s--
          $timer.html(s + ' seconds')
        }
      }, 1000)
    })
    if (chrome) {
      $frame.attr('src', 'about:blank')
    }
  }

  function hexToBytes (s) {
    return s.match(/([0-9a-fA-F]{2})/g).map(v => parseInt(v, 16))
  }

  function stringToBytes (s) {
    return s.split('').map(v => v.charCodeAt(0))
  }

  function bytesToString (a) {
    return String.fromCharCode.apply(String, a)
  }

  function addCSSHead (body) {
    const style = document.createElement('style')
    style.type = 'text/css'
    style.innerHTML = body
    document.head.appendChild(style)
  }

  function humanBytes (bytes, precision) {
  // http://stackoverflow.com/a/18650828
    bytes = parseInt(bytes, 10)
    if (bytes === 0) return '0 Byte'
    const k = 1024
    const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toPrecision(2)) + ' ' + sizes[i]
  }

  function getNextZIndex () {
  // Calculate: max(zIndex) + 1
    let zIndexMax = 0
    try {
      $('div').each(function () {
        const z = parseInt($(this).css('z-index'), 10)
        if (z > zIndexMax) {
          zIndexMax = z
        }
      })
    } catch (e) {} finally {
      if (zIndexMax < 20000) {
        zIndexMax = 20006
      }
    }
    return zIndexMax + 1
  }

  async function showConfirm (id, text, onConfirm, onNotConfirm, thisArg) {
  // Skip
    const dialogs = JSON.parse(await GM.getValue('dialogs', '[]'))
    if (dialogs.indexOf(id) !== -1) {
      onConfirm.call(thisArg)
      return
    }

    const popup = popUp('confirm' + id, function () {}, thisArg)
    const $div = popup.node
    $div.append(text)
    $div.append('<br>')
    $('<input type="button" value="Yes">').click(function () {
      popup.close()
      onConfirm.call(thisArg)
    }).appendTo($div)

    $('<input type="button" value="No">').click(function () {
      popup.close()
      if (onNotConfirm) {
        onNotConfirm.call(thisArg)
      }
    }).appendTo($div)

    $div.append('<br>')

    $('<input type="checkbox" value="remember">').click(async function () {
      const dialogs = JSON.parse(await GM.getValue('dialogs', '[]'))
      if (this.checked) {
        if (dialogs.indexOf(id) === -1) {
          dialogs.push(id)
          await GM.setValue('dialogs', JSON.stringify(dialogs))
        }
      } else {
        if (dialogs.indexOf(id) !== -1) {
          dialogs.splice(dialogs.indexOf(id), 1)
          await GM.setValue('dialogs', JSON.stringify(dialogs))
        }
      }
    }).appendTo($div)
    $div.append(' Always "Yes". Do not show this message again!')
  }

  function setStatus (text, success) {
    addStatus(text, success, true)
  }

  function addStatus (text, success, clear) {
    if (!document.getElementById('multiochhelper')) {
      alert(`${scriptName}\n\n${text}`)
      return
    }
    let $status = $('#multiochhelper_status')
    if (!document.getElementById('multiochhelper_status_text')) {
      if (!document.getElementById('multiochhelper_status')) {
        const $div = $('#multiochhelper')
        $status = $('<div>').prependTo($div)
        $status.attr('id', 'multiochhelper_status')
      } else {
        $status.empty()
      }
      const $loader = $('<div>').appendTo($status)
      $loader.attr('id', 'multiochhelper_status_loader')
      const $statustext = $('<div>').appendTo($status)
      $statustext.attr('id', 'multiochhelper_status_text')
      const $statusclear = $('<div>').appendTo($status)
      $statusclear.attr('id', 'multiochhelper_status_clear')
    }

    const $statustext = $('#multiochhelper_status_text')
    if (clear) {
      $statustext.empty()
    } else if ($statustext.html().trim() !== '') {
      $statustext.append(document.createElement('br'))
    }
    $status.show()

    $statustext.append(text)
    if (success === 1) {
      $statustext.css('color', '#33FF99')
    } else if (success === 0) {
      $statustext.css('color', 'orange')
    } else if (success === -1) {
      $statustext.css('color', 'cyan')
    } else {
      $statustext.css('color', 'white')
    }
  }

  function showOnlyStatus () {
    const $status = $('#multiochhelper_status')
    $status.siblings().not('#multiochhelper_status').remove()
  }

  function getMultiOCHWebsiteURL (links) {
    return multi[currentdebrid].getOpenWebsiteURL(links)
  }

  function openWebsite (links, cb) {
  // Call cb() and navigate to the website
    if (!links) {
      cb(false)
      return
    }

    if (cb) {
      cb()
    }
    const url = getMultiOCHWebsiteURL(links)

    if (settings.newTab) {
      if (typeof GM.openInTab === 'undefined') {
        window.open(url)
      } else {
        GM.openInTab(url)
      }
    } else {
      document.location.href = url
    }
  }

  async function useCache (urls, cb) {
    urls = '' + urls
    const cachedDownloadLinks = JSON.parse(await GM.getValue('cachedDownloadLinks', '{}')) // [datestring,downloadlink,multihoster]
    if (urls in cachedDownloadLinks) {
      if (confirm(scriptName + '\n\nLink was found in cache.\nUse cached link?\n\nFrom: ' + (new Date(cachedDownloadLinks[urls][0])) + '\nWith: ' + cachedDownloadLinks[urls][2] + '\n' + cachedDownloadLinks[urls][1].join('\n'))) {
        cb(cachedDownloadLinks[urls][1])
        return true
      }
    }
    return false
  }
  async function cacheLink (urls, datetime, downloadLinks, multihoster) {
    if (!Array.isArray(downloadLinks)) {
      const parts = downloadLinks.split('\n')
      downloadLinks = []
      for (let i = 0; i < parts.length; i++) {
        if ($.trim(parts[i])) {
          downloadLinks.push($.trim(parts[i]))
        }
      }
    }
    if (downloadLinks.length === 0) return

    urls = '' + urls
    const cachedDownloadLinks = JSON.parse(await GM.getValue('cachedDownloadLinks', '{}'))
    cachedDownloadLinks[urls] = [datetime, downloadLinks, multihoster]
    await GM.setValue('cachedDownloadLinks', JSON.stringify(cachedDownloadLinks))
  }

  function showExtractedLinks (links) {
    if (document.querySelector('.alertlinkscont')) {
      alert(links.join('\n'))
      $('.alertlinkscont').remove()
      return
    }
    $('<style type="text/css">.alertlinkscont{transition: left 500ms;}.alertlinkscont a{font-size:12px;user-select:all; font-family: monospace;} .alertlinkscont a:link,.alertlinkscont a:hover{color:black; text-decoration:none;}.alertlinkscont a:visited{color:rgb(70,0,120); text-decoration:none;}</style>').appendTo('head')
    const $div = $('<div class="alertlinkscont"></div>')
    $div.appendTo(document.body)
    $div.css({
      zIndex: 10000,
      position: 'fixed',
      top: '20px',
      left: '20px',
      minWidth: '300px',
      minHeight: '300px',
      background: 'white',
      color: 'black',
      border: '2px solid black',
      borderRadius: '5px',
      padding: '20px 25px 10px',
      fontFamily: 'monospace',
      fontSize: '12px',
      overflow: 'auto'
    })
    for (let i = 0; i < links.length; i++) {
      $div[0].innerHTML += '<a target="_blank" href="' + links[i] + '">' + links[i] + '</a><br>\n'
    }
    $div[0].innerHTML += '<br><br>\n'
    window.setTimeout(function moveMenuIntoView () {
      $div.css('maxHeight', (document.documentElement.clientHeight - 100) + 'px')
      $div.css('maxWidth', (document.documentElement.clientWidth - 40) + 'px')
      $div.css('left', Math.max(20, 0.5 * (document.body.clientWidth - $div[0].clientWidth)) + 'px')
    }, 0)
  }

  async function generateLinks (urls, cb) {
  // Check cache
    if (await useCache(urls, cb)) {
      return
    }

    await multi[currentdebrid].getLinks(urls, cb)
  }

  async function download (urls, cb) {
  // Get one/first download link and open it immediately/start download
    if (urls.length > 1) {
      alert(scriptName + '\n\nOnly the first link will be opened!')
    }

    await generateLinks(urls, function (result, code) {
      if (cb) {
        cb()
      }
      if (result && result[0]) {
        addStatus('Opening download...', -1)
        if (window.top === window) {
          document.location.href = result[0]
        } else {
          // Changing location may be blocked by sandboxed iframe
          window.top.location.href = result[0]
        }
      } else if (code === -2) {
        // Error was already handled
        console.log('download() in generateLinks(): error already handled')
      } else if (!code) {
        addStatus('An error occured: No downloadlink to open', 0)
      }
    })
  }

  async function clipboard (urls, cb) {
  // Get download links and copy them into clipboard
    generateLinks(urls, function (result, code) {
      if (result) {
        let succeeded = false
        setStatus('Trying to set clipboard', -1)
        window.setTimeout(function () {
          if (succeeded) {
            return
          }
          setStatus('Trying GM_setClipboard()', -1)
          try {
            GM_setClipboard(result.join('\r\n'))
            setStatus('Copied to clipboard', 1)
          } catch (e) {
            setStatus('Failed to access clipboard 02', 0)
            alert('Failed to access clipboard.\n\nLinks will appear in next dialog window')
            alert(result.join('\r\n'))
          }
        }, 3000)
        try {
          GM.setClipboard(result.join('\r\n')).then(function () {
            setStatus('Copied to clipboard', 1)
            succeeded = true
          }, function () {
            setStatus('Failed to access clipboard 01', 0)
          })
        } catch (e) {
          setStatus('Clipboard not supported by this browser', 0)
          alert(result.join('\n'))
        };
      } else if (code === -2) {
        // Error was already handled
        console.log('clipboard() in generateLinks(): error already handled')
      } else {
        setStatus('An error occured: No downloadlinks found', 0)
      }
      if (cb) {
        cb()
      }
    })
  }

  async function sendToJD (urls, cb) {
  // Get download links and send them to JDownloader
    generateLinks(urls, function (result, code) {
      if (result) {
        setStatus('Waiting for JDownloader', -1)

        // Comment should be the original page in case of multiple links
        let comment = urls[0]
        if (urls.length > 1) {
          if (showOneclickFromHighlighScriptAllLinksLoc) {
            comment = showOneclickFromHighlighScriptAllLinksLoc
          } else if (showOneclickFromHighlighScriptSelectedLinksLoc) {
            comment = showOneclickFromHighlighScriptAllLinksLoc
          } else {
            comment = document.location.href
          }
        }

        GM.xmlHttpRequest({
          method: 'POST',
          url: JDOWNLOADER + 'flash/add',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            Referer: scriptReferer,
            'User-Agent': scriptReferer
          },
          // data: "source="+encodeURIComponent(scriptReferer)+"&urls="+encodeURIComponent(result.join("\r\n")), // Moved "source" to Referer
          // data: "comment="+encodeURIComponent(comment)+"&urls="+encodeURIComponent(result.join("\r\n")), // See ExternInterfaceImpl.java
          data: 'source=' + encodeURIComponent(scriptReferer) + '&comment=' + encodeURIComponent(comment) + '&urls=' + encodeURIComponent(result.join('\r\n')), // See ExternInterfaceImpl.java
          onload: function (resp) {
            if (cb) {
              cb()
            }
            if (resp.status === 204 || resp.responseText.startsWith('success')) {
              setStatus('Sent to JDownloader', 1)
            } else {
              setStatus('JDownloader rejected the request', 0)
            }
          },
          onerror: function (resp) {
            if (cb) {
              cb()
            }
            setStatus('JDownloader is not running', 0)
          }

        })
      } else if (code === -2) {
        // Error was already handled
        console.log('sendToJD() in generateLinks(): error already handled')
        if (cb) {
          cb()
        }
      } else {
        if (cb) {
          cb()
        }
        addStatus('No links to send', 0)
      }
    })
  }

  function showLinks (urls, cb, append, n) {
    const popup = popUp('showLinks')
    const $div = popup.node
    const $loader = $('<div style="width:20px; height:20px;" class="ochspinner"></div>').appendTo($div)

    const $frame = $('<iframe width="900" height="500" style="border:0">').appendTo($div)
    $frame.bind('load', function (e) {
      $($frame[0].contentDocument.getElementsByTagName('head')[0]).append('<link rel="stylesheet" href="https://www.nopremium.pl/css/style.css" type="text/css" />')
      const $body = $($frame[0].contentDocument.getElementsByTagName('body')[0])
      multi[currentdebrid].getResults(urls, function ($node) {
        $loader.remove()
        $body.append($node)
        $body.find('a').each(function () {
          // Open links in new window
          this.setAttribute('target', '_blank')
        })
        if (cb) {
          cb()
        }
      })
    })
    if (chrome) {
      $frame.attr('src', 'about:blank')
    }
  }

  function decryptClickNLoad (cb, jk, cryptedBase64) {
  // Get all the links by decrypting the Click'n'Load form
  // return False for any error
  // return True, run cb() and open the menu if Click'n'Load was successfully decoded

    if (!cryptedBase64 && !(document.getElementsByName('crypted').length && document.getElementsByName('jk').length)) {
      return false // Click'n'Load not avaiblabe
    }

    setStatus("Trying to decrypt Click'n'Load", -1)

    try {
    // Key/IV
      if (!jk) {
        jk = document.getElementsByName('jk')[0].value
      }
      if (jk.indexOf('return') !== -1) {
        jk = eval(jk + '; f();') // eslint-disable-line no-eval
      }

      const key = hexToBytes(jk)
      const iv = key.slice(0)

      // Text
      if (!cryptedBase64) {
        cryptedBase64 = document.getElementsByName('crypted')[0].value
      }
      const cryptedString = atob(cryptedBase64)
      const cryptedBytes = stringToBytes(cryptedString)

      // Decrypt
      const textBytes = slowAES.decrypt(cryptedBytes, slowAES.modeOfOperation.CBC, key, iv)
      let text = bytesToString(textBytes)

      text = text.replace('\r', '')

      const splitted = text.split('\n')

      const links = []
      for (let i = 0; i < splitted.length; i++) {
      // Remove any line that is not a http link
        const t = $.trim(splitted[i])
        if (t && t.substring(0, 4) === 'http') {
          links.push(t)
        }
      }

      const N = links.length

      if (N === 0) {
        return false // Click'n'Load probably failed, try another method...
      }

      if (cb) {
        cb()
      }
      menu(links)
      setStatus('Found ' + (N === 1 ? 'one link' : (N + ' links')), 1)
      return true
    } catch (e) {
      alert("Click'N'Load failed:\n" + e)
      return false // Click'n'Load probably failed, try another method...
    }

  /*
  // Get all the links by decrypting the Click'n'Load form
  if(!document.getElementsByName('crypted').length || !document.getElementsByName('jk').length) {
    if(cb) {
      cb();
    }
    return;
  }

  setStatus("Trying linkdecrypter.com",-1);
  const crypted = document.getElementsByName('crypted')[0].value;
  const jk = document.getElementsByName('jk')[0].value;

  GM.xmlHttpRequest( {
    method: "POST",
    url: "http://linkdecrypter.com/api/?t=cnl2",
    data: 'crypted=' + encodeURIComponent(crypted) + '&jk=' + encodeURIComponent(btoa(jk)),
    headers: {
      "User-agent": "Mozilla/5.0 (X11;U;Linux i686;es-ES;rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8",
      "Accept": "application/atom+xml,application/xml,text/xml",
      "Content-type" : "application/x-www-form-urlencoded"
    },
    onload: function(response) {
      if(cb) {
        cb();
      }
      const N = response.responseText.split("\n").length;
      if(!response.responseText || response.responseText.indexOf("ERROR(CNL2)") !== -1 ||  N === 0) {
        setStatus("An error occurred while handling the response of linkdecrypter.com",0);
      } else {
        menu(response.responseText);
        setStatus("Found "+(N===1?"one link":(N+" links")),1);
      }
    }
  });
  */
  }

  function getAllSerienjunkiesLinks (cb) {
  // Get all download links from a serienjunkies.org download page (i.e. the page right after the captcha)
    const urls = [] // [  [partnumber0,link0]  ,  [partnumber1,link1]  ,  .... ]
    let total = 0

    const rap = document.getElementById('rap')
    const table = rap.getElementsByTagName('table')[0]
    const forms = table.getElementsByTagName('form')

    let j = 1 // part number, in order to make sure that sorting of the links is the same as on the page.
    // This is only a fallback in case there is no visible part number in the actual downloadlink/filename.
    for (let i = 0; i < forms.length; i++) {
      const url = forms[i].action
      if (url.indexOf('mirror') !== -1 || url.indexOf('firstload') !== -1) {
        continue
      }
      GM.xmlHttpRequest({
        method: 'GET',
        url,
        onload: (function (j) {
          return function (response) {
            const loc = response.finalUrl // Actual link after posible redirections
            if (response.finalUrl.match(/part*(\d+)\./)) { // Try to guess part number
              const part = response.finalUrl.match(/part*(\d+)\./)[1]
              urls.push([parseInt(part, 10), loc])
            } else { // fallback part number
              urls.push([j, loc])
            }
            setStatus('Decrypting: ' + urls.length + '/' + total, total === urls.length ? 1 : -1)
            if (total === urls.length) {
            // Got all links
              cb(urls)
            }
          }
        }(j))
      })
      j++
    }
    total = j - 1
  };

  function getSerienjunkiesLinks (cb) {
  // Get all the links from the page
    getAllSerienjunkiesLinks(function (urls) {
      if (cb) {
        cb()
      }

      urls = urls.sort(function (a, b) {
        if (a[0] > b[0]) return 1
        else if (a[0] < b[0]) return -1
        return 0
      })

      let alllinks = ''
      for (let i = 0; i < urls.length; ++i) {
        alllinks += urls[i][1] + '\n'
      }
      menu(alllinks)
    })
  }

  function getFilecryptcc (jddata, cb) {
  // Get all the links by decrypting the Click'n'Load form
    const fieldJk = jddata[0]
    const fieldCrypted = jddata[1]

    const r = decryptClickNLoad(cb, fieldJk, fieldCrypted)
    if (!r) {
      setStatus("Could not find Click'n'Load", -1)
      if (cb) {
        cb()
      }
    }
  }

  function getSafeLinkingNetLinks (cb) {
  // Get all the links by following each link

    const crypticUrls = []
    $('div.links-container.result-form a.result-a').each(function () {
      if (this.getAttribute('href') && this.getAttribute('href').indexOf('/d/') !== -1) { crypticUrls.push(this.getAttribute('href')) }
    })

    const urls = []
    let total = 0

    let j = 1
    for (let i = 0; i < crypticUrls.length; i++) {
      GM.xmlHttpRequest({
        method: 'GET',
        url: crypticUrls[i],
        onload: (function (j) {
          return function (response) {
            const loc = response.finalUrl // Actual link after posible redirections
            urls.push(loc)
            setStatus('Decrypting: ' + urls.length + '/' + total, -1)
            if (total === urls.length) {
            // Got all links
              cb()
              menu(urls)
              setStatus('Found ' + (total === 1 ? 'one link' : (total + ' links')), 1)
            }
          }
        }(j))
      })
      j++
    }
    total = j - 1
  };

  const linkSelectorFilter = {
    _filter: function (key) {
      const a = Array.prototype.slice.call(arguments, 1)
      return function () {
        linkSelectorFilter[key].apply(linkSelectorFilter, a)
      }
    },

    all: function (trs) {
      for (let i = 0; i < trs.length; i++) {
        trs[i].$check.prop('checked', true)
      }
    },
    none: function (trs) {
      for (let i = 0; i < trs.length; i++) {
        trs[i].$check.prop('checked', false)
      }
    },
    flip: function (trs) {
      for (let i = 0; i < trs.length; i++) {
        trs[i].$check.prop('checked', !trs[i].$check.prop('checked'))
      }
    },

    has: function (trs, inpFilter) {
      const s = inpFilter.val()
      for (let i = 0; i < trs.length; i++) {
        if (trs[i].link.indexOf(s) !== -1) {
          trs[i].$check.prop('checked', !trs[i].$check.prop('checked'))
        }
      }
    },

    host: function (trs, $selHost) {
      const h = $selHost.val()

      for (let i = 0; i < trs.length; i++) {
        if (trs[i].host === h) {
          trs[i].$check.prop('checked', !trs[i].$check.prop('checked'))
        }
      }
    },

    fromto: function (trs, $table, $thead, $th) {
      const _self = this

      for (let i = 0; i < trs.length; i++) {
        trs[i].$check.prop('disabled', true)
      }
      $table.find('td').hover(function () {
        $(this).parent().find('td').each(function (i, e) {
          $(e).css('background', 'PaleGreen')
        })
      }, function () {
        $(this).parent().find('td').each(function (i, e) {
          $(e).css('background', '')
        })
      })
      $thead.find('th').css('display', 'none')
      $th.css('display', '')
      $th.html('Select from where to start')

      $table.find('td').click(function () {
        const from = $(this.parentNode).data('index')
        $(this).parent().find('td').css('background', 'PaleGreen')
        $table.find('td').unbind('click mouseenter mouseleave')
        $th.html('Select where to stop')

        $table.find('td').hover(function () {
          const to = $(this.parentNode).data('index')
          $table.find('td').each(function (i, e) {
            const $e = $(e)
            const j = $e.parent().data('index')
            if (j > from && j <= to) {
              $e.css('background', 'DarkSeaGreen')
            } else if (j > from && j > to) {
              $e.css('background', '')
            }
          })
          if ($(this).parent().data('index') > from) $(this).parent().find('td').css('background', 'PaleGreen')
        })
        $table.find('td').filter(function (i, e) { return $(e.parentNode).data('index') > from }).click(function () {
          const to = $(this.parentNode).data('index') + 1

          $table.find('td').unbind('click mouseenter mouseleave')
          $(this).parent().find('td').css('background', 'PaleGreen')
          $table.find('td').each(function (i, e) {
            const $e = $(e)
            const j = $e.parent().data('index')
            if (j < from || j >= to) {
              $e.css('display', 'none')
            }
          })

          const ntrs = trs.slice(from, to)
          for (let i = 0; i < ntrs.length; i++) {
            ntrs[i].$check.prop('disabled', false)
          }

          $th.html('Select ')
          $('<button>').appendTo($th).text('all').click(_self._filter('all', ntrs))
          $('<button>').appendTo($th).text('none').click(_self._filter('none', ntrs))
          $('<button>').appendTo($th).text('flip').click(_self._filter('flip', ntrs))
          $('<button>').appendTo($th).text('return to all links').click(function () {
            $table.find('td').each(function (i, e) {
              const $e = $(e)
              $e.css('display', '')
              $e.css('background', '')
            })
            $thead.find('th').css('display', '')
            $th.css('display', 'none')
            $th.html('')
            for (let i = 0; i < trs.length; i++) {
              trs[i].$check.prop('disabled', false)
            }
          })
          $th[0].scrollIntoView()

          return false
        })
      })
    },

    every: function (trs, $table, $thead, $th) {
      const _self = this

      for (let i = 0; i < trs.length; i++) {
        trs[i].$check.prop('disabled', true)
      }
      $table.find('td').hover(function () {
        $(this).parent().find('td').each(function (i, e) {
          $(e).css('background', 'PaleGreen')
        })
      }, function () {
        $(this).parent().find('td').each(function (i, e) {
          $(e).css('background', '')
        })
      })
      $thead.find('th').css('display', 'none')
      $th.css('display', '')
      $th.html('Select from where to start')

      $table.find('td').click(function () {
        const from = $(this.parentNode).data('index')
        $(this).parent().find('td').css('background', 'PaleGreen')
        $table.find('td').unbind('click mouseenter mouseleave')
        $th.html('Select next')

        $table.find('td').hover(function () {
          const to = $(this.parentNode).data('index')
          const diff = to - from
          if (to < from + 2) {
            $table.find('td').filter(function (i, e) { return $(e.parentNode).data('index') > from + 1 }).css('background', '')
          } else {
            $table.find('td').filter(function (i, e) { return $(e.parentNode).data('index') > from + 1 }).each(function (i, e) {
              const j = $(this.parentNode).data('index')
              if ((j - from) % diff === 0 && j > from + 1) {
                $(this).css('background', 'PaleGreen')
              } else {
                $(this).css('background', '')
              }
            })
            $(this).parent().find('td').css('background', 'DarkSeaGreen')
          }
        }).click(function () {
          const to = $(this.parentNode).data('index')

          if (to < from + 2) return false

          $(this).parent().find('td').css('background', 'PaleGreen')

          const diff = to - from

          $table.find('td').unbind('click mouseenter mouseleave')

          $table.find('td').each(function (i, e) {
            const $e = $(e)
            const j = $e.parent().data('index')
            if ((j - from) % diff !== 0 || j < from) {
              $e.css('display', 'none')
            }
          })

          const ntrs = []
          for (let i = 0; i < trs.length; i++) {
            if ((i - from) % diff === 0 && i >= from) {
              trs[i].$check.prop('disabled', false)
              ntrs.push(trs[i])
            }
          }

          $th.html('Select ')
          $('<button>').appendTo($th).text('all').click(_self._filter('all', ntrs))
          $('<button>').appendTo($th).text('none').click(_self._filter('none', ntrs))
          $('<button>').appendTo($th).text('flip').click(_self._filter('flip', ntrs))
          $('<button>').appendTo($th).text('return to all links').click(function () {
            $table.find('td').each(function (i, e) {
              const $e = $(e)
              $e.css('display', '')
              $e.css('background', '')
            })
            $thead.find('th').css('display', '')
            $th.css('display', 'none')
            $th.html('')
            for (let i = 0; i < trs.length; i++) {
              trs[i].$check.prop('disabled', false)
            }
          })
          $th[0].scrollIntoView()

          return false
        })
      })
    }

  }

  function linkSelector (links) {
    const filter = function (key) {
      const a = Array.prototype.slice.call(arguments, 1)
      return function () {
        linkSelectorFilter[key].apply(linkSelectorFilter, a)
      }
    }

    const trs = []

    const selectedLinks = []
    // Coyp array and remove empty elements
    for (let i = 0; i < links.length; i++) {
      const t = $.trim(links[i])
      if (t) {
        selectedLinks.push(t)
      }
    }

    if (linksBeforeSelection === false) {
      linksBeforeSelection = links.slice(0) // Save all links for later selections
    }
    const allLinks = linksBeforeSelection.slice(0)

    const popup = popUp('linkSelector')
    const $div = popup.node
    const $loader = $('<div style="width:20px; height:20px;" class="ochspinner"></div>').appendTo($div)
    $div.css('overflow', 'none')
    const $frame = $('<iframe style="border:0">').appendTo($div)
    $frame.attr('width', window.innerWidth - 190)
    $frame.attr('height', window.innerHeight - 120)
    $frame.bind('load', function (e) {
      const $body = $($frame[0].contentDocument.getElementsByTagName('body')[0])

      const $main = $('<div>').appendTo($body)

      const $table = $('<table>').appendTo($main)
      const $thead = $('<thead>').appendTo($table)

      const $tr0 = $('<tr>').appendTo($thead)
      const $th0 = $('<th>').appendTo($tr0).attr('colspan', 2)
      const $tr1 = $('<tr>').appendTo($thead)
      const $th1 = $('<th>').appendTo($tr1).attr('colspan', 2)
      const $tr2 = $('<tr>').appendTo($thead)
      const $th2 = $('<th>').appendTo($tr2).attr('colspan', 2)
      const $tr3 = $('<tr>').appendTo($thead)
      const $th3 = $('<th>').appendTo($tr3).attr('colspan', 2)
      const $tr4 = $('<tr>').appendTo($thead)
      const $th4 = $('<th>').appendTo($tr4).attr('colspan', 2)

      $('<span>Select: <span>').appendTo($th0)
      $('<button>').appendTo($th0).text('all').click(filter('all', trs))
      $('<button>').appendTo($th0).text('none').click(filter('none', trs))
      $('<button>').appendTo($th0).text('flip').click(filter('flip', trs))

      $('<button>').appendTo($th1).text('Select from ... to ...').click(filter('fromto', trs, $table, $thead, $th4))
      $('<button>').appendTo($th1).text('Select every ...').click(filter('every', trs, $table, $thead, $th4))

      $('<span> Filter:<span>').appendTo($th2)
      const inpFilter = $('<input>').appendTo($th2).attr('type', 'text')
      $('<button>').appendTo($th2).text('Flip with filter').click(filter('has', trs, inpFilter))

      $('<span> Host filter:<span>').appendTo($th3)
      const $selHost = $('<select>').appendTo($th3)
      $('<button>').appendTo($th3).text('Flip with host filter').click(filter('has', trs, $selHost))

      const allhosts = []
      for (let i = 0; i < allLinks.length; i++) {
        const $tr = $('<tr>').data('index', i).appendTo($table)
        const $td0 = $('<td>').appendTo($tr)
        const $check = $('<input>').appendTo($td0).attr('type', 'checkbox').attr('id', 'link_checkbox_' + i).prop('checked', selectedLinks.indexOf(allLinks[i]) !== -1)
        const $td1 = $('<td>').appendTo($tr)
        $('<label>').attr('for', 'link_checkbox_' + i).text(allLinks[i]).css('font-family', 'monospace').appendTo($td1)

        const host = allLinks[i].split('/')[2].replace(/^www\./, '')
        if (allhosts.indexOf(host) === -1) {
          allhosts.push(host)
        }

        trs.push({ $tr, $check, link: allLinks[i], host })
      }

      for (let i = 0; i < allhosts.length; i++) {
        $('<option>').val(allhosts[i]).text(allhosts[i]).appendTo($selHost)
      }

      $('<button>').appendTo($main).text('Apply').click(function () {
        const nlinks = []
        for (let i = 0; i < trs.length; i++) {
          if (trs[i].$check.prop('checked')) {
            nlinks.push(trs[i].link)
          }
        }
        if (nlinks.length === 0) {
          alert('No links selected?!')
          return
        }
        menu(nlinks)
        setStatus((nlinks.length === 1 ? 'One link' : (nlinks.length + ' links')) + ' selected', 1)
        popup.close()
      })

      $loader.remove()
    })
    if (chrome) {
      $frame.attr('src', 'about:blank')
    }
  }

  function menu (links) {
  // normalize links:
    if (!Array.isArray(links)) {
      const parts = links.split('\n')
      links = []
      for (let i = 0; i < parts.length; i++) {
        if ($.trim(parts[i])) {
          links.push($.trim(parts[i]))
        }
      }
    }

    const $c = $('#multiochhelper ul')
    $c.html('')

    const $select = $('<select>')
    const m = links[0].match(/https?:\/\/(.+?)\//)
    if (!m) {
      console.log(scriptName + ": Not a valid link: '" + links[0] + "'")
      return
    }
    const host = m[1]
    let hoster = host.split('.')
    hoster.pop()
    hoster = hoster.pop().replace('-', '')
    $.each(multi, function (key, val) {
      const $option = $('<option></option>').val(key).html(val.name).appendTo($select)
      if (key === currentdebrid) {
        $option[0].selected = true
      }
      if (multi[key].isOnline(hoster)) {
        $option.css('color', 'green')
      } else {
        $option.css('color', '#F00')
      }
    })
    let $entry = menuentry($select)
    $select.bind('change', function (ev) {
      const $this = $(this)
      // Change hoster
      currentdebrid = $this.val()

      // Check general support
      if (multi[currentdebrid].isOnline(hoster)) {
      // Check first link for support on this multi hoster
        multi[currentdebrid].checkLink(links[0], function (result) {
          if (!result) {
            alert(scriptName + '\n\n' + host + ' is not supported by this hoster or the file is offline.\n\nChecked: ' + links[0])
          }
        })
      } else {
        alert(scriptName + '\n\n' + host + ' is not supported by ' + multi[currentdebrid].name)
      }

      // Add "Remember" checkbox
      if (!$this.parent().find('#remember').length) {
        const $div = $('<div>')
        const $check = $('<input id="remember" type="checkbox" value="remember" title="Remember selection">').click(async function () {
          if (this.checked) {
            currentdebrid = $select.val()
            await GM.setValue('currentdebrid', currentdebrid)
            setStatus('Switched to ' + multi[currentdebrid].name, 1)
            $div.remove()
          }
        })
        $div.append($check).append('Remember')
        $this.parent().append($div)
      }
    })

    $entry = menuentry('Direct download')
    $entry.click(function () { mouse('download', links) })

    $entry = menuentry('Copy to clipboard')
    $entry.click(function () { mouse('clipboard', links) })

    if (settings.jDownloaderSupport) {
      $entry = menuentry('Send to JDownloader')
      $entry.attr('id', 'multiochhelperjdbutton')
      $entry.hide()
      $entry.click(function () { mouse('sendToJD', links) })
      GM.xmlHttpRequest({
        method: 'GET',
        url: JDOWNLOADER + 'flash/',
        onerror: function () {
        },
        onload: function (resp) {
          if (resp && resp.responseText && resp.responseText.startsWith('JDownloader')) {
            $('#multiochhelperjdbutton').show()
          }
        }
      })
    }

    if (!showOneclickFromHighlighScriptAllLinks) {
      $entry = menuentry('Show generated links')
      $entry.click(function () { mouse('showLinks', links) })
    }

    $entry = menuentry('Show extracted links')
    $entry.click(function () {
      if (window.parent.parent !== window) {
        window.parent.parent.postMessage({ iAm: 'Unrestrict.li', type: 'alert', str: links.join('\n') }, '*')
        alert(links.join('\n'))
      } else {
        showExtractedLinks(links)
      }
    })

    if (!showOneclickFromHighlighScriptAllLinks && (links.length > 1 || linksBeforeSelection !== false)) {
      $entry = menuentry('Select links')
      $entry.click(function () { linkSelector(links) })
    }

    if (!showOneclickFromHighlighScriptAllLinks) {
      $entry = menuentry()
      $('<a style="color:white !important;">Open Website</a>').attr('href', getMultiOCHWebsiteURL(links)).appendTo($entry)
    }

    if (showOneclickFromHighlighScriptAllLinks && showOneclickFromHighlighScriptAllLinksLinks) {
      $entry = $(menuentry('Use all links on page...'))
      $entry.click(function () {
      // Switch to all links instead of one
        const links = showOneclickFromHighlighScriptAllLinksLinks
        showOneclickFromHighlighScriptAllLinksLinks = ''
        menu(links)
        $('#multiochhelper div:empty:not(:first)').remove()
        setStatus('All links!', 1)
      })
    }

    if (showOneclickFromHighlighScriptSelectedLinks && showOneclickFromHighlighScriptSelectedLinksLinks) {
      $entry = $(menuentry('Use selected links...'))
      $entry.click(function () {
      // Switch to selected links instead of one
        const links = showOneclickFromHighlighScriptSelectedLinksLinks
        showOneclickFromHighlighScriptSelectedLinksLinks = ''
        menu(links)
        $('#multiochhelper div:empty:not(:first)').remove()
        setStatus('Using selected links!', 1)
      })
    }

    if (!showOneclickFromHighlighScriptAllLinks) {
      $entry = menuentry($('<span style="cursor:default; color:silver">Userscript menu</span>').click(function (ev) { ev.stopPropagation(); aboutMe() }))
      $entry.css('cursor', 'default')
      $('<span style="cursor:pointer; color:White; border: 1px solid White; border-radius:3px; padding:0px; margin-left:20px; font-weight:bold ; " title="Close menu">X</span>').click(function () { $('#multiochhelper').remove() }).appendTo($entry)
    }
  }

  function loader () {
  // Show an animation, return function to remove the loader
    $('#multiochhelper_status_loader').parent().show()
    const $div = $('<div class="ochspinner"></div>').appendTo($('#multiochhelper_status_loader'))
    return function () {
      $div.remove()
    }
  }

  async function mouse (action, linkText) {
  // decide what to do after a mouse click
    const removeImg = loader()
    if (action === 'download') {
      await download(linkText, removeImg)
    } else if (action === 'showLinks') {
      showLinks(linkText, removeImg)
    } else if (action === 'openWebsite') {
      openWebsite(linkText)
    } else if (action === 'clipboard') {
      await clipboard(linkText, removeImg)
    } else if (action === 'menu') {
      removeImg()
      menu(linkText)
    } else if (action === 'sendToJD') {
      await sendToJD(linkText, removeImg)
    }
  }

  function menuentry (html) {
    const $li = $('<li>')
    if (html) {
      $li.append(html)
    }
    $li.appendTo('#multiochhelper ul')
    return $li
  }

  function button (label) {
    addCSSHead(`
  #multiochhelper,#multiochhelper * {
    font-family:Sans-Serif !important;
    padding:0px; margin:0px;
  }
  #multiochhelper a, #multiochhelper a:link,#multiochhelper a:visited {
    text-decoration:underline !important;
    color:#3788e8 !important;
    font-style:none !important;
  }
  #multiochhelper a:hover {
    text-decoration:none !important;
    color:#3788e8 !important;
    font-style:none !important;
  }
  #multiochhelper ul li,#multiochhelper_status {
    margin:1px 1px;
    padding:1px 5px;
    font-size:13px;
    text-shadow:0 -1px 0 #333333;
    color:White;
    border:1px solid #8B3D92;
    background-color:#B555C5;
    background:radial-gradient(ellipse at center, #B555C5, #8B3D92);
    list-style:none outside;
  }
  #multiochhelper div#multiochhelper_status_loader {
    float:left;
  }
  #multiochhelper div#multiochhelper_status_text {
    float:left;
  }
  #multiochhelper div#multiochhelper_status_clear {
    clear:left;
  }
  #multiochhelper ul li {
    cursor:pointer;
  }
  #multiochhelper ul li:hover {
    background-color:#CC6BDD;
    background:radial-gradient(ellipse at center, #CC6BDD, #8B3D92);
  }
  #multiochhelper select,#multiochhelper input {
    border-radius:0;
    box-shadow:none;
    text-shadow:none;
    border:none;
    background:white;
    color:black;
  }

  ${SPINNERCSS}
  `)

    // div container
    const zi = getNextZIndex()
    const $div = $('<div>').appendTo(document.body)
    $div.attr('id', 'multiochhelper')
    $div.attr('style', 'z-index:' + zi + '; position:fixed; background:#E6E6E6; color:Black; border:#B555C5 2px solid;border-radius:5px; padding:3px;')
    if (settings.position[0] === 'top') {
      $div.css('top', '0%')
    } else {
      $div.css('bottom', '0%')
    }
    if (settings.position[1] === 'left') {
      $div.css('left', '0%')
    } else {
      $div.css('right', '0%')
    }
    // Status
    const $status = $('<div>').appendTo($div).hide()
    $status.attr('id', 'multiochhelper_status')
    const $loader = $('<div>').appendTo($status)
    $loader.attr('id', 'multiochhelper_status_loader')
    const $statustext = $('<div>').appendTo($status)
    $statustext.attr('id', 'multiochhelper_status_text')
    const $statusclear = $('<div>').appendTo($status)
    $statusclear.attr('id', 'multiochhelper_status_clear')

    const $ul = $('<ul>').appendTo($div)

    // Button
    const $entry = menuentry(label || (multi[currentdebrid].name.charAt(0).toUpperCase() + multi[currentdebrid].name.slice(1)))

    $ul.append($entry)

    return $entry
  }

  const isSetup = await GM.getValue('setup', false)

  // Update hoster status
  let updatinghosters = false
  if (isSetup) {
    for (const key in multi) {
      if (multi[key].updateStatusURLpattern.test(document.location.href)) { //  usually in this is true in the iframe which is defined below
        multi[key].updateStatus()
        updatinghosters = true
        break
      }
    }
  }

  // Create iframes to check hoster status:
  if (!updatinghosters && isSetup) {
    const now = new Date()
    for (const key in multi) {
      if ('updateStatusURL' in multi[key] && (now - multi[key].lastUpdate) > (settings.updateHosterStatusInterval * 60 * 60 * 1000)) {
        const $iframe = $('<embed>').appendTo(document.body)
        $iframe.bind('load', function () {
          const frame = this
          window.setTimeout(function () { $(frame).remove() }, 3000)
        })
        $iframe.attr('src', multi[key].updateStatusURL)
      }
    }
  }

  // Setup
  if (!updatinghosters) {
    if (!isSetup) {
      await aboutMe()
      if (!confirm(scriptName + ' Setup\n\nPlease take some time to configure ' + scriptName + ' and then save the settings!\n\nPress cancel to continue with the default configuration!')) {
        await GM.setValue('setup', true)
        alert(scriptName + '\n\nDefault settings will be used.')
        document.location.reload()
      }
    }
  }

  if (document.location.href.indexOf('nopremium.pl') !== -1) {
  // nopremium.pl Website
    if (document.location.search.substring(0, 6) === '?link:') {
    // Insert link on nopremium.pl
      $('#filesList').val(decodeURIComponent(document.location.search.substring(6)))
    }
  } else if (document.location.href.indexOf('www.premiumize.me') !== -1) {
  // premiumize.me Website
    if (document.location.search.substring(0, 6) === '?link:') {
    // Insert link on nopremium.pl
      $('textarea').val(decodeURIComponent(document.location.search.substring(6)))
    }
  } else if (document.location.href.indexOf('download.serienjunkies.org') !== -1) {
  // Serienjunkies
    if (!document.querySelector('.g-recaptcha')) { // if not on captcha page
      const $b = button('Decrypt links')
      $b.click(function (ev) {
        const removeImg = loader()
        getSerienjunkiesLinks(removeImg)
      })
    }
  } else if (document.location.href === 'http://filecloud.io/download.html') {
  // filecloud.io
    if (unsafeWindow.__currentUrl) {
      showOneclickButton = true
      showOneclickLink = decodeURIComponent(unsafeWindow.__currentUrl)
    }
  } else if (document.location.href.indexOf('filecrypt.cc') !== -1) {
  // filecrypt.cc folder
    if (document.location.href.indexOf('helper.html') !== -1) { // if not on captcha page
      window.addEventListener('message', function filecryptmessage (event) {
        if (event.data && typeof (event.data) === 'object') {
          window.opener.postMessage({ filecryptData: JSON.stringify(event.data) }, '*') // Send message back to the opening window
          window.removeEventListener('message', filecryptmessage) // Prevent further messages from creating several buttons
        }
      }, false)
    } else if (document.location.href.indexOf('Container') !== -1) { // if not on captcha page
      const $b = button("Please open the Click'n'Load Popup (several times)")
      $b.click(function () {
        $('#cnl_btn').click()
      })
      window.addEventListener('message', function filecryptmessage2 (event) { // Receive messages from the popup
        if (event.data && typeof (event.data) === 'object' && 'filecryptData' in event.data) {
          window.removeEventListener('message', filecryptmessage2) // Prevent further messages from creating several buttons
          setStatus('Decrypting', -1)
          const removeImg = loader()
          getFilecryptcc(JSON.parse(event.data.filecryptData), removeImg)
        }
      }, false)
    }
  } else if (document.location.href.substring(7, 22) === 'protected.to/f-') {
  // http://protected.to folder
    if (document.querySelectorAll('.links a').length > 0) { // If not on captcha page
      showOneclickButton = true
      showOneclickLink = ''
      $('.links a').each(function () {
        showOneclickLink += decodeURIComponent(this.href) + '\n'
      })
    }
  } else if (document.location.href.substring(8, 23) === 'safelinking.net') {
  // safelinking.net folder
    if (!document.getElementById('captcha-wrapper')) {
      const $b = button('Decrypt links')
      $b.click(function (ev) {
        const removeImg = loader()
        getSafeLinkingNetLinks(removeImg)
      })
    }
  } else if (document.location.href.indexOf('.firedrive.com/share/') !== -1) {
  // firedrive.com folder
    showOneclickButton = true
    showOneclickLink = ''
    $('a.pf_item_link:visible').each(function () {
      showOneclickLink += decodeURIComponent(this.href) + '\n'
    })
  } else if (document.location.href.indexOf('rapidgator.net/folder/') !== -1) {
  // Rapidgator folder
    showOneclickButton = true
    showOneclickLink = ''
    $('#grid tbody a').each(function () {
      showOneclickLink += decodeURIComponent(this.href) + '\n'
    })
  } else if (document.location.hostname === "dailyuploads.net" && currentdebrid === 'premiumize.me') {
    // Dailyuploads.net: submit direct download link (after captcha was solved) to premiumize.me instead of link
    if (document.querySelector('div.banner div.inner a>img[src*="redbutton.png"]')) {
      showOneclickButton = true
      showOneclickLink = document.querySelector('div.banner div.inner a>img[src*="redbutton.png"]').parentNode.href
    } else {
      showOneclickButton = false
      button("Please solve the captcha first")
    }
  } else if (document.location.hostname === 'multiup.org') {
  // Multiup.org mirrors
    showOneclickButton = document.querySelectorAll('button[link]').length > 0
    showOneclickLink = Array.from(document.querySelectorAll('button[link]')).map(b => b.getAttribute('link')).join('\n')
  } else if (document.location.href.substring(0, 55) === 'https://cvzi.github.io/Userscripts/index.html?link=sync') {
    // Window opened from Helper script to sync hoster status (see postMessage events below)
    showOneclickButton = false
    const message = 'Updating hoster status...'
    const h1 = document.body.appendChild(document.createElement('h1'))
    h1.appendChild(document.createTextNode(scriptHightligherName + ': ' + message))
    setTitle('')
    window.setTimeout(function () {
      const h2 = document.body.appendChild(document.createElement('h2'))
      h2.appendChild(document.createTextNode('You may close this tab now'))
    }, 4000)
  } else if (document.location.href.substring(0, 51) === 'https://cvzi.github.io/Userscripts/index.html?link=') {
    // Iframe for a X-Frame-Options website
    showOneclickButton = true
    showOneclickLink = decodeURIComponent(document.location.search.match(/link=(.+)/)[1])
  } else {
    // One click hoster website
    showOneclickButton = true
    showOneclickLink = decodeURIComponent(document.location.href)
  }

  if (showOneclickButton) {
    let mouseOverAvailable = true

    // Split links into array
    const splitted = showOneclickLink.split('\n')
    showOneclickLink = []
    for (let i = 0; i < splitted.length; i++) {
      if ($.trim(splitted[i])) {
        showOneclickLink.push($.trim(splitted[i]))
      }
    }

    const $b = button()

    $b.bind('mousedown',
      function (ev) {
        mouseOverAvailable = false
        if (ev.which === 3) { // Right click
          mouse(settings.rightClick, showOneclickLink)
        } else if (ev.which === 2) { // Middle click
          mouse(settings.middleClick, showOneclickLink)
        } else if (ev.which === 1) { // Left click {
          mouse(settings.leftClick, showOneclickLink)
        }
      })
    if (settings.mouseOver !== 'none') {
      let ti = false
      $b.on({
        mouseover: function () {
          if (!mouseOverAvailable) { return }
          ti = setTimeout(function () {
            if (!mouseOverAvailable) { return }
            mouseOverAvailable = false
            mouse(settings.mouseOver, showOneclickLink)
          }, settings.mouseOverDelay)
        },
        mouseout: function () {
          if (ti !== false) clearTimeout(ti)
        }
      })
    }
    // Prevent context menu on right click
    if (settings.rightClick !== 'none') {
      $b[0].addEventListener('contextmenu', e => e.preventDefault(), false)
    }
  }

  // Handle messages from the highlight script
  window.addEventListener('message', function (e) {
    if (typeof e.data !== 'object' || !('iAm' in e.data) || e.data.iAm !== 'Unrestrict.li') {
      return
    }

    switch (e.data.type) {
      case 'alllinks':
        if (showOneclickFromHighlighScriptAllLinks) {
          return
        }
        showOneclickFromHighlighScriptAllLinks = true
        showOneclickFromHighlighScriptAllLinksLoc = e.data.loc
        showOneclickFromHighlighScriptAllLinksLinks = e.data.links.join('\n')
        if ($('#multiochhelper ul li').length > 1) { // Menu already opened
          menu(showOneclickLink)
        }
        break
      case 'selectedlinks':
        if (showOneclickFromHighlighScriptSelectedLinks) {
          return
        }
        showOneclickFromHighlighScriptSelectedLinks = true
        showOneclickFromHighlighScriptSelectedLinksLoc = e.data.loc
        showOneclickFromHighlighScriptSelectedLinksLinks = e.data.links.join('\n')
        if ($('#multiochhelper ul li').length > 1) { // Menu already opened
          menu(showOneclickLink)
        }
        break
      case 'requesthosterstatus': {
        window.setTimeout(function () {
          const h3 = document.body.appendChild(document.createElement('h3'))
          h3.appendChild(document.createTextNode('This will only take a few seconds'))
        }, 0)
        const o = {}
        for (const key in multi) {
          o[key] = multi[key].status
        }
        e.source.postMessage({ iAm: 'Unrestrict.li', type: 'hosterstatus', str: JSON.stringify(o) }, '*')
        break
      }
    }
  }, true)
})()