Guest User

Untitled

a guest
Jul 8th, 2025
37
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. javascript:(function() {
  2.  
  3.   const CODE_NAME = "LinkArchiver";
  4.   console.log(`${CODE_NAME} Session started at ${new Date().toISOString()}`);
  5.  
  6.  
  7.   const style = document.createElement('style');
  8.   style.innerHTML = `
  9.     #bookmarklet-ui {
  10.       position: fixed;
  11.       top: 50%;
  12.       left: 50%;
  13.       transform: translate(-50%, -50%);
  14.       background: rgba(255, 255, 255, 0.95);
  15.       padding: 20px;
  16.       border: 1px solid #333;
  17.       border-radius: 8px;
  18.       box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  19.       z-index: 9999;
  20.       font-family: Arial, sans-serif;
  21.       max-width: 600px;
  22.       width: 90%;
  23.     }
  24.     #bookmarklet-ui h3 {
  25.       margin: 0 0 10px;
  26.       color: #333;
  27.     }
  28.     #bookmarklet-ui label {
  29.       display: block;
  30.       margin-bottom: 5px;
  31.       color: #333;
  32.     }
  33.     #pattern, #pasted-links, .replace-rule input {
  34.       width: 100%;
  35.       padding: 8px;
  36.       margin-bottom: 10px;
  37.       border: 1px solid #ccc;
  38.       border-radius: 4px;
  39.       box-sizing: border-box;
  40.     }
  41.     #pasted-links {
  42.       resize: vertical;
  43.       min-height: 100px;
  44.       max-height: 300px;
  45.     }
  46.     #suggestions {
  47.       max-height: 200px;
  48.       overflow-y: auto;
  49.       margin-bottom: 10px;
  50.       border: 1px solid #ccc;
  51.       border-radius: 4px;
  52.       padding: 5px;
  53.       background: #f9f9f9;
  54.     }
  55.     #suggestions div {
  56.       padding: 5px;
  57.       cursor: pointer;
  58.       color: #333;
  59.     }
  60.     #suggestions div:hover {
  61.       background: #e0e0e0;
  62.     }
  63.     #start, #close, #pause-resume, #add-rule {
  64.       padding: 8px 16px;
  65.       margin-right: 10px;
  66.       border: none;
  67.       border-radius: 4px;
  68.       cursor: pointer;
  69.     }
  70.     #start {
  71.       background: #007bff;
  72.       color: white;
  73.     }
  74.     #start:hover {
  75.       background: #0056b3;
  76.     }
  77.     #close {
  78.       background: #dc3545;
  79.       color: white;
  80.     }
  81.     #close:hover {
  82.       background: #c82333;
  83.     }
  84.     #pause-resume {
  85.       background: #ffc107;
  86.       color: #333;
  87.     }
  88.     #pause-resume:hover {
  89.       background: #e0a800;
  90.     }
  91.     #add-rule {
  92.       background: #28a745;
  93.       color: white;
  94.     }
  95.     #add-rule:hover {
  96.       background: #218838;
  97.     }
  98.     #toast {
  99.       position: fixed;
  100.       bottom: 20px;
  101.       right: 20px;
  102.       background: rgba(51, 51, 51, 0.9);
  103.       color: white;
  104.       padding: 10px 20px;
  105.       border-radius: 5px;
  106.       display: none;
  107.       z-index: 10000;
  108.       font-family: Arial, sans-serif;
  109.     }
  110.     #mode-toggle {
  111.       margin-bottom: 10px;
  112.     }
  113.     #rate-limit-warning {
  114.       color: #d33;
  115.       font-size: 0.9em;
  116.       margin-top: 10px;
  117.     }
  118.     #replace-rules {
  119.       margin-bottom: 10px;
  120.     }
  121.     .replace-rule {
  122.       display: flex;
  123.       gap: 10px;
  124.       margin-bottom: 10px;
  125.     }
  126.     .replace-rule select {
  127.       padding: 8px;
  128.       border: 1px solid #ccc;
  129.       border-radius: 4px;
  130.     }
  131.     @media (prefers-color-scheme: dark) {
  132.       #bookmarklet-ui {
  133.         background: rgba(30, 30, 30, 0.95);
  134.         border-color: #666;
  135.       }
  136.       #bookmarklet-ui h3, #bookmarklet-ui label {
  137.         color: #ddd;
  138.       }
  139.       #pattern, #pasted-links, .replace-rule input, .replace-rule select {
  140.         background: #333;
  141.         color: #ddd;
  142.         border-color: #666;
  143.       }
  144.       #suggestions {
  145.         background: #2a2a2a;
  146.         border-color: #666;
  147.       }
  148.       #suggestions div {
  149.         color: #ddd;
  150.       }
  151.       #suggestions div:hover {
  152.         background: #444;
  153.       }
  154.     }
  155.   `;
  156.   try {
  157.     document.head.appendChild(style);
  158.   } catch (e) {
  159.     console.error(`${CODE_NAME}: Failed to inject styles: ${e.message}`);
  160.     return;
  161.   }
  162.  
  163.  
  164.   const existingUi = document.getElementById('bookmarklet-ui');
  165.   if (existingUi) {
  166.     existingUi.remove();
  167.   }
  168.  
  169.  
  170.   const ui = document.createElement('div');
  171.   ui.id = 'bookmarklet-ui';
  172.   ui.innerHTML = `
  173.     <h3>${CODE_NAME}</h3>
  174.     <div id="mode-toggle">
  175.       <label><input type="radio" name="mode" value="scrape" checked> Scrape from current page</label>
  176.       <label><input type="radio" name="mode" value="paste"> Use pasted links</label>
  177.     </div>
  178.     <div id="pasted-links-container" style="display: none;">
  179.       <label for="pasted-links">Paste your links (one per line):</label>
  180.       <textarea id="pasted-links" placeholder="https://example.com\nhttps://another.com"></textarea>
  181.     </div>
  182.     <label for="pattern">Regex Pattern (optional):</label>
  183.     <input type="text" id="pattern" placeholder="e.g., .*example\\.com.*">
  184.     <div id="replace-rules">
  185.       <label>Replace Rules (optional):</label>
  186.       <div class="replace-rule">
  187.         <select class="replace-type">
  188.           <option value="domain">Domain</option>
  189.           <option value="path">Path</option>
  190.         </select>
  191.         <input type="text" class="replace-what" placeholder="e.g., olddomain.com or /path">
  192.         <input type="text" class="replace-with" placeholder="e.g., newdomain.com or /newpath">
  193.       </div>
  194.     </div>
  195.     <button id="add-rule">+ Add Rule</button>
  196.     <div id="suggestions"></div>
  197.     <div>
  198.       <button id="start">Start Archiving</button>
  199.       <button id="pause-resume">Pause</button>
  200.       <button id="close">Close</button>
  201.     </div>
  202.     <div id="rate-limit-warning" style="display: none;">
  203.       Note: The Wayback Machine allows up to 100 saves per day for anonymous users. For more than 100 links, run this multiple times on different days.
  204.     </div>
  205.   `;
  206.   try {
  207.     document.body.appendChild(ui);
  208.   } catch (e) {
  209.     console.error(`${CODE_NAME}: Failed to inject UI: ${e.message}`);
  210.     return;
  211.   }
  212.  
  213.  
  214.   setTimeout(() => {
  215.     const patternInput = document.getElementById('pattern');
  216.     if (patternInput) {
  217.       patternInput.focus();
  218.     } else {
  219.       console.error(`${CODE_NAME}: Cannot find #pattern input. UI may not have rendered correctly.`);
  220.     }
  221.   }, 100);
  222.  
  223.  
  224.   const toast = document.createElement('div');
  225.   toast.id = 'toast';
  226.   try {
  227.     document.body.appendChild(toast);
  228.   } catch (e) {
  229.     console.error(`${CODE_NAME}: Failed to inject toast: ${e.message}`);
  230.     return;
  231.   }
  232.  
  233.  
  234.   function collectMediaLinks() {
  235.     const links = new Set();
  236.    
  237.    
  238.     ['a', 'img', 'video'].forEach(tag => {
  239.       Array.from(document.getElementsByTagName(tag)).forEach(el => {
  240.         const src = tag === 'a' ? el.href :
  241.                    (tag === 'video' ? (el.src || (el.querySelector('source')?.src)) : el.src);
  242.         if (src && src.includes('://')) {
  243.           try {
  244.             const url = new URL(src);
  245.             if (['http:', 'https:'].includes(url.protocol)) {
  246.               links.add(src);
  247.             }
  248.           } catch (e) {
  249.             console.warn(`${CODE_NAME}: Invalid URL skipped: ${src}`);
  250.           }
  251.         }
  252.       });
  253.     });
  254.    
  255.     return Array.from(links);
  256.   }
  257.  
  258.  
  259.   let isPaused = false;
  260.   let archivingPromise = null;
  261.  
  262.  
  263.   const scrapeRadio = document.querySelector('input[value="scrape"]');
  264.   const pasteRadio = document.querySelector('input[value="paste"]');
  265.   const pastedLinksContainer = document.getElementById('pasted-links-container');
  266.   const patternInput = document.getElementById('pattern');
  267.   const suggestionsDiv = document.getElementById('suggestions');
  268.   const rateLimitWarning = document.getElementById('rate-limit-warning');
  269.   const addRuleButton = document.getElementById('add-rule');
  270.   const pauseResumeButton = document.getElementById('pause-resume');
  271.   const closeButton = document.getElementById('close');
  272.  
  273.   if (!scrapeRadio || !pasteRadio || !pastedLinksContainer || !patternInput || !suggestionsDiv || !rateLimitWarning || !addRuleButton || !pauseResumeButton || !closeButton) {
  274.     console.error(`${CODE_NAME}: One or more UI elements missing. Check webpage for CSP or DOM restrictions.`);
  275.     return;
  276.   }
  277.  
  278.   function toggleMode() {
  279.     pastedLinksContainer.style.display = pasteRadio.checked ? 'block' : 'none';
  280.     generateSuggestions();
  281.   }
  282.  
  283.   scrapeRadio.addEventListener('change', toggleMode);
  284.   pasteRadio.addEventListener('change', toggleMode);
  285.  
  286.  
  287.   addRuleButton.addEventListener('click', () => {
  288.     const ruleDiv = document.createElement('div');
  289.     ruleDiv.className = 'replace-rule';
  290.     ruleDiv.innerHTML = `
  291.       <select class="replace-type">
  292.         <option value="domain">Domain</option>
  293.         <option value="path">Path</option>
  294.       </select>
  295.       <input type="text" class="replace-what" placeholder="e.g., olddomain.com or /path">
  296.       <input type="text" class="replace-with" placeholder="e.g., newdomain.com or /newpath">
  297.     `;
  298.     document.getElementById('replace-rules').appendChild(ruleDiv);
  299.   });
  300.  
  301.  
  302.   function isValidReplaceValue(type, value) {
  303.     if (type === 'domain') {
  304.       return /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
  305.     }
  306.     return value.length > 0;
  307.   }
  308.  
  309.  
  310.   function generateSuggestions() {
  311.     suggestionsDiv.innerHTML = '';
  312.     const links = pasteRadio.checked ? getPastedLinks() : collectMediaLinks();
  313.    
  314.    
  315.     const fullUrlsTitle = document.createElement('div');
  316.     fullUrlsTitle.textContent = 'Full URLs:';
  317.     fullUrlsTitle.style.fontWeight = 'bold';
  318.     suggestionsDiv.appendChild(fullUrlsTitle);
  319.    
  320.     links.slice(0, 10).forEach(url => {
  321.       const urlItem = document.createElement('div');
  322.       urlItem.textContent = url;
  323.       urlItem.style.cursor = 'pointer';
  324.       urlItem.onclick = () => {
  325.         patternInput.value = url;
  326.         patternInput.dispatchEvent(new Event('input', { bubbles: true }));
  327.       };
  328.       suggestionsDiv.appendChild(urlItem);
  329.     });
  330.  
  331.    
  332.     const patternsTitle = document.createElement('div');
  333.     patternsTitle.textContent = 'Regex Patterns:';
  334.     patternsTitle.style.fontWeight = 'bold';
  335.     patternsTitle.style.marginTop = '10px';
  336.     suggestionsDiv.appendChild(patternsTitle);
  337.  
  338.     try {
  339.       const domains = [...new Set(links.map(url => {
  340.         try { return new URL(url).hostname; }
  341.         catch (e) { return null; }
  342.       }).filter(Boolean))];
  343.      
  344.       domains.forEach(domain => {
  345.        
  346.         const domainPattern = document.createElement('div');
  347.         domainPattern.textContent = `^https?://(www\\.)?${domain.replace('.', '\\.')}(/.*)?`;
  348.         domainPattern.style.cursor = 'pointer';
  349.         domainPattern.onclick = () => {
  350.           patternInput.value = domainPattern.textContent;
  351.           patternInput.dispatchEvent(new Event('input', { bubbles: true }));
  352.         };
  353.         suggestionsDiv.appendChild(domainPattern);
  354.  
  355.        
  356.         const pathPatterns = new Set();
  357.         links.filter(url => url.includes(domain)).forEach(url => {
  358.           try {
  359.             const parsed = new URL(url);
  360.             if (parsed.pathname !== '/') {
  361.               const pathSegments = parsed.pathname.split('/').filter(Boolean);
  362.               let pathPattern = '';
  363.              
  364.              
  365.               pathSegments.forEach((segment, i) => {
  366.                 if (i > 0) pathPattern += '\\/';
  367.                 pathPattern += segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  368.                 const fullPattern = `^https?://(www\\.)?${domain.replace('.', '\\.')}(\\/${pathPattern}(/.*)?)`;
  369.                 pathPatterns.add(fullPattern);
  370.               });
  371.             }
  372.           } catch (e) { /* skip invalid */ }
  373.         });
  374.  
  375.        
  376.         pathPatterns.forEach(pattern => {
  377.           const patternItem = document.createElement('div');
  378.           patternItem.textContent = pattern;
  379.           patternItem.style.cursor = 'pointer';
  380.           patternItem.style.marginLeft = '15px';
  381.           patternItem.onclick = () => {
  382.             patternInput.value = pattern;
  383.             patternInput.dispatchEvent(new Event('input', { bubbles: true }));
  384.           };
  385.           suggestionsDiv.appendChild(patternItem);
  386.         });
  387.       });
  388.     } catch (e) {
  389.       console.error(`${CODE_NAME}: Error generating suggestions: ${e.message}`);
  390.       showToast('Error generating suggestions');
  391.     }
  392.   }
  393.  
  394.   generateSuggestions();
  395.  
  396.  
  397.   function getPastedLinks() {
  398.     const pastedText = document.getElementById('pasted-links').value;
  399.     return pastedText.split('\n').map(url => url.trim()).filter(url => url && url.includes('://'));
  400.   }
  401.  
  402.  
  403.   patternInput.addEventListener('input', () => {
  404.     const pattern = patternInput.value;
  405.     suggestionsDiv.innerHTML = '';
  406.     const links = pasteRadio.checked ? getPastedLinks() : collectMediaLinks();
  407.    
  408.     if (pattern) {
  409.       try {
  410.         const regex = new RegExp(pattern, 'i');
  411.         const matchingLinks = links.filter(url => regex.test(url));
  412.        
  413.        
  414.         const fullUrlsTitle = document.createElement('div');
  415.         fullUrlsTitle.textContent = 'Matching Full URLs:';
  416.         fullUrlsTitle.style.fontWeight = 'bold';
  417.         suggestionsDiv.appendChild(fullUrlsTitle);
  418.        
  419.         matchingLinks.slice(0, 20).forEach(url => {
  420.           const match = document.createElement('div');
  421.           match.textContent = url;
  422.           match.style.cursor = 'pointer';
  423.           match.onclick = () => {
  424.             patternInput.value = url;
  425.             patternInput.dispatchEvent(new Event('input', { bubbles: true }));
  426.           };
  427.           suggestionsDiv.appendChild(match);
  428.         });
  429.       } catch (e) {
  430.         showToast('Invalid regex pattern');
  431.         console.error(`${CODE_NAME}: Invalid regex pattern: ${e.message}`);
  432.       }
  433.     } else {
  434.       generateSuggestions();
  435.     }
  436.   });
  437.  
  438.  
  439.   function applyReplaceRules(url, rules) {
  440.     let modifiedUrl = url;
  441.     for (const rule of rules) {
  442.       try {
  443.         const parsedUrl = new URL(modifiedUrl);
  444.         if (rule.type === 'domain' && parsedUrl.hostname.toLowerCase() === rule.replaceWhat.toLowerCase()) {
  445.           if (isValidReplaceValue('domain', rule.replaceWith)) {
  446.             parsedUrl.hostname = rule.replaceWith;
  447.             modifiedUrl = parsedUrl.toString();
  448.             console.log(`${CODE_NAME}: Replaced domain ${rule.replaceWhat} with ${rule.replaceWith} for ${url}`);
  449.           } else {
  450.             console.error(`${CODE_NAME}: Invalid replacement domain: ${rule.replaceWith}`);
  451.           }
  452.         } else if (rule.type === 'path' && parsedUrl.pathname.toLowerCase().includes(rule.replaceWhat.toLowerCase())) {
  453.           if (isValidReplaceValue('path', rule.replaceWith)) {
  454.             parsedUrl.pathname = parsedUrl.pathname.replace(new RegExp(rule.replaceWhat.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'), rule.replaceWith);
  455.             modifiedUrl = parsedUrl.toString();
  456.             console.log(`${CODE_NAME}: Replaced path ${rule.replaceWhat} with ${rule.replaceWith} for ${url}`);
  457.           } else {
  458.             console.error(`${CODE_NAME}: Invalid replacement path: ${rule.replaceWith}`);
  459.           }
  460.         }
  461.       } catch (e) {
  462.         console.error(`${CODE_NAME}: Error applying rule to ${url}: ${e.message}`);
  463.       }
  464.     }
  465.     return modifiedUrl;
  466.   }
  467.  
  468.  
  469.   document.getElementById('start').addEventListener('click', () => {
  470.     const pattern = patternInput.value;
  471.     const replaceRules = Array.from(document.querySelectorAll('.replace-rule')).map(ruleDiv => ({
  472.       type: ruleDiv.querySelector('.replace-type').value,
  473.       replaceWhat: ruleDiv.querySelector('.replace-what').value.trim(),
  474.       replaceWith: ruleDiv.querySelector('.replace-with').value.trim()
  475.     })).filter(rule => rule.replaceWhat && rule.replaceWith);
  476.     const links = pasteRadio.checked ? getPastedLinks() : collectMediaLinks();
  477.     let filteredLinks = links;
  478.  
  479.    
  480.     if (pattern) {
  481.       try {
  482.         const regex = new RegExp(pattern, 'i');
  483.         filteredLinks = links.filter(url => regex.test(url));
  484.       } catch (e) {
  485.         showToast('Invalid regex pattern');
  486.         console.error(`${CODE_NAME}: Invalid regex pattern: ${e.message}`);
  487.         return;
  488.       }
  489.     }
  490.  
  491.    
  492.     const linksToArchive = filteredLinks.map(url => applyReplaceRules(url, replaceRules));
  493.  
  494.     if (linksToArchive.length === 0) {
  495.       showToast('No links to archive');
  496.       console.log(`${CODE_NAME}: No links to archive`);
  497.       return;
  498.     }
  499.  
  500.     if (linksToArchive.length > 100) {
  501.       rateLimitWarning.style.display = 'block';
  502.     }
  503.  
  504.     console.log(`${CODE_NAME}: About to archive ${linksToArchive.length} URLs`);
  505.     document.getElementById('bookmarklet-ui').style.display = 'none';
  506.     archivingPromise = archiveLinks(linksToArchive);
  507.   });
  508.  
  509.  
  510.   pauseResumeButton.addEventListener('click', () => {
  511.     isPaused = !isPaused;
  512.     pauseResumeButton.textContent = isPaused ? 'Resume' : 'Pause';
  513.     showToast(isPaused ? 'Archiving paused' : 'Archiving resumed');
  514.     console.log(`${CODE_NAME}: Archiving ${isPaused ? 'paused' : 'resumed'}`);
  515.   });
  516.  
  517.  
  518.   closeButton.addEventListener('click', () => {
  519.     isPaused = true;
  520.     document.getElementById('bookmarklet-ui').remove();
  521.     showToast('Bookmarklet closed');
  522.     console.log(`${CODE_NAME}: Bookmarklet closed`);
  523.   });
  524.  
  525.  
  526.   function showToast(message) {
  527.     const toast = document.getElementById('toast');
  528.     toast.textContent = message;
  529.     toast.style.display = 'block';
  530.     setTimeout(() => {
  531.       toast.style.display = 'none';
  532.     }, 3000);
  533.   }
  534.  
  535.  
  536.   async function archiveLinks(links) {
  537.     let failedLinks = [];
  538.     let consecutiveErrors = 0;
  539.     const maxRetries = 3;
  540.     let retryCount = 0;
  541.  
  542.     async function attemptArchive(url, attempt = 1) {
  543.       if (isPaused) {
  544.         await new Promise(resolve => {
  545.           const checkResume = setInterval(() => {
  546.             if (!isPaused) {
  547.               clearInterval(checkResume);
  548.               resolve();
  549.             }
  550.           }, 100);
  551.         });
  552.       }
  553.       try {
  554.         showToast(`Archiving ${links.indexOf(url) + 1}/${links.length}: ${url}`);
  555.         console.log(`${CODE_NAME}: Archiving ${links.indexOf(url) + 1}/${links.length}: ${url}`);
  556.         await fetch(`https://web.archive.org/save/${encodeURIComponent(url)}`, { mode: 'no-cors' });
  557.         console.log(`${CODE_NAME}: Save attempted for ${url}. Check https://web.archive.org/web/*/${encodeURIComponent(url)} for the snapshot`);
  558.         consecutiveErrors = 0;
  559.         return true;
  560.       } catch (e) {
  561.         console.error(`${CODE_NAME}: Error archiving ${url} (Attempt ${attempt}): ${e.message}`);
  562.         showToast(`Error archiving ${url}`);
  563.         consecutiveErrors++;
  564.         return false;
  565.       }
  566.     }
  567.  
  568.     while (retryCount <= maxRetries) {
  569.       const currentFailed = [];
  570.       for (const url of links) {
  571.         if (isPaused) {
  572.           await new Promise(resolve => {
  573.             const checkResume = setInterval(() => {
  574.               if (!isPaused) {
  575.                 clearInterval(checkResume);
  576.                 resolve();
  577.               }
  578.             }, 100);
  579.           });
  580.         }
  581.         const success = await attemptArchive(url, retryCount + 1);
  582.         if (!success) {
  583.           currentFailed.push(url);
  584.         }
  585.         await new Promise(resolve => setTimeout(resolve, 1000));
  586.       }
  587.       if (currentFailed.length === 0) break;
  588.       if (consecutiveErrors >= 3 && retryCount < maxRetries) {
  589.         showToast(`Multiple errors detected. Waiting 5 minutes before retrying...`);
  590.         console.log(`${CODE_NAME}: Multiple consecutive errors. Waiting 5 minutes before retry ${retryCount + 2}`);
  591.         await new Promise(resolve => setTimeout(resolve, 5 * 60 * 1000));
  592.         consecutiveErrors = 0;
  593.       }
  594.       links = currentFailed;
  595.       failedLinks = currentFailed;
  596.       retryCount++;
  597.     }
  598.  
  599.     if (failedLinks.length > 0) {
  600.       console.log(`${CODE_NAME}: The following links failed to archive after ${maxRetries} retries:`);
  601.       failedLinks.forEach(url => console.log(url));
  602.       showToast(`Archiving completed with ${failedLinks.length} failures. Check console for details.`);
  603.     } else {
  604.       showToast('Archiving completed successfully');
  605.       console.log(`${CODE_NAME}: Archiving completed successfully`);
  606.     }
  607.     document.getElementById('bookmarklet-ui').remove();
  608.   }
  609. })();
Advertisement
Add Comment
Please, Sign In to add comment