NexusPHP PT Helper Plus

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

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

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

QingJ © 2025

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