// ==UserScript==
// @name YouTube Auto Redirect & Theater Mode + Sub Count
// @version 2.5
// @description Redirect channel root/featured to /videos, auto-enable theater mode, and show total subscriptions count (top + bottom)
// @match https://www.youtube.com/*
// @run-at document-end
// @grant none
// @namespace https://gf.qytechs.cn/users/1513610
// ==/UserScript==
/*
* YouTube Script Functionality:
* 1. Auto-redirects channel pages (@username or @username/featured) to /videos
* 2. Automatically enables theater mode on watch pages
* 3. Counts and displays total subscriptions in /feed/channels
* 4. Features:
* - Configurable settings (theater mode, sub count display)
* - Accessibility support (reduced motion, ARIA)
* - Persistent settings via localStorage
* - Visual feedback and error handling
*/
(function () {
"use strict";
// Configuration defaults
const DEFAULT_CONFIG = {
theaterMode: true,
showSubCount: true,
scrollDelay: 1000,
maxScrollAttempts: 20,
reducedMotion: false,
bannerStyle: {
fontSize: "18px",
fontWeight: "bold",
padding: "10px",
color: "#fff",
background: "#c00",
margin: "10px 0",
borderRadius: "8px",
textAlign: "center"
}
};
// Load or initialize config
let config = JSON.parse(localStorage.getItem("ytScriptConfig")) || DEFAULT_CONFIG;
// Save config to localStorage
function saveConfig() {
localStorage.setItem("ytScriptConfig", JSON.stringify(config));
}
const channelRegex = /^https:\/\/www\.youtube\.com\/@[\w-]+(?:\/featured)?\/?$/;
function enableTheater() {
if (!config.theaterMode) return false;
try {
const btn = document.querySelector('button[data-tooltip-title="Theater mode (t)"]');
if (btn) {
btn.click();
console.log("✅ Theater mode enabled.");
return true;
}
return false;
} catch (error) {
console.error("⚠️ Failed to enable theater mode:", error);
return false;
}
}
function handleUrl(url) {
try {
if (channelRegex.test(url)) {
location.replace(url.replace(/\/(featured)?\/?$/, "") + "/videos");
return;
}
if (url.includes("/watch")) {
let tries = 0;
const iv = setInterval(() => {
try {
if (config.theaterMode && enableTheater() || ++tries > 15) clearInterval(iv);
} catch (error) {
console.error("⚠️ Theater mode attempt failed:", error);
if (tries > 15) clearInterval(iv);
}
}, 700);
}
if (url.includes("/feed/channels")) {
if (config.showSubCount) countSubscriptions().catch(e => console.error("⚠️ Subscription count failed:", e));
}
} catch (error) {
console.error("⚠️ URL handling failed:", error);
}
}
async function countSubscriptions() {
try {
// Auto-scroll until no new content is loaded
let lastHeight = 0;
while (true) {
window.scrollTo(0, document.documentElement.scrollHeight);
await new Promise(r => setTimeout(r, 1000));
let newHeight = document.documentElement.scrollHeight;
if (newHeight === lastHeight) break;
lastHeight = newHeight;
}
// Count channel items
const channels = document.querySelectorAll("ytd-channel-renderer, ytd-grid-channel-renderer");
const count = channels.length;
// Reusable banner
function makeBanner(id) {
const div = document.createElement("div");
div.id = id;
div.textContent = `📺 Subscribed Channels: ${count}`;
div.style.cssText = "font-size:18px;font-weight:bold;padding:10px;color:#fff;background:#c00;margin:10px 0;border-radius:8px;text-align:center;";
return div;
}
const container = document.querySelector("ytd-section-list-renderer") || document.body;
// Top banner
if (!document.getElementById("sub-count-top")) {
container.prepend(makeBanner("sub-count-top"));
}
// Bottom banner
if (!document.getElementById("sub-count-bottom")) {
container.append(makeBanner("sub-count-bottom"));
window.scrollTo(0, document.documentElement.scrollHeight);
}
} catch (error) {
console.error("⚠️ Failed to count subscriptions:", error);
const errorBanner = document.createElement("div");
errorBanner.textContent = "⚠️ Failed to load subscription count";
errorBanner.style.cssText = "font-size:18px;font-weight:bold;padding:10px;color:#fff;background:#900;margin:10px 0;border-radius:8px;text-align:center;";
(document.querySelector("ytd-section-list-renderer") || document.body).prepend(errorBanner);
}
}
function updateBanners(count) {
// Update or create top banner
const updateOrCreateBanner = (id) => {
let banner = document.getElementById(id);
if (!banner) {
banner = document.createElement("div");
banner.id = id;
banner.setAttribute("role", "status");
banner.setAttribute("aria-live", "polite");
banner.style.cssText = Object.entries(config.bannerStyle)
.map(([key, value]) => `${key}:${value}`)
.join(';');
const container = document.querySelector("ytd-section-list-renderer, ytd-browse") || document.body;
id.includes('top') ? container.prepend(banner) : container.append(banner);
}
banner.textContent = `📺 Subscribed Channels: ${count}`;
};
updateOrCreateBanner("sub-count-top");
updateOrCreateBanner("sub-count-bottom");
}
function createErrorBanner() {
const errorBanner = document.createElement("div");
errorBanner.textContent = "⚠️ Failed to load subscription count";
errorBanner.style.cssText = "font-size:18px;font-weight:bold;padding:10px;color:#fff;background:#900;margin:10px 0;border-radius:8px;text-align:center;";
(document.querySelector("ytd-section-list-renderer, ytd-browse") || document.body).prepend(errorBanner);
}
// Save scroll position before navigation
let lastScrollPosition = 0;
document.addEventListener('scroll', () => {
lastScrollPosition = window.scrollY;
});
// Restore scroll position after navigation
window.addEventListener('load', () => {
if (lastScrollPosition > 0) {
window.scrollTo(0, lastScrollPosition);
}
});
// Wait until everything is loaded
try {
window.addEventListener("load", () => {
try {
// detect SPA navigations
let lastUrl = location.href;
new MutationObserver(() => {
try {
if (location.href !== lastUrl) {
lastUrl = location.href;
handleUrl(lastUrl);
}
} catch (observerError) {
console.error("⚠️ URL observer failed:", observerError);
}
}).observe(document, { childList: true, subtree: true });
// initial
handleUrl(location.href);
} catch (loadError) {
console.error("⚠️ Initial load failed:", loadError);
}
});
} catch (initError) {
console.error("⚠️ Script initialization failed:", initError);
}
})();