Greasy Fork is available in English.

NexusPHP PT Helper Plus

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

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