Simple Sponsor Skipper Fixed

Skips annoying intros, sponsors and other YouTube segments using the SponsorBlock API. Fixed config page and safer runtime.

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        Simple Sponsor Skipper Fixed
// @author      mthsk, fixed version
// @homepage    https://codeberg.org/mthsk/userscripts/src/branch/master/simple-sponsor-skipper
// @match       *://m.youtube.com/*
// @match       *://youtu.be/*
// @match       *://www.youtube.com/*
// @match       *://www.youtube-nocookie.com/embed/*
// @match       *://odysee.com/*
// @match       *://yt.artemislena.eu/*
// @match       *://tube.cadence.moe/*
// @match       *://y.com.sb/*
// @match       *://invidious.esmailelbob.xyz/*
// @match       *://invidious.flokinet.to/*
// @match       *://inv.frail.com.br/*
// @match       *://invidious.garudalinux.org/*
// @match       *://invidious.kavin.rocks/*
// @match       *://inv.nadeko.net/*
// @match       *://invidious.namazso.eu/*
// @match       *://iv.nboeck.de/*
// @match       *://invidious.nerdvpn.de/*
// @match       *://youtube.owacon.moe/*
// @match       *://inv.pistasjis.net/*
// @match       *://invidious.projectsegfau.lt/*
// @match       *://inv.bp.projectsegfau.lt/*
// @match       *://inv.in.projectsegfau.lt/*
// @match       *://inv.us.projectsegfau.lt/*
// @match       *://vid.puffyan.us/*
// @match       *://invidious.sethforprivacy.com/*
// @match       *://invidious.slipfox.xyz/*
// @match       *://invidious.snopyta.org/*
// @match       *://inv.vern.cc/*
// @match       *://invidious.weblibre.org/*
// @match       *://youchu.be/*
// @match       *://yewtu.be/*
// @grant       GM.addElement
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.notification
// @grant       GM.openInTab
// @grant       GM.registerMenuCommand
// @grant       GM.xmlHttpRequest
// @allFrames   true
// @connect     sponsor.ajay.app
// @connect     sponsorblock.kavin.rocks
// @connect     sponsorblock.gleesh.net
// @connect     sb.theairplan.com
// @connect     *
// @require     https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @run-at      document-start
// @version     2026.05-fixed
// @license     AGPL-3.0-or-later
// @description Skips annoying intros, sponsors and other YouTube segments using the SponsorBlock API. Fixed config page and safer runtime.
// @namespace   https://greasyfork.org/users/751327
// ==/UserScript==

(async function () {
    "use strict";

    const SCRIPT_NAME = "Simple Sponsor Skipper";

    const DEFAULT_SETTINGS = {
        categories: [
            "preview",
            "sponsor",
            "outro",
            "music_offtopic",
            "selfpromo",
            "poi_highlight",
            "interaction",
            "intro"
        ],
        upvotes: -2,
        notifications: true,
        disable_hashing: false,
        instance: "sponsor.ajay.app",
        darkmode: -1
    };

    const CATEGORY_OPTIONS = [
        { id: "sponsor", label: "Skip sponsor segments" },
        { id: "intro", label: "Skip intro segments" },
        { id: "outro", label: "Skip outro segments" },
        { id: "interaction", label: "Skip interaction reminder segments" },
        { id: "selfpromo", label: "Skip self-promotion segments" },
        { id: "preview", label: "Skip preview segments" },
        { id: "music_offtopic", label: "Skip non-music segments in music videos" },
        { id: "filler", label: "Skip filler segments, very aggressive" }
    ];

    const INSTANCE_OPTIONS = [
        { value: "sponsor.ajay.app", label: "sponsor.ajay.app (Official)" },
        { value: "sponsorblock.kavin.rocks", label: "sponsorblock.kavin.rocks" },
        { value: "sponsorblock.gleesh.net", label: "sponsorblock.gleesh.net" },
        { value: "sb.theairplan.com", label: "sb.theairplan.com" }
    ];

    const PLAYER_SELECTOR = [
        "#movie_player video",
        "video#player_html5_api",
        "video#player",
        "video#video",
        "video#vjs_video_3_html5_api",
        "video"
    ].join(", ");

    let s3settings = null;
    let activeVideoId = "";
    let activeCleanup = null;
    let navigationTimer = null;

    if (typeof GM.registerMenuCommand === "undefined") {
        GM.registerMenuCommand = function () {};
    }

    if (typeof GM.notification === "undefined") {
        GM.notification = function (notification) {
            console.log(`${SCRIPT_NAME}: ${notification.title || ""} ${notification.text || ""}`);
        };
    }

    function log(message) {
        console.log(`${new Date().toTimeString().split(" ")[0]} - ${SCRIPT_NAME}: ${message}`);
    }

    function isIntegerLike(value) {
        return !Number.isNaN(value) &&
            parseInt(Number(value), 10) == value &&
            !Number.isNaN(parseInt(value, 10));
    }

    function normalizeCategories(categories, notifications) {
        if (isIntegerLike(categories)) {
            const bitmask = Number(categories);
            const converted = [];

            if (bitmask & 2) converted.push("intro");
            if (bitmask & 4) converted.push("outro");
            if (bitmask & 8) converted.push("interaction");
            if (bitmask & 16) converted.push("selfpromo");
            if (bitmask & 32) converted.push("preview");
            if (bitmask & 64) converted.push("music_offtopic");
            if (bitmask & 128) converted.push("filler");
            if ((bitmask & 1) || converted.length === 0) converted.push("sponsor");
            if (notifications) converted.push("poi_highlight");

            return [...new Set(converted)];
        }

        if (!Array.isArray(categories)) {
            return [...DEFAULT_SETTINGS.categories];
        }

        const valid = new Set([
            "sponsor",
            "intro",
            "outro",
            "interaction",
            "selfpromo",
            "preview",
            "music_offtopic",
            "filler",
            "poi_highlight"
        ]);

        const cleaned = categories.filter(category => valid.has(category));

        if (!cleaned.length) {
            cleaned.push("sponsor");
        }

        return [...new Set(cleaned)];
    }

    function sanitizeInstance(value) {
        let instance = String(value || DEFAULT_SETTINGS.instance).trim();

        instance = instance.replace(/\s*\(Official\)\s*/gi, "");
        instance = instance.replace(/^https?:\/\//i, "");
        instance = instance.split("/")[0];
        instance = instance.split("?")[0];
        instance = instance.split("#")[0];
        instance = instance.trim();

        if (!/^[a-z0-9.-]+(?::[0-9]+)?$/i.test(instance)) {
            return DEFAULT_SETTINGS.instance;
        }

        return instance || DEFAULT_SETTINGS.instance;
    }

    async function loadSettings() {
        let stored = await GM.getValue("s3settings");

        const isPaleMoon =
            navigator.userAgent.toLowerCase().includes("pale moon") ||
            navigator.userAgent.toLowerCase().includes("mypal") ||
            navigator.userAgent.toLowerCase().includes("male poon");

        if (!stored || typeof stored !== "object" || Object.keys(stored).length === 0) {
            stored = { ...DEFAULT_SETTINGS };
            if (isPaleMoon) stored.disable_hashing = true;
            await GM.setValue("s3settings", stored);
            log("Default settings saved.");
        }

        const normalized = {
            ...DEFAULT_SETTINGS,
            ...stored,
            categories: normalizeCategories(stored.categories, stored.notifications),
            upvotes: Number.isFinite(Number(stored.upvotes)) ? parseInt(stored.upvotes, 10) : DEFAULT_SETTINGS.upvotes,
            notifications: Boolean(stored.notifications),
            disable_hashing: Boolean(stored.disable_hashing),
            instance: sanitizeInstance(stored.instance),
            darkmode: [-1, 0, 1].includes(parseInt(stored.darkmode, 10)) ? parseInt(stored.darkmode, 10) : -1
        };

        await GM.setValue("s3settings", normalized);
        return normalized;
    }

    function openConfigUrl() {
        const url = new URL(location.href);
        url.hostname = url.hostname.replace("youtube-nocookie.com", "youtube.com");

        if (url.pathname.startsWith("/embed/")) {
            const id = url.pathname.replace("/embed/", "").split("/")[0];
            url.pathname = "/watch";
            url.search = `?v=${encodeURIComponent(id)}`;
        }

        if (url.pathname.startsWith("/v/")) {
            const id = url.pathname.replace("/v/", "").split("/")[0];
            url.pathname = "/watch";
            url.search = `?v=${encodeURIComponent(id)}`;
        }

        url.hash = "s3config";
        return url.toString();
    }

    function isConfigPage() {
        return location.hash.toLowerCase() === "#s3config";
    }

    function onReady(callback) {
        if (document.readyState === "loading") {
            document.addEventListener("DOMContentLoaded", callback, { once: true });
        } else {
            callback();
        }
    }

    function add(parent, tag, attrs = {}) {
        return GM.addElement(parent, tag, attrs);
    }

    function applyTheme(value) {
        const dark =
            value === 1 ||
            (value === -1 &&
                window.matchMedia &&
                window.matchMedia("(prefers-color-scheme: dark)").matches);

        document.body.classList.toggle("dark-theme", dark);
    }

    function renderConfig() {
        const docHtml = document.createElement("html");
        const head = add(docHtml, "head");

        add(head, "meta", { charset: "utf-8" });
        add(head, "title", { textContent: `${SCRIPT_NAME} Configuration` });

        add(head, "style", {
            textContent: `
                :root {
                    color-scheme: light dark;
                    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
                }

                body {
                    margin: 0;
                    min-height: 100vh;
                    background: #f6f7f9;
                    color: #111827;
                    display: grid;
                    place-items: center;
                    padding: 24px;
                    box-sizing: border-box;
                }

                body.dark-theme {
                    background: #0b0f19;
                    color: #f9fafb;
                }

                main {
                    width: min(720px, 100%);
                    background: white;
                    border-radius: 20px;
                    padding: 28px;
                    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
                    box-sizing: border-box;
                }

                body.dark-theme main {
                    background: #111827;
                    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45);
                }

                h1 {
                    margin: 0 0 6px;
                    font-size: 28px;
                }

                p {
                    margin: 0 0 24px;
                    color: #6b7280;
                }

                body.dark-theme p {
                    color: #9ca3af;
                }

                form {
                    display: grid;
                    gap: 18px;
                }

                fieldset {
                    border: 1px solid #e5e7eb;
                    border-radius: 14px;
                    padding: 16px;
                    display: grid;
                    gap: 12px;
                }

                body.dark-theme fieldset {
                    border-color: #374151;
                }

                legend {
                    padding: 0 8px;
                    font-weight: 700;
                }

                .row {
                    display: flex;
                    align-items: center;
                    gap: 10px;
                }

                .field {
                    display: grid;
                    gap: 6px;
                }

                label {
                    cursor: pointer;
                }

                input[type="text"],
                input[type="number"],
                select {
                    width: 100%;
                    box-sizing: border-box;
                    padding: 10px 12px;
                    border: 1px solid #d1d5db;
                    border-radius: 10px;
                    background: white;
                    color: #111827;
                    font: inherit;
                }

                body.dark-theme input[type="text"],
                body.dark-theme input[type="number"],
                body.dark-theme select {
                    background: #030712;
                    border-color: #374151;
                    color: #f9fafb;
                }

                .buttons {
                    display: flex;
                    gap: 12px;
                    flex-wrap: wrap;
                }

                button {
                    border: 0;
                    border-radius: 999px;
                    padding: 10px 16px;
                    font: inherit;
                    font-weight: 700;
                    cursor: pointer;
                }

                button.primary {
                    background: #111827;
                    color: white;
                }

                body.dark-theme button.primary {
                    background: #f9fafb;
                    color: #111827;
                }

                button.secondary {
                    background: #e5e7eb;
                    color: #111827;
                }

                body.dark-theme button.secondary {
                    background: #374151;
                    color: #f9fafb;
                }

                .hint {
                    font-size: 13px;
                    color: #6b7280;
                    margin: 0;
                }

                body.dark-theme .hint {
                    color: #9ca3af;
                }
            `
        });

        const body = add(docHtml, "body");
        const main = add(body, "main");

        add(main, "h1", { textContent: SCRIPT_NAME });
        add(main, "p", { textContent: "Configure which SponsorBlock segments should be skipped." });

        const form = add(main, "form");

        const categoryFieldset = add(form, "fieldset");
        add(categoryFieldset, "legend", { textContent: "Segments" });

        for (const option of CATEGORY_OPTIONS) {
            const row = add(categoryFieldset, "div", { className: "row" });
            add(row, "input", {
                type: "checkbox",
                id: option.id
            });
            add(row, "label", {
                for: option.id,
                textContent: option.label
            });
        }

        const behaviorFieldset = add(form, "fieldset");
        add(behaviorFieldset, "legend", { textContent: "Behavior" });

        const upvotesField = add(behaviorFieldset, "div", { className: "field" });
        add(upvotesField, "label", {
            for: "upvotes",
            textContent: "Minimum segment votes"
        });
        add(upvotesField, "input", {
            type: "number",
            id: "upvotes",
            step: "1"
        });
        add(upvotesField, "p", {
            className: "hint",
            textContent: "-2 is permissive. Higher values skip only more trusted segments."
        });

        const notificationsRow = add(behaviorFieldset, "div", { className: "row" });
        add(notificationsRow, "input", {
            type: "checkbox",
            id: "notifications"
        });
        add(notificationsRow, "label", {
            for: "notifications",
            textContent: "Enable desktop notifications and point-of-interest hints"
        });

        const hashingRow = add(behaviorFieldset, "div", { className: "row" });
        add(hashingRow, "input", {
            type: "checkbox",
            id: "disable_hashing"
        });
        add(hashingRow, "label", {
            for: "disable_hashing",
            textContent: "Disable video ID hashing, only needed for old browser compatibility"
        });

        const instanceField = add(behaviorFieldset, "div", { className: "field" });
        add(instanceField, "label", {
            for: "instance",
            textContent: "SponsorBlock database instance"
        });

        add(instanceField, "input", {
            id: "instance",
            type: "text",
            list: "instances",
            autocomplete: "off",
            spellcheck: "false"
        });

        const dataList = add(instanceField, "datalist", { id: "instances" });
        for (const instance of INSTANCE_OPTIONS) {
            add(dataList, "option", {
                value: instance.value,
                label: instance.label
            });
        }

        const themeField = add(behaviorFieldset, "div", { className: "field" });
        add(themeField, "label", {
            for: "darkmode",
            textContent: "Theme"
        });

        const themeSelect = add(themeField, "select", { id: "darkmode" });
        add(themeSelect, "option", { value: "-1", textContent: "Auto" });
        add(themeSelect, "option", { value: "0", textContent: "Light" });
        add(themeSelect, "option", { value: "1", textContent: "Dark" });

        const buttons = add(form, "div", { className: "buttons" });
        add(buttons, "button", {
            type: "button",
            id: "btnsave",
            className: "primary",
            textContent: "Save settings"
        });
        add(buttons, "button", {
            type: "button",
            id: "btnclose",
            className: "secondary",
            textContent: "Close"
        });
        add(buttons, "button", {
            type: "button",
            id: "btnreset",
            className: "secondary",
            textContent: "Reset defaults"
        });

        document.documentElement.replaceWith(docHtml);
        document.title = `${SCRIPT_NAME} Configuration`;

        for (const option of CATEGORY_OPTIONS) {
            document.getElementById(option.id).checked = s3settings.categories.includes(option.id);
        }

        document.getElementById("upvotes").value = String(s3settings.upvotes);
        document.getElementById("notifications").checked = Boolean(s3settings.notifications);
        document.getElementById("disable_hashing").checked = Boolean(s3settings.disable_hashing);
        document.getElementById("instance").value = sanitizeInstance(s3settings.instance);
        document.getElementById("darkmode").value = String(s3settings.darkmode);

        applyTheme(s3settings.darkmode);

        document.getElementById("darkmode").addEventListener("change", event => {
            applyTheme(parseInt(event.target.value, 10));
        });

        document.getElementById("btnsave").addEventListener("click", saveConfig);
        document.getElementById("btnreset").addEventListener("click", resetConfig);
        document.getElementById("btnclose").addEventListener("click", closeConfig);
    }

    async function saveConfig() {
        const categories = [];

        for (const option of CATEGORY_OPTIONS) {
            if (document.getElementById(option.id).checked) {
                categories.push(option.id);
            }
        }

        if (!categories.length) {
            categories.push("sponsor");
        }

        const notifications = document.getElementById("notifications").checked;

        if (notifications) {
            categories.push("poi_highlight");
        }

        s3settings = {
            categories: [...new Set(categories)],
            upvotes: parseInt(document.getElementById("upvotes").value, 10),
            notifications,
            disable_hashing: document.getElementById("disable_hashing").checked,
            instance: sanitizeInstance(document.getElementById("instance").value),
            darkmode: parseInt(document.getElementById("darkmode").value, 10)
        };

        if (!Number.isFinite(s3settings.upvotes)) {
            s3settings.upvotes = DEFAULT_SETTINGS.upvotes;
        }

        await GM.setValue("s3settings", s3settings);

        const button = document.getElementById("btnsave");
        button.textContent = "Saved";
        button.disabled = true;

        setTimeout(() => {
            button.textContent = "Save settings";
            button.disabled = false;
        }, 1500);

        log("Settings saved.");
    }

    async function resetConfig() {
        s3settings = { ...DEFAULT_SETTINGS };
        await GM.setValue("s3settings", s3settings);
        renderConfig();
        log("Settings reset.");
    }

    function closeConfig() {
        const url = new URL(location.href);
        url.hash = "";
        location.replace(url.toString());
    }

    function durationString(seconds) {
        const safeSeconds = Math.max(0, Math.floor(Number(seconds) || 0));
        const hours = Math.floor(safeSeconds / 3600);
        const minutes = Math.floor((safeSeconds % 3600) / 60);
        const secs = safeSeconds % 60;

        if (hours > 0) {
            return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
        }

        return `${minutes}:${String(secs).padStart(2, "0")}`;
    }

    function shuffleCopy(array) {
        const copy = [...array];

        for (let i = copy.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [copy[i], copy[j]] = [copy[j], copy[i]];
        }

        return copy;
    }

    async function sha256(message) {
        const msgBuffer = new TextEncoder().encode(message);
        const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
        return Array
            .from(new Uint8Array(hashBuffer))
            .map(byte => byte.toString(16).padStart(2, "0"))
            .join("");
    }

    function requestJson(url) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: "GET",
                url,
                headers: {
                    Accept: "application/json"
                },
                timeout: 15000,
                onload(response) {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response.responseText);
                    } else if (response.status === 404) {
                        resolve("[]");
                    } else {
                        reject(new Error(`HTTP ${response.status}`));
                    }
                },
                onerror() {
                    reject(new Error("Network error"));
                },
                ontimeout() {
                    reject(new Error("Request timed out"));
                }
            });
        });
    }

    async function fetchSegments(videoId) {
        const instance = sanitizeInstance(s3settings.instance);
        const categories = shuffleCopy(s3settings.categories);
        const encodedCategories = encodeURIComponent(JSON.stringify(categories));

        let url;

        if (s3settings.disable_hashing) {
            url = `https://${instance}/api/skipSegments?videoID=${encodeURIComponent(videoId)}&categories=${encodedCategories}`;
            const text = await requestJson(url);
            const segments = JSON.parse(text);
            return [{
                videoID: videoId,
                segments: Array.isArray(segments) ? segments : []
            }];
        }

        const hash = await sha256(videoId);
        url = `https://${instance}/api/skipSegments/${hash.substring(0, 4)}?categories=${encodedCategories}`;

        const text = await requestJson(url);
        const parsed = JSON.parse(text);

        return Array.isArray(parsed) ? parsed : [];
    }

    function getVotes(segment) {
        return Number(segment.votes ?? segment.upvotes ?? 0);
    }

    function processSegments(segments) {
        if (!Array.isArray(segments)) {
            return {
                skipSegments: [],
                highlightTime: null,
                beforeCount: 0
            };
        }

        const sorted = [...segments].sort((a, b) => {
            const aStart = Number(a.segment?.[0] ?? 0);
            const bStart = Number(b.segment?.[0] ?? 0);
            return aStart - bStart;
        });

        const skipSegments = [];
        let highlight = null;
        let highlightVotes = s3settings.upvotes - 1;

        for (const segment of sorted) {
            if (!Array.isArray(segment.segment) || segment.segment.length < 2) {
                continue;
            }

            const start = Number(segment.segment[0]);
            const end = Number(segment.segment[1]);
            const votes = getVotes(segment);

            if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {
                continue;
            }

            if (segment.category === "poi_highlight") {
                if (votes > highlightVotes) {
                    highlight = segment;
                    highlightVotes = votes;
                }
                continue;
            }

            if (votes < s3settings.upvotes) {
                continue;
            }

            const last = skipSegments[skipSegments.length - 1];

            if (last && last.segment[1] >= start) {
                if (end > last.segment[1]) {
                    last.segment[1] = end;
                    last.category = last.category === segment.category ? last.category : "combined";
                }
                continue;
            }

            skipSegments.push({
                ...segment,
                segment: [start, end]
            });
        }

        return {
            skipSegments,
            highlightTime: highlight ? Number(highlight.segment[0]) : null,
            beforeCount: segments.length
        };
    }

    function getVideoIdFromUrl() {
        const url = new URL(location.href);
        const host = url.hostname;

        if (host === "youtu.be") {
            return url.pathname.slice(1).split("/")[0] || "";
        }

        if (url.searchParams.has("v")) {
            return url.searchParams.get("v") || "";
        }

        if (url.pathname.startsWith("/embed/")) {
            return url.pathname.replace("/embed/", "").split("/")[0] || "";
        }

        if (url.pathname.startsWith("/v/")) {
            return url.pathname.replace("/v/", "").split("/")[0] || "";
        }

        return "";
    }

    function waitForPlayer(timeoutMs = 12000) {
        return new Promise(resolve => {
            const start = Date.now();

            const timer = setInterval(() => {
                const player = document.querySelector(PLAYER_SELECTOR);

                if (player && player.readyState >= 1) {
                    clearInterval(timer);
                    resolve(player);
                    return;
                }

                if (Date.now() - start > timeoutMs) {
                    clearInterval(timer);
                    resolve(null);
                }
            }, 100);
        });
    }

    function getFavicon() {
        const icon = document.head?.querySelector("link[rel='icon'][href], link[rel='shortcut icon'][href]");
        return icon ? icon.href : undefined;
    }

    function notify(notification) {
        if (!s3settings.notifications || window.self !== window.top) {
            return;
        }

        try {
            GM.notification({
                silent: true,
                timeout: 5000,
                ...notification
            });
        } catch (error) {
            log(`Notification failed: ${error.message}`);
        }
    }

    async function go(videoId) {
        if (!videoId || videoId === activeVideoId) {
            return;
        }

        activeVideoId = videoId;

        if (typeof activeCleanup === "function") {
            activeCleanup();
            activeCleanup = null;
        }

        log(`New video ID: ${videoId}`);

        let apiResponse;

        try {
            apiResponse = await fetchSegments(videoId);
        } catch (error) {
            log(`SponsorBlock request failed: ${error.message}`);
            return;
        }

        const matching = apiResponse.find(item => item.videoID === videoId);

        if (!matching) {
            log("No matching SponsorBlock result found.");
            return;
        }

        const {
            skipSegments,
            highlightTime,
            beforeCount
        } = processSegments(matching.segments);

        const player = await waitForPlayer();

        if (!player) {
            log("No video player found.");
            return;
        }

        const favicon = getFavicon();

        const showHighlightNotification = () => {
            if (highlightTime !== null && player.currentTime < highlightTime) {
                notify({
                    title: "Point of interest found",
                    text: `This video has a highlight at ${durationString(highlightTime)}.\nClick to skip to it.\n\u00AD\n${document.title} (${videoId})`,
                    image: favicon,
                    onclick: () => {
                        player.currentTime = highlightTime;
                    }
                });
            }
        };

        if (!skipSegments.length) {
            showHighlightNotification();

            const playHandler = showHighlightNotification;
            player.addEventListener("play", playHandler);

            activeCleanup = () => {
                player.removeEventListener("play", playHandler);
            };

            return;
        }

        let newDuration = Number(skipSegments[0].videoDuration || player.duration || 0);

        if (Number.isFinite(newDuration) && newDuration > 0) {
            for (const segment of skipSegments) {
                newDuration -= segment.segment[1] - segment.segment[0];
            }
        }

        notify({
            title: "Skippable segments found",
            text:
                `Received ${beforeCount} segment${beforeCount === 1 ? "" : "s"}, ` +
                `${skipSegments.length} active after processing.` +
                (Number.isFinite(newDuration) && newDuration > 0 ? `\nDuration after skips: ${durationString(newDuration)}` : "") +
                (highlightTime !== null ? `\nHighlight: ${durationString(highlightTime)}` : "") +
                `\n\u00AD\n${document.title} (${videoId})`,
            image: favicon,
            onclick: highlightTime !== null
                ? () => {
                    player.currentTime = highlightTime;
                }
                : undefined
        });

        let index = 0;
        let previousTime = -1;

        const timeUpdateHandler = () => {
            const currentId = getVideoIdFromUrl();

            if (currentId && currentId !== videoId) {
                if (typeof activeCleanup === "function") {
                    activeCleanup();
                    activeCleanup = null;
                }
                return;
            }

            const currentTime = player.currentTime;

            if (currentTime < previousTime) {
                index = skipSegments.findIndex(segment => currentTime < segment.segment[1]);
                if (index < 0) index = skipSegments.length;
            }

            while (index < skipSegments.length && currentTime >= skipSegments[index].segment[1]) {
                index++;
            }

            if (
                !player.paused &&
                index < skipSegments.length &&
                currentTime >= skipSegments[index].segment[0] &&
                currentTime < skipSegments[index].segment[1]
            ) {
                const segment = skipSegments[index];
                player.currentTime = segment.segment[1];

                notify({
                    title: `Skipped ${segment.category.replace("music_offtopic", "non-music").replace("selfpromo", "self-promotion")}`,
                    text: `Segment ${index + 1} of ${skipSegments.length}\n\u00AD\n${document.title} (${videoId})`,
                    image: favicon
                });

                log(`Skipped ${segment.category} from ${segment.segment[0]} to ${segment.segment[1]}`);
                index++;
            }

            previousTime = player.currentTime;
        };

        const playHandler = showHighlightNotification;

        player.addEventListener("timeupdate", timeUpdateHandler);
        player.addEventListener("play", playHandler);

        activeCleanup = () => {
            player.removeEventListener("timeupdate", timeUpdateHandler);
            player.removeEventListener("play", playHandler);
        };
    }

    function checkCurrentVideoSoon() {
        clearTimeout(navigationTimer);

        navigationTimer = setTimeout(() => {
            const videoId = getVideoIdFromUrl();

            if (!videoId) {
                activeVideoId = "";
                if (typeof activeCleanup === "function") {
                    activeCleanup();
                    activeCleanup = null;
                }
                return;
            }

            go(videoId);
        }, 250);
    }

    function startWatchers() {
        checkCurrentVideoSoon();

        window.addEventListener("yt-navigate-finish", checkCurrentVideoSoon, true);
        window.addEventListener("yt-page-data-updated", checkCurrentVideoSoon, true);
        window.addEventListener("popstate", checkCurrentVideoSoon, true);
        window.addEventListener("hashchange", () => {
            if (isConfigPage()) {
                renderConfig();
            } else {
                checkCurrentVideoSoon();
            }
        }, true);

        onReady(() => {
            if (!document.body) return;

            const observer = new MutationObserver(() => {
                checkCurrentVideoSoon();
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    s3settings = await loadSettings();

    if (window.self === window.top) {
        GM.registerMenuCommand("Configuration", () => {
            location.href = openConfigUrl();
            setTimeout(() => location.reload(), 50);
        });
    }

    if (isConfigPage() && window.self === window.top) {
        onReady(renderConfig);
        return;
    }

    startWatchers();
})();