Multi-OCH Helper Highlight links

nopremium.pl and premiumize.me. Highlight one-click-hoster links and include Multi-OCH Helper button

As of 17. 12. 2020. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Multi-OCH Helper Highlight links
// @namespace   cuzi
// @license     MIT
// @copyright   2014, cuzi (https://openuserjs.org/users/cuzi)
// @description nopremium.pl and premiumize.me. Highlight one-click-hoster links and include Multi-OCH Helper button
// @homepageURL https://openuserjs.org/scripts/cuzi/Multi-OCH_Helper_Highlight_links
// @icon        https://greatest.deepsurf.us/system/screenshots/screenshots/000/003/478/original/icon_ampel.png
// @include     *
// @exclude     *.yahoo.*
// @exclude     *.google.*
// @exclude     *.youtube.*
// @exclude     *.bing.com*
// @exclude     *duckduckgo.com*
// @exclude     *bandcamp.com*
// @exclude     *.tumblr.com*
// @version     10.12
// @grant       GM.setValue
// @grant       GM.getValue
// @require     https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require     https://greatest.deepsurf.us/scripts/25445-och-list/code/OCH%20List.js
// ==/UserScript==

/* globals getOCH, GM, $, alert, NodeFilter */

(async function () {
  'use strict'

  const MAXTEXTNODES = 10000

  const scriptName = 'Multi-OCH Helper Highlight links'
  const mainScriptName = 'Multi-OCH Helper'
  const syncHostersHost = 'https://cvzi.github.io/'
  const syncHostersUrl = syncHostersHost + 'Userscripts/index.html?link=sync'
  const ignoreList = [['some.love.txt', 30]]
  const chrome = ~navigator.userAgent.indexOf('Chrome')

  const $J = $.noConflict(true)

  const config = {
    mouseOverDelay: 700,
    frameWidth: '170px',
    frameHeight: '200px',
    colorHosterAvailableBG: 'green',
    colorHosterAvailableFG: 'white',
    colorHosterUnavailableBG: 'rgba(255,0,0,0.5)',
    colorHosterUnavailableFG: 'white',
    colorLinkOfflineBG: 'rgba(255,0,0,0.5)',
    colorLinkOfflineFG: 'silver',
    maxRequestsPerPage: 2,
    updateHosterStatusInterval: 24 * 7 // weekly update
  }

  // These hosters are supported by but have a X-Frame-Options enabled or simply do not work without javascript.
  const frameHosterWhitelist = [

  ]

  const multi = {
    'nopremium.pl': new function () {
      const self = this
      this.key = 'nopremium.pl'
      this.name = 'NoPremium.pl'
      this.homepage = 'https://www.nopremium.pl/'

      const mapHosterName = (name) => name.replace('-', '')

      this.updateStatusURL = 'https://www.nopremium.pl/'
      this.updateStatusURLpattern = /https?:\/\/www\.nopremium\.pl.*/

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

      this.updateStatus = async function () { // Update list of online hosters
        if (document.location.href.match(self.updateStatusURL)) {
        // Read and save current status of all hosters
          self.status = {}
          $J('#servers a[title]').each(function () {
            var name = mapHosterName(this.title)
            self.status[name] = true
          })
          await GM.setValue(self.key + '_status', JSON.stringify(self.status))
          await GM.setValue(self.key + '_status_time', '' + (new Date()))
          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]
    }(),
    'premiumize.me': new function () {
      const self = this
      this.key = 'premiumize.me'
      this.name = 'premiumize'
      this.homepage = 'https://www.premiumize.me/'

      this.updateStatusURL = 'https://www.premiumize.me/hosters'
      this.updateStatusURLpattern = /https:\/\/www\.premiumize\.me\/hosters\/?/

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

      this.updateStatus = () => null // This works only with api key, which only the main script has
      this.isOnline = (hostername) => hostername in self.status && self.status[hostername]
    }()
  }

  function matchHoster (str) {
    // Return name of first hoster that matches, otherwise return false
    for (let i = 0; i < ignoreList.length; i++) {
      if (str.indexOf(...ignoreList[i]) !== -1) {
        return false
      }
    }
    for (const name in OCH) {
      for (var i = 0; i < OCH[name].pattern.length; i++) {
        if (OCH[name].pattern[i].test(str)) {
          return name
        }
      }
    }
    return false
  }

  // All suitable urls are saved in this array:
  const alllinks = []

  function frameSrc (src) {
  // Prevent websites from busting the iframe by using a second "sandboxed" iframe
  // It's a kind of magic.
    const framesrc = 'data:text/html,' + encodeURIComponent(`<!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>HTML5</title>
      <style>* { margin:0px; padding:0px; }</style>
      <script>
      function addlistener() {
        window.addEventListener("message", function(e){
          if(! "iAm" in e.data || e.data.iAm != "Unrestrict.li") return;
          document.getElementById("mysandyframe").contentWindow.postMessage(e.data,'*');
        }, true);
      }
      </script>
    </head>
    <body onload="addlistener();">
      <iframe
      id="mysandyframe"
      sandbox
      scrolling="no"
      frameborder="0"
      seamless="seamless"
      src="${src}"
      style="border: 0; width:${config.frameWidth}; height:${config.frameHeight}">
    </body>
  </html>`)
    return framesrc
  }

  function showMenu (jlink, textContent) {
  // Show the button

    let link
    if (textContent) {
      link = jlink.text()
    } else {
      link = jlink.attr('href')
    }

    // Create iframe
    let frame
    if ('info' in GM && 'scriptHandler' in GM.info && GM.info.scriptHandler === 'Greasemonkey') {
      // Greasemonkey bug https://github.com/greasemonkey/greasemonkey/issues/2574
      frame = $J('<embed></embed>')
    } else {
      frame = $J('<iframe></iframe>')
    }

    if (frameHosterWhitelist.indexOf(jlink.data('hoster')) === -1) {
      frame.attr('src', 'https://cvzi.github.io/Userscripts/index.html?link=' + encodeURIComponent(link))
    } else {
      frame.attr('src', frameSrc(link))
    }

    frame.attr('scrolling', 'no')
    frame.attr('frameborder', 'no')
    frame.attr('seamless', 'seamless')
    const p = jlink.offset()
    frame.css({
      position: 'absolute',
      background: 'white',
      width: config.frameWidth,
      height: config.frameHeight,
      top: p.top + 15,
      left: p.left,
      padding: '1px',
      boxShadow: '3px 3px 5px #444',
      border: '4px solid #9055c5',
      borderRadius: '0 5px 5px 5px'
    })
    frame.appendTo(document.body)

    // Send all links on this page to the "Multi-OCH Helper"
    setInterval(function () {
      if (frame[0].contentWindow) {
        frame[0].contentWindow.postMessage({ iAm: 'Unrestrict.li', type: 'alllinks', links: alllinks, loc: document.location.href }, '*')
      }
    }, 500)

    // Check whether more links are selected
    const sel = window.getSelection()
    const selelectedLinks = []
    if (!sel.isCollapsed) {
      for (var j = 0; j < sel.rangeCount; j++) {
        const frag = sel.getRangeAt(j).cloneContents()
        const span = document.createElement('span')
        span.appendChild(frag)
        const a = span.getElementsByTagName('a')
        for (var i = 0; i < a.length; i++) {
          const url = a[i].href
          const m = matchHoster(url)
          if (url && m !== false) {
            selelectedLinks.push(url)
          }
        }
      }
    }
    if (selelectedLinks.length > 0) {
      const iv = setInterval(function () {
        if (frame[0].contentWindow) {
          frame[0].contentWindow.postMessage({ iAm: 'Unrestrict.li', type: 'selectedlinks', links: selelectedLinks, loc: document.location.href }, '*')
        }
      }, 500)
      window.setTimeout(() => clearInterval(iv), 10000)
    }

    // Close frame on first click and prevent the <a>-element from opening a new window
    jlink.data('onclick', jlink[0].onclick)
    jlink[0].onclick = null
    jlink.one('click', function (event) {
      event.stopImmediatePropagation()
      event.preventDefault()
      const jthis = $J(this)

      // Close frame
      frame.remove()
      // Restore onclick event
      this.onclick = jthis.data('onclick')
      // Restore mouseover event
      jthis.data('mouseOverAvailable', true)
      jthis.data('mouseOverTimeout', false)

      return false
    })
  }

  let firstAttach = true
  const attachEvents = function () {
    const links = []

    // Normalize hoster object: Replace single patterns with arrays [RegExp]
    if (firstAttach) {
      for (const name in OCH) {
        if (!Array.isArray(OCH[name].pattern)) {
          OCH[name].pattern = [OCH[name].pattern]
        }
      }
      firstAttach = false
    }

    // Find all text nodes that contain "http://"
    const nodes = []
    const walk = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
      acceptNode: function (node) {
        if (node.parentNode.href || node.parentNode.parentNode.href || node.parentNode.parentNode.parentNode.href) {
          return NodeFilter.FILTER_REJECT
        }
        if (node.parentNode.tagName === 'TEXTAREA' || node.parentNode.parentNode.tagName === 'TEXTAREA') {
          return NodeFilter.FILTER_REJECT
        }
        if (node.data.match(/(\s|^)https?:\/\/\w+/)) {
          return NodeFilter.FILTER_ACCEPT
        }
      }
    }, false)
    let node = walk.nextNode()
    while (node) {
      nodes.push(node)
      node = walk.nextNode()
    }

    // For each found text nodes check whether the URL is a valid OCH URL
    for (let i = 0; i < nodes.length && i < MAXTEXTNODES; i++) {
      if (nodes[i].data === '') {
        continue
      }
      const httpPosition = nodes[i].data.indexOf('http')
      if (httpPosition === -1) {
        continue
      }

      let urlnode
      if (httpPosition > 0) {
        urlnode = nodes[i].splitText(httpPosition) // Split leading text
      } else {
        urlnode = nodes[i]
      }
      const stop = urlnode.data.match(/$|\s/)[0] // Find end of URL
      if (stop !== '') { // If empty string, we found $ end of string
        const nextnode = urlnode.splitText(urlnode.data.indexOf(stop)) // Split trailing text
        if (nextnode.data !== '' && nextnode.data.indexOf('http') !== -1) { // The trailing text might contain another URL
          nodes.push(nextnode)
        }
      }

      // Check whether the URL is a OCH. If so, create an <a> element
      const url = urlnode.data
      const m = matchHoster(url)
      if (url && url && m !== false) {
      // Create <a> element
        const a = document.createElement('a')
        a.href = url
        a.appendChild(urlnode.parentNode.replaceChild(a, urlnode))

        const li = $J(a)
        links.push({
          hoster: m,
          url: url,
          element: li
        })
        alllinks.push(url)
      }
    }

    // Find actual <a> links
    const al = document.getElementsByTagName('a')
    for (let i = 0; i < al.length; i++) {
      if (al[i].dataset && al[i].dataset.linkValidatedAs) {
        continue // Link was already checked
      }
      const url = al[i].href
      const mH = matchHoster(url)
      if (mH !== false) {
        const li = $J(al[i])
        links.push({
          hoster: mH,
          url: url,
          element: li
        })
        alllinks.push(url)
      }
    }

    // Attach mouseover/out events to all the links
    for (let i = 0; i < links.length; i++) {
      const a = links[i].element
      const hoster = links[i].hoster

      if ('attached' in links[i] || a.data('hoster')) { // Already attached 6
        continue
      }

      if (OCH[hoster].multi.length === 0) { // Not supported by nopremium.pl according to hardcoded rules
        continue
      }
      let notsupported = true
      for (const debrid in multi) {
        if (multi[debrid].isOnline(hoster)) {
          notsupported = false
          break
        }
      }

      if (notsupported) {
        continue // Not supported by nopremium.pl according to status
      }

      links[i].attached = true

      // if(links[i].data('hosterAvailable')) {
      //  links[i].attr("style","background:"+config.colorHosterAvailableBG+"; color:"+config.colorHosterAvailableFG+";");
      // } else {
      //  links[i].attr("style","background:"+config.colorHosterUnavailableBG+"; color:"+config.colorHosterUnavailableFG+";");
      // }
      a.attr('style', 'background:' + config.colorHosterAvailableBG + '; color:' + config.colorHosterAvailableFG + ';')

      a.data('hoster', hoster)

      a.data('mouseOverAvailable', true)
      a.data('mouseOverTimeout', false)
      a.on({
        mouseover: function () {
          const link = $J(this)

          if (!link.data('mouseOverAvailable')) {
            return
          }
          link.data('mouseOverTimeout', setTimeout(function () {
            if (!link.data('mouseOverAvailable')) {
              return
            }
            link.data('mouseOverAvailable', false)
            showMenu(link)
          }, config.mouseOverDelay))
        },
        mouseout: function () {
          const link = $J(this)

          if (link.data('mouseOverTimeout') !== false) {
            clearTimeout(link.data('mouseOverTimeout'))
            link.data('mouseOverTimeout', false)
          }
        }
      })
    }

    return links.length
  }

  // Get OCH list
  const OCH = getOCH()

  // Init hosters
  for (const key in multi) {
    await multi[key].init()
  }

  // This is the start of everything
  let numberFoundLinks = 0
  window.setTimeout(function () {
    numberFoundLinks = attachEvents()
  }, 0)
  window.setTimeout(function () {
    if (numberFoundLinks === 0) {
      numberFoundLinks = attachEvents()
    }
  }, 1500) // Let's try again.

  // Update hoster status
  for (const key in multi) {
    if (multi[key].updateStatusURLpattern.test(document.location.href)) {
      multi[key].updateStatus()
      break
    }
  }

  // Create iframes to update hoster status:
  const now = new Date()
  for (const key in multi) {
    if ((now - multi[key].lastUpdate) > (7 * 24 * 60 * 60 * 1000)) {
      if (document.getElementById('multiochhelper')) {
      // Button is visible on this page
        window.setTimeout(() => window.postMessage({ iAm: 'Unrestrict.li', type: 'requesthosterstatus' }, '*'), 1000)
      } else if (chrome) {
      // Chrome: we can use iframe to load Multi-OCH_Helper script in the frame
        const $iframe = $J('<iframe>').appendTo(document.body)
        $iframe.bind('load', function () {
          const frame = this
          window.setTimeout(() => $J(frame).remove(), 4000)

          if ($iframe[0].contentWindow) {
            $iframe[0].contentWindow.postMessage({ iAm: 'Unrestrict.li', type: 'requesthosterstatus' }, '*')
          }
        })
        $iframe.attr('src', syncHostersUrl)
      } else {
      // Greasemonkey: we need to open a new tab to communicate with the Multi-OCH_Helper script
        if (document.location.href.startsWith(syncHostersHost)) {
          window.setTimeout(() => window.postMessage({ iAm: 'Unrestrict.li', type: 'requesthosterstatus' }, '*'), 1000)
        } else {
          const w = window.open(syncHostersUrl)
          window.setTimeout(function () {
            if (w) {
              w.postMessage({ iAm: 'Unrestrict.li', type: 'requesthosterstatus' }, '*')
            }
            window.setTimeout(function () {
              if (w) {
                w.close()
              }
            }, 3000)
          }, 1000)
        }
      }
      break
    }
  }

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

    switch (e.data.type) {
      case 'alert':
        // Alert on page, not in frame
        alert(e.data.str)
        break

      case 'findLinks':
        // Research links
        window.setTimeout(function () {
          numberFoundLinks = attachEvents()
        }, 0)
        break

      case 'hosterstatus': {
        // Update hoster status, this script has no API access on premiumize, so it cannot update the hosters itself
        const data = JSON.parse(e.data.str)
        const result = {}
        for (const key in multi) {
          if (data && key in data) {
            await GM.setValue(key + '_status', JSON.stringify(data[key]))
            result[key] = Object.keys(data[key]).length
            multi[key].status = data[key]
          }
          await GM.setValue(key + '_status_time', '' + (new Date()))
        }
        console.log(scriptName + ': Received hoster status from ' + mainScriptName + ': ' + JSON.stringify(result))
        break
      }
    }
  }, true)
})()