Last active
December 20, 2025 15:08
-
-
Save bulletinmybeard/b96122b2ea96229d12416c21dcd8be1c to your computer and use it in GitHub Desktop.
Userscript - Link Checker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Download Link Checker | |
| // @namespace https://rschu.me/ | |
| // @version 1.0.0 | |
| // @description Check the status of download links on any page | |
| // @author bulletinmybeard | |
| // @match *://example.com/* | |
| // @grant GM_xmlhttpRequest | |
| // @grant GM_addStyle | |
| // @connect filehost1.com | |
| // @connect filehost2.com | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // Configure the download link domains to check | |
| const TARGET_HOSTS = ['filehost1.com', 'filehost2.com']; | |
| const BATCH_SIZE = 5; | |
| const TIMEOUT = 10000; | |
| // Inject styles using GM_addStyle | |
| GM_addStyle(` | |
| #link-checker-btn { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 99999; | |
| padding: 10px 20px; | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: bold; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.3); | |
| transition: background-color 0.3s, transform 0.1s; | |
| } | |
| #link-checker-btn:hover { | |
| background-color: #45a049; | |
| } | |
| #link-checker-btn:active { | |
| transform: scale(0.95); | |
| } | |
| #link-checker-btn:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| #link-checker-btn.checking { | |
| background-color: #ff9800; | |
| } | |
| #link-checker-modal { | |
| display: none; | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background-color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| z-index: 100000; | |
| max-width: 80%; | |
| max-height: 80%; | |
| overflow-y: auto; | |
| min-width: 400px; | |
| } | |
| #link-checker-modal::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| z-index: -1; | |
| } | |
| #link-checker-modal .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| } | |
| #link-checker-modal h2 { | |
| margin: 0; | |
| color: #333; | |
| } | |
| #link-checker-modal .close-btn { | |
| background: none; | |
| border: none; | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: #999; | |
| } | |
| #results-content { | |
| min-height: 100px; | |
| } | |
| #results-content::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #results-content::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| border-radius: 4px; | |
| } | |
| #results-content::-webkit-scrollbar-thumb { | |
| background: #888; | |
| border-radius: 4px; | |
| } | |
| #results-content::-webkit-scrollbar-thumb:hover { | |
| background: #555; | |
| } | |
| .result-item { | |
| margin-bottom: 10px; | |
| padding: 10px; | |
| border-radius: 5px; | |
| word-break: break-all; | |
| } | |
| .result-item.accessible { | |
| background-color: #f0f8f0; | |
| border-left: 4px solid #4CAF50; | |
| } | |
| .result-item.inaccessible { | |
| background-color: #fff0f0; | |
| border-left: 4px solid #f44336; | |
| } | |
| .result-item a.accessible { | |
| color: #0066cc; | |
| } | |
| .result-item a.inaccessible { | |
| color: #cc0000; | |
| } | |
| .section-title { | |
| margin: 15px 0 10px; | |
| } | |
| .section-title.ok { | |
| color: green; | |
| } | |
| .section-title.fail { | |
| color: red; | |
| } | |
| .summary { | |
| margin-bottom: 20px; | |
| } | |
| .summary .ok { | |
| color: green; | |
| } | |
| .summary .fail { | |
| color: red; | |
| } | |
| .toast { | |
| position: fixed; | |
| top: 70px; | |
| right: 20px; | |
| z-index: 99999; | |
| padding: 12px 20px; | |
| background-color: #333; | |
| color: white; | |
| border-radius: 5px; | |
| font-size: 14px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| @media (max-width: 600px) { | |
| #link-checker-modal { | |
| max-width: 95%; | |
| min-width: 300px; | |
| } | |
| #link-checker-btn { | |
| padding: 8px 16px; | |
| font-size: 12px; | |
| top: 10px; | |
| right: 10px; | |
| } | |
| } | |
| `); | |
| let checkButton; | |
| let resultsModal; | |
| let isChecking = false; | |
| function createCheckButton() { | |
| checkButton = document.createElement('button'); | |
| checkButton.id = 'link-checker-btn'; | |
| checkButton.textContent = 'Check Links'; | |
| checkButton.addEventListener('click', performLinkCheck); | |
| document.body.appendChild(checkButton); | |
| } | |
| function createResultsModal() { | |
| resultsModal = document.createElement('div'); | |
| resultsModal.id = 'link-checker-modal'; | |
| resultsModal.innerHTML = ` | |
| <div class="header"> | |
| <h2>Link Check Results</h2> | |
| <button class="close-btn">×</button> | |
| </div> | |
| <div id="results-content"> | |
| <p>Checking links...</p> | |
| </div> | |
| `; | |
| document.body.appendChild(resultsModal); | |
| resultsModal.querySelector('.close-btn').addEventListener('click', () => { | |
| resultsModal.style.display = 'none'; | |
| }); | |
| window.addEventListener('click', (e) => { | |
| if (e.target === resultsModal) { | |
| resultsModal.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| function extractLinks() { | |
| const links = new Set(); | |
| const linkElements = document.querySelectorAll('a[href]'); | |
| linkElements.forEach(element => { | |
| try { | |
| const url = new URL(element.href); | |
| const hostname = url.hostname.toLowerCase().replace('www.', ''); | |
| if (TARGET_HOSTS.some(host => hostname.includes(host))) { | |
| links.add(element.href); | |
| } | |
| } catch (e) { | |
| // Invalid URL, skip | |
| } | |
| }); | |
| // Also check for URLs in text content | |
| const hostPattern = TARGET_HOSTS.map(h => h.replace(/\./g, '\\.')).join('|'); | |
| const textPattern = new RegExp(`https?://(?:www\\.)?(${hostPattern})[^\\s<>"']*`, 'gi'); | |
| const walker = document.createTreeWalker( | |
| document.body, | |
| NodeFilter.SHOW_TEXT, | |
| null, | |
| false | |
| ); | |
| let node; | |
| while (node = walker.nextNode()) { | |
| const matches = node.textContent.match(textPattern); | |
| if (matches) { | |
| matches.forEach(url => links.add(url)); | |
| } | |
| } | |
| return Array.from(links); | |
| } | |
| function checkLinkStatus(url) { | |
| return new Promise((resolve) => { | |
| GM_xmlhttpRequest({ | |
| method: 'HEAD', | |
| url: url, | |
| timeout: TIMEOUT, | |
| onload: function(response) { | |
| resolve({ | |
| url: url, | |
| status: response.status, | |
| statusText: response.statusText, | |
| finalUrl: response.finalUrl || url, | |
| redirected: response.finalUrl && response.finalUrl !== url, | |
| accessible: response.status >= 200 && response.status < 400 | |
| }); | |
| }, | |
| onerror: function() { | |
| resolve({ | |
| url: url, | |
| status: 0, | |
| statusText: 'Network Error', | |
| finalUrl: url, | |
| redirected: false, | |
| accessible: false | |
| }); | |
| }, | |
| ontimeout: function() { | |
| resolve({ | |
| url: url, | |
| status: 0, | |
| statusText: 'Timeout', | |
| finalUrl: url, | |
| redirected: false, | |
| accessible: false | |
| }); | |
| } | |
| }); | |
| }); | |
| } | |
| async function performLinkCheck() { | |
| if (isChecking) return; | |
| isChecking = true; | |
| checkButton.textContent = 'Checking...'; | |
| checkButton.classList.add('checking'); | |
| const links = extractLinks(); | |
| if (links.length === 0) { | |
| showToast(`No ${TARGET_HOSTS.join(' or ')} links found on this page.`); | |
| resetButton(); | |
| return; | |
| } | |
| resultsModal.style.display = 'block'; | |
| const resultsContent = document.getElementById('results-content'); | |
| resultsContent.innerHTML = `<p>Checking ${links.length} links...</p>`; | |
| const results = []; | |
| for (let i = 0; i < links.length; i += BATCH_SIZE) { | |
| const batch = links.slice(i, i + BATCH_SIZE); | |
| const batchResults = await Promise.all(batch.map(checkLinkStatus)); | |
| results.push(...batchResults); | |
| resultsContent.innerHTML = `<p>Checked ${Math.min(i + BATCH_SIZE, links.length)} of ${links.length} links...</p>`; | |
| } | |
| showResults(results); | |
| resetButton(); | |
| } | |
| function showResults(results) { | |
| const resultsContent = document.getElementById('results-content'); | |
| if (results.length === 1 && results[0].message) { | |
| resultsContent.innerHTML = `<p>${results[0].message}</p>`; | |
| return; | |
| } | |
| const accessible = results.filter(r => r.accessible); | |
| const inaccessible = results.filter(r => !r.accessible); | |
| let html = ` | |
| <div class="summary"> | |
| <p><strong>Total links checked:</strong> ${results.length}</p> | |
| <p><strong>Accessible:</strong> <span class="ok">${accessible.length}</span></p> | |
| <p><strong>Inaccessible:</strong> <span class="fail">${inaccessible.length}</span></p> | |
| </div> | |
| `; | |
| if (accessible.length > 0) { | |
| html += '<h3 class="section-title ok">[OK] Accessible Links</h3>'; | |
| html += '<div>'; | |
| accessible.forEach(result => { | |
| html += ` | |
| <div class="result-item accessible"> | |
| <div><strong>URL:</strong> <a href="${result.url}" target="_blank" class="accessible">${result.url}</a></div> | |
| <div><strong>Status:</strong> ${result.status} ${result.statusText}</div> | |
| ${result.redirected ? `<div><strong>Redirected to:</strong> ${result.finalUrl}</div>` : ''} | |
| </div> | |
| `; | |
| }); | |
| html += '</div>'; | |
| } | |
| if (inaccessible.length > 0) { | |
| html += '<h3 class="section-title fail">[FAIL] Inaccessible Links</h3>'; | |
| html += '<div>'; | |
| inaccessible.forEach(result => { | |
| html += ` | |
| <div class="result-item inaccessible"> | |
| <div><strong>URL:</strong> <a href="${result.url}" target="_blank" class="inaccessible">${result.url}</a></div> | |
| <div><strong>Status:</strong> ${result.status} ${result.statusText}</div> | |
| </div> | |
| `; | |
| }); | |
| html += '</div>'; | |
| } | |
| resultsContent.innerHTML = html; | |
| } | |
| function showToast(message) { | |
| const toast = document.createElement('div'); | |
| toast.className = 'toast'; | |
| toast.textContent = message; | |
| document.body.appendChild(toast); | |
| requestAnimationFrame(() => { | |
| toast.style.opacity = '1'; | |
| }); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| function resetButton() { | |
| isChecking = false; | |
| checkButton.textContent = 'Check Links'; | |
| checkButton.classList.remove('checking'); | |
| } | |
| // Initialize when DOM is ready | |
| if (document.body) { | |
| createCheckButton(); | |
| createResultsModal(); | |
| } else { | |
| document.addEventListener('DOMContentLoaded', () => { | |
| createCheckButton(); | |
| createResultsModal(); | |
| }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment