Download In JavLibrary

Download directly in javlibrary video detail page

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name                Download In JavLibrary
// @name:zh-CN          JavLibrary 下载
// @description         Download directly in javlibrary video detail page
// @description:zh-CN   直接在 javlibrary 影片详情页进行下载
// @version             0.3.0
// @author              jferroal
// @license             GPL-3.0
// @require             https://greasyfork.org/scripts/31793-jmul/code/JMUL.js?version=209567
// @include             http://www.javlibrary.com/*
// @grant               GM_xmlhttpRequest
// @run-at              document-end
// @namespace           https://greasyfork.org/users/34556
// ==/UserScript==

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
const { Element } = window.JMUL || { Element: {} };
const { ClickActionFactory } = require('./elements.logic');
const DEFAULT_ACTION_TYPE = 'unknown';

class PanelButton {
  constructor (type, label) {
    this.type = type;
    this.btn = new Element('button');
    this.btn.setInnerText(label);
    this.initStyle();
  }
  
  initStyle () {
    this.btn.setCss({
      width: '22px',
      height: '22px',
      boxSizing: 'border-box',
      marginLeft: '4px',
      cursor: 'pointer',
    });
  }
  
  bindClick (task) {
    const gfn = new (ClickActionFactory.create(this.type))(task).cb;
    this.btn.listen('click', (e) => gfn(e, task));
  }
  
  updateCss (styles) {
    this.btn.setCss(styles);
  }
  
  appendTo (parent) {
    return this.btn.appendTo(parent);
  }
}

class PanelButtonFactory {
  constructor () {
  }
  
  static create (type) {
    switch (type) {
      case 'active':
        return new PanelButton(type, '⏩');
      case 'waiting':
        return new PanelButton(type, '•');
      case 'Paused':
        return new PanelButton(type, '⏸');
      case 'Removed':
        return new PanelButton(type, '⌦');
      case 'Completed':
        return new PanelButton(type, '?');
      case 'Error':
        return new PanelButton(type, '❌');
      case 'unknown':
      default:
        const button = new PanelButton(type, '?');
        button.updateCss({
          color: 'white',
          backgroundColor: 'grey',
          borderRadius: '50%',
        });
        return button;
    }
  }
}

PanelButton.instance = undefined;

class Panel {
  constructor (task) {
    this.element = new Element('section');
    this.initStyles();
    this.initButton(task);
  }
  
  initStyles () {
    this.element.setCss({
      display: 'flex',
      margin: '-4px 0',
    });
  }
  
  initButton (task) {
    const button = PanelButtonFactory.create(DEFAULT_ACTION_TYPE);
    button.bindClick(task);
    button.appendTo(this.element);
  }
  
  appendTo (parent) {
    parent.setCss({
      display: 'flex',
      margin: '4px 15%',
    });
    parent.appendChild(this.element);
  }
}

