Greasy Fork is available in English.
Automatically scrolls to the next YouTube Shorts and Facebook Reels when the current one finishes or if a specific ad class is detected.
// ==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();
})();