Files
shorts-remover/userscript.js
2025-10-31 20:08:11 -03:00

265 lines
10 KiB
JavaScript

// ==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();
}
})();