// ==UserScript==
// @name Real-Debrid Premium Link Converter
// @version 4.6
// @grant GM.xmlHttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @namespace https://greasyfork.org/en/users/807108-jeremy-r
// @include *://*
// @exclude https://real-debrid.com/*
// @description Convert standard links into premium links using Real-Debrid
// @icon https://icons.duckduckgo.com/ip2/real-debrid.com.ico
// @run-at document-end
// @author JRem
// @license MIT
// ==/UserScript==
let targetURLs = GM_getValue('targetURLs', []);
let token = GM_getValue('api_token', []);
// Register menu commands for the script
function createMenu() {
GM_registerMenuCommand("Update API Token", updatetoken);
GM_registerMenuCommand("Refresh DDL Domains", updateDDLDomains);
console.log("Menu commands registered.");
}
// Ensure `updateDDLDomains` runs at least once every 24 hours
function ensureUpdateDDLDomains() {
const lastRunTimestamp = GM_getValue('lastUpdateTimestamp', 0);
const now = Date.now();
if (now - lastRunTimestamp >= 24 * 60 * 60 * 1000) {
console.log("More than 24 hours since last update. Running `updateDDLDomains`...");
updateDDLDomains();
GM_setValue('lastUpdateTimestamp', now);
} else {
console.log("`updateDDLDomains` already ran within the last 24 hours.");
}
}
// Fetch the domains from Real-Debrid API and update the domain cache
function updateDDLDomains() {
console.log("Updating DDL Domains...");
GM.xmlHttpRequest({
method: 'GET',
url: 'https://api.real-debrid.com/rest/1.0/hosts/domains',
headers: {
'Authorization': `Bearer ${GM_getValue('api_token', '')}`
},
onload: function (response) {
if (response.status === 200) {
try {
const domains = JSON.parse(response.responseText);
if (domains.length > 0) {
targetURLs = domains; // Replace the array with the new list
GM_setValue('targetURLs', targetURLs);
GM_setValue('lastUpdateTimestamp', Date.now());
console.log('Fetched domains:', targetURLs);
showToast(`${domains.length} domains found and saved.`);
} else {
console.error('No domains found. Re-fetching...');
showToast('No domains found. Attempting re-fetch...');
updateDDLDomains();
}
} catch (error) {
console.error('Failed to parse the API response:', error);
}
} else {
console.error('Failed to fetch domains. Status:', response.status);
}
},
onerror: function () {
console.error('An error occurred while fetching DDL domains.');
}
});
}
// Update the API token
function updatetoken() {
GM.xmlHttpRequest({
method: 'GET',
url: 'https://real-debrid.com/apitoken',
onload: function (response) {
if (response.status === 200) {
const responseText = response.responseText;
const tokenMatch = responseText.match(/document\.querySelectorAll\('input\[name=private_token\]'\)\[0\]\.value\s*=\s*'([^']+)'/);
if (tokenMatch && tokenMatch[1]) {
const token = tokenMatch[1];
GM_setValue('api_token', token); // Save the token
console.log('API token updated successfully:', token); // Debug output
showToast('API token updated successfully.');
} else {
console.error('Failed to extract the token. Prompting user...');
const manualToken = prompt('API token not found in the response. Please enter it manually:');
if (manualToken) {
GM_setValue('api_token', manualToken);
console.log('API token manually entered:', manualToken);
showToast('API token saved successfully.');
} else {
console.error('No token entered. Unable to proceed.');
showToast('Token update failed. Please try again.');
}
}
} else {
console.error('Failed to fetch the API token. Status:', response.status);
}
},
onerror: function () {
console.error('An error occurred while updating the API token.');
}
});
}
// Display a toast message in the center of the screen
function showToast(message) {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.position = 'fixed';
toast.style.top = '50%';
toast.style.left = '50%';
toast.style.transform = 'translate(-50%, -50%)';
toast.style.backgroundColor = '#333';
toast.style.color = '#fff';
toast.style.padding = '10px 20px';
toast.style.borderRadius = '5px';
toast.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
toast.style.zIndex = '9999';
toast.style.fontSize = '16px';
toast.style.textAlign = 'center';
toast.style.animation = 'fadeOut 3s ease forwards';
document.body.appendChild(toast);
// Remove the toast after the animation
setTimeout(() => {
document.body.removeChild(toast);
}, 3000);
// Add fade-out animation
const style = document.createElement('style');
style.textContent = `
@keyframes fadeOut {
0% { opacity: 1; }
80% { opacity: 0.8; }
100% { opacity: 0; }
}
`;
document.head.appendChild(style);
}
// Check if the token and domain list are set
function checkInitialization() {
const token = GM_getValue('api_token', '');
if (!token) {
console.log('API token not found. Attempting to fetch...');
updatetoken();
}
if (targetURLs.length === 0) {
console.log('Domain list is empty. Fetching domains...');
updateDDLDomains();
}
ensureUpdateDDLDomains();
}
const processedURLs = new Set(); // Track processed URLs
// Check if the current URL contains any of the target URLs
function isTargetURL() {
const currentUrl = window.location.href;
return targetURLs.some(target => currentUrl.includes(target));
}
// Generate a regex pattern based on targetURLs
function generateUrlRegex() {
const domains = targetURLs.map(url => url.replace(/\./g, '\\.') + '[^\s"]*');
const pattern = `https:\\/\\/(?:${domains.join('|')})`;
return new RegExp(pattern, 'g');
}
function createFastDownloadButton(linkElement, fileURL) {
let button = document.createElement('button');
button.innerHTML = 'Send to RD';
button.style.marginLeft = '5px'; // Add space to the left of the button
button.style.padding = '2px 5px'; // Add space to the right of the button
button.style.backgroundColor = 'black'; // Set button background color to red
button.style.color = 'white'; // Optional: Set button text color to white for better contrast
button.style.borderRadius = '5px'; // Add rounded corners
button.onclick = () => {
GM.xmlHttpRequest({
method: 'POST',
url: 'https://app.real-debrid.com/rest/1.0/unrestrict/link',
headers: {
"authorization": `Bearer ${token}`
},
data: `link=${encodeURIComponent(fileURL)}&password=`,
onload: (response) => {
const jsonData = JSON.parse(response.responseText);
if (jsonData.download !== undefined) {
linkElement.href = jsonData.download;
linkElement.textContent = jsonData.filename;
} else {
linkElement.textContent += ' - failed';
}
button.remove();
}
});
};
linkElement.setAttribute('realdebrid', 'true');
linkElement.insertAdjacentElement('afterend', button);
}
// Create a button for magnet links
function createMagnetButton(linkElement, fileURL) {
let button = document.createElement('button');
button.innerHTML = 'Send Magnet to RD';
button.style.marginLeft = '5px';
button.style.padding = '2px 5px';
button.style.backgroundColor = 'green';
button.style.color = 'white';
button.style.borderRadius = '5px';
button.onclick = () => {
GM.xmlHttpRequest({
method: 'POST',
url: 'https://api.real-debrid.com/rest/1.0/torrents/addMagnet',
headers: {
"authorization": `Bearer ${token}`,
"Content-Type": "application/x-www-form-urlencoded"
},
data: `magnet=${encodeURIComponent(fileURL)}`,
onload: (response) => {
console.log('[addMagnet] Response status:', response.status);
console.log('[addMagnet] Response text:', response.responseText);
if (response.status === 201) {
try {
const responseData = JSON.parse(response.responseText);
console.log('[addMagnet] Parsed response data:', responseData);
const torrentId = responseData.id;
console.log('[addMagnet] Parsed torrentId data:', torrentId);
showToast('Magnet successfully added to Real-Debrid.');
// Now attempt to select all files
GM.xmlHttpRequest({
method: 'POST',
url: `https://api.real-debrid.com/rest/1.0/torrents/selectFiles/${torrentId}`,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: 'files=all',
onload: (selectResponse) => {
console.log('[selectFiles] Response status:', selectResponse.status);
console.log('[selectFiles] Response text:', selectResponse.responseText);
if (selectResponse.status === 200 || selectResponse.status === 204) {
console.log('Successfully selected all files for torrentId:', torrentId);
showToast('All files selected for download.');
} else {
console.error('Failed to select files. Status:', selectResponse.status);
showToast('Failed to select all files.');
}
},
onerror: () => {
console.error('Error in selectFiles request.');
showToast('Error selecting files.');
}
});
} catch (error) {
console.error('Error parsing addMagnet response:', error);
showToast('Error processing response.');
}
} else {
// Maybe the server uses other status codes for success? but 201 is OK for creation.
console.error('Failed to add magnet link. Status:', response.status, 'Response:', response.responseText);
showToast('Failed to add magnet link.');
}
button.remove();
},
onerror: () => {
console.error('Error in addMagnet request.');
showToast('Error adding magnet link.');
button.remove();
}
});
};
linkElement.setAttribute('realdebrid-magnet', 'true');
linkElement.insertAdjacentElement('afterend', button);
}
function processTextNode(textNode) {
const textContent = textNode.textContent;
const urlRegex = generateUrlRegex();
let match;
while ((match = urlRegex.exec(textContent)) !== null) {
const url = match[0];
if (processedURLs.has(url)) {
continue; // Skip URLs that have already been processed
}
for (let targetURL of targetURLs) {
if (url.includes(targetURL)) {
const linkElement = document.createElement('a');
linkElement.href = url;
linkElement.textContent = url;
linkElement.style.display = 'block'; // Ensure each link appears on a new line
textNode.parentNode.insertBefore(linkElement, textNode);
createFastDownloadButton(linkElement, url);
processedURLs.add(url); // Mark URL as processed
}
}
}
}
function processPreElements() {
let preElements = document.getElementsByTagName('pre');
for (let i = 0; i < preElements.length; i++) {
let preElement = preElements[i];
let textNodes = Array.from(preElement.childNodes).filter(node => node.nodeType === Node.TEXT_NODE);
textNodes.forEach(node => processTextNode(node));
}
}
// Process links
function processLinks() {
const links = document.querySelectorAll('a[href]');
links.forEach(link => {
const href = link.href;
if (href.startsWith('magnet:?')) {
if (!link.hasAttribute('realdebrid-magnet')) {
createMagnetButton(link, href);
}
} else if (targetURLs.some(domain => href.includes(domain))) {
if (!link.hasAttribute('realdebrid')) {
createFastDownloadButton(link, href);
}
}
});
}
function observeLinks() {
const hostname = location.hostname.replace(/^www\./, '').toLowerCase();
// If we're on a DDL domain, skip all processing
if (targetURLs.some(domain => hostname === domain.toLowerCase())) {
console.log(`Skipping processing on DDL domain: ${hostname}`);
return;
}
processLinks();
if (!isTargetURL()) {
document.querySelectorAll('a[href]').forEach(link => {
const href = link.href;
if (link.hasAttribute('realdebrid') || href.length < 20) return;
if (targetURLs.some(domain => href.includes(domain))) {
createFastDownloadButton(link, href);
processedURLs.add(href);
}
});
processPreElements();
const iterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
const tag = node.parentNode?.tagName?.toLowerCase();
if (!tag || ['script', 'style', 'button', 'video', 'select', 'menu', 'nav'].includes(tag)) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
let node;
while ((node = iterator.nextNode())) {
if (!node.parentNode.querySelector('a[realdebrid]')) {
processTextNode(node);
}
}
}
}
// Debounce function to limit how often observeLinks is called
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Observe the document body for changes
const observer = new MutationObserver(debounce(observeLinks, 500));
observer.observe(document.body, { childList: true, subtree: true });
// Initialize the script when the page loads
window.onload = () => {
createMenu(); // Register the menu commands
checkInitialization(); // Ensure token and domain list are set
observeLinks; //observe the links when the page first loads
};