YouTube GVDS1750 (GarbageVideoDisposalSystem, >1750 view)

English Only - Youtube recommends garbage videos with 0-1750 views, so we put them in the garbage disposal.

// ==UserScript==
// @name         YouTube GVDS1750 (GarbageVideoDisposalSystem, >1750 view)
// @namespace    http://tampermonkey.net/
// @version      2
// @description  English Only - Youtube recommends garbage videos with 0-1750 views, so we put them in the garbage disposal.
// @author       sir rob
// @include      https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

// ---------------------------------------------------------------------------

// Known issues:
// UI when open sits over homepage genre selection - has issues with 1.6k view vids? wtf - misc. aesthetics - Only 20 something videos are filtered on /watch (dynamic load issue) - emoji on reopenbutton moves around? idk - The hate on pineapple on pizza

// Future plans/ideas:
// call replacement renderer to replace empty blocks - resizable/dragable UI - clickable links in UI - add support for more languages - mine crypto in unsuspecting users browsers teehee

// Changelog so I can start remembering what issues I've fixed and what I've fucked up again:
// v1.40 Fixed char limit inop when scrolling - Channelgrab working - open/close/open crashes fixed with new "memory" system (lol)
// v1.41 Wow, it mostly works? Code cleaned up, some shit reorganized, other code removed = 20 lines less and twice the functionality from v1.39!!! Even with changelog!
// v1.42 new memory, somehow simpler than the last? - Tested in Opera/Chrome/Firefox - add video player monitor
// v1.43 replace player sniffer, replace with full screen listener grabs from browser not youtube
// v1.44 new badvideo logic, able to set a threshold for the filter in preparation for filtering on /watch -
// v1.45 another new badvideo logic, previous was unstable/unable to differentiate 1.4k from 1.4m, or anything under 1000 - Removed redundant logic that was getting in the way.
// v1.46-50..rolled back to v1.43, I don't want to talk about it (several blocks of new logic were as reliable as my 04' Land Rover)
// v1.51 start work on 1750 view filter for /watch - add red 1 pixel border around open button, centered emoji (its off, heck) - implemented random bits of logging for debug (why didn't I do this earlier..)
// v1.52 fine tuning on filter - added "no view" to /watch filter
// v1.53 let reopenBtn render when refreshing or going direct to /watch video page.. Added redundant logic that will get in the way (I don't know which one is the one thats working, and they're all working atm so..)
// v1.54 aesthetics time, changes to sizes of stuff, reopenbtn is just a toggle now, always visible can open/close UI
// v1.55 UI panel loads deleted videos on page load when UI is closed
// v1.56 short half ass attempt at making the deleted videos linked to their original url
// v1.57 remove the above - clean up UI panel and make it a solid size
// v2 add removal for "viewing" because youtube thinks I want to watch some snot nosed kid play 10 fps dayz along with 2 other people, or some dude printing something, made it v2 so it updates for the 2 people that downloaded this script for some reason


// these settings should probably stay the same past v1.30ish
let g_VideosFiltering = true;
let g_ShortsFiltering = true;
let g_RemoveContainAdsSign = true;
let g_RemovedVideosMap = new Map();
let g_PanelVisible = false; // default to closed UI, visible reopenBtn

// Always show reopenbutton, now opens and closes UI + stays visible at all times
function ShowReopenButton() {
    let reopenBtn = document.getElementById("reopen-panel-btn");

    if (!reopenBtn) {
        reopenBtn = document.createElement("div");
        reopenBtn.innerText = "✦"; // change to whatever emoji you want
        reopenBtn.id = "reopen-panel-btn";
        reopenBtn.style.position = "absolute";
        reopenBtn.style.top = "19px";
        reopenBtn.style.left = "50px";
        reopenBtn.style.width = "16px";
        reopenBtn.style.height = "16px";
        reopenBtn.style.display = "flex";
        reopenBtn.style.alignItems = "center";
        reopenBtn.style.justifyContent = "center";
        reopenBtn.style.borderRadius = "50%";
        reopenBtn.style.backgroundColor = "white";
        reopenBtn.style.border = "1px solid red";
        reopenBtn.style.cursor = "pointer";
        reopenBtn.style.zIndex = "9999";
        reopenBtn.style.pointerEvents = "auto";
        reopenBtn.style.fontSize = "16px";

        document.body.appendChild(reopenBtn);
    }

    reopenBtn.onclick = () => {
        const panel = document.getElementById("removed-videos-panel");
        if (panel && panel.style.display !== "none") {
            panel.style.display = "none";
            g_PanelVisible = false;
        } else {
            CreateRemovedVideosPanel();
            g_PanelVisible = true;
        }
    };
}

