NexusPHP PT Helper Plus

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

当前为 2020-01-01 提交的版本,查看 最新版本

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