anon cannon

2021. 8. 28. 오후 2:44:08

// ==UserScript==
// @name        anon cannon
// @namespace   Violentmonkey Scripts
// @match       https://anonfiles.com/*
// @grant       none
// @version     1.0
// @author      -
// @description 2021. 8. 28. 오후 2:44:08
// @grant       GM_xmlhttpRequest
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @require     https://cdn.jsdelivr.net/npm/[email protected]/StreamSaver.min.js
// @run-at document-end
// ==/UserScript==


// idbKeyval init
let idbKeyval = function(t) {
  "use strict";

  function e(t) {
    return new Promise(((e, n) => {
      t.oncomplete = t.onsuccess = () => e(t.result), t.onabort = t.onerror = () => n(t.error)
    }))
  }

  function n(t, n) {
    const r = indexedDB.open(t);
    r.onupgradeneeded = () => r.result.createObjectStore(n);
    const o = e(r);
    return (t, e) => o.then((r => e(r.transaction(n, t).objectStore(n))))
  }
  let r;

  function o() {
    return r || (r = n("keyval-store", "keyval")), r
  }

  function u(t, n) {
    return t("readonly", (t => (t.openCursor().onsuccess = function() {
      this.result && (n(this.result), this.result.continue())
    }, e(t.transaction))))
  }
  return t.clear = function(t = o()) {
    return t("readwrite", (t => (t.clear(), e(t.transaction))))
  }, t.createStore = n, t.del = function(t, n = o()) {
    return n("readwrite", (n => (n.delete(t), e(n.transaction))))
  }, t.entries = function(t = o()) {
    const e = [];
    return u(t, (t => e.push([t.key, t.value]))).then((() => e))
  }, t.get = function(t, n = o()) {
    return n("readonly", (n => e(n.get(t))))
  }, t.getMany = function(t, n = o()) {
    return n("readonly", (n => Promise.all(t.map((t => e(n.get(t)))))))
  }, t.keys = function(t = o()) {
    const e = [];
    return u(t, (t => e.push(t.key))).then((() => e))
  }, t.promisifyRequest = e, t.set = function(t, n, r = o()) {
    return r("readwrite", (r => (r.put(n, t), e(r.transaction))))
  }, t.setMany = function(t, n = o()) {
    return n("readwrite", (n => (t.forEach((t => n.put(t[1], t[0]))), e(n.transaction))))
  }, t.update = function(t, n, r = o()) {
    return r("readwrite", (r => new Promise(((o, u) => {
      r.get(t).onsuccess = function() {
        try {
          r.put(n(this.result), t), o(e(r.transaction))
        } catch (t) {
          u(t)
        }
      }
    }))))
  }, t.values = function(t = o()) {
    const e = [];
    return u(t, (t => e.push(t.value))).then((() => e))
  }, t
}({});

function formatBytes(bytes, decimals = 2) {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', '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)).toFixed(dm)) + ' ' + sizes[i];
}


function doFetch(url, options = {
  method: 'GET',
  responseType: 'blob'
}) {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      url: url,
      method: options.method,
      responseType: options.responseType,
      headers: options.headers,
      data: options.data,
      onload: result => {
        console.debug(result)
        if (result.status == 206 || result.status == 200) {
          resolve(result.response);
        } else {
          console.log(result)
          alert("불러오기 에러 발생 : " + url)
          reject(result.status);
        }
      }
    });
  });
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

const fileID = location.href.match(/anonfiles.com\/([a-zA-Z0-9]+)/)[1]
let cdn = document.querySelector("#download-url").href

const unit = 1024 * 256

let globalQueue = []
let globalQueueDone = []
let globalQueueNum = 16
let globalCurrentDownloaded = 0

let dw = document.querySelector("#download-wrapper > div.col-xs-12.col-md-4.text-center")

function getInfo() {
  return new Promise((resolve, reject) => {
    fetch(`https://api.anonfiles.com/v2/file/${fileID}/info`)
      .then(res => res.json())
      .then(json => resolve(json))
  })
}

function byteDivider(byteSize) {
  let byteDivide = Object()
  const iter = parseInt(byteSize / unit)
  for (let i = 0; i < iter; i++) {
    byteDivide[i] = [i * unit, (i + 1) * unit - 1]
    if (i === iter - 1) {
      byteDivide[i] = [i * unit, byteSize - 1]
    }
  }
  return [iter, byteDivide]
}

function downloadRange(name, num, range, link) {
  const queueName = `${name}_${num}`
  return new Promise(async (resolve, reject) => {
    let done = false
    while (!done && globalQueue.indexOf(queueName) === -1 && globalQueueDone.indexOf(queueName) === -1) {
      if (globalQueue.length < globalQueueNum) {

        console.debug(`${num}_download_start`)

        globalQueue.push(queueName)

        idbKeyval.keys()
          .then(k => {
            if (k.indexOf(`${name}_${num}`) === -1) {
              doFetch(link, {
                  method: 'GET',
                  headers: {
                    'Range': `bytes=${range[0]}-${range[1]}`
                  },
                  responseType: 'arraybuffer'
                })
                .then(buffer => {
                  idbKeyval.set(`${name}_${num}`, buffer)
                    .then(e => {
                      globalCurrentDownloaded += range[1] - range[0] + 1
                      globalQueue.splice(globalQueue.indexOf(queueName), 1)
                      globalQueueDone.push(queueName)

                      console.debug(`${num}_download_end`)

                      done = true
                    })
                })
            } else {
              globalCurrentDownloaded += range[1] - range[0] + 1
              globalQueue.splice(globalQueue.indexOf(queueName), 1)
              globalQueueDone.push(queueName)

              console.debug(`${num}_download_backup`)

              done = true
            }
          })


      } else {
        await sleep(100)
      }
    }
  })
}

