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