Real-Debrid Enhancer

Enhance Real-Debrid with clickable rows, copy and debrid buttons, grid layout, and improved layout management on torrents and downloader pages.

目前为 2024-07-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Real-Debrid Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.0
  5. // @description Enhance Real-Debrid with clickable rows, copy and debrid buttons, grid layout, and improved layout management on torrents and downloader pages.
  6. // @author UnderPL
  7. // @match https://real-debrid.com/torrents*
  8. // @match https://real-debrid.com/downloader*
  9. // @grant GM_setClipboard
  10. // @grant GM_addStyle
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. let copyButton, debridButton;
  18.  
  19. GM_addStyle(`
  20.  
  21. .tr.g1:not(.warning), .tr.g2:not(.warning), .tr.g1:not(.warning) + tr, .tr.g2:not(.warning) + tr {
  22. cursor: pointer;
  23. position: relative;
  24. }
  25. .tr.g1.selected, .tr.g2.selected, .tr.g1.selected + tr, .tr.g2.selected + tr {
  26. background-color: rgba(0, 255, 0, 0.3);
  27. }
  28.  
  29. .tr.g1, .tr.g2 {
  30. border-top: 2px solid black/* Green border on top */
  31.  
  32. }
  33.  
  34. .tr.g1 + tr, .tr.g2 + tr {
  35. border-bottom: 2px solid black; /* Green border on bottom */
  36.  
  37. }
  38. #buttonContainer {
  39. position: fixed;
  40. bottom: 10px;
  41. right: 10px;
  42. display: flex;
  43. flex-direction: column;
  44. gap: 10px;
  45. z-index: 9999;
  46. }
  47. #buttonContainer button {
  48. padding: 10px;
  49. background-color: #4CAF50;
  50. color: white;
  51. border: none;
  52. cursor: pointer;
  53. font-size: 16px;
  54. }
  55. #facebox .content {
  56. width: 90vw !important;
  57. max-width: 1200px !important;
  58. display: flex !important;
  59. flex-wrap: wrap !important;
  60. justify-content: space-between !important;
  61. }
  62. .torrent-info {
  63. width: calc(33.33% - 20px);
  64. margin-bottom: 20px;
  65. border: 1px solid #ccc;
  66. padding: 10px;
  67. box-sizing: border-box;
  68. }
  69. `);
  70.  
  71. function initializeApplication() {
  72. if (window.location.href.includes('/torrents')) {
  73. cleanupTorrentPageLayout();
  74. createFloatingButtons();
  75. makeItemsSelectable();
  76. updateFloatingButtonsVisibility();
  77. setupTorrentInfoWindowObserver();
  78. checkForTorrentInfoWindow();
  79. setupItemHoverEffects();
  80. movePaginationToBottomRight();
  81. addSwitchToGridLayoutButton();
  82. //switchToGridLayout
  83. } else if (window.location.href.includes('/downloader')) {
  84. addExtractUrlsButtonToDownloader();
  85. addCopyLinksButton();
  86. }
  87. }
  88.  
  89. function movePaginationToBottomRight() {
  90. const parentElement = document.querySelector('div.full_width_wrapper');
  91. const formElement = parentElement.querySelector('form:nth-child(1)');
  92. const pageElements = parentElement.querySelectorAll('div.full_width_wrapper > strong, div.full_width_wrapper > a[href^="./torrents?p="]');
  93. const containerDiv = document.createElement('div');
  94. const marginSize = '5px';
  95. const fontSize = '16px';
  96.  
  97. containerDiv.style.position = 'absolute';
  98. containerDiv.style.right = '0';
  99. containerDiv.style.bottom = '0';
  100. containerDiv.style.display = 'flex';
  101. containerDiv.style.gap = marginSize;
  102. containerDiv.style.fontSize = fontSize;
  103.  
  104. pageElements.forEach(page => {
  105. containerDiv.appendChild(page);
  106. });
  107.  
  108. formElement.style.position = 'relative';
  109. formElement.appendChild(containerDiv);
  110. }
  111.  
  112. function createFloatingButtons() {
  113. const container = document.createElement('div');
  114. container.id = 'buttonContainer';
  115.  
  116. debridButton = document.createElement('button');
  117. debridButton.addEventListener('click', sendSelectedLinksToDebrid);
  118.  
  119. copyButton = document.createElement('button');
  120. copyButton.addEventListener('click', copySelectedLinksToClipboard);
  121.  
  122. container.appendChild(debridButton);
  123. container.appendChild(copyButton);
  124. document.body.appendChild(container);
  125.  
  126. return container;
  127. }
  128.  
  129. function updateFloatingButtonsVisibility() {
  130. const selectedLinks = getSelectedItemLinks();
  131. const count = selectedLinks.length;
  132.  
  133. if (count > 0) {
  134. debridButton.textContent = `Debrid (${count})`;
  135. copyButton.textContent = `Copy Selected to Clipboard (${count})`;
  136. debridButton.style.display = 'block';
  137. copyButton.style.display = 'block';
  138. } else {
  139. debridButton.style.display = 'none';
  140. copyButton.style.display = 'none';
  141. }
  142. }
  143.  
  144. function makeItemsSelectable() {
  145. const rows = document.querySelectorAll('.tr.g1, .tr.g2');
  146. rows.forEach(row => {
  147. const warningSpan = row.querySelector('span.px10 strong');
  148. if (!warningSpan || warningSpan.textContent !== 'Warning:') {
  149. const nextRow = row.nextElementSibling;
  150. const clickHandler = () => {
  151. row.classList.toggle('selected');
  152. if (nextRow) {
  153. nextRow.classList.toggle('selected');
  154. }
  155. if (row.classList.contains('selected')) {
  156. row.style.backgroundColor = 'rgba(0, 255, 0, 0.3)';
  157. if (nextRow) nextRow.style.backgroundColor = 'rgba(0, 255, 0, 0.3)';
  158. } else {
  159. row.style.backgroundColor = '';
  160. if (nextRow) nextRow.style.backgroundColor = '';
  161. }
  162. updateFloatingButtonsVisibility();
  163. };
  164. row.addEventListener('click', clickHandler);
  165. if (nextRow) {
  166. nextRow.addEventListener('click', clickHandler);
  167. }
  168. } else {
  169. row.classList.add('warning');
  170. if (row.nextElementSibling) {
  171. row.nextElementSibling.classList.add('warning');
  172. }
  173. }
  174. });
  175.  
  176. const entries = document.querySelectorAll('.torrent-entry');
  177. entries.forEach(entry => {
  178. entry.addEventListener('click', () => {
  179. entry.classList.toggle('selected');
  180. if (entry.classList.contains('selected')) {
  181. entry.style.backgroundColor = 'rgba(0, 255, 0, 0.3)';
  182. } else {
  183. entry.style.backgroundColor = '';
  184. }
  185. updateFloatingButtonsVisibility();
  186. });
  187. });
  188. }
  189.  
  190. function setupItemHoverEffects() {
  191. const rows = document.querySelectorAll('.tr.g1, .tr.g2');
  192. rows.forEach(row => {
  193. const nextRow = row.nextElementSibling;
  194. if (nextRow && !nextRow.classList.contains('g1') && !nextRow.classList.contains('g2')) {
  195. row.addEventListener('mouseenter', () => {
  196. if (!row.classList.contains('selected')) {
  197. row.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  198. nextRow.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  199. }
  200. });
  201. row.addEventListener('mouseleave', () => {
  202. if (!row.classList.contains('selected')) {
  203. row.style.backgroundColor = '';
  204. nextRow.style.backgroundColor = '';
  205. }
  206. });
  207. nextRow.addEventListener('mouseenter', () => {
  208. if (!row.classList.contains('selected')) {
  209. row.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  210. nextRow.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  211. }
  212. });
  213. nextRow.addEventListener('mouseleave', () => {
  214. if (!row.classList.contains('selected')) {
  215. row.style.backgroundColor = '';
  216. nextRow.style.backgroundColor = '';
  217. }
  218. });
  219. }
  220. });
  221.  
  222. const entries = document.querySelectorAll('.torrent-entry');
  223. entries.forEach(entry => {
  224. entry.addEventListener('mouseenter', () => {
  225. if (!entry.classList.contains('selected')) {
  226. entry.style.backgroundColor = 'rgba(0, 255, 0, 0.1)';
  227. }
  228. });
  229. entry.addEventListener('mouseleave', () => {
  230. if (!entry.classList.contains('selected')) {
  231. entry.style.backgroundColor = '';
  232. }
  233. });
  234. });
  235. }
  236.  
  237. function getSelectedItemLinks() {
  238. const selectedLinks = [];
  239. const selectedRows = document.querySelectorAll('.tr.g1.selected, .tr.g2.selected');
  240. const selectedEntries = document.querySelectorAll('.torrent-entry.selected');
  241.  
  242. selectedRows.forEach(row => {
  243. const textarea = row.nextElementSibling.querySelector('textarea');
  244. if (textarea) {
  245. selectedLinks.push(textarea.value);
  246. }
  247. });
  248.  
  249. selectedEntries.forEach(entry => {
  250. const textarea = entry.querySelector('textarea');
  251. if (textarea) {
  252. selectedLinks.push(textarea.value);
  253. }
  254. });
  255.  
  256. return selectedLinks;
  257. }
  258.  
  259. function copySelectedLinksToClipboard() {
  260. const selectedLinks = getSelectedItemLinks();
  261. if (selectedLinks.length > 0) {
  262. const clipboardText = selectedLinks.join('\n');
  263. GM_setClipboard(clipboardText);
  264. }
  265. }
  266.  
  267. function sendSelectedLinksToDebrid(e) {
  268. e.preventDefault();
  269. const selectedLinks = getSelectedItemLinks();
  270. if (selectedLinks.length > 0) {
  271. const form = document.createElement('form');
  272. form.method = 'POST';
  273. form.action = './downloader';
  274.  
  275. const input = document.createElement('textarea');
  276. input.name = 'links';
  277. input.value = selectedLinks.join('\n');
  278. form.appendChild(input);
  279.  
  280. document.body.appendChild(form);
  281. form.submit();
  282. document.body.removeChild(form);
  283. }
  284. }
  285.  
  286. function extractUrlsFromText(text) {
  287. const urlRegex = /(?:(?:https?):\/\/|www\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$])/igm;
  288. return text.match(urlRegex) || [];
  289. }
  290.  
  291. function addExtractUrlsButtonToDownloader() {
  292. const textarea = document.getElementById('links');
  293. if (textarea) {
  294. const button = document.createElement('button');
  295. button.id = 'extractUrlsButton';
  296. button.textContent = 'Extract URL(s)';
  297. button.style.position = 'absolute';
  298. button.style.right = '28px';
  299. button.style.top = '0';
  300. button.addEventListener('click', function(e) {
  301. e.preventDefault();
  302. const content = textarea.value;
  303. const urls = extractUrlsFromText(content);
  304. textarea.value = urls.join('\n');
  305. });
  306.  
  307. textarea.parentNode.style.position = 'relative';
  308. textarea.parentNode.appendChild(button);
  309. }
  310. }
  311.  
  312. function addCopyLinksButton() {
  313. const linksContainer = document.querySelector('#links-container');
  314. if (linksContainer && linksContainer.children.length > 0) {
  315. const originalButton = document.querySelector('#sub_links');
  316. if (originalButton) {
  317. const copyButton = originalButton.cloneNode(true);
  318. copyButton.id = 'copy_links';
  319. copyButton.value = 'Copy links';
  320. copyButton.type = 'button';
  321. copyButton.style.display = 'block';
  322. copyButton.style.margin = '0 auto';
  323. copyButton.style.float = 'none'
  324. copyButton.style.marginBottom = '10px'
  325.  
  326. copyButton.addEventListener('click', function(e) {
  327. e.preventDefault();
  328. const links = Array.from(document.querySelectorAll('#links-container .link-generated a'))
  329. .filter(a => a.textContent.includes('DOWNLOAD'))
  330. .map(a => a.href)
  331. .join('\n');
  332.  
  333. if (links) {
  334. GM_setClipboard(links);
  335. copyButton.value = 'Copy Links ✔️';
  336. setTimeout(() => {
  337. copyButton.value = 'Copy links';
  338. }, 1500);
  339. }
  340. });
  341.  
  342. linksContainer.insertAdjacentElement('afterend', copyButton);
  343. }
  344. }
  345. }
  346.  
  347.  
  348. function cleanupTorrentPageLayout() {
  349. const textContainer = document.querySelector('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper');
  350. if (textContainer) {
  351. Array.from(textContainer.childNodes).forEach(node => {
  352. if (node.nodeType === Node.TEXT_NODE) {
  353. node.remove();
  354. }
  355. });
  356. }
  357.  
  358. const brElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper br');
  359. brElements.forEach(br => br.remove());
  360.  
  361. const centerElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper center');
  362. centerElements.forEach(center => center.remove());
  363.  
  364. const contentSeparatorMiniElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper div.content_separator_mini');
  365. contentSeparatorMiniElements.forEach(div => div.remove());
  366.  
  367. const h2Elements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper h2');
  368. h2Elements.forEach(h2 => h2.remove());
  369.  
  370. const spanElements = document.querySelectorAll('html.cufon-active.cufon-ready body div#block div#contentblock div#wrapper_global div.main_content_wrapper div.full_width_wrapper span.px10');
  371. spanElements.forEach(span => span.remove());
  372. }
  373.  
  374. function redesignTorrentInfoWindow() {
  375. const facebox = document.getElementById('facebox');
  376. if (facebox) {
  377. const content = facebox.querySelector('.content');
  378. if (content) {
  379. content.style.width = '90vw';
  380. content.style.maxWidth = '1200px';
  381. content.style.display = 'flex';
  382. content.style.flexWrap = 'wrap';
  383. content.style.justifyContent = 'space-between';
  384.  
  385. // Store the original buttons with their event listeners
  386. const startButtons = Array.from(content.querySelectorAll('input[type="button"][value="Start my torrent"]'));
  387.  
  388. const torrentInfos = content.innerHTML.split('<h2>Torrent Files</h2>').filter(info => info.trim() !== '');
  389.  
  390. content.innerHTML = '';
  391.  
  392. torrentInfos.forEach((info, index) => {
  393. const div = document.createElement('div');
  394. div.className = 'torrent-info';
  395.  
  396. // Create a temporary div to parse the HTML
  397. const tempDiv = document.createElement('div');
  398. tempDiv.innerHTML = '<h2>Torrent Files</h2>' + info;
  399.  
  400. // Move the content except the button
  401. while (tempDiv.firstChild) {
  402. if (tempDiv.firstChild.tagName !== 'INPUT' || tempDiv.firstChild.type !== 'button') {
  403. div.appendChild(tempDiv.firstChild);
  404. } else {
  405. tempDiv.removeChild(tempDiv.firstChild);
  406. }
  407. }
  408.  
  409. // Append the original button with its event listeners
  410. if (startButtons[index]) {
  411. div.appendChild(startButtons[index]);
  412. }
  413.  
  414. content.appendChild(div);
  415. });
  416. }
  417. }
  418. }
  419.  
  420. function setupTorrentInfoWindowObserver() {
  421. const observer = new MutationObserver((mutations) => {
  422. mutations.forEach((mutation) => {
  423. if (mutation.addedNodes && mutation.addedNodes.length > 0) {
  424. for (let node of mutation.addedNodes) {
  425. if (node.id === 'facebox') {
  426. redesignTorrentInfoWindow();
  427. }
  428. }
  429. }
  430. });
  431. });
  432.  
  433. observer.observe(document.body, { childList: true, subtree: true });
  434. }
  435.  
  436. function checkForTorrentInfoWindow() {
  437. const intervalId = setInterval(() => {
  438. const facebox = document.getElementById('facebox');
  439. if (facebox) {
  440. redesignTorrentInfoWindow();
  441. clearInterval(intervalId);
  442. }
  443. }, 1000);
  444. }
  445.  
  446. function createGridLayout(columnCount) {
  447. const table = document.querySelector('table[width="100%"]');
  448. if (!table) return;
  449.  
  450. const container = document.createElement('div');
  451. container.id = 'torrent-grid-container';
  452. container.style.display = 'flex';
  453. container.style.flexWrap = 'wrap';
  454. container.style.justifyContent = 'space-between';
  455.  
  456. const rows = table.querySelectorAll('tr');
  457. for (let i = 1; i < rows.length; i += 2) {
  458. const torrentDiv = createGridItemFromTableRows(rows[i], rows[i + 1]);
  459. container.appendChild(torrentDiv);
  460. }
  461.  
  462. table.parentNode.replaceChild(container, table);
  463. applyGridLayoutStyles(columnCount);
  464. adjustImageSizeInNewLayout();
  465. moveDeleteLinkToEnd();
  466. makeItemsSelectable();
  467. setupItemHoverEffects();
  468. }
  469.  
  470. function applyGridLayoutStyles(columnCount) {
  471. const width = `calc(${100 / columnCount}% - 20px)`;
  472. GM_addStyle(`
  473. #torrent-grid-container {
  474. width: 100%;
  475. max-width: 1200px;
  476. margin: 0 auto;
  477. }
  478. .torrent-entry {
  479. width: ${width};
  480. margin-bottom: 20px;
  481. border: 1px solid #ccc;
  482. padding: 10px;
  483. box-sizing: border-box;
  484. cursor: pointer;
  485. }
  486. .torrent-entry.selected {
  487. background-color: rgba(0, 255, 0, 0.3) !important;
  488. }
  489. .torrent-entry:hover:not(.selected) {
  490. background-color: rgba(0, 255, 0, 0.1);
  491. }
  492. .torrent-entry td {
  493. display: block;
  494. width: 100%;
  495. }
  496. .torrent-entry tr {
  497. display: block;
  498. }
  499. .torrent-entry form {
  500. margin-top: 10px;
  501. }
  502. .torrent-entry textarea {
  503. min-height: 2.5em;
  504. max-height: 6em;
  505. overflow-y: auto;
  506. resize: vertical;
  507. }
  508. `);
  509. }
  510.  
  511. function adjustImageSizeInNewLayout() {
  512. document.querySelectorAll('#torrent-grid-container .torrent-entry form input[type="image"]').forEach(function(img) {
  513. img.style.width = '10%';
  514. img.style.height = 'auto';
  515. img.style.display = 'inline-block';
  516. img.style.marginLeft = '10px';
  517. });
  518.  
  519. document.querySelectorAll('#torrent-grid-container .torrent-entry form').forEach(function(form) {
  520. form.style.display = 'flex';
  521. form.style.alignItems = 'center';
  522. });
  523. }
  524.  
  525. function moveDeleteLinkToEnd() {
  526. document.querySelectorAll('.torrent-entry').forEach(entry => {
  527. const deleteLink = entry.querySelector('a[href*="del"]');
  528. if (deleteLink) {
  529. // Create a container for the delete link
  530. const deleteContainer = document.createElement('div');
  531. deleteContainer.classList.add('delete-container');
  532. deleteContainer.style.position = 'absolute';
  533. deleteContainer.style.right = '0';
  534. deleteContainer.style.top = '0';
  535. deleteContainer.style.display = 'flex';
  536. deleteContainer.style.alignItems = 'center';
  537. deleteContainer.style.height = '100%';
  538. deleteContainer.style.paddingRight = '10px';
  539.  
  540. // Move the delete link into the new container
  541. deleteContainer.appendChild(deleteLink);
  542. entry.appendChild(deleteContainer);
  543.  
  544. // Ensure the parent .torrent-entry has relative positioning
  545. entry.style.position = 'relative';
  546. }
  547. });
  548. }
  549.  
  550. function createGridItemFromTableRows(mainRow, detailRow) {
  551. const div = document.createElement('div');
  552. div.className = 'torrent-entry';
  553. div.innerHTML = mainRow.innerHTML + detailRow.innerHTML;
  554.  
  555. div.addEventListener('click', () => {
  556. div.classList.toggle('selected');
  557. updateFloatingButtonsVisibility();
  558. });
  559.  
  560. return div;
  561. }
  562.  
  563. function addSwitchToGridLayoutButton() {
  564. const button = document.createElement('button');
  565. button.textContent = 'Switch Layout';
  566. button.id = 'switchLayoutButton';
  567. button.style.position = 'fixed';
  568. button.style.top = '10px';
  569. button.style.right = '20px';
  570. button.style.zIndex = '1000';
  571. button.addEventListener('click', switchToGridLayout);
  572. document.body.appendChild(button);
  573. }
  574.  
  575. function switchToGridLayout() {
  576. const columnCount = 3; // You can adjust this number as needed
  577. createGridLayout(columnCount);
  578. setupItemHoverEffects();
  579. makeItemsSelectable();
  580. updateFloatingButtonsVisibility();
  581.  
  582. const button = document.getElementById('switchLayoutButton');
  583. if (button) {
  584. button.remove();
  585. }
  586. }
  587.  
  588. if (document.readyState === 'complete') {
  589. initializeApplication();
  590. } else {
  591. window.addEventListener('load', initializeApplication);
  592. }
  593. })();