// ==UserScript==
// @name YouTube No-Shorts
// @name:zh-CN YouTube 油管去除短视频
// @name:zh-TW YouTube 油管去除短视频
// @namespace http://tampermonkey.net/
// @version 2.4
// @description Hide all Shorts/Short Videos, older browsers are not supported. Added toggle function.
// @description:zh-CN 隐藏所有 Shorts/短视频,不支持旧浏览器。添加了开关功能。
// @description:zh-TW 隐藏所有 Shorts/短视频,不支持旧浏览器。添加了开关功能。
// @author dogchild
// @match https://www.youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
"use strict";
/* -------------------- Config / constants -------------------- */
const VERSION = "2.4-optimized";
// style element ids
const CSS_HOME_ID = "__anti_shorts_css_home_v2_4";
const CSS_SEARCH_ID = "__anti_shorts_css_search_v2_4";
// CSS used (only injected on supported browsers)
const CSS_SHORTS = `
ytd-rich-grid-media:has(a[href*="/shorts/"]),
ytd-video-renderer:has(a[href*="/shorts/"]),
ytd-grid-video-renderer:has(a[href*="/shorts/"]),
ytd-rich-item-renderer:has(a[href*="/shorts/"]),
ytd-compact-video-renderer:has(a[href*="/shorts/"]),
ytd-playlist-video-renderer:has(a[href*="/shorts/"]),
ytd-rich-grid-row:has(a[href*="/shorts/"]),
/* new style grid shelf that contains shorts */
grid-shelf-view-model:has(ytm-shorts-lockup-view-model),
grid-shelf-view-model:has(ytm-shorts-lockup-view-model-v2),
/* older reels / shelves fallback */
ytd-reel-shelf-renderer,
ytd-rich-shelf-renderer[is-shorts],
ytd-shelf-renderer {
display: none !important;
}
`;
// debug flag (can be turned off to avoid console spam)
const DEBUG = false;
const dbg = (...args) => { if (DEBUG) console.debug("[Anti-Shorts " + VERSION + "]", ...args); };
/* -------------------- Feature detection (cached) -------------------- */
const HAS_SUPPORT = (function detectHas() {
try {
return CSS && typeof CSS.supports === "function" && CSS.supports("selector(:has(*))");
} catch (e) {
return false;
}
})();
if (!HAS_SUPPORT) {
console.warn("[Anti-Shorts] CSS :has() not supported — script requires modern browser. (v" + VERSION + ")");
// still register menu so user can change settings (though no effect)
}
/* -------------------- i18n labels (cached) -------------------- */
const IS_ZH = (navigator.language || "").toLowerCase().startsWith("zh");
const LABEL = {
home_on: IS_ZH ? "主页:隐藏 Shorts(已启用)" : "Home: Hide Shorts (ON)",
home_off: IS_ZH ? "主页:隐藏 Shorts(已禁用)" : "Home: Hide Shorts (OFF)",
search_on: IS_ZH ? "搜索页:隐藏 Shorts(已启用)" : "Search: Hide Shorts (ON)",
search_off: IS_ZH ? "搜索页:隐藏 Shorts(已禁用)" : "Search: Hide Shorts (OFF)",
status: IS_ZH ? "状态: 主页 {H} / 搜索页 {S}" : "Status: Home {H} / Search {S}",
warn_no_has: IS_ZH ? "[Anti-Shorts] 当前浏览器不支持 CSS :has(),脚本需要现代浏览器。" : "[Anti-Shorts] Browser does not support CSS :has(); script requires modern browser."
};
/* -------------------- persistent storage (GM) -------------------- */
const KEY_HOME = "anti_shorts_home_enabled";
const KEY_SEARCH = "anti_shorts_search_enabled";
// read initial (GM_getValue is sync in Tampermonkey/VM)
let homeEnabled = typeof GM_getValue === "function" ? GM_getValue(KEY_HOME, true) : true;
let searchEnabled = typeof GM_getValue === "function" ? GM_getValue(KEY_SEARCH, true) : true;
/* -------------------- page-type detection -------------------- */
// return one of "home", "search", "settings"
function computePageType() {
const p = (location.pathname || "").toLowerCase();
const q = (location.search || "").toLowerCase();
// search page heuristics
if (p.startsWith("/results") || q.includes("search_query=")) return "search";
// settings/management pages heuristics
if (p.startsWith("/settings") || p.startsWith("/account") || p.startsWith("/channel_switcher") ||
p.includes("/preferences") || p.includes("/privacy") || p.includes("/notifications")) {
return "settings";
}
// everything else is considered home (per your requirement)
return "home";
}
let lastPageType = null; // cache last page type to avoid redundant DOM ops
/* -------------------- style injection helpers -------------------- */
function injectCssIfAbsent(id, cssText) {
if (!HAS_SUPPORT) return false;
if (document.getElementById(id)) return false;
const s = document.createElement("style");
s.id = id;
s.textContent = cssText;
document.documentElement.appendChild(s);
dbg("Injected CSS:", id);
return true;
}
function removeCssIfPresent(id) {
const el = document.getElementById(id);
if (!el) return false;
el.remove();
dbg("Removed CSS:", id);
return true;
}
/* -------------------- efficient update logic -------------------- */
// Only change DOM when pageType OR flags change.
// lastApplied records the last combination for short-circuiting.
let lastApplied = { pageType: null, homeEnabled: null, searchEnabled: null };
function updateInjectionForRouteImmediate() {
if (!HAS_SUPPORT) {
console.warn(LABEL.warn_no_has);
return;
}
const pageType = computePageType();
// if nothing changed, do nothing
if (lastApplied.pageType === pageType &&
lastApplied.homeEnabled === homeEnabled &&
lastApplied.searchEnabled === searchEnabled) {
dbg("No change in pageType/flags, skip style ops.");
lastPageType = pageType;
return;
}
dbg("Applying styles for pageType:", pageType, "homeEnabled:", homeEnabled, "searchEnabled:", searchEnabled);
// Home logic: apply CSS_HOME_ID only when pageType is "home" and homeEnabled true
if (pageType === "home" && homeEnabled) {
injectCssIfAbsent(CSS_HOME_ID, CSS_SHORTS);
} else {
removeCssIfPresent(CSS_HOME_ID);
}
// Search logic: apply CSS_SEARCH_ID only when pageType is "search" and searchEnabled true
if (pageType === "search" && searchEnabled) {
injectCssIfAbsent(CSS_SEARCH_ID, CSS_SHORTS);
} else {
removeCssIfPresent(CSS_SEARCH_ID);
}
lastApplied.pageType = pageType;
lastApplied.homeEnabled = homeEnabled;
lastApplied.searchEnabled = searchEnabled;
lastPageType = pageType;
}
// Called on locationchange events; we delay a tiny amount to allow SPA to settle.
let pendingRouteTimer = null;
function scheduleMaybeUpdateRoute(delay = 50) {
// if same page type already scheduled, keep only one timer
if (pendingRouteTimer !== null) {
// keep earliest scheduled; do nothing (we won't stack timers)
return;
}
pendingRouteTimer = setTimeout(() => {
pendingRouteTimer = null;
updateInjectionForRouteImmediate();
}, delay);
}
/* -------------------- menu registration (only when needed) -------------------- */
let menuIds = { home: null, search: null, status: null };
function safeUnregister(id) {
try {
if (typeof GM_unregisterMenuCommand === "function" && id) {
GM_unregisterMenuCommand(id);
}
} catch (e) {
// ignore - not supported or already removed
}
}
function registerMenu() {
// Unregister previous (if any)
safeUnregister(menuIds.home);
safeUnregister(menuIds.search);
safeUnregister(menuIds.status);
const homeLabel = homeEnabled ? LABEL.home_on : LABEL.home_off;
const searchLabel = searchEnabled ? LABEL.search_on : LABEL.search_off;
try {
menuIds.home = (typeof GM_registerMenuCommand === "function")
? GM_registerMenuCommand(homeLabel, () => {
homeEnabled = !homeEnabled;
try { GM_setValue(KEY_HOME, homeEnabled); } catch (e) {}
// re-register to update label
registerMenu();
// apply changes immediately
updateInjectionForRouteImmediate();
})
: null;
menuIds.search = (typeof GM_registerMenuCommand === "function")
? GM_registerMenuCommand(searchLabel, () => {
searchEnabled = !searchEnabled;
try { GM_setValue(KEY_SEARCH, searchEnabled); } catch (e) {}
registerMenu();
updateInjectionForRouteImmediate();
})
: null;
const statusText = LABEL.status.replace("{H}", homeEnabled ? (IS_ZH ? "开" : "ON") : (IS_ZH ? "关" : "OFF"))
.replace("{S}", searchEnabled ? (IS_ZH ? "开" : "ON") : (IS_ZH ? "关" : "OFF"));
menuIds.status = (typeof GM_registerMenuCommand === "function")
? GM_registerMenuCommand(statusText, () => { alert(statusText); })
: null;
} catch (e) {
// swallow to avoid breaking page
dbg("registerMenu error:", e);
}
}
/* -------------------- SPA location hooking (idempotent) -------------------- */
let locationHooked = false;
function hookLocationChangeOnce(handler) {
if (locationHooked) return;
locationHooked = true;
// Wrap pushState/replaceState idempotently: avoid double-wrapping
const wrapOnce = (obj, fnName) => {
const orig = obj[fnName];
if (orig.__anti_shorts_wrapped) return;
const wrapped = function () {
const res = orig.apply(this, arguments);
try { window.dispatchEvent(new Event("locationchange")); } catch (e) {}
return res;
};
wrapped.__anti_shorts_wrapped = true;
obj[fnName] = wrapped;
};
wrapOnce(history, "pushState");
wrapOnce(history, "replaceState");
window.addEventListener("popstate", () => window.dispatchEvent(new Event("locationchange")));
// YouTube-specific navigation finish event
window.addEventListener("yt-navigate-finish", () => window.dispatchEvent(new Event("locationchange")));
// main handler
window.addEventListener("locationchange", handler);
}
/* -------------------- init -------------------- */
function init() {
dbg("init start, HAS_SUPPORT=", HAS_SUPPORT);
// register menu (even if HAS not supported, menu lets user control prefs)
registerMenu();
if (!HAS_SUPPORT) {
console.warn(LABEL.warn_no_has);
return;
}
// initial apply (no delay)
updateInjectionForRouteImmediate();
// hook SPA route changes and schedule re-evaluate (short delay)
hookLocationChangeOnce(() => scheduleMaybeUpdateRoute(50));
dbg("init done");
}
// run init safely
try {
init();
} catch (e) {
// Never throw errors to host page
console.error("[Anti-Shorts] init error:", e);
}
/* -------------------- public debug helpers (optional) -------------------- */
// window.__antiShorts = { updateInjectionForRouteImmediate, scheduleMaybeUpdateRoute, registerMenu };
})();