function queueMaker() {
  getInfo()
    .then(async json => {
      const fileSize = json.data.file.metadata.size.bytes
      const divided = byteDivider(fileSize)
      const fileName = document.querySelector("#site-wrapper h1").innerText
      
      let cdnNum = await GM_getValue('cdn')
      if (cdnNum) {
        cdn = `https://cdn-${cdnNum}.anonfiles.com/${cdn.match(/anonfiles.com\/(.*)/)[1]}`
      }
      for (let i = 0; i < divided[0]; i++) {
        downloadRange(fileName, i, divided[1][i], cdn)
      }
      supervisor(fileName, fileSize, divided[0])
    })
}

function progressMaker(max) {
  let progress = document.createElement('progress')
  progress.setAttribute('id', 'anon-cannon-progress')
  progress.setAttribute('value', '0')
  progress.setAttribute('max', `${max}`)
  progress.setAttribute('style', 'width: 100%')

  let text = document.createElement('p')
  text.innerText = '준비 중...'

  dw.appendChild(progress)
  dw.appendChild(text)

  return [progress, text]
}

async function supervisor(name, totalSize, iter) {
  let allDone = false
  let pastSize = 0
  let sizeDelta = 0
  let downloadSpeend = 0

  let progresses = progressMaker(totalSize)

  while (!allDone) {
    await sleep(1000)
    sizeDelta = globalCurrentDownloaded - pastSize
    progresses[0].setAttribute('value', `${globalCurrentDownloaded}`)
    progresses[1].innerText = `${formatBytes(globalCurrentDownloaded)}/${formatBytes(totalSize)}`
    pastSize = globalCurrentDownloaded
    if (globalCurrentDownloaded === totalSize) {
      progresses[1].innerText = `다운로드 완료, 디스크에서 처리 중...`
      await idbDownload(name, totalSize, iter)
      progresses[1].innerText = `다운로드 완료!`
      allDone = true
    }
  }
}

async function idbDownload(fileName, fileSize, iter) {
  let fileStream = streamSaver.createWriteStream(fileName, {
    size: fileSize
  })
  let writer = fileStream.getWriter()
  for (let step = 0; step < iter; step++) {
    await idbKeyval.get(`${fileName}_${step}`)
      .then(b => {
        writeRetry(writer, new Uint8Array(b), 10)
      })
  }
  await writer.close()
  for (let step = 0; step < iter; step++) {
    idbKeyval.del(`${fileName}_${step}`)
      .catch(e => {
        alert("IDB 삭제 에러")
        alert(e)
      })
  }
}

function writeRetry(writer, data, counter) {
  try {
    writer.write(data)
  } catch (e) {
    if (counter) {
      writeRetry(writer, data, counter - 1)
    } else {
      alert("IDB 다운로드 에러")
      writer.abort()
      alert(e)
    }
  }
}

function main() {
  queueMaker()
}

GM_registerMenuCommand('Anonfiles 다운로드', main, 'D')

function timeout(ms, promise) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error('TIMEOUT'))
    }, ms)

    promise
      .then(value => {
        clearTimeout(timer)
        resolve(value)
      })
      .catch(reason => {
        clearTimeout(timer)
        reject(reason)
      })
  })
}

function ping(url) {
  return new Promise((resolve, reject) => {
    const startFetch = performance.now();
    doFetch(url, {method: 'HEAD'})
    .then((response) => {
      resolve(performance.now() - startFetch)
    })
    .catch(e => {
      reject(false)
    })
  })
}

function scan() {
  let pings = Object()
  return new Promise(async (resolve, reject) => {
    let tmpP = document.createElement('p')
    tmpP.innerText = '검색 준비중...'
    dw.appendChild(tmpP)
    for (let i = 101; i < 134; i++) {
      tmpP.innerText = `${i-100}/${33}`
      await timeout(500, ping(`https://cdn-${i}.anonfiles.com/${cdn.match(/anonfiles.com\/(.*)/)[1]}`))
        .then(function(delta) {
        pings[i] = Math.floor(delta)
      }).catch(function(err) {});
    }
    dw.removeChild(tmpP)
    let best = 500
    let cdnNum = 0
    for (let k of Object.keys(pings)) {
      if (pings[k] < best) {
        cdnNum = k
        best = pings[k]
      }
    }
    
    if (cdnNum) {
      await GM_setValue('cdn', cdnNum)
      alert(`검색이 완료되었습니다 : cdn-${cdnNum}, ${best}ms, 다운로드 서버를 전환했습니다`)
    } else {
      alert(`검색이 완료되었습니다 : 모든 서버에 대한 ping이 100ms를 초과합니다.`)
    }
  })
}

GM_registerMenuCommand('Anonfiles 최적 서버 검색', scan, 'S')