YouTube Auto Redirect & Theater Mode + Sub Count + UnShort Shorts + Time Remaining

Redirect channel root/featured to /videos, auto-enable theater mode, show subscription count, redirect Shorts to full watch view, and display remaining video time

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube Auto Redirect & Theater Mode + Sub Count + UnShort Shorts + Time Remaining
// @version      4.0
// @description  Redirect channel root/featured to /videos, auto-enable theater mode, show subscription count, redirect Shorts to full watch view, and display remaining video time
// @match        https://www.youtube.com/*
// @run-at       document-start
// @grant        none
// @namespace    https://greasyfork.org/users/1513610
// ==/UserScript==


(function () {
  'use strict';

  // ======================
  // CONFIGURATION
  // ======================
  const DEFAULT_CONFIG = {
    theaterMode: false,
    showSubCount: true,
    showTimeRemaining: true,
    scrollDelay: 1200,
    maxScrollAttempts: 50,
    reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)')
      .matches,
    bannerStyle: {
      fontSize: '16px',
      fontWeight: 'bold',
      padding: '12px',
      color: '#fff',
      background: 'rgba(204, 0, 0, 0.9)',
      margin: '10px 0',
      borderRadius: '8px',
      textAlign: 'center',
      zIndex: '1000',
      position: 'relative',
    },
  };
  let config =
    JSON.parse(localStorage.getItem('ytScriptConfig')) || DEFAULT_CONFIG;
  const channelRegex =
    /^https:\/\/www\.youtube\.com\/@[\w.-]+(?:\/featured)?\/?$/;
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

  // ======================
  // FEATURE: SHORTS REDIRECT
  // ======================
  function redirectShorts(url) {
    if (url.includes('/shorts/')) {
      const videoId = url.split('/shorts/').pop().split(/[?&]/)[0];
      if (videoId) {
        location.replace(`https://www.youtube.com/watch?v=${videoId}`);
      }
    }
  }

  // ======================
  // FEATURE: THEATER MODE
  // ======================
  function enableTheater() {
    if (
      !config.theaterMode ||
      document.querySelector('ytd-watch-flexy[theater]')
    )
      return;
    try {
      const theaterButton = document.querySelector('button.ytp-size-button');
      if (theaterButton) {
        theaterButton.click();
        console.log('✅ Theater mode enabled.');
      }
    } catch (error) {
      console.error('⚠️ Failed to enable theater mode:', error);
    }
  }

  // ======================
  // FEATURE: REVERT DUBBED AUDIO
  // ======================
  async function revertToOriginalAudio() {
    try {
      const settingsButton = document.querySelector(
        'button.ytp-settings-button'
      );
      if (!settingsButton) return;

      settingsButton.click();
      await sleep(200);

      const menuItems = document.querySelectorAll('.ytp-menuitem-label');
      const audioTrackButton = Array.from(menuItems).find(el =>
        el.innerText.includes('Audio track')
      );

      if (!audioTrackButton) {
        return settingsButton.click();
      }

      audioTrackButton.click();
      await sleep(200);

      const audioOptions = document.querySelectorAll('.ytp-menuitem-label');
      const originalOption = Array.from(audioOptions).find(el =>
        el.innerText.toLowerCase().includes('(original)')
      );

      if (
        originalOption &&
        !originalOption.closest('.ytp-menuitem[aria-checked="true"]')
      ) {
        originalOption.click();
        console.log('✅ Audio reverted to original track.');
      } else {
        if (document.querySelector('.ytp-panel-menu')) settingsButton.click();
      }
    } catch (error) {
      console.error('⚠️ Audio revert failed:', error);
      if (document.querySelector('.ytp-panel-menu'))
        document.querySelector('button.ytp-settings-button')?.click();
    }
  }

  // ======================
  // FEATURE: COUNT SUBSCRIPTIONS
  // ======================
  async function countSubscriptions() {
    const bannerCss = Object.entries(config.bannerStyle)
      .map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`)
      .join(';');
    const tempBanner = document.createElement('div');
    tempBanner.id = 'sub-count-loading';
    tempBanner.textContent = '⏳ Loading all channels, please wait...';
    tempBanner.style.cssText = bannerCss;
    const container =
      document.querySelector('ytd-section-list-renderer, ytd-browse') ||
      document.body;
    container.prepend(tempBanner);

    let lastHeight = 0;
    for (let i = 0; i < config.maxScrollAttempts; i++) {
      window.scrollTo(0, document.documentElement.scrollHeight);
      await sleep(config.scrollDelay);
      if (document.documentElement.scrollHeight === lastHeight) break;
      lastHeight = document.documentElement.scrollHeight;
    }

    tempBanner.remove();
    const channels = document.querySelectorAll(
      'ytd-channel-renderer, ytd-grid-channel-renderer'
    );
    updateBanners(channels.length, bannerCss, container);

    await sleep(100);
    window.scrollTo({
      top: document.documentElement.scrollHeight,
      behavior: config.reducedMotion ? 'auto' : 'smooth',
    });
  }

  function updateBanners(count, bannerCss, container) {
    const createOrUpdate = (id, prepend = false) => {
      let banner = document.getElementById(id);
      if (!banner) {
        banner = document.createElement('div');
        banner.id = id;
        banner.setAttribute('role', 'status');
        banner.setAttribute('aria-live', 'polite');
        container[prepend ? 'prepend' : 'append'](banner);
      }
      banner.style.cssText = bannerCss;
      banner.textContent = `📺 Subscribed Channels: ${count}`;
    };
    createOrUpdate('sub-count-top', true);
    createOrUpdate('sub-count-bottom');
  }

  // ======================
  // FEATURE: TIME REMAINING (FIXED)
  // ======================
  let timeRemainingInterval = null;

  function timeToSeconds(timeString) {
    const parts = timeString.split(':');
    let seconds = 0;
    for (let i = 0; i < parts.length; i++) {
      seconds = seconds * 60 + parseInt(parts[i], 10);
    }
    return seconds;
  }

  function secondsToTime(totalSeconds) {
    if (totalSeconds < 0) totalSeconds = 0;

    const hours = Math.floor(totalSeconds / 3600);
    const minutes = Math.floor((totalSeconds % 3600) / 60);
    const seconds = totalSeconds % 60;

    const parts = [
      hours.toString().padStart(2, '0'),
      minutes.toString().padStart(2, '0'),
      seconds.toString().padStart(2, '0'),
    ];

    return parts.join(':');
  }

  function calculateRemaining(currentTime, totalTime) {
    if (!totalTime) return 'Not initialized';
    const remainingSeconds =
      timeToSeconds(totalTime) - timeToSeconds(currentTime);
    return secondsToTime(remainingSeconds);
  }

  function updateTimeDisplay() {
    const currentTimeElement = document.querySelector('.ytp-time-current');
    const durationElement = document.querySelector('.ytp-time-duration');

    if (!currentTimeElement || !durationElement) return;

    // Get or set the original duration FIRST, before any modifications
    let originalDuration = durationElement.getAttribute(
      'data-original-duration'
    );
    if (!originalDuration) {
      // Store the ORIGINAL unmodified duration
      originalDuration = durationElement.textContent;
      durationElement.setAttribute('data-original-duration', originalDuration);
    }

    const currentTime = currentTimeElement.textContent;
    const remaining = calculateRemaining(currentTime, originalDuration);

    // Now modify the display
    durationElement.textContent = `${originalDuration} => ${remaining}`;
  }

  function startTimeRemaining() {
    if (!config.showTimeRemaining) return;

    stopTimeRemaining();

    setTimeout(() => {
      // Clear the data attribute on new video to force re-initialization
      const durationElement = document.querySelector('.ytp-time-duration');
      if (durationElement) {
        durationElement.removeAttribute('data-original-duration');
      }

      updateTimeDisplay();
      timeRemainingInterval = setInterval(updateTimeDisplay, 500);
      console.log('✅ Time remaining display started.');
    }, 2000);
  }

  function stopTimeRemaining() {
    if (timeRemainingInterval) {
      clearInterval(timeRemainingInterval);
      timeRemainingInterval = null;

      // Clear the modified display when stopping
      const durationElement = document.querySelector('.ytp-time-duration');
      if (durationElement) {
        const originalDuration = durationElement.getAttribute(
          'data-original-duration'
        );
        if (originalDuration) {
          durationElement.textContent = originalDuration;
        }
        durationElement.removeAttribute('data-original-duration');
      }

      console.log('🛑 Time remaining display stopped.');
    }
  }

  // ======================
  // URL AND NAVIGATION HANDLER
  // ======================
  function handleUrl(url) {
    // Always stop time remaining when navigating
    stopTimeRemaining();

    if (url.includes('/shorts/')) {
      redirectShorts(url);
    } else if (channelRegex.test(url)) {
      location.replace(`${url.replace(/\/featured\/?$|$/, '')}/videos`);
    } else if (url.includes('/watch')) {
      setTimeout(() => {
        enableTheater();
        revertToOriginalAudio();
        startTimeRemaining(); // Start time remaining for watch pages
      }, 1000);
    } else if (url.includes('/feed/channels') && config.showSubCount) {
      countSubscriptions().catch(e =>
        console.error('⚠️ Subscription count failed:', e)
      );
    }
  }

  // Use YouTube's custom navigation event for SPAs
  window.addEventListener('yt-navigate-finish', () => {
    handleUrl(location.href);
    const scrollPos = sessionStorage.getItem('ytScrollPos');
    if (scrollPos && location.href.includes('/feed/channels')) {
      window.scrollTo(0, parseInt(scrollPos, 10));
    }
  });

  // Handle initial page load
  handleUrl(location.href);

  // Persist scroll position within the session
  window.addEventListener(
    'scroll',
    () => sessionStorage.setItem('ytScrollPos', window.scrollY),
    { passive: true }
  );

  // Cleanup on page unload
  window.addEventListener('beforeunload', () => {
    stopTimeRemaining();
  });
})();