- // ==UserScript==
- // @name NexusPHP PT Helper Plus
- // @name:zh-CN NexusPHP PT 助手增强版
- // @namespace https://greasyfork.org/zh-CN/users/7326
- // @version 0.0.9
- // @description 基于NexusPHP的PT网站的辅助脚本
- // @description:zh-CN 适用于基于 NexusPHP 的 PT 站的辅助脚本
- // @author 〃萝卜
- // @match *://*.hdhome.org/*
- // @match *://*.pthome.net/*
- // @match *://*.byr.cn/*
- // @match *://*.tjupt.org/*
- // @match *://*.hdsky.me/*
- // @match *://*.btschool.club/*
- // @match *://*.et8.org/*
- // @match *://*.msg.vg/*
- // @match *://*.beitai.pt/*
- // @match *://*.52pt.site/*
- // @match *://*.cnscg.club/*
- // @match *://*.hdstreet.club/*
- // @match *://*.moecat.best/*
- // @note V0.0.9 新增pthome的支持,优化passkey获取流程,修复提取url的地址参数
- // @grant unsafeWindow
- // @grant GM_addStyle
- // @grant GM_setClipboard
- // @run-at document-end
- // ==/UserScript==
-
- 'use strict';
-
- let domParser = null, passkey = localStorage.getItem('passkey');
- let isHalfPage = false;
-
- /**
- * @class
- * @memberof LuCI
- * @hideconstructor
- * @classdesc
- *
- * Slightly modified version of `LuCI.dom` (https://github.com/openwrt/luci/blob/5d55a0a4a9c338f64818ac73b7d5f28079aa95b7/modules/luci-base/htdocs/luci-static/resources/luci.js#L2080),
- * which is licensed under Apache License 2.0 (https://github.com/openwrt/luci/blob/master/LICENSE).
- *
- * The `dom` class provides convenience method for creating and
- * manipulating DOM elements.
- */
- const dom = {
- /**
- * Tests whether the given argument is a valid DOM `Node`.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {*} e
- * The value to test.
- *
- * @returns {boolean}
- * Returns `true` if the value is a DOM `Node`, else `false`.
- */
- elem: function(e) {
- return (e != null && typeof(e) == 'object' && 'nodeType' in e);
- },
-
- /**
- * Parses a given string as HTML and returns the first child node.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {string} s
- * A string containing an HTML fragment to parse. Note that only
- * the first result of the resulting structure is returned, so an
- * input value of `<div>foo</div> <div>bar</div>` will only return
- * the first `div` element node.
- *
- * @returns {Node}
- * Returns the first DOM `Node` extracted from the HTML fragment or
- * `null` on parsing failures or if no element could be found.
- */
- parse: function(s) {
- var elem;
-
- try {
- domParser = domParser || new DOMParser();
- let d = domParser.parseFromString(s, 'text/html');
- elem = d.body.firstChild || d.head.firstChild;
- }
- catch(e) {}
-
- if (!elem) {
- try {
- dummyElem = dummyElem || document.createElement('div');
- dummyElem.innerHTML = s;
- elem = dummyElem.firstChild;
- }
- catch (e) {}
- }
-
- return elem || null;
- },
-
- /**
- * Tests whether a given `Node` matches the given query selector.
- *
- * This function is a convenience wrapper around the standard
- * `Node.matches("selector")` function with the added benefit that
- * the `node` argument may be a non-`Node` value, in which case
- * this function simply returns `false`.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {*} node
- * The `Node` argument to test the selector against.
- *
- * @param {string} [selector]
- * The query selector expression to test against the given node.
- *
- * @returns {boolean}
- * Returns `true` if the given node matches the specified selector
- * or `false` when the node argument is no valid DOM `Node` or the
- * selector didn't match.
- */
- matches: function(node, selector) {
- var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
- return m ? m.call(node, selector) : false;
- },
-
- /**
- * Returns the closest parent node that matches the given query
- * selector expression.
- *
- * This function is a convenience wrapper around the standard
- * `Node.closest("selector")` function with the added benefit that
- * the `node` argument may be a non-`Node` value, in which case
- * this function simply returns `null`.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {*} node
- * The `Node` argument to find the closest parent for.
- *
- * @param {string} [selector]
- * The query selector expression to test against each parent.
- *
- * @returns {Node|null}
- * Returns the closest parent node matching the selector or
- * `null` when the node argument is no valid DOM `Node` or the
- * selector didn't match any parent.
- */
- parent: function(node, selector) {
- if (this.elem(node) && node.closest)
- return node.closest(selector);
-
- while (this.elem(node))
- if (this.matches(node, selector))
- return node;
- else
- node = node.parentNode;
-
- return null;
- },
-
- /**
- * Appends the given children data to the given node.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {*} node
- * The `Node` argument to append the children to.
- *
- * @param {*} [children]
- * The childrens to append to the given node.
- *
- * When `children` is an array, then each item of the array
- * will be either appended as child element or text node,
- * depending on whether the item is a DOM `Node` instance or
- * some other non-`null` value. Non-`Node`, non-`null` values
- * will be converted to strings first before being passed as
- * argument to `createTextNode()`.
- *
- * When `children` is a function, it will be invoked with
- * the passed `node` argument as sole parameter and the `append`
- * function will be invoked again, with the given `node` argument
- * as first and the return value of the `children` function as
- * second parameter.
- *
- * When `children` is is a DOM `Node` instance, it will be
- * appended to the given `node`.
- *
- * When `children` is any other non-`null` value, it will be
- * converted to a string and appened to the `innerHTML` property
- * of the given `node`.
- *
- * @returns {Node|null}
- * Returns the last children `Node` appended to the node or `null`
- * if either the `node` argument was no valid DOM `node` or if the
- * `children` was `null` or didn't result in further DOM nodes.
- */
- append: function(node, children) {
- if (!this.elem(node))
- return null;
-
- if (Array.isArray(children)) {
- for (var i = 0; i < children.length; i++)
- if (this.elem(children[i]))
- node.appendChild(children[i]);
- else if (children !== null && children !== undefined)
- node.appendChild(document.createTextNode('' + children[i]));
-
- return node.lastChild;
- }
- else if (typeof(children) === 'function') {
- return this.append(node, children(node));
- }
- else if (this.elem(children)) {
- return node.appendChild(children);
- }
- else if (children !== null && children !== undefined) {
- node.innerHTML = '' + children;
- return node.lastChild;
- }
-
- return null;
- },
-
- /**
- * Replaces the content of the given node with the given children.
- *
- * This function first removes any children of the given DOM
- * `Node` and then adds the given given children following the
- * rules outlined below.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {*} node
- * The `Node` argument to replace the children of.
- *
- * @param {*} [children]
- * The childrens to replace into the given node.
- *
- * When `children` is an array, then each item of the array
- * will be either appended as child element or text node,
- * depending on whether the item is a DOM `Node` instance or
- * some other non-`null` value. Non-`Node`, non-`null` values
- * will be converted to strings first before being passed as
- * argument to `createTextNode()`.
- *
- * When `children` is a function, it will be invoked with
- * the passed `node` argument as sole parameter and the `append`
- * function will be invoked again, with the given `node` argument
- * as first and the return value of the `children` function as
- * second parameter.
- *
- * When `children` is is a DOM `Node` instance, it will be
- * appended to the given `node`.
- *
- * When `children` is any other non-`null` value, it will be
- * converted to a string and appened to the `innerHTML` property
- * of the given `node`.
- *
- * @returns {Node|null}
- * Returns the last children `Node` appended to the node or `null`
- * if either the `node` argument was no valid DOM `node` or if the
- * `children` was `null` or didn't result in further DOM nodes.
- */
- content: function(node, children) {
- if (!this.elem(node))
- return null;
-
- while (node.firstChild)
- node.removeChild(node.firstChild);
-
- return this.append(node, children);
- },
-
- /**
- * Sets attributes or registers event listeners on element nodes.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {*} node
- * The `Node` argument to set the attributes or add the event
- * listeners for. When the given `node` value is not a valid
- * DOM `Node`, the function returns and does nothing.
- *
- * @param {string|Object<string, *>} key
- * Specifies either the attribute or event handler name to use,
- * or an object containing multiple key, value pairs which are
- * each added to the node as either attribute or event handler,
- * depending on the respective value.
- *
- * @param {*} [val]
- * Specifies the attribute value or event handler function to add.
- * If the `key` parameter is an `Object`, this parameter will be
- * ignored.
- *
- * When `val` is of type function, it will be registered as event
- * handler on the given `node` with the `key` parameter being the
- * event name.
- *
- * When `val` is of type object, it will be serialized as JSON and
- * added as attribute to the given `node`, using the given `key`
- * as attribute name.
- *
- * When `val` is of any other type, it will be added as attribute
- * to the given `node` as-is, with the underlying `setAttribute()`
- * call implicitely turning it into a string.
- */
- attr: function(node, key, val) {
- if (!this.elem(node))
- return null;
-
- var attr = null;
-
- if (typeof(key) === 'object' && key !== null)
- attr = key;
- else if (typeof(key) === 'string')
- attr = {}, attr[key] = val;
-
- for (key in attr) {
- if (!attr.hasOwnProperty(key) || attr[key] == null)
- continue;
-
- switch (typeof(attr[key])) {
- case 'function':
- node.addEventListener(key, attr[key]);
- break;
-
- case 'object':
- node.setAttribute(key, JSON.stringify(attr[key]));
- break;
-
- default:
- node.setAttribute(key, attr[key]);
- }
- }
- },
-
- /**
- * Creates a new DOM `Node` from the given `html`, `attr` and
- * `data` parameters.
- *
- * This function has multiple signatures, it can be either invoked
- * in the form `create(html[, attr[, data]])` or in the form
- * `create(html[, data])`. The used variant is determined from the
- * type of the second argument.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {*} html
- * Describes the node to create.
- *
- * When the value of `html` is of type array, a `DocumentFragment`
- * node is created and each item of the array is first converted
- * to a DOM `Node` by passing it through `create()` and then added
- * as child to the fragment.
- *
- * When the value of `html` is a DOM `Node` instance, no new
- * element will be created but the node will be used as-is.
- *
- * When the value of `html` is a string starting with `<`, it will
- * be passed to `dom.parse()` and the resulting value is used.
- *
- * When the value of `html` is any other string, it will be passed
- * to `document.createElement()` for creating a new DOM `Node` of
- * the given name.
- *
- * @param {Object<string, *>} [attr]
- * Specifies an Object of key, value pairs to set as attributes
- * or event handlers on the created node. Refer to
- * {@link LuCI.dom#attr dom.attr()} for details.
- *
- * @param {*} [data]
- * Specifies children to append to the newly created element.
- * Refer to {@link LuCI.dom#append dom.append()} for details.
- *
- * @throws {InvalidCharacterError}
- * Throws an `InvalidCharacterError` when the given `html`
- * argument contained malformed markup (such as not escaped
- * `&` characters in XHTML mode) or when the given node name
- * in `html` contains characters which are not legal in DOM
- * element names, such as spaces.
- *
- * @returns {Node}
- * Returns the newly created `Node`.
- */
- create: function() {
- var html = arguments[0],
- attr = arguments[1],
- data = arguments[2],
- elem;
-
- if (!(attr instanceof Object) || Array.isArray(attr))
- data = attr, attr = null;
-
- if (Array.isArray(html)) {
- elem = document.createDocumentFragment();
- for (var i = 0; i < html.length; i++)
- elem.appendChild(this.create(html[i]));
- }
- else if (this.elem(html)) {
- elem = html;
- }
- else if (html.charCodeAt(0) === 60) {
- elem = this.parse(html);
- }
- else {
- elem = document.createElement(html);
- }
-
- if (!elem)
- return null;
-
- this.attr(elem, attr);
- this.append(elem, data);
-
- return elem;
- },
-
- /**
- * The ignore callback function is invoked by `isEmpty()` for each
- * child node to decide whether to ignore a child node or not.
- *
- * When this function returns `false`, the node passed to it is
- * ignored, else not.
- *
- * @callback LuCI.dom~ignoreCallbackFn
- * @param {Node} node
- * The child node to test.
- *
- * @returns {boolean}
- * Boolean indicating whether to ignore the node or not.
- */
-
- /**
- * Tests whether a given DOM `Node` instance is empty or appears
- * empty.
- *
- * Any element child nodes which have the CSS class `hidden` set
- * or for which the optionally passed `ignoreFn` callback function
- * returns `false` are ignored.
- *
- * @instance
- * @memberof LuCI.dom
- * @param {Node} node
- * The DOM `Node` instance to test.
- *
- * @param {LuCI.dom~ignoreCallbackFn} [ignoreFn]
- * Specifies an optional function which is invoked for each child
- * node to decide whether the child node should be ignored or not.
- *
- * @returns {boolean}
- * Returns `true` if the node does not have any children or if
- * any children node either has a `hidden` CSS class or a `false`
- * result when testing it using the given `ignoreFn`.
- */
- isEmpty: function(node, ignoreFn) {
- for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
- if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
- return false;
-
- return true;
- }
- };
-
- function E() { return dom.create.apply(dom, arguments); }
-
- function override(object, method, newMethod) {
- const original = object[method];
-
- object[method] = function(...args) {
- return newMethod.apply(this, [original.bind(this)].concat(args));
- };
-
- Object.assign(object[method], original);
- }
-
- function getTorrentURL(url) {
- const u = new URL(url);
- const id = u.searchParams.get('id');
- u.pathname = '/download.php';
- u.hash = '';
- u.search = '';
- u.searchParams.set('id', id);
- u.searchParams.set('passkey', passkey);
- return u.href;
- }
-
- function savePasskeyFromUrl(url) {
- passkey = new URL(url).searchParams.get('passkey');
- if (passkey)
- localStorage.setItem('passkey', passkey);
- else
- localStorage.removeItem('passkey');
- }
-
- function shouldSkipThis(trNode){
- if(trNode.className.indexOf("half") >= 0){
- // 如果是两行节点 ==> 特殊之处是:他的子节点中有一个是和他同className的
- if(trNode.querySelector("." + trNode.className) == null){
- return true;
- }
- isHalfPage = true;
- }
- return false;
- }
-
- function addListSelect(trlist) {
- trlist[0].prepend(E('td', {
- class: 'colhead',
- align: 'center'
- }, '链接'));
- trlist[0].prepend(E('td', {
- class: 'colhead',
- align: 'center',
- style: 'padding: 0px'
- }, E('button', {
- class: 'btn',
- style: 'font-size: 9pt;',
- click: function() {
- passkey = localStorage.getItem('passkey');
- if (!passkey) {
- alert('No passkey!');
- return;
- }
- let text = '';
- for (let i of this.parentElement.parentElement.parentElement.getElementsByClassName('my_selected')) {
- text += getTorrentURL(i.getElementsByTagName('a')[1].href) + '\n';
- }
- GM_setClipboard(text);
- this.innerHTML = "<span style='color:red'>已复制</span>";
- }
- }, '复制')));
-
- let mousedown = false;
- for (var i = 1; i < trlist.length; ++i) {
- if(shouldSkipThis(trlist[i])) continue; // 对于某些跨行的需要跳过某些行
- const seltd = E('td', {
- class: 'rowfollow nowrap',
- style: 'padding: 0px;',
- align: 'center',
- rowSpan: isHalfPage ? '2':'1',
- mousedown: function(e) {
- e.preventDefault();
- mousedown = true;
- this.firstChild.click();
- },
- mouseenter: function() {
- if (mousedown)
- this.firstChild.click();
- }
- }, E('input', {
- type: 'checkbox',
- style: 'zoom: 1.5;',
- click: function() {
- this.parentElement.parentElement.classList.toggle('my_selected');
- },
- mousedown: function(e) { e.stopPropagation(); }
- }));
-
- const copytd = seltd.cloneNode();
- copytd.append(E('button', {
- class: 'btn',
- click: function() {
- passkey = localStorage.getItem('passkey');
- if (!passkey) {
- alert('No passkey!');
- return;
- }
- GM_setClipboard(getTorrentURL(this.parentElement.nextElementSibling.nextElementSibling.getElementsByTagName('a')[0].href));
- this.innerHTML = "<span style='color:red'>已复制</span>";
- }
- }, '复制'));
-
- trlist[i].prepend(copytd);
- trlist[i].prepend(seltd);
- }
-
- document.addEventListener('mouseup', function(e) {
- if (mousedown) {
- e.preventDefault();
- mousedown = false;
- }
- });
- }
-
- function modifyAnchor(a, url) {
- a.href = url;
- a.addEventListener('click', function(ev) {
- ev.preventDefault();
- ev.stopPropagation();
- GM_setClipboard(this.href);
- if (!this.getAttribute('data-copied')) {
- this.setAttribute('data-copied', '1');
- this.parentElement.previousElementSibling.innerHTML += '(已复制)';
- }
- });
- }
-
- (function() {
- GM_addStyle(`<style>
- .my_selected { background-color: rgba(0, 0, 0, 0.4); }
- td.rowfollow button { font-size: 9pt; }
- </style>`);
-
- if(localStorage.getItem('passkey') == null){
- var insIframe = document.createElement("iframe");
- insIframe.src="/usercp.php";
- document.body.appendChild(insIframe);
- }
- switch (location.pathname) {
- case '/torrents.php': {
- const trlist = document.querySelectorAll('.torrents > tbody > tr');
- addListSelect(trlist);
- }
- break;
- case '/details.php': {
- let dlAnchor = document.getElementById('direct_link'); // tjupt.org
- if (!dlAnchor) {
- var trlist = document.querySelectorAll('#outer > h1 + table > tbody > tr');
- const names = ['种子链接'];
- for (let i of trlist) {
- const name = i.firstElementChild.innerText;
- if (names.includes(name)) {
- dlAnchor = i.querySelector("a");
- break;
- }
- }
- }
- if (dlAnchor) {
- const url = dlAnchor.getAttribute('href') || dlAnchor.getAttribute('data-clipboard-text'); // hdhome.org || tjupt.org
- modifyAnchor(dlAnchor, url);
- savePasskeyFromUrl(url);
- } else {
- let text = '没有 passkey, 点此打开控制面板获取 passkey';
- let url = null;
- if (passkey) {
- url = getTorrentURL(location);
- const u = new URL(url);
- u.searchParams.set('passkey', '***');
- text = u.href;
- }
- const a = E('a', { href: '/usercp.php' }, text);
- if (url)
- modifyAnchor(a, url);
-
- trlist[0].insertAdjacentElement('afterend', E('tr', [
- E('td', {
- class: 'rowhead nowrap',
- valign: 'top',
- align: 'right'
- }, '种子链接'),
- E('td', {
- class: 'rowfollow',
- valign: 'top',
- align: 'left'
- }, a)
- ]));
- }
- }
- break;
- case '/usercp.php': {
- const url = new URL(location);
- if(!url.searchParams.get('action')) {
- const names = ['passkey', '密钥'];
- for (let i of document.querySelectorAll('#outer > .main + table tr')) {
- const name = i.firstElementChild.innerText;
- if (names.includes(name)) { // 修复因为多个元素导致的错误
- passkey = i.lastElementChild.innerText;
- i.lastElementChild.innerHTML += ' (已获取)';
- break;
- }
- }
- if (passkey)
- localStorage.setItem('passkey', passkey);
- else
- localStorage.removeItem('passkey');
- }
- }
- break;
- case '/userdetails.php': {
- override(unsafeWindow, 'getusertorrentlistajax', function(original, userid, type, blockid) {
- if (original(userid, type, blockid)) {
- const blockdiv = document.getElementById(blockid);
- addListSelect(blockdiv.getElementsByTagName('tr'));
- return true;
- }
- return false;
- });
- }
- break;
- }
- })();