// replacement sniffer for fullscreen, much simpler too!
function HandleFullscreenChange() {
    const isFullscreen = !!document.fullscreenElement;

    const panel = document.getElementById("removed-videos-panel");
    const reopenBtn = document.getElementById("reopen-panel-btn");

    if (isFullscreen) {
        if (panel) panel.style.display = "none";
        if (reopenBtn) reopenBtn.style.display = "none";
    } else {
        if (g_PanelVisible && panel) panel.style.display = "block";
        if (reopenBtn) reopenBtn.style.display = "block";
    }
}

document.addEventListener("fullscreenchange", HandleFullscreenChange);

function FilterWatchSidebar() {
    const lockups = document.querySelectorAll("yt-lockup-view-model.lockup");

    lockups.forEach((el, index) => {
        const viewsSpan = Array.from(el.querySelectorAll("span"))
            .find(span => {
                const txt = span.textContent.trim().toLowerCase();
                return txt.includes("views") || txt.includes("watching");
            });

        if (!viewsSpan) {
            console.log(`Lockup #${index + 1}: No views/watch count span found`);
            return;
        }

        const viewsText = viewsSpan.textContent.trim().toLowerCase();

        if (viewsText.includes("no views") || viewsText === "") {
            console.log(`Removing sidebar video with "no views" or empty text:`, viewsText);
            el.remove();
            return;
        }

        const match = viewsText.match(/([\d.,]+)\s*([KMkm]?)/);
        if (!match) {
            console.log(`Lockup #${index + 1}: Failed to match count in "${viewsText}"`);
            return;
        }

        let value = parseFloat(match[1].replace(',', '.'));
        const suffix = match[2].toLowerCase();
        if (isNaN(value)) {
            console.log(`Lockup #${index + 1}: Parsed NaN from "${viewsText}"`);
            return;
        }

        if (suffix === 'k') value *= 1000;
        else if (suffix === 'm') value *= 1000000;

        if (value < 1750) {
            console.log(`Removing sidebar video (count: ${Math.round(value)}):`, viewsText);
            el.remove();
        } else {
            console.log(`Keeping sidebar video (count: ${Math.round(value)}):`, viewsText);
        }
    });
}

function CreateRemovedVideosPanel() {
    let panel = document.getElementById("removed-videos-panel");
    if (!panel) {
        panel = document.createElement("div");
        panel.id = "removed-videos-panel";
        panel.style.position = "absolute";
        panel.style.top = "10px";
        panel.style.left = "260px";
        panel.style.background = "rgba(0, 0, 0, 0.65)";
        panel.style.color = "rgba(255, 255, 255, 0.75)";
        panel.style.padding = "8px 10px";
        panel.style.borderRadius = "8px";
        panel.style.zIndex = "9999";
        panel.style.fontSize = "11px";
        panel.style.maxWidth = "130px";
        panel.style.maxHeight = "80px";
        panel.style.overflowY = "hidden"; panel.style.height = "80px";
        panel.style.pointerEvents = "none";
        panel.style.userSelect = "none";
        panel.style.display = "none";

        panel.innerHTML = `
            <div style="font-weight:bold; margin-bottom:6px;">Removed Videos</div>
            <ul id='removed-videos-list' style='padding-left: 14px; margin: 0; height: 50px; overflow-y: auto; pointer-events: auto;'></ul>
        `;

        const closeBtn = document.createElement("button");
        closeBtn.innerText = "✕";
        closeBtn.style.position = "absolute";
        closeBtn.style.top = "2px";
        closeBtn.style.right = "4px";
        closeBtn.style.background = "transparent";
        closeBtn.style.color = "#fff";
        closeBtn.style.border = "none";
        closeBtn.style.cursor = "pointer";
        closeBtn.style.fontSize = "12px";
        closeBtn.style.pointerEvents = "auto";
        closeBtn.onclick = () => {
            g_PanelVisible = false;
            panel.style.display = "none";
            ShowReopenButton();
        };
        panel.appendChild(closeBtn);

        const clearBtn = document.createElement("button");
        clearBtn.innerText = "Clear";
        clearBtn.style.position = "absolute";
        clearBtn.style.bottom = "4px";
        clearBtn.style.left = "8px";
        clearBtn.style.background = "#444";
        clearBtn.style.color = "#fff";
        clearBtn.style.border = "none";
        clearBtn.style.cursor = "pointer";
        clearBtn.style.fontSize = "10px";
        clearBtn.style.pointerEvents = "auto";
        clearBtn.onclick = () => {
            g_RemovedVideosMap.clear();
            document.getElementById("removed-videos-list").innerHTML = "";
        };
        panel.appendChild(clearBtn);

        document.body.appendChild(panel);
    }

    panel.style.display = g_PanelVisible ? "block" : "none";
}

