Greasy Fork is available in English.

Auto Scroll YouTube Shorts

Automatically scrolls to the next YouTube Shorts and Facebook Reels when the current one finishes or if a specific ad class is detected.

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Auto Scroll YouTube Shorts
// @namespace    facelook.hk
// @version      1.10
// @description  Automatically scrolls to the next YouTube Shorts and Facebook Reels when the current one finishes or if a specific ad class is detected.
// @author       FacelookHK
// @match        https://www.youtube.com/*
// @match        https://www.facebook.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let lastCurrentTime = -1;
    let lastPlayedTime = 0;
    let lastVideoElement = null;
    let lastVideoSrc = "";
    let lastMoveTime = 0;
    const MOVE_COOLDOWN = 2000;

    function canMove() {
        return Date.now() - lastMoveTime > MOVE_COOLDOWN;
    }

    let moveTimer = null;
    function delayedMove() {
        if (moveTimer) return;
        moveTimer = setTimeout(() => { moveTimer = null; moveToNextShort(); }, 750);
    }

    function moveToNextShort() {
        if (!canMove()) return;
        lastMoveTime = Date.now();

        // Reset YouTube state
        lastCurrentTime = -1;
        lastPlayedTime = 0;
        lastVideoElement = null;
        lastVideoSrc = "";

        const currentUrl = window.location.href;

        if (currentUrl.indexOf('youtube') != -1) {
            const nextButton = document.querySelector('ytd-button-renderer[button-next] button[aria-label="Next video"]');
            if (nextButton) nextButton.click();
            console.log("Moved to next video, button found:", !!nextButton);
            console.log("Moved to next video.");
        } else if (currentUrl.indexOf('facebook') != -1) {
            const nextCard = document.querySelector('[aria-label="Next Card"]');
            if (nextCard) {
                nextCard.click();
                console.log("Moved to next video.");
            }
        }
    }

    let lastFacebookSlider = null;
    function checkFacebookSliderProgress() {
        if (!canMove()) return;
        const slider = document.querySelector('[aria-label="Change Position"][role="slider"]');
        if (!slider) return;
        if (slider !== lastFacebookSlider) {
            lastFacebookSlider = slider;
            console.log("Detecting video end... (Facebook Reels)");
        }

        const currentPosition = parseFloat(slider.getAttribute('aria-valuenow'));
        const maxPosition = parseFloat(slider.getAttribute('aria-valuemax'));

        if (!isNaN(maxPosition) && !isNaN(currentPosition) && maxPosition - currentPosition <= 0.5) {
            console.log(`Facebook video near end (${currentPosition}/${maxPosition}). Moving next.`);
            delayedMove();
        }
    }

    function checkYouTubeVideoProgress() {
        if (!canMove()) return;

        // Get the active reel container - try is-active attribute first, then fall back to the playing video
        let activeReel = document.querySelector('ytd-reel-video-renderer[is-active]');
        if (!activeReel) {
            const playingVideo = Array.from(document.querySelectorAll('ytd-reel-video-renderer video'))
                .find(v => !v.paused && !v.ended);
            if (playingVideo) activeReel = playingVideo.closest('ytd-reel-video-renderer');
        }

        if (!activeReel) return;

        // 0. Immediate Ad Class Detection
        // Checks if the specific ad component exists within the currently active reel
        const adComponent = activeReel.querySelector('.ytwReelsAdCardButtonedViewModelHostIsClickableAdComponent');
        if (adComponent) {
            console.log("Ad class detected. Moving next immediately.");
            moveToNextShort();
            return;
        }

        // Check for sponsored badge
        const sponsoredBadge = activeReel.querySelector('div.yt-badge-shape__text');
        if (sponsoredBadge && sponsoredBadge.textContent.trim() === 'Sponsored') {
            console.log("Sponsored video detected. Moving next immediately.");
            moveToNextShort();
            return;
        }

        const video = activeReel.querySelector('video');

        if (!video) return;

        // Detect new video
        if (video !== lastVideoElement || video.src !== lastVideoSrc) {
            console.log("Detecting video end... (YouTube Shorts)");
            lastVideoElement = video;
            lastVideoSrc = video.src;
            lastPlayedTime = 0;
            lastCurrentTime = -1;
            if (moveTimer) { clearTimeout(moveTimer); moveTimer = null; }
            return;
        }

        const duration = video.duration;
        const currentTime = video.currentTime;

        // 1. Duration Check (Video Finishing)
        if (!isNaN(duration) && duration > 0 && duration - currentTime <= 0.5) {
            console.log(`Video finishing (Time: ${currentTime}/${duration}). Moving next.`);
            delayedMove();
            return;
        }

        // 2. Loop Detection
        const timeJumpedBack = lastPlayedTime > 1 && currentTime < lastPlayedTime - 1;
        if (timeJumpedBack) {
            const wasNearEnd = (duration - lastPlayedTime < 2);
            if (wasNearEnd) {
                console.log(`Video looped (Last: ${lastPlayedTime}, Dur: ${duration}). Moving next.`);
                delayedMove();
                return;
            } else {
                console.log(`Manual replay detected (Last: ${lastPlayedTime}). Not skipping.`);
                lastPlayedTime = currentTime;
                lastCurrentTime = currentTime;
                return;
            }
        }

        lastCurrentTime = currentTime;
        lastPlayedTime = currentTime;
    }

    let activeInterval = null;

    function startInterval() {
        const url = window.location.href;
        const isFacebook = url.indexOf('facebook') != -1;
        const isYouTube = url.indexOf('youtube') != -1;
        const isValidPath = url.indexOf('/reel') != -1 || url.indexOf('/shorts') != -1;

        if (!isValidPath) {
            if (activeInterval) { clearInterval(activeInterval); activeInterval = null; }
            return;
        }

        if (activeInterval) return;

        if (isFacebook) activeInterval = setInterval(checkFacebookSliderProgress, 250);
        else if (isYouTube) activeInterval = setInterval(checkYouTubeVideoProgress, 250);
    }

    let lastUrl = window.location.href;
    new MutationObserver(() => {
        if (window.location.href !== lastUrl) {
            lastUrl = window.location.href;
            if (activeInterval) { clearInterval(activeInterval); activeInterval = null; lastFacebookSlider = null; }
            startInterval();
        }
    }).observe(document, { subtree: true, childList: true });

    startInterval();
})();