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-22 提交的版本,查看 最新版本

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