// ==UserScript==
// @name 8chan Spoiler Disabler
// @namespace http://tampermonkey.net/
// @version 1.4
// @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
});
})();