// Reverted AddToRemovedVideosPanel to original version without links
function AddToRemovedVideosPanel(title, channelHandle) {
    if (!title) return;
    const map = g_RemovedVideosMap;
    const list = document.getElementById("removed-videos-list");
    if (!list) return;

    const key = title + "|" + channelHandle;
    if (map.has(key)) {
        map.set(key, map.get(key) + 1);
        const item = document.getElementById("removed-" + key);
        if (item) item.innerText = `${title.slice(0, 20)}... (x${map.get(key)}) - ${channelHandle}`;
    } else {
        map.set(key, 1);
        const li = document.createElement("li");
        li.id = "removed-" + key;
        li.innerText = `${title.slice(0, 20)}... (x1) - ${channelHandle}`;
        li.style.pointerEvents = "none";
        list.appendChild(li);
    }
}

function IsBadVideo(videoViews) {
    if (!videoViews) return false;
    let text = videoViews.innerText;
    if (text.length == 0) return false;

    let numbersExists = false;
    for (let i = 0; i < text.length; i++) {
        if (IsNumber(text[i])) {
            numbersExists = true;
            break;
        }
    }

    let twoWordsExists = false;
    for (let i = 0; i < text.length - 2; i++) {
        if (IsNumber(text[i]) && IsSeparator(text[i + 1]) && IsNumber(text[i + 2])) {
            let sample = text.substring(i, i + 5);
            let parsed = parseFloat(sample.replace(',', '.'));
            if (parsed && parsed >= 1.75) {
                twoWordsExists = true;
                break;
            }
        }

        if (!IsNumber(text[i]) && IsSpace(text[i + 1]) && !IsNumber(text[i + 2])) {
            twoWordsExists = true;
            break;
        }
    }

    return !numbersExists || !twoWordsExists;
}

function UpdateVideoFiltering() {
    if (g_PanelVisible) {
        CreateRemovedVideosPanel();
    } else {
        ShowReopenButton();
    }

    if (!g_VideosFiltering || !IsHomepage()) return;

    let videosList = document.getElementsByClassName("style-scope ytd-rich-item-renderer");
    for (let i = 0; i < videosList.length; i++) {
        if (videosList[i].id != "content") continue;

        const videoElement = videosList[i];
        const parent = videoElement.parentElement;
        let titleEl = videoElement.querySelector("#video-title");
        let handleEl = videoElement.querySelector("a.yt-simple-endpoint.style-scope.yt-formatted-string[href^='/@']");
        let viewsEl = videoElement.querySelector(".inline-metadata-item.style-scope.ytd-video-meta-block");

        let title = titleEl ? titleEl.innerText.trim() : "Unknown Title";
        let channelHandle = handleEl ? handleEl.getAttribute("href").replace("/", "") : "Unknown";

        const isLowView = IsBadVideo(viewsEl);
        const hasProgress = videoElement.querySelector("#progress") != null;

        if (isLowView || hasProgress) {
            AddToRemovedVideosPanel(title, channelHandle);
            parent.remove();
        }
    }
}

function IsHomepage() {
    return location.pathname === "/";
}
function IsNumber(i) { return (i >= '0' && i <= '9'); }
function IsSpace(i) { return i == ' '; }
function IsSeparator(i) { return i == '.' || i == ','; }

function RemoveContainAdsSign() {
    if (g_RemoveContainAdsSign) {
        const styleElement = document.createElement('style');
        document.head.appendChild(styleElement);
        const sheet = styleElement.sheet;
        sheet.insertRule(".ytInlinePlayerControlsTopLeftControls { display: none }", 0);
    }
}

document.addEventListener("yt-navigate-finish", () => {
    g_RemovedVideosMap.clear();
    g_PanelVisible = false;
    CreateRemovedVideosPanel();
    setTimeout(UpdateVideoFiltering, 350);
    RemoveContainAdsSign();
    if (location.pathname.startsWith("/watch")) {
        setTimeout(FilterWatchSidebar, 500);
    }
});

window.addEventListener("load", () => {
    if (location.pathname.startsWith("/watch")) {
        ShowReopenButton();
        setTimeout(() => {
            try {
                FilterWatchSidebar();
            } catch (e) {
                console.error("FilterWatchSidebar failed:", e);
            }
        }, 1000);
    }
});

["message", "load", "scrollend", "click"].forEach(evt =>
    window.addEventListener(evt, () => setTimeout(UpdateVideoFiltering, 200))
);


// video where I make this script, subscribe to support me (: https://www.youtube.com/watch?v=xvFZjo5PgG0


// this used to be under 120 lines, es ist, als ware ich ein BMW ingenieur. Die halfte davon ist definitiv nicht nutzlos und ist absolut notwendig, um wie vorgesehen zu funktionieren