module.exports = { Panel };

},{"./elements.logic":2}],2:[function(require,module,exports){
const { TokyoToSho, TaskPanel } = require('./requests');
const { Utils } = require('./utils');

class UnknownClickAction {
  constructor (task) {
    this.task = task;
  }
  
  get cb () {
    return (event) => {
      event.preventDefault();
      new TokyoToSho().search(this.task.name).then((response) => {
        const magnets = (new Utils.TokyoToShoParser(response.responseText)).matchAll();
        if (magnets && magnets.length) {
          this.task.chooseBestMagnet(magnets);
          new TaskPanel().start(this.task);
        } else {
          alert('无可用资源');
        }
      });
    }
  }
}

class ActiveClickAction {
  constructor (task) {
    this.task = task;
  }
  
  get cb () {
    return (event) => {
      event.preventDefault();
    }
  }
}

class WaitingClickAction {
  constructor (task) {
    this.task = task;
  }
  
  get cb () {
    return (event) => {
      event.preventDefault();
    }
  }
}

class PausedClickAction {
  constructor (task) {
    this.task = task;
  }
  
  get cb () {
    return (event) => {
      event.preventDefault();
    }
  }
}

class RemovedClickAction {
  constructor (task) {
    this.task = task;
  }
  
  get cb () {
    return (event) => {
      event.preventDefault();
    }
  }
}

class ErrorClickAction {
  constructor (task) {
    this.task = task;
  }
  
  get cb () {
    return (event) => {
      event.preventDefault();
    }
  }
}

class CompletedClickAction {
  constructor (task) {
    this.task = task;
  }
  
  get cb () {
    return (event) => {
      event.preventDefault();
    }
  }
}

class ClickActionFactory {
  constructor () {
  }
  
  static create (type) {
    if (!ClickActionFactory.caches[ type ]) {
      switch (type) {
        case 'active':
          ClickActionFactory.caches[ type ] = ActiveClickAction;
          break;
        case 'waiting':
          ClickActionFactory.caches[ type ] = WaitingClickAction;
          break;
        case 'Paused':
          ClickActionFactory.caches[ type ] = PausedClickAction;
          break;
        case 'Removed':
          ClickActionFactory.caches[ type ] = RemovedClickAction;
          break;
        case 'Completed':
          ClickActionFactory.caches[ type ] = CompletedClickAction;
          break;
        case 'Error':
          ClickActionFactory.caches[ type ] = ErrorClickAction;
          break;
        case 'unknown':
        default:
          ClickActionFactory.caches[ type ] = UnknownClickAction;
      }
    }
    return ClickActionFactory.caches[ type ];
  }
}

ClickActionFactory.caches = {};

module.exports = { ClickActionFactory };

},{"./requests":4,"./utils":6}],3:[function(require,module,exports){
const { Utils } = require('./utils');
const { Panel } = require('./elements');

(function () {
  const href = window.location.href;
  if (/http:\/\/www\.javlibrary\.com\/.*\/\?v=.*/.test(href)) {
    const tasks = Utils.generateTasks(href);
    init(tasks);
    
    function init (tasks) {
      tasks.forEach((task) => {
        const statusBar = new Panel(task);
        statusBar.appendTo(task.panelParent);
      });
    }
  }
  
}());

},{"./elements":1,"./utils":6}],4:[function(require,module,exports){
// Search result from tokyotosho
const { Request, Header } = window.JMUL || { Request: {}, Header: {} };

class TokyoToSho {
  constructor (_options = {}) {
    if (!TokyoToSho.instance) {
      this.options = _options;
      this.initHeaders();
      this.host = 'https://www.tokyotosho.info';
      TokyoToSho.instance = this;
    }
    return TokyoToSho.instance;
  }
  
  initHeaders () {
    this.options.headers = new Header({
      ':authority': 'www.tokyotosho.info',
      ':scheme': 'https',
      'accept': 'text / html, application/xhtml+xml,application/xml;q=0.9, image/webp,*/*;q=0.8',
      'accept-encoding;': 'gzip, deflate, sdch, br',
      'accept-language': 'zh-CN, en-US;q=0.8, en;q=0.6, zh;q=0.4',
      'cache-control': 'no-cache',
    });
  }
  
  search (target) {
    const request = new Request(this.options);
    request.setMethod('GET');
    request.setUrl(this.host + `/search.php?terms=${target}`, ``);
    return request.send();
  }
}

TokyoToSho.instance = undefined;

class TaskPanel {
  constructor (_options = {}) {
    if (!TaskPanel.instance) {
      this.options = _options;
      this.initHeaders();
      this.host = 'http://localhost:6800/jsonrpc';
      TaskPanel.instance = this;
    }
    return TaskPanel.instance;
  }
  
  initHeaders () {
    this.options.headers = new Header({ 'Content-Type': 'application/json' });
  }
  
  start (task) {
    const request = new Request(this.options);
    request.setMethod('POST');
    request.setUrl(this.host);
    request.setData(task.generateRequestStr());
    return request.send();
  }
}

TaskPanel.instance = undefined;

module.exports = { TokyoToSho, TaskPanel };

},{}],5:[function(require,module,exports){
const { Element } = window.JMUL || { JMElement: {} };

class Task {
  constructor (name = '') {
    this.name = name;
  }
  
  setName (name) {
    this.name = name;
  }
  
  setPanelParent (el) {
    this.panelParent = new Element(el);
  }
  
  setProgressBarParent (el) {
    this.progressBarParent = new Element(el);
  }
  
  setMagnetLink (magnet) {
    this.magnet = magnet;
  }
  
  chooseBestMagnet (magnets) {
    this.setMagnetLink(magnets.reduce((best, magnet) => {
      const current = {
        link: magnet.link,
        score: (magnet.sCount || 0) * 10 + (magnet.cCount || 0) * 5 + (magnet.lCount || 0) * 2,
        size: parseInt(magnet.size.slice(0, -2), 10) * (magnet.size.indexOf('GB') > -1 ? 1000 : 1),
      };
      if (current.score < best.score) return best;
      if (current.score > best.score) return current;
      if (current.size < best.size) return best;
      return current;
    }, { link: '', score: 0, size: 0 }));
  }
  
  setServerStatus (serverTask) {
    this.completedLength = serverTask.completedLength;
    this.totalLength = serverTask.totalLength;
    this.status = serverTask.status;
  }
  
  generateRequestStr () {
    return JSON.stringify({
      jsonrpc: '2.0',
      id: this.name,
      method: 'aria2.addUri',
      params: [ [ this.magnet.link ] ],
    })
  }
  
  static joinName (tasks) {
    return tasks.reduce((res, t) => res += t.name + ';', '');
  }
  
  static fromSingleElem (elem) {
    const task = new Task();
    task.setName(elem.children[ 0 ].children[ 0 ].children[ 0 ].children[ 1 ].textContent);
    task.setPanelParent(elem);
    task.setProgressBarParent(elem.children[ 0 ].children[ 0 ].children[ 0 ].children[ 1 ]);
    return task;
  }
  
  static fromListElem (elem) {
    const task = new Task();
    task.setName(elem.children[ 0 ].children[ 0 ].textContent);
    task.setPanelParent(elem);
    task.setProgressBarParent(elem.children[ 0 ].children[ 0 ]);
    return task;
  }
  
  static fromHomeElem (elem) {
    const task = new Task();
    task.setName(elem.children[ 0 ].textContent);
    task.setPanelParent(elem);
    task.setProgressBarParent(elem.children[ 0 ]);
    return task;
  }
}

module.exports = { Task };

},{}],6:[function(require,module,exports){
const { Task } = require('./task');

function convertHTMLElementsToArray (elements) {
  const result = [];
  if (elements && elements.length) {
    for (let i = 0; i < elements.length; i += 1) {
      result.push(elements.item(i));
    }
  }
  return result;
}

const PageType = {
  SINGLE_VIEW: 100,
  VIDEO_LIST: 200,
  HOMEPAGE: 300,
};

class Utils {
  static pageType (href) {
    if (/http:\/\/www\.javlibrary\.com\/.*\/\?v=.*/.test(href)) {
      return PageType.SINGLE_VIEW;
    }
    if (/http:\/\/www\.javlibrary\.com\/.*\/vl_.*/.test(href)) {
      return PageType.VIDEO_LIST;
    }
    return PageType.HOMEPAGE;
  }
  
  static getTaskElements (type) {
    switch (type) {
      case PageType.SINGLE_VIEW:
        return [ document.getElementById('video_id') ];
      case PageType.VIDEO_LIST:
        return convertHTMLElementsToArray(document.getElementsByClassName('video'));
      case PageType.HOMEPAGE:
      default:
        return convertHTMLElementsToArray(document.getElementsByClassName('post-headline'));
    }
  }
  
  static generateTasks (href) {
    const type = Utils.pageType(href);
    const elements = Utils.getTaskElements(type);
    return elements.reduce((res, e) => {
      switch (type) {
        case PageType.SINGLE_VIEW:
          res.push(Task.fromSingleElem(e));
          break;
        case PageType.VIDEO_LIST:
          res.push(Task.fromListElem(e));
          break;
        case PageType.HOMEPAGE:
        default:
          res.push(Task.fromHomeElem(e));
          break;
      }
      return res;
    }, []);
  }
}

Utils.PageType = PageType;
Utils.TokyoToShoParser = class TokyoToShoParser {
  constructor (pageContent) {
    this.pageContent = pageContent;
    this.magnetLinkPattern = /<a href="(magnet:\?xt=urn:btih:.*?)">/gi;
    this.seederCountPattern = /S: <span style="color: .*?">(\d+)<\/span>/gi;
    this.leederCountPattern = /L: <span style="color: .*?">(\d+)<\/span>/gi;
    this.completedCountPattern = /C: <span style="color: .*?">(\d+)<\/span>/gi;
    this.sizePattern = /\| Size: (.*?) \|/gi;
  }
  
  matchAll () {
    const result = [];
    let [ mlMatch, scMatch, lcMatch, ccMatch, szMatch ] = [ undefined, undefined, undefined, undefined, undefined ];
    do {
      [ mlMatch, scMatch, lcMatch, ccMatch, szMatch ] = [
        this.magnetLinkPattern.exec(this.pageContent),
        this.seederCountPattern.exec(this.pageContent),
        this.leederCountPattern.exec(this.pageContent),
        this.completedCountPattern.exec(this.pageContent),
        this.sizePattern.exec(this.pageContent),
      ];
      if (mlMatch) {
        result.push({
          link: mlMatch[ 1 ].trim(),
          sCount: scMatch[ 1 ],
          lCount: lcMatch[ 1 ],
          cCount: ccMatch[ 1 ],
          size: (szMatch && szMatch[ 1 ]) || '0MB',
        });
      }
    } while (mlMatch);
    this.magnetLinkPattern.index = this.seederCountPattern.index = this.leederCountPattern.index = 0;
    this.completedCountPattern.index = this.sizePattern.index = 0;
    return result;
  }
};

module.exports = { Utils };

},{"./task":5}]},{},[3]);