Remove Youtube annoying irrelevant random rookie streamers recommendation

Stop offering free labor to Youtube's smart recommendation (no-brainer spam) system.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name              Remove Youtube annoying irrelevant random rookie streamers recommendation
// @name:zh-CN        移除Youtube的无关随机新人直播推荐
// @name:ja           Youtubeの関連動画から無関連新人配信を除去
// @namespace         https://github.com/LifeJustDLC/
// @version           2.0.20241101
// @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 logger = {
		scriptID: "teihen-youtube-recommendation",
		get logTag() {
			return "[" + this.scriptID.replaceAll("-", " ") + "]";
		},
		debug: function (...args) {
			console.debug(this.logTag, ...args);
		},
		info: function (...args) {
			console.info(this.logTag, ...args);
		},
		error: function (...args) {
			console.error(this.logTag, ...args);
		},
	};

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

	let timer = null;
  let obs = null;

	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() {
    cleanUp();
		if (!location.pathname.startsWith("/watch")) {
			return;
		}
		logger.info("new page detected:", location.href);
		if (document.hidden === false) {
			onVideoPage();
		} else {
			document.addEventListener(
				"visibilitychange",
				() => {
					if (document.hidden === false) onVideoPage();
				},
				{ once: true }
			);
		}
	}

  function cleanUp() {
		if (timer !== null) {
      clearInterval(timer);
      timer = null;
    }
    if (obs !== null) {
      obs.disconnect();
      obs = null;
    }
  }

	function onVideoPage() {
    (new Promise((resolve, reject) => {
      let tStart = Date.now();
      timer = setInterval(() => {
        if ($$("ytd-compact-video-renderer").length > 0) {
          clearInterval(timer);
          timer = null;
          resolve();
        }
        if (Date.now() - tStart > 30_000) {
          reject();
        }
      }, 1000);
    })).then(() => {
      purge(threshold);
      obs = new MutationObserver((mrs) => {
        for (const mr of mrs) {
          for (const elm of mr.addedNodes) {
            if (elm.name !== "ytd-compact-video-renderer") continue;
            if (isTarget(elm)) hideTarget(elm);
          }
        }
        const numLast = $$("ytd-compact-video-renderer").length
        purge(threshold);
        if ($$("ytd-compact-video-renderer").length < numLast) {
          logger.error("something wrong")
        }
      });
      obs.observe(
        $('#contents:has(> ytd-compact-video-renderer)'),
        {childList: true}
      );
    }, () => {
      logger.error("observer installation timeout");
    });
	}

  const elmRefCache = new WeakMap();

  const langRegStrs = {
    en: "watching|views",
    zhja: "人|次|回",
    es: "lo|vistas",
    fr: "spectateurs|(?:de )?visionnements",
    de: "aktive|Aufrufe",
    ko: "명|회",
    ru: "просмотров"
  };
  let regexCache = null;

  function getClassificationCode(elm) {
    if (elmRefCache.has(elm)) return -10; // cached
    elmRefCache.set(elm, true);
    if (regexCache === null) {
      for (const langRegStr of Object.values(langRegStrs)) {
        const langReg = new RegExp(langRegStr);
        if (langReg.test($('#metadata-line > span:first-of-type', elm)?.innerText)) {
          regexCache = new RegExp(`\\b([\\d,.]+)\\s?(\\S{0,4})\\s?(${langRegStr})`);
          break;
        } else {
          return -11; // might be a playlist or no viewer
        }
      }
    }
    const mch = $('#metadata-line > span:first-of-type', elm)?.innerText.match(regexCache);
    if (mch === undefined) return 22; // another no viewer edge case
    if (mch === null) return 21; // no viewer
    if (mch[2] !== '') return -20; // more than 1k
    if (mch[1].search(/[,.]/) > 0) return -21; // more than 1k
    if (Number(mch[1]) < threshold) return 20; // less than threshold

    logger.error("unknown edge case")
    console.log(elm)
    return -50; // unknown edge case
  }

  function isTarget(elm) {
    return getClassificationCode(elm) > 0;
  }

  function hideTarget(elm) {
    // elm.style.opacity = 0.15; // for debug
    elm.style.display = "none";
    console.log(elm);
  }

	function purge(threshold) {
		try {
			const targets = $$("ytd-compact-video-renderer").filter(isTarget);
			if (targets.length === 0) return;
			targets.forEach(hideTarget);
		} catch (err) {
			logger.error(err);
		}
	}

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