diff --git a/link-manager.js b/link-manager.js new file mode 100644 index 0000000..d194c4f --- /dev/null +++ b/link-manager.js @@ -0,0 +1,613 @@ +// ==UserScript== +// @name Check dead links and add multi links +// @namespace http://tampermonkey.net/ +// @version v0.6.9 +// @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 +// ==/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); + } +})(); \ No newline at end of file