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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
  });
})();