// ==UserScript== // @name Invidious - Shorts Detection & Removal // @namespace http://tampermonkey.net/ // @version 1.3.0 // @description processes all thumbnails immediately for seamless scrolling. // @author https://kuuro.net // @match https://inv.nadeko.net/* // @grant none // ==/UserScript== (() => { 'use strict'; const DEBUG = false; const THRESHOLD = 97.5; // Change only if you know what you're doing const SHORTS_CSS = `.short-detected{display:none;background-color:rgba(255,0,0,0.3);border:1px solid red;}`; const POOL_SIZE = 6; const POOL_CANVAS_W = 110; const POOL_CANVAS_H = 180; const SCALED_WIDTH = 33; const SCALED_HEIGHT = 54; const Y_POS = 63; const RIGHT_X = 67; const CENTER_W = 100; const CENTER_H = 180; const CROP_W = 100; const CROP_H = 54; const CROP_Y = 63; const stats = { totalProcessed: 0, totalDetected: 0, processingTimes: [], batchTimes: [], comparisonTimes: [], totalBatchTime: 0 }; const processedImages = new WeakSet(); let poolIndex = 0; const canvasPool = []; const addStyles = css => { const s = document.createElement('style'); s.textContent = css; document.head.appendChild(s); }; addStyles(SHORTS_CSS); const createOffscreen = (w, h) => { if (typeof OffscreenCanvas !== 'undefined') { return new OffscreenCanvas(w, h); } else { const c = document.createElement('canvas'); c.width = w; c.height = h; return c; } }; for (let i = 0; i < POOL_SIZE; i++) { canvasPool.push(createOffscreen(POOL_CANVAS_W, POOL_CANVAS_H)); } const getCanvasFromPool = (w, h) => { const c = canvasPool[poolIndex]; poolIndex = (poolIndex + 1) % canvasPool.length; if (c.width !== w) c.width = w; if (c.height !== h) c.height = h; const ctx = c.getContext('2d', { willReadFrequently: true }); ctx.clearRect(0, 0, w, h); return c; }; const compareCanvases = (canvas1, canvas2) => { const compareStart = DEBUG ? performance.now() : 0; const ctx1 = canvas1.getContext('2d', { willReadFrequently: true }); const ctx2 = canvas2.getContext('2d', { willReadFrequently: true }); const data1 = ctx1.getImageData(0, 0, canvas1.width, canvas1.height).data; const data2 = ctx2.getImageData(0, 0, canvas2.width, canvas2.height).data; const len = data1.length; const maxDiff = (len / 4) * 3 * 255; const thresholdDiff = maxDiff * (1 - THRESHOLD / 100); let diff = 0; for (let i = 0; i < len; i += 4) { diff += Math.abs(data1[i] - data2[i]); diff += Math.abs(data1[i + 1] - data2[i + 1]); diff += Math.abs(data1[i + 2] - data2[i + 2]); if (diff > thresholdDiff) { if (DEBUG) stats.comparisonTimes.push(performance.now() - compareStart); return 100 - (diff / maxDiff) * 100; } } if (DEBUG) stats.comparisonTimes.push(performance.now() - compareStart); return 100 - (diff / maxDiff) * 100; }; const createCanvasPart = (sourceImage, x, width, height, filterString = 'none') => { const canvas = getCanvasFromPool(width, height); const ctx = canvas.getContext('2d', { willReadFrequently: true }); ctx.filter = filterString; ctx.drawImage(sourceImage, x, 0, width, height, 0, 0, width, height); return canvas; }; const cropCanvas = (sourceCanvas, x, y, width, height) => { const c = getCanvasFromPool(width, height); const ctx = c.getContext('2d', { willReadFrequently: true }); ctx.drawImage(sourceCanvas, x, y, width, height, 0, 0, width, height); return c; }; const createFilteredCanvas = sourceImage => { const finalCanvas = getCanvasFromPool(CENTER_W, CENTER_H); const finalCtx = finalCanvas.getContext('2d', { willReadFrequently: true }); const centerCanvas = createCanvasPart(sourceImage, 110, 100, 180, 'brightness(100%) blur(5px)'); const leftCanvas = createCanvasPart(sourceImage, 0, 110, 180, 'brightness(320%) blur(16px)'); const rightCanvas = createCanvasPart(sourceImage, 210, 110, 180, 'brightness(320%) blur(16px)'); finalCtx.drawImage(centerCanvas, 0, 0); finalCtx.drawImage(leftCanvas, 0, Y_POS, SCALED_WIDTH, SCALED_HEIGHT); finalCtx.drawImage(rightCanvas, RIGHT_X, Y_POS, SCALED_WIDTH, SCALED_HEIGHT); return { finalCompositeCanvas: finalCanvas, originalCenterCanvas: centerCanvas }; }; const processThumbnailCore = thumbnail => { const { finalCompositeCanvas, originalCenterCanvas } = createFilteredCanvas(thumbnail); const middleBlurredSection = cropCanvas(finalCompositeCanvas, 0, CROP_Y, CROP_W, CROP_H); const originalMiddle = cropCanvas(originalCenterCanvas, 0, CROP_Y, CROP_W, CROP_H); const similarityPercentage = compareCanvases(originalMiddle, middleBlurredSection); if (similarityPercentage >= THRESHOLD) { const parent = thumbnail.closest('.pure-u-1 .pure-u-md-1-4'); if (parent) { parent.classList.add('short-detected'); if (DEBUG) stats.totalDetected++; } } }; const processThumbnail = thumbnail => { if (processedImages.has(thumbnail)) return; processedImages.add(thumbnail); if (thumbnail.complete && thumbnail.naturalHeight !== 0) { const start = DEBUG ? performance.now() : 0; processThumbnailCore(thumbnail); if (DEBUG) { stats.processingTimes.push(performance.now() - start); stats.totalProcessed++; } } else { thumbnail.addEventListener('load', () => { const start = DEBUG ? performance.now() : 0; processThumbnailCore(thumbnail); if (DEBUG) { stats.processingTimes.push(performance.now() - start); stats.totalProcessed++; } }, { once: true }); } }; const processAllThumbnails = () => { const imgs = Array.from(document.querySelectorAll('div.thumbnail img')); const toProcess = imgs.filter(img => !processedImages.has(img)); if (toProcess.length === 0) return; if (DEBUG) console.log(`%c[Shorts Detector] Processing ${toProcess.length} thumbnails...`, 'color: #00aaff; font-weight: bold;'); const batchStart = performance.now(); const processChunk = (startIndex) => { const chunkSize = 10; const endIndex = Math.min(startIndex + chunkSize, toProcess.length); for (let i = startIndex; i < endIndex; i++) { processThumbnail(toProcess[i]); } if (endIndex < toProcess.length) { requestAnimationFrame(() => processChunk(endIndex)); } else { if (DEBUG) { const totalTime = performance.now() - batchStart; stats.totalBatchTime += totalTime; console.log(`%c[Shorts Detector] Batch complete: ${toProcess.length} processed in ${totalTime.toFixed(2)}ms`, 'color: #00ff00;'); logStats(); } } }; processChunk(0); }; let mutationPending = false; const handleMutations = () => { mutationPending = false; processAllThumbnails(); }; const observer = new MutationObserver(() => { if (!mutationPending) { mutationPending = true; requestAnimationFrame(handleMutations); } }); const container = document.querySelector('.pure-g') || document.body; observer.observe(container, { childList: true, subtree: true }); const logStats = () => { if (!DEBUG) return; const avgProcessing = stats.processingTimes.length > 0 ? (stats.processingTimes.reduce((a, b) => a + b, 0) / stats.processingTimes.length).toFixed(2) : 0; const avgComparison = stats.comparisonTimes.length > 0 ? (stats.comparisonTimes.reduce((a, b) => a + b, 0) / stats.comparisonTimes.length).toFixed(2) : 0; console.log('%c[Shorts Detector] Performance Stats', 'color: #00ff00; font-weight: bold; font-size: 14px;'); console.log(`┌─────────────────────────────────────────┐`); console.log(`│ Total Processed: ${stats.totalProcessed.toString().padStart(4)} thumbnails │`); console.log(`│ Shorts Detected: ${stats.totalDetected.toString().padStart(4)} shorts │`); console.log(`│ Detection Rate: ${((stats.totalDetected / stats.totalProcessed * 100) || 0).toFixed(1).padStart(4)}% │`); console.log(`├─────────────────────────────────────────┤`); console.log(`│ Avg Processing Time: ${avgProcessing.padStart(6)} ms │`); console.log(`│ Avg Comparison Time: ${avgComparison.padStart(6)} ms │`); console.log(`│ Total Batch Time: ${stats.totalBatchTime.toFixed(2).padStart(6)} ms │`); console.log(`├─────────────────────────────────────────┤`); if (stats.processingTimes.length > 0) { console.log(`│ Min Processing: ${Math.min(...stats.processingTimes).toFixed(2).padStart(6)} ms │`); console.log(`│ Max Processing: ${Math.max(...stats.processingTimes).toFixed(2).padStart(6)} ms │`); } console.log(`└─────────────────────────────────────────┘`); }; if (DEBUG) { setInterval(() => { if (stats.totalProcessed > 0) { logStats(); } }, 30000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', processAllThumbnails); } else { processAllThumbnails(); } })();