// ==UserScript== // @name 8chan Spoiler Disabler // @namespace https://greasyfork.org/en/scripts/533173-8chan-spoiler-disabler // @version 1.4.2 // @description Disables image and video spoilers on 8chan (moe/se/cc) for both thread and catalog pages by replacing custom and default spoiler placeholders with thumbnails, including dynamically loaded posts. // @author impregnator // @match https://8chan.moe/* // @match https://8chan.se/* // @match https://8chan.cc/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Function to process images and replace spoiler placeholders with thumbnails function processImages(images, isCatalog = false) { images.forEach(img => { // Check if the image is a spoiler placeholder (custom or default) if (img.src.includes('custom.spoiler') || img.src.includes('spoiler.png')) { let fullFileUrl; if (isCatalog) { // Catalog: Get the href from the parent const link = img.closest('a.linkThumb'); if (link) { // Construct the thumbnail URL based on the thread URL fullFileUrl = link.href; const threadMatch = fullFileUrl.match(/\/([a-z0-9]+)\/res\/([0-9]+)\.html$/i); if (threadMatch && threadMatch[1] && threadMatch[2]) { const board = threadMatch[1]; const threadId = threadMatch[2]; // Fetch the thread page to find the actual image URL fetchThreadImage(board, threadId).then(thumbnailUrl => { if (thumbnailUrl) { img.src = thumbnailUrl; } }); } } } else { // Thread: Get the parent element containing the full-sized file URL const link = img.closest('a.imgLink'); if (link) { // Extract the full-sized file URL fullFileUrl = link.href; // Extract the file hash (everything after /.media/ up to the extension) const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i); if (fileHash && fileHash[1]) { // Construct the thumbnail URL using the current domain const thumbnailUrl = `${window.location.origin}/.media/t_${fileHash[1]}`; // Replace the spoiler image with the thumbnail img.src = thumbnailUrl; } } } } }); } // Function to fetch the thread page and extract the thumbnail URL async function fetchThreadImage(board, threadId) { try { const response = await fetch(`https://${window.location.host}/${board}/res/${threadId}.html`); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); // Find the first image in the thread's OP post const imgLink = doc.querySelector('.uploadCell a.imgLink'); if (imgLink) { const fullFileUrl = imgLink.href; const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i); if (fileHash && fileHash[1]) { return `${window.location.origin}/.media/t_${fileHash[1]}`; } } return null; } catch (error) { console.error('Error fetching thread image:', error); return null; } } // Process existing images on page load const isCatalogPage = window.location.pathname.includes('catalog.html'); if (isCatalogPage) { const initialCatalogImages = document.querySelectorAll('.catalogCell a.linkThumb img'); processImages(initialCatalogImages, true); } else { const initialThreadImages = document.querySelectorAll('.uploadCell img'); processImages(initialThreadImages, false); } // Set up MutationObserver to handle dynamically added posts const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { // Check each added node for new images mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (isCatalogPage) { const newCatalogImages = node.querySelectorAll('.catalogCell a.linkThumb img'); processImages(newCatalogImages, true); } else { const newThreadImages = node.querySelectorAll('.uploadCell img'); processImages(newThreadImages, false); } } }); } }); }); // Observe changes to the document body, including child nodes and subtrees observer.observe(document.body, { childList: true, subtree: true }); })();