Greasy Fork is available in English.

删除Youtube的无关随机新人直播推荐

Youtube的算法也是底边吗?

// ==UserScript==
// @name              Remove Youtube annoying irrelevant random rookie streamers recommendation
// @name:zh-CN        删除Youtube的无关随机新人直播推荐
// @name:ja           Youtubeの関連動画から無関連新人配信を除外する
// @namespace         https://github.com/LifeJustDLC/
// @version           1.5.20240811
// @description       Stop offering free labor to Youtube's smart recommendation (no-brainer spam) system.
// @description:zh-CN Youtube的算法也是底边吗?
// @description:ja    無関係の新人配信を関連動画に強引に差し込んで、エンジニアの脳味噌も底辺ってわけ?
// @author            lijd
// @match             https://www.youtube.com/*
// @icon              https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @run-at            document-end
// @grant             GM_registerMenuCommand
// @grant             GM_getValue
// @grant             GM_setValue
// @license           MIT
// ==/UserScript==

(function () {
	"use strict";

	let asis; // policies blah blah
	if (window.trustedTypes) {
		asis = trustedTypes.createPolicy("asis", {
			createHTML: (str) => str,
		});
	} else {
		asis = { createHTML: (str) => str };
	}

	const scriptID = "teihen-youtube-recommendation";
	const logTag = "[" + scriptID.replaceAll("-", " ") + "]";
	const logger = {
		debug(...args) {
			console.debug(logTag, ...args);
		},
		info(...args) {
			console.info(logTag, ...args);
		},
		error(...args) {
			console.error(logTag, ...args);
		},
	};

	let threshold = GM_getValue("threshold") ?? 250;
	// ↑ current highest irrelevant record: 250,
	// lowest likely relevent record: 177
	GM_registerMenuCommand("change threshold", showThresholdSetting, {
		autoClose: true,
	});

	let timer = NaN;

	onUrlChange(); // dealing with SPA, borrowed from ViolentMonkey
	if (self.navigation) {
		navigation.addEventListener("navigatesuccess", onUrlChange);
	} else {
		let u = location.href;
		new MutationObserver(
			() => u !== (u = location.href) && onUrlChange()
		).observe(document, { subtree: true, childList: true });
	}

	function onUrlChange() {
		if (timer !== NaN) clearInterval(timer);
		if (!location.pathname.startsWith("/watch")) {
			return;
		}
		logger.info("new page detected:", location.href);
		if (document.hidden === false) {
			onVideoPage();
		} else {
			document.addEventListener("visibilitychange", onVisibilityChange, {
				once: true,
			});
		}
	}

	function onVisibilityChange() {
		if (document.hidden === false) onVideoPage();
	}

	function onVideoPage() {
		let before = document.getElementsByTagName(
			"ytd-compact-video-renderer"
		).length;
		let last = Date.now();
		timer = setInterval(() => {
			logger.debug("polling...");
			let now = Date.now();
			let after = document.getElementsByTagName(
				"ytd-compact-video-renderer"
			).length;
			if (after === 0) {
				before = after;
				return;
			}
			if (after === before) {
				// after <= before ?
				if (now - last > 30_000) {
					logger.info("remove timer");
					clearInterval(timer);
				}
				return;
			}
			before = after;
			last = now;
			purge();
		}, 3000);
	}

	function purge() {
		try {
			logger.info("purging...");
			const targets = $$("ytd-compact-video-renderer").filter((elm) => {
				const meta_line = $("#metadata-line", elm).innerText;
				// ↑ innerText! otherwise it will contain noise
				if (meta_line === "") return true; // edge case for I don't know
				const meta_span = $("#metadata-line span", elm).textContent;
				return (
					!meta_span.match(/\d+/) || // edge case for "No Viewer"
					parseInt(meta_span.match(/\d{1,3} /)) < threshold
				);
			});
			if (targets.length === 0) return;
			targets.forEach((elm) => {
				elm.remove();
				logger.info(
					"removed streamer:",
					$("a:has(#video-title)", elm).href
				);
			});
			// if (timer !== NaN) clearInterval(timer);
		} catch (err) {
			logger.error(err);
			// if (timer !== NaN) clearInterval(timer);
		}
	}

	function showThresholdSetting() {
		const style = /* css */ `
            #teihen-setting {
                position: fixed;
                z-index: 100000;
                right: 0;
                margin: 3em;
                padding: 3em;
                border-radius: 1em;
                background-color: #282828;
            }
            .teihen-setting-font {
                font-size: 2em;
            }
        `;
		const origHTML = /* html */ `
            <div id="teihen-setting">
                <input
                    id="teihen-setting-threshold"
                    class="teihen-setting"
                    type="number"
                    max="999"
                    min="1"
                    value="${threshold}"
                />
                <button id="teihen-setting-threshold-confirm" class="teihen-setting-font">confirm</button>
                <style>
                    ${style}
                </style>
            </div>
        `;
		const safeHTML = asis.createHTML(origHTML);

		$("body").insertAdjacentHTML("beforeend", safeHTML);
		$("#teihen-setting-threshold-confirm").addEventListener("click", () => {
			threshold = Number($("#teihen-setting-threshold").value);
			GM_setValue("threshold", threshold);
			$("#teihen-setting").remove();
		});
	}

	function $(selector, elm = null) {
		if (elm) return elm.querySelector(selector);
		return document.querySelector(selector);
	}
	function $$(selector, elm = null) {
		if (elm) return [...elm.querySelectorAll(selector)];
		return [...document.querySelectorAll(selector)];
	}
})();