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