NexusPHP PT Helper

Helper script for NexusPHP based PT sites.

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址