YouTube - SmartSponsorBlock

Automatically skip sponsor segments in YouTube videos using SponsorBlock API

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouTube - SmartSponsorBlock
// @description  Automatically skip sponsor segments in YouTube videos using SponsorBlock API
// @namespace    http://tampermonkey.net/
// @icon         https://cdn-icons-png.flaticon.com/64/2504/2504965.png
// @version      0.0.5
// @author       rxm
// @match        https://www.youtube.com/*
// @grant        GM_xmlhttpRequest
// @connect      api.sponsor.ajay.app
// @license      MIT
// ==/UserScript==

(() => {
    'use strict';

    /* ================= CONFIG ================= */

    const SB_API = 'https://api.sponsor.ajay.app/api/skipSegments';

    // Only skip true sponsor segments
    const SKIP_CATEGORIES = ['sponsor'];

    // Small buffer to avoid skipping into another segment
    const SKIP_PADDING = 0.35;

    /* ================= STATE ================= */

    const segmentCache = new Map(); // videoId -> segments[]
    let currentVideoId = null;
    let currentVideoEl = null;

    /* ================= UTILS ================= */

    const log = (...args) => console.debug('[SmartSponsorBlock]', ...args);

    function getVideoId() {
        try {
            const url = new URL(location.href);

            // Normal videos
            if (url.searchParams.get('v')) {
                return url.searchParams.get('v');
            }

            // Shorts
            const shorts = location.pathname.match(/\/shorts\/([^/?#]+)/);
            if (shorts) {
                return shorts[1];
            }

            // Fallback
            const meta = document.querySelector('meta[itemprop="videoId"]');
            if (meta) {
                return meta.content;
            }
        } catch {}
        return null;
    }

    function getVideoElement() {
        return document.querySelector('video');
    }

    /* ================= SPONSORBLOCK ================= */

    function fetchSegments(videoId, callback) {
        if (segmentCache.has(videoId)) {
            callback(segmentCache.get(videoId));
            return;
        }

        const url =
            `${SB_API}?videoID=${encodeURIComponent(videoId)}` +
            `&categories=${encodeURIComponent(JSON.stringify(SKIP_CATEGORIES))}`;

        GM_xmlhttpRequest({
            method: 'GET',
            url,
            headers: { Accept: 'application/json' },
            onload: res => {
                try {
                    const data = JSON.parse(res.responseText);
                    const segments = Array.isArray(data)
                        ? data
                              .map(d => ({
                                  start: d.segment[0],
                                  end: d.segment[1],
                                  category: d.category
                              }))
                              .filter(s => s.end > s.start)
                              .sort((a, b) => a.start - b.start)
                        : [];

                    // Merge overlaps
                    const merged = [];
                    for (const s of segments) {
                        const last = merged[merged.length - 1];
                        if (!last || s.start > last.end) {
                            merged.push({ ...s });
                        } else {
                            last.end = Math.max(last.end, s.end);
                        }
                    }

                    segmentCache.set(videoId, merged);
                    callback(merged);
                } catch (e) {
                    log('Failed to parse SponsorBlock response', e);
                    callback([]);
                }
            },
            onerror: () => callback([]),
            ontimeout: () => callback([])
        });
    }

    /* ================= SKIP LOGIC ================= */

    function attachSkipper(videoId) {
        const video = getVideoElement();
        if (!video || !videoId) return;

        // Same element & same video → nothing to do
        if (video === currentVideoEl && videoId === currentVideoId) return;

        currentVideoEl = video;
        currentVideoId = videoId;

        fetchSegments(videoId, segments => {
            if (!segments.length) return;

            let nextIndex = 0;

            const skipIfNeeded = () => {
                const t = video.currentTime;

                while (nextIndex < segments.length) {
                    const seg = segments[nextIndex];
                    if (t >= seg.start && t < seg.end) {
                        video.currentTime = +(seg.end + SKIP_PADDING).toFixed(2);
                        log(`Skipped sponsor: ${seg.start} → ${seg.end}`);
                        nextIndex++;
                    } else if (t < seg.start) {
                        break;
                    } else {
                        nextIndex++;
                    }
                }
            };

            const onSeeking = () => {
                nextIndex = segments.findIndex(s => video.currentTime < s.end);
                if (nextIndex === -1) nextIndex = segments.length;

                const inside = segments.find(
                    s => video.currentTime >= s.start && video.currentTime < s.end
                );

                if (inside) {
                    video.currentTime = +(inside.end + SKIP_PADDING).toFixed(2);
                    log('Seeked into sponsor, skipped');
                    nextIndex = segments.indexOf(inside) + 1;
                }
            };

            video.addEventListener('timeupdate', skipIfNeeded);
            video.addEventListener('seeking', onSeeking);

            // Cleanup if YouTube replaces the video element
            const watcher = setInterval(() => {
                if (!document.contains(video) || getVideoElement() !== video) {
                    video.removeEventListener('timeupdate', skipIfNeeded);
                    video.removeEventListener('seeking', onSeeking);
                    clearInterval(watcher);
                    currentVideoEl = null;
                    currentVideoId = null;
                }
            }, 1500);
        });
    }

    /* ================= PAGE OBSERVER ================= */

    function handleNavigation() {
        setTimeout(() => {
            const videoId = getVideoId();
            if (videoId) attachSkipper(videoId);
        }, 500); // allow player to initialize
    }

    function observePage() {
        document.addEventListener('yt-navigate-finish', handleNavigation);
        document.addEventListener('yt-page-data-updated', handleNavigation);

        // Fallback (YouTube sometimes misses events)
        setInterval(handleNavigation, 3000);
    }

    observePage();
})();