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