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