NexusPHP PT Helper Plus

基于NexusPHP的PT网站的辅助脚本

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==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;
	}
})();