Multi-OCH Helper

nopremium.pl and premiumize.me. Inserts a direct download link on several one-click-hosters and some container/folder providers.

安裝腳本?
作者推薦腳本

您可能也會喜歡 Multi-OCH Helper Highlight links

安裝腳本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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
// @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.4

// @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://clicknupload.space/*
// @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://usersdrive.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.7.1/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)
})()