// ==UserScript== // @name Check dead links and add multi links // @namespace http://tampermonkey.net/ // @version v0.7.0 // @description Check dead links and updates their status right next to links and adds file link to multi. // @author Gameil // @match https://f95zone.to/threads/*/ // @icon https://www.google.com/s2/favicons?sz=64&domain=f95zone.to // @grant GM_xmlhttpRequest // @updateURL https://git.zonies.xyz/Ryahn/F95Zone-Scripts/raw/branch/main/link-manager.js // @downloadURL https://git.zonies.xyz/Ryahn/F95Zone-Scripts/raw/branch/main/link-manager.js // ==/UserScript== (function () { 'use strict'; let email = localStorage.getItem('mixdrop-email'); let key = localStorage.getItem('mixdrop-key'); if (!email) { email = prompt('Please enter your Mixdrop email:'); localStorage.setItem('mixdrop-email', email); } if (!key) { key = prompt('Please enter your Mixdrop key:'); localStorage.setItem('mixdrop-key', key); } //API Keys for some hosts const apiMIXDROP = { email: email, key: key, }; // Approved files hosts. Add/Remove or Rename const ApprovedFileHost = [ 'ANONYMFILE', 'BUNKRR', 'BUNKR', 'CATBOX', 'DELAFIL', 'DOWNLOAD.GG', 'DOWNLOADGG', 'DROPMEFILES', 'FASTUPLOAD', 'FILESADMIN', 'FILEMAIL', 'FILESDP', 'FILESFM', 'GOFILE', 'GDRIVE', 'HEXUPLOAD', 'KRAKENFILES', 'MEDIAFIRE', 'MEGA', 'MIXDROP', 'OSHI', 'PIXELDRAIN', 'PROTON', 'SENDGB', 'TERMINAL', 'TRANSFER.SH', 'TRANSFERT', 'FREEFR', 'UP2SHARE', 'UPLOADHAVEN', 'UPLOADNOW', 'WDHO', 'WETRANSFER', 'WORKUPLOAD', 'YOURFILESTORE', //Below are unsupported or dead hosts 'ANONFILE', 'NOPY', 'RACATY', 'ZIPPYSHARE', ]; const OP = document.querySelector('.bbWrapper'); // Grab OP. var button1Active = true, button2Active = true; // to prevent multiple clicks (function addButton() { const actionBar = document.querySelector('.actionBar-set--internal'); if (!actionBar) { setTimeout(addButton, 500); return; } // Creating check link button const checkDeadLinkButton = document.createElement('a'); checkDeadLinkButton.innerHTML = ` Check Links`; checkDeadLinkButton.classList.add('actionBar-action'); checkDeadLinkButton.style.cursor = 'pointer'; checkDeadLinkButton.addEventListener('click', checkLinks); actionBar.appendChild(checkDeadLinkButton); // Creating generate links for multi button const generateMultiButton = document.createElement('a'); generateMultiButton.innerHTML = ` Generate Links for Multi`; generateMultiButton.classList.add('actionBar-action'); generateMultiButton.style.cursor = 'pointer'; generateMultiButton.style.marginLeft = '10px'; generateMultiButton.addEventListener('click', generateMultiLinks); actionBar.appendChild(generateMultiButton); })(); function generateMultiLinks(event) { // Disable button for 5 second to prevent missclick if (!button2Active) return; button2Active = false; event.target.parentElement.style.color = 'grey'; const workingIcon = addStatus(event.target.parentElement.parentElement); workingIcon.style.color = '#ec5555'; const pageURL = document.URL; const match = pageURL.match(/\.(\d+)(\/|$)/); if (match && match[1]) { makeGMRequest({ method: 'GET', url: 'https://f95zone.to/sam/donor_ddl.php?id=' + match[1], timeout: 20000, }) .then((response) => { if (response.status === 200) { parseDLLPage(response.responseText); } else { console.log('Unable to parse DonorDLL'); updateStatus(workingIcon, 'Error'); setTimeout(() => { workingIcon.remove(); }, 4000); } }) .catch((error) => { updateStatus(workingIcon, error); setTimeout(() => { workingIcon.remove(); }, 4000); }); } setTimeout(() => { button2Active = true; event.target.parentElement.style.color = 'unset'; }, 5000); function parseDLLPage(page) { if (!page) return; // Parse the response page and grab file links const parsedPage = new DOMParser().parseFromString( page, 'text/html' ); const links = parsedPage.querySelectorAll('a.file-select'); const textNodes = getAllTextNodes(OP.lastChild); links.forEach((link) => { let linkText; if ( !link.previousElementSibling && link.parentElement.previousElementSibling ) { linkText = link.parentElement.previousElementSibling.textContent.trim(); } else { linkText = link.previousElementSibling.textContent.trim(); } for (let i = 0; i < textNodes.length; i++) { if ( textNodes[i].previousElementSibling && textNodes[i].previousElementSibling.title == 'Multi Link' ) { continue; } const elementText = textNodes[i].textContent.trim(); if ( linkText === elementText || linkText + ':' === elementText ) { addMultiLink(textNodes[i], link.dataset.filename); return; } } }); if (links.length == 0) { updateStatus(workingIcon, 'Not Supported'); } else { updateStatus(workingIcon, 'Available'); } setTimeout(() => { workingIcon.remove(); }, 2000); } // Create a go to multi button and add it behind link's main label function addMultiLink(element, filename) { const gotoMultiButton = document.createElement('a'); gotoMultiButton.innerHTML = ``; gotoMultiButton.target = '_blank'; gotoMultiButton.title = 'Multi Link'; gotoMultiButton.href = 'https://upload.multizone.pw/#search=' + filename; element.parentElement.insertBefore(gotoMultiButton, element); } // Grab all text nodes from download section of OP. Fuck html parsing. function getAllTextNodes(container) { let allNodes = []; function traverse(node) { if ( node.nodeName == '#text' && node.parentNode.tagName !== 'A' && !['-', ':', ''].includes(node.textContent.trim()) ) { allNodes.push(node); } for (let i = 0; i < node.childNodes.length; i++) { let childNode = node.childNodes[i]; traverse(childNode); } } traverse(container); return allNodes; } } // Check each download link function checkLinks(event) { if (!button1Active) return; button1Active = false; event.target.style.color = 'grey'; // Selecting all links inside the OP const downloadLinks = OP.lastChild.querySelectorAll('a.link'); downloadLinks.forEach((linkElement, index) => { const domainName = linkElement.textContent.trim(); if (!isFileHost(domainName)) return; const statusIcon = addStatus(linkElement); setTimeout(() => { const requestLink = linkElement.href; switch (domainName) { case 'PIXELDRAIN': processPIXELDRAIN(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'MEGA': processMEGA(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'GOFILE': processGOFILE(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'FILESFM': processFILESFM(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'MEDIAFIRE': processMEDIAFIRE(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'WORKUPLOAD': processWORKUPLOAD(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'DELAFIL': processDELAFIL(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'MIXDROP': processMIXDROP(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'WETRANSFER': processWETRANSFER(requestLink).then((response) => updateStatus(statusIcon, response) ); break; case 'UPLOADHAVEN': setTimeout(() => { processUPLOADHAVEN(requestLink).then((response) => updateStatus(statusIcon, response) ); }, index * 100); break; case 'ANONFILE': case 'NOPY': case 'ZIPPYSHARE': case 'RACATY': updateStatus(statusIcon, 'Not Supported'); break; default: makeGMRequest({ url: requestLink, }) .then((response) => { response.status === 200 ? updateStatus(statusIcon, 'Available') : updateStatus(statusIcon, 'Not Available'); }) .catch((error) => updateStatus(statusIcon, error)); } }, index * 50); }); setTimeout(() => { button1Active = true; event.target.style.color = 'unset'; }, 5000); } // adds status icons to be displayed next to links function addStatus(link) { const statusElement = document.createElement('i'); statusElement.style = 'padding-left: 7px; padding-right: 5px; scale:1.3;'; statusElement.style.color = 'grey'; statusElement.style.cursor = 'default'; statusElement.classList = 'fad fa-spinner fa-pulse'; link.append(statusElement); return statusElement; } // Update the status icon function updateStatus(icon, status) { switch (status) { case 'Available': setIconProperties(icon, 'fad fa-check', 'green', 'Live'); break; case 'Not Available': setIconProperties(icon, 'fad fa-unlink', 'red', 'Dead Link'); break; case 'Error': setIconProperties( icon, 'fal fa-exclamation-triangle', 'orangered', 'Error', 1.2 ); break; case 'Timeout': setIconProperties(icon, 'fad fa-clock', 'orange', 'Timeout'); break; case 'Not Supported': default: setIconProperties( icon, 'fad fa-question', 'grey', 'Unsupported' ); break; } function setIconProperties(icon, iconClass, color, title, scale = 1.3) { icon.classList = iconClass; icon.style.color = color; icon.style.scale = scale; icon.title = title; } } // Check if the link is a file host function isFileHost(link) { if (ApprovedFileHost.includes(link)) return true; return false; } // Make a GM_xmlRequest function makeGMRequest(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'HEAD', timeout: 15000, ...options, onload: (res) => resolve(res), ontimeout: () => reject('Timeout'), onerror: (error) => { console.log('Error:', error); reject('Error'); }, }); }); } // Process host links idividually that need more work function processPIXELDRAIN(link) { // Cature the file ID for both file and folder links. const matchU = link.match(/\/u\/([^\/]+)$/); const matchL = link.match(/\/l\/([^\/]+)$/); var newLink; if (matchU && matchU[1]) { newLink = 'https://pixeldrain.com/api/file/' + matchU[1]; } else if (matchL && matchL[1]) { newLink = 'https://pixeldrain.com/api/list/' + matchL[1]; } else { return new Promise.reject('Error'); } const options = { url: newLink, }; return makeGMRequest(options) .then((res) => (res.status === 200 ? 'Available' : 'Not Available')) .catch((error) => error); } function processMEGA(link) { // MEGA typically has 3 types of url, capture ID from each case. const captureId = link.match(/(?:file\/|#\!|folder\/)([^\/#!]+)/); const id = captureId ? captureId[1] : null; if (!id) return 'Error'; const options = { method: 'POST', url: 'https://g.api.mega.co.nz/cs?id=0&n=' + id, data: 'a=g&ad=1&p=' + id, headers: { 'Content-Type': 'application/json', }, }; return makeGMRequest(options) .then((resolve) => resolve.responseText === '-2' ? 'Available' : 'Not Available' ) .catch((error) => error); } function processGOFILE(link) { const options = { method: 'GET', url: link, headers: { 'Content-Type': 'text/html', 'User-Agent': 'curl', }, }; return makeGMRequest(options) .then((resolve) => { const parsedPage = new DOMParser().parseFromString( resolve.responseText, 'text/html' ); return parsedPage.title == 'Gofile - Free Unlimited File Sharing and Storage' ? 'Not Available' : 'Available'; }) .catch((error) => error); } function processFILESFM(link) { const options = { method: 'GET', url: link, headers: { 'Content-Type': 'text/html', 'User-Agent': 'curl', }, }; return makeGMRequest(options) .then((resolve) => { const parsedPage = new DOMParser().parseFromString( resolve.responseText, 'text/html' ); return parsedPage.title == 'File upload & sharing. Send large photos and videos. Online cloud storage.' ? 'Not Available' : 'Available'; }) .catch((error) => error); } function processMEDIAFIRE(link) { return makeGMRequest({ url: link, }) .then((response) => { if (response.status === 404) { return 'Not Available'; } else if (response.status === 200) { return response.finalUrl.match('error.php') ? 'Not Available' : 'Available'; } }) .catch((error) => error); } function processWORKUPLOAD(link) { const id = link.match(/\/file\/([^\/#]+)/); var apiLink; if (id && id[1]) apiLink = 'https://workupload.com/api/file/getDownloadServer/' + id[1]; else return 'Error'; const options = { method: 'GET', url: apiLink, headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*', Cookie: 'token=tjuqjuuobv97ma180qn6vg92lu', 'User-Agent': 'curl', }, }; return makeGMRequest(options) .then((resolve) => { try { return JSON.parse(resolve.response).success ? 'Available' : 'Not Available'; } catch (error) { const parsedPage = new DOMParser().parseFromString( resolve.responseText, 'text/html' ); return parsedPage.title == 'workupload - Are you a human?' ? 'Error' : 'Available'; } }) .catch((error) => error); } function processDELAFIL(link) { return makeGMRequest({ url: link, }) .then((response) => { if (response.status === 200) { return response.finalUrl.match('error.html') ? 'Not Available' : 'Available'; } else { return response.status === 404 ? 'Not Available' : 'Error'; } }) .catch((error) => error); } function processMIXDROP(link) { const id = link.match(/\/f\/([^\/#]+)/); if (!id[1]) return Promise.resolve('Error'); const apiLink = `https://api.mixdrop.ag/fileinfo2?email=${apiMIXDROP.email}&key=${apiMIXDROP.key}&ref[]=${id[1]}`; return makeGMRequest({ method: 'GET', url: apiLink, }) .then((response) => { try { return JSON.parse(response.response).result[id[1]].status == 'OK' ? 'Available' : 'Not Available'; } catch (error) { return 'Error'; } }) .catch((error) => error); } function processUPLOADHAVEN(link) { return makeGMRequest({ url: link, }) .then((response) => response.status === 200 ? 'Available' : 'Not Available' ) .catch((error) => error); } function processWETRANSFER(link) { return makeGMRequest({ method: 'GET', url: link, }) .then((response) => { try { const page = new DOMParser().parseFromString( response.responseText, 'text/html' ); const data = JSON.parse( page.querySelector('#__NEXT_DATA__').textContent ); return data.props.pageProps.metadata === null ? 'Not Available' : 'Available'; } catch (error) { return 'Error'; } }) .catch((error) => error); } })();