NexusPHP PT Helper Plus

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

当前为 2019-12-29 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name NexusPHP PT Helper Plus
  3. // @name:zh-CN NexusPHP PT 助手增强版
  4. // @namespace https://greasyfork.org/zh-CN/users/7326
  5. // @version 0.0.1
  6. // @description 基于NexusPHP的PT网站的辅助脚本
  7. // @description:zh-CN 适用于基于 NexusPHP 的 PT 站的辅助脚本
  8. // @author ysc3839 〃萝卜
  9. // @match *://*/torrents.php
  10. // @grant unsafeWindow
  11. // @grant GM_addStyle
  12. // @grant GM_setClipboard
  13. // @run-at document-end
  14. // ==/UserScript==
  15.  
  16. 'use strict';
  17.  
  18. let domParser = null, passkey = localStorage.getItem('passkey');
  19.  
  20. /**
  21. * @class
  22. * @memberof LuCI
  23. * @hideconstructor
  24. * @classdesc
  25. *
  26. * Slightly modified version of `LuCI.dom` (https://github.com/openwrt/luci/blob/5d55a0a4a9c338f64818ac73b7d5f28079aa95b7/modules/luci-base/htdocs/luci-static/resources/luci.js#L2080),
  27. * which is licensed under Apache License 2.0 (https://github.com/openwrt/luci/blob/master/LICENSE).
  28. *
  29. * The `dom` class provides convenience method for creating and
  30. * manipulating DOM elements.
  31. */
  32. const dom = {
  33. /**
  34. * Tests whether the given argument is a valid DOM `Node`.
  35. *
  36. * @instance
  37. * @memberof LuCI.dom
  38. * @param {*} e
  39. * The value to test.
  40. *
  41. * @returns {boolean}
  42. * Returns `true` if the value is a DOM `Node`, else `false`.
  43. */
  44. elem: function(e) {
  45. return (e != null && typeof(e) == 'object' && 'nodeType' in e);
  46. },
  47.  
  48. /**
  49. * Parses a given string as HTML and returns the first child node.
  50. *
  51. * @instance
  52. * @memberof LuCI.dom
  53. * @param {string} s
  54. * A string containing an HTML fragment to parse. Note that only
  55. * the first result of the resulting structure is returned, so an
  56. * input value of `<div>foo</div> <div>bar</div>` will only return
  57. * the first `div` element node.
  58. *
  59. * @returns {Node}
  60. * Returns the first DOM `Node` extracted from the HTML fragment or
  61. * `null` on parsing failures or if no element could be found.
  62. */
  63. parse: function(s) {
  64. var elem;
  65.  
  66. try {
  67. domParser = domParser || new DOMParser();
  68. let d = domParser.parseFromString(s, 'text/html');
  69. elem = d.body.firstChild || d.head.firstChild;
  70. }
  71. catch(e) {}
  72.  
  73. if (!elem) {
  74. try {
  75. dummyElem = dummyElem || document.createElement('div');
  76. dummyElem.innerHTML = s;
  77. elem = dummyElem.firstChild;
  78. }
  79. catch (e) {}
  80. }
  81.  
  82. return elem || null;
  83. },
  84.  
  85. /**
  86. * Tests whether a given `Node` matches the given query selector.
  87. *
  88. * This function is a convenience wrapper around the standard
  89. * `Node.matches("selector")` function with the added benefit that
  90. * the `node` argument may be a non-`Node` value, in which case
  91. * this function simply returns `false`.
  92. *
  93. * @instance
  94. * @memberof LuCI.dom
  95. * @param {*} node
  96. * The `Node` argument to test the selector against.
  97. *
  98. * @param {string} [selector]
  99. * The query selector expression to test against the given node.
  100. *
  101. * @returns {boolean}
  102. * Returns `true` if the given node matches the specified selector
  103. * or `false` when the node argument is no valid DOM `Node` or the
  104. * selector didn't match.
  105. */
  106. matches: function(node, selector) {
  107. var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
  108. return m ? m.call(node, selector) : false;
  109. },
  110.  
  111. /**
  112. * Returns the closest parent node that matches the given query
  113. * selector expression.
  114. *
  115. * This function is a convenience wrapper around the standard
  116. * `Node.closest("selector")` function with the added benefit that
  117. * the `node` argument may be a non-`Node` value, in which case
  118. * this function simply returns `null`.
  119. *
  120. * @instance
  121. * @memberof LuCI.dom
  122. * @param {*} node
  123. * The `Node` argument to find the closest parent for.
  124. *
  125. * @param {string} [selector]
  126. * The query selector expression to test against each parent.
  127. *
  128. * @returns {Node|null}
  129. * Returns the closest parent node matching the selector or
  130. * `null` when the node argument is no valid DOM `Node` or the
  131. * selector didn't match any parent.
  132. */
  133. parent: function(node, selector) {
  134. if (this.elem(node) && node.closest)
  135. return node.closest(selector);
  136.  
  137. while (this.elem(node))
  138. if (this.matches(node, selector))
  139. return node;
  140. else
  141. node = node.parentNode;
  142.  
  143. return null;
  144. },
  145.  
  146. /**
  147. * Appends the given children data to the given node.
  148. *
  149. * @instance
  150. * @memberof LuCI.dom
  151. * @param {*} node
  152. * The `Node` argument to append the children to.
  153. *
  154. * @param {*} [children]
  155. * The childrens to append to the given node.
  156. *
  157. * When `children` is an array, then each item of the array
  158. * will be either appended as child element or text node,
  159. * depending on whether the item is a DOM `Node` instance or
  160. * some other non-`null` value. Non-`Node`, non-`null` values
  161. * will be converted to strings first before being passed as
  162. * argument to `createTextNode()`.
  163. *
  164. * When `children` is a function, it will be invoked with
  165. * the passed `node` argument as sole parameter and the `append`
  166. * function will be invoked again, with the given `node` argument
  167. * as first and the return value of the `children` function as
  168. * second parameter.
  169. *
  170. * When `children` is is a DOM `Node` instance, it will be
  171. * appended to the given `node`.
  172. *
  173. * When `children` is any other non-`null` value, it will be
  174. * converted to a string and appened to the `innerHTML` property
  175. * of the given `node`.
  176. *
  177. * @returns {Node|null}
  178. * Returns the last children `Node` appended to the node or `null`
  179. * if either the `node` argument was no valid DOM `node` or if the
  180. * `children` was `null` or didn't result in further DOM nodes.
  181. */
  182. append: function(node, children) {
  183. if (!this.elem(node))
  184. return null;
  185.  
  186. if (Array.isArray(children)) {
  187. for (var i = 0; i < children.length; i++)
  188. if (this.elem(children[i]))
  189. node.appendChild(children[i]);
  190. else if (children !== null && children !== undefined)
  191. node.appendChild(document.createTextNode('' + children[i]));
  192.  
  193. return node.lastChild;
  194. }
  195. else if (typeof(children) === 'function') {
  196. return this.append(node, children(node));
  197. }
  198. else if (this.elem(children)) {
  199. return node.appendChild(children);
  200. }
  201. else if (children !== null && children !== undefined) {
  202. node.innerHTML = '' + children;
  203. return node.lastChild;
  204. }
  205.  
  206. return null;
  207. },
  208.  
  209. /**
  210. * Replaces the content of the given node with the given children.
  211. *
  212. * This function first removes any children of the given DOM
  213. * `Node` and then adds the given given children following the
  214. * rules outlined below.
  215. *
  216. * @instance
  217. * @memberof LuCI.dom
  218. * @param {*} node
  219. * The `Node` argument to replace the children of.
  220. *
  221. * @param {*} [children]
  222. * The childrens to replace into the given node.
  223. *
  224. * When `children` is an array, then each item of the array
  225. * will be either appended as child element or text node,
  226. * depending on whether the item is a DOM `Node` instance or
  227. * some other non-`null` value. Non-`Node`, non-`null` values
  228. * will be converted to strings first before being passed as
  229. * argument to `createTextNode()`.
  230. *
  231. * When `children` is a function, it will be invoked with
  232. * the passed `node` argument as sole parameter and the `append`
  233. * function will be invoked again, with the given `node` argument
  234. * as first and the return value of the `children` function as
  235. * second parameter.
  236. *
  237. * When `children` is is a DOM `Node` instance, it will be
  238. * appended to the given `node`.
  239. *
  240. * When `children` is any other non-`null` value, it will be
  241. * converted to a string and appened to the `innerHTML` property
  242. * of the given `node`.
  243. *
  244. * @returns {Node|null}
  245. * Returns the last children `Node` appended to the node or `null`
  246. * if either the `node` argument was no valid DOM `node` or if the
  247. * `children` was `null` or didn't result in further DOM nodes.
  248. */
  249. content: function(node, children) {
  250. if (!this.elem(node))
  251. return null;
  252.  
  253. while (node.firstChild)
  254. node.removeChild(node.firstChild);
  255.  
  256. return this.append(node, children);
  257. },
  258.  
  259. /**
  260. * Sets attributes or registers event listeners on element nodes.
  261. *
  262. * @instance
  263. * @memberof LuCI.dom
  264. * @param {*} node
  265. * The `Node` argument to set the attributes or add the event
  266. * listeners for. When the given `node` value is not a valid
  267. * DOM `Node`, the function returns and does nothing.
  268. *
  269. * @param {string|Object<string, *>} key
  270. * Specifies either the attribute or event handler name to use,
  271. * or an object containing multiple key, value pairs which are
  272. * each added to the node as either attribute or event handler,
  273. * depending on the respective value.
  274. *
  275. * @param {*} [val]
  276. * Specifies the attribute value or event handler function to add.
  277. * If the `key` parameter is an `Object`, this parameter will be
  278. * ignored.
  279. *
  280. * When `val` is of type function, it will be registered as event
  281. * handler on the given `node` with the `key` parameter being the
  282. * event name.
  283. *
  284. * When `val` is of type object, it will be serialized as JSON and
  285. * added as attribute to the given `node`, using the given `key`
  286. * as attribute name.
  287. *
  288. * When `val` is of any other type, it will be added as attribute
  289. * to the given `node` as-is, with the underlying `setAttribute()`
  290. * call implicitely turning it into a string.
  291. */
  292. attr: function(node, key, val) {
  293. if (!this.elem(node))
  294. return null;
  295.  
  296. var attr = null;
  297.  
  298. if (typeof(key) === 'object' && key !== null)
  299. attr = key;
  300. else if (typeof(key) === 'string')
  301. attr = {}, attr[key] = val;
  302.  
  303. for (key in attr) {
  304. if (!attr.hasOwnProperty(key) || attr[key] == null)
  305. continue;
  306.  
  307. switch (typeof(attr[key])) {
  308. case 'function':
  309. node.addEventListener(key, attr[key]);
  310. break;
  311.  
  312. case 'object':
  313. node.setAttribute(key, JSON.stringify(attr[key]));
  314. break;
  315.  
  316. default:
  317. node.setAttribute(key, attr[key]);
  318. }
  319. }
  320. },
  321.  
  322. /**
  323. * Creates a new DOM `Node` from the given `html`, `attr` and
  324. * `data` parameters.
  325. *
  326. * This function has multiple signatures, it can be either invoked
  327. * in the form `create(html[, attr[, data]])` or in the form
  328. * `create(html[, data])`. The used variant is determined from the
  329. * type of the second argument.
  330. *
  331. * @instance
  332. * @memberof LuCI.dom
  333. * @param {*} html
  334. * Describes the node to create.
  335. *
  336. * When the value of `html` is of type array, a `DocumentFragment`
  337. * node is created and each item of the array is first converted
  338. * to a DOM `Node` by passing it through `create()` and then added
  339. * as child to the fragment.
  340. *
  341. * When the value of `html` is a DOM `Node` instance, no new
  342. * element will be created but the node will be used as-is.
  343. *
  344. * When the value of `html` is a string starting with `<`, it will
  345. * be passed to `dom.parse()` and the resulting value is used.
  346. *
  347. * When the value of `html` is any other string, it will be passed
  348. * to `document.createElement()` for creating a new DOM `Node` of
  349. * the given name.
  350. *
  351. * @param {Object<string, *>} [attr]
  352. * Specifies an Object of key, value pairs to set as attributes
  353. * or event handlers on the created node. Refer to
  354. * {@link LuCI.dom#attr dom.attr()} for details.
  355. *
  356. * @param {*} [data]
  357. * Specifies children to append to the newly created element.
  358. * Refer to {@link LuCI.dom#append dom.append()} for details.
  359. *
  360. * @throws {InvalidCharacterError}
  361. * Throws an `InvalidCharacterError` when the given `html`
  362. * argument contained malformed markup (such as not escaped
  363. * `&` characters in XHTML mode) or when the given node name
  364. * in `html` contains characters which are not legal in DOM
  365. * element names, such as spaces.
  366. *
  367. * @returns {Node}
  368. * Returns the newly created `Node`.
  369. */
  370. create: function() {
  371. var html = arguments[0],
  372. attr = arguments[1],
  373. data = arguments[2],
  374. elem;
  375.  
  376. if (!(attr instanceof Object) || Array.isArray(attr))
  377. data = attr, attr = null;
  378.  
  379. if (Array.isArray(html)) {
  380. elem = document.createDocumentFragment();
  381. for (var i = 0; i < html.length; i++)
  382. elem.appendChild(this.create(html[i]));
  383. }
  384. else if (this.elem(html)) {
  385. elem = html;
  386. }
  387. else if (html.charCodeAt(0) === 60) {
  388. elem = this.parse(html);
  389. }
  390. else {
  391. elem = document.createElement(html);
  392. }
  393.  
  394. if (!elem)
  395. return null;
  396.  
  397. this.attr(elem, attr);
  398. this.append(elem, data);
  399.  
  400. return elem;
  401. },
  402.  
  403. /**
  404. * The ignore callback function is invoked by `isEmpty()` for each
  405. * child node to decide whether to ignore a child node or not.
  406. *
  407. * When this function returns `false`, the node passed to it is
  408. * ignored, else not.
  409. *
  410. * @callback LuCI.dom~ignoreCallbackFn
  411. * @param {Node} node
  412. * The child node to test.
  413. *
  414. * @returns {boolean}
  415. * Boolean indicating whether to ignore the node or not.
  416. */
  417.  
  418. /**
  419. * Tests whether a given DOM `Node` instance is empty or appears
  420. * empty.
  421. *
  422. * Any element child nodes which have the CSS class `hidden` set
  423. * or for which the optionally passed `ignoreFn` callback function
  424. * returns `false` are ignored.
  425. *
  426. * @instance
  427. * @memberof LuCI.dom
  428. * @param {Node} node
  429. * The DOM `Node` instance to test.
  430. *
  431. * @param {LuCI.dom~ignoreCallbackFn} [ignoreFn]
  432. * Specifies an optional function which is invoked for each child
  433. * node to decide whether the child node should be ignored or not.
  434. *
  435. * @returns {boolean}
  436. * Returns `true` if the node does not have any children or if
  437. * any children node either has a `hidden` CSS class or a `false`
  438. * result when testing it using the given `ignoreFn`.
  439. */
  440. isEmpty: function(node, ignoreFn) {
  441. for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
  442. if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
  443. return false;
  444.  
  445. return true;
  446. }
  447. };
  448.  
  449. function E() { return dom.create.apply(dom, arguments); }
  450.  
  451. function override(object, method, newMethod) {
  452. const original = object[method];
  453.  
  454. object[method] = function(...args) {
  455. return newMethod.apply(this, [original.bind(this)].concat(args));
  456. };
  457.  
  458. Object.assign(object[method], original);
  459. }
  460.  
  461. function getTorrentURL(url) {
  462. const u = new URL(url);
  463. const id = u.searchParams.get('id');
  464. u.pathname = '/download.php';
  465. u.hash = '';
  466. u.search = '';
  467. u.searchParams.set('id', id);
  468. u.searchParams.set('passkey', passkey);
  469. return u.href;
  470. }
  471.  
  472. function savePasskeyFromUrl(url) {
  473. passkey = new URL(url).searchParams.get('passkey');
  474. if (passkey)
  475. localStorage.setItem('passkey', passkey);
  476. else
  477. localStorage.removeItem('passkey');
  478. }
  479.  
  480. function addListSelect(trlist) {
  481. trlist[0].prepend(E('td', {
  482. class: 'colhead',
  483. align: 'center'
  484. }, '链接'));
  485. trlist[0].prepend(E('td', {
  486. class: 'colhead',
  487. align: 'center',
  488. style: 'padding: 0px'
  489. }, E('button', {
  490. class: 'btn',
  491. style: 'font-size: 9pt;',
  492. click: function() {
  493. if (!passkey) {
  494. alert('No passkey!');
  495. return;
  496. }
  497. let text = '';
  498. for (let i of this.parentElement.parentElement.parentElement.getElementsByClassName('my_selected')) {
  499. text += getTorrentURL(i.getElementsByTagName('a')[1].href) + '\n';
  500. }
  501. GM_setClipboard(text);
  502. this.innerHTML = '已复制';
  503. }
  504. }, '复制')));
  505.  
  506. let mousedown = false;
  507. for (var i = 1; i < trlist.length; ++i) {
  508. const seltd = E('td', {
  509. class: 'rowfollow nowrap',
  510. style: 'padding: 0px;',
  511. align: 'center',
  512. mousedown: function(e) {
  513. e.preventDefault();
  514. mousedown = true;
  515. this.firstChild.click();
  516. },
  517. mouseenter: function() {
  518. if (mousedown)
  519. this.firstChild.click();
  520. }
  521. }, E('input', {
  522. type: 'checkbox',
  523. style: 'zoom: 1.5;',
  524. click: function() {
  525. this.parentElement.parentElement.classList.toggle('my_selected');
  526. },
  527. mousedown: function(e) { e.stopPropagation(); }
  528. }));
  529.  
  530. const copytd = seltd.cloneNode();
  531. copytd.append(E('button', {
  532. class: 'btn',
  533. click: function() {
  534. if (!passkey) {
  535. alert('No passkey!');
  536. return;
  537. }
  538. GM_setClipboard(getTorrentURL(this.parentElement.nextElementSibling.nextElementSibling.getElementsByTagName('a')[0].href));
  539. this.innerHTML = '已复制';
  540. }
  541. }, '复制'));
  542.  
  543. trlist[i].prepend(copytd);
  544. trlist[i].prepend(seltd);
  545. }
  546.  
  547. document.addEventListener('mouseup', function(e) {
  548. if (mousedown) {
  549. e.preventDefault();
  550. mousedown = false;
  551. }
  552. });
  553. }
  554.  
  555. function modifyAnchor(a, url) {
  556. a.href = url;
  557. a.addEventListener('click', function(ev) {
  558. ev.preventDefault();
  559. ev.stopPropagation();
  560. GM_setClipboard(this.href);
  561. if (!this.getAttribute('data-copied')) {
  562. this.setAttribute('data-copied', '1');
  563. this.parentElement.previousElementSibling.innerHTML += '(已复制)';
  564. }
  565. });
  566. }
  567.  
  568. (function() {
  569. GM_addStyle(`<style>
  570. .my_selected { background-color: rgba(0, 0, 0, 0.4); }
  571. td.rowfollow button { font-size: 9pt; }
  572. </style>`);
  573.  
  574. switch (location.pathname) {
  575. case '/torrents.php': {
  576. const trlist = document.querySelectorAll('.torrents > tbody > tr');
  577. addListSelect(trlist);
  578. }
  579. break;
  580. case '/details.php': {
  581. let dlAnchor = document.getElementById('direct_link'); // tjupt.org
  582. if (!dlAnchor) {
  583. var trlist = document.querySelectorAll('#outer > h1 + table > tbody > tr');
  584. const names = ['种子链接'];
  585. for (let i of trlist) {
  586. const name = i.firstElementChild.innerText;
  587. if (names.includes(name)) {
  588. dlAnchor = i.lastElementChild.firstElementChild;
  589. break;
  590. }
  591. }
  592. }
  593. if (dlAnchor) {
  594. const url = dlAnchor.getAttribute('href') || dlAnchor.getAttribute('data-clipboard-text'); // hdhome.org || tjupt.org
  595. modifyAnchor(dlAnchor, url);
  596. savePasskeyFromUrl(url);
  597. } else {
  598. let text = '没有 passkey, 点此打开控制面板获取 passkey';
  599. let url = null;
  600. if (passkey) {
  601. url = getTorrentURL(location);
  602. const u = new URL(url);
  603. u.searchParams.set('passkey', '***');
  604. text = u.href;
  605. }
  606. const a = E('a', { href: '/usercp.php' }, text);
  607. if (url)
  608. modifyAnchor(a, url);
  609.  
  610. trlist[0].insertAdjacentElement('afterend', E('tr', [
  611. E('td', {
  612. class: 'rowhead nowrap',
  613. valign: 'top',
  614. align: 'right'
  615. }, '种子链接'),
  616. E('td', {
  617. class: 'rowfollow',
  618. valign: 'top',
  619. align: 'left'
  620. }, a)
  621. ]));
  622. }
  623. }
  624. break;
  625. case '/usercp.php': {
  626. const url = new URL(location);
  627. if(!url.searchParams.get('action')) {
  628. const names = ['passkey', '密钥'];
  629. for (let i of document.querySelectorAll('#outer > .main + table tr')) {
  630. const name = i.firstElementChild.innerText;
  631. if (names.includes(name)) {
  632. passkey = i.lastElementChild.innerText;
  633. i.lastElementChild.innerHTML += ' (已获取)';
  634. break;
  635. }
  636. }
  637. if (passkey)
  638. localStorage.setItem('passkey', passkey);
  639. else
  640. localStorage.removeItem('passkey');
  641. }
  642. }
  643. break;
  644. case '/userdetails.php': {
  645. override(unsafeWindow, 'getusertorrentlistajax', function(original, userid, type, blockid) {
  646. if (original(userid, type, blockid)) {
  647. const blockdiv = document.getElementById(blockid);
  648. addListSelect(blockdiv.getElementsByTagName('tr'));
  649. return true;
  650. }
  651. return false;
  652. });
  653. }
  654. break;
  655. }
  656. })();