YouTube Chapter Navigation (Shift + Arrows)

Hold Shift and use ← / → to jump between chapters. Works with both manual and auto-generated chapters.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         YouTube Chapter Navigation (Shift + Arrows)
// @namespace    -
// @version      1.0
// @description  Hold Shift and use ← / → to jump between chapters. Works with both manual and auto-generated chapters.
// @author       MrGoatsy
// @match        https://www.youtube.com/watch*
// @grant        none
// @license      CC-BY-NC-ND-4.0
// ==/UserScript==

(() => {
    'use strict';

    // --- Configuration ---
    const DEBUG = false;
    // If you are more than this many seconds into a chapter, the first press of ArrowLeft will rewind to its start.
    // A second press (or a press within this threshold) will go to the previous chapter.
    const REWIND_THRESHOLD = 2.0;

    // --- Script State (resets on new video) ---
    let state = {
        player: null,
        chapters: null, // This will be our cache
        isReady: false
    };

    const log = (...args) => DEBUG && console.log('[YT-ChapterNav]', ...args);

    /**
     * Fetches chapter timestamps, trying the official API first and falling back to visual scraping.
     * @param {HTMLDivElement} player - The #movie_player element.
     * @returns {number[]|null} Sorted array of chapter start times in seconds.
     */
    const fetchChapters = (player) => {
        try { // API method
            const chapters = player.getVideoData()?.chapters;
            if (Array.isArray(chapters) && chapters.length > 1) {
                log('Chapters found via API.');
                return chapters.map(c => (c.chapterRenderer?.timeRangeStartMillis ?? 0) / 1000).sort((a, b) => a - b);
            }
        } catch {}

        try { // Scrape method
            const progressBar = document.querySelector('.ytp-progress-bar');
            const markers = document.querySelectorAll('.ytp-chapter-hover-container');
            const duration = player.getDuration();
            if (!progressBar || markers.length < 2 || !duration) return null;
            const totalWidth = progressBar.clientWidth;
            if (!totalWidth) return null;

            log('Chapters found via progress bar scraping.');
            const times = [0];
            let accumulatedWidth = 0;
            for (let i = 0; i < markers.length - 1; i++) {
                accumulatedWidth += markers[i].clientWidth;
                times.push((accumulatedWidth / totalWidth) * duration);
            }
            return [...new Set(times)].sort((a, b) => a - b);
        } catch {
            return null;
        }
    };

    /**
     * Handles the keydown event for chapter navigation.
     * @param {KeyboardEvent} e
     */
    const onKeydown = (e) => {
        // --- Navigation Logic ---
        // Check if Shift is held, the player is ready, and an arrow key is pressed.
        if (!state.isReady || !e.shiftKey || !['ArrowRight', 'ArrowLeft'].includes(e.key)) {
            return;
        }

        const activeElement = document.activeElement;
        const isTyping = activeElement?.isContentEditable || ['INPUT', 'TEXTAREA'].includes(activeElement?.tagName);
        if (isTyping) return;

        // Prevent default browser actions for Shift+Arrow (e.g., text selection)
        e.preventDefault();
        e.stopPropagation();

        // Fetch and cache chapters on the first use
        if (state.chapters === null) {
            state.chapters = fetchChapters(state.player) || [];
        }
        if (state.chapters.length === 0) return log('No chapters to navigate.');

        const currentTime = state.player.getCurrentTime();
        const currentIndex = state.chapters.findLastIndex(t => t <= currentTime);
        if (currentIndex === -1) return;

        if (e.key === 'ArrowRight') {
            if (currentIndex < state.chapters.length - 1) {
                state.player.seekTo(state.chapters[currentIndex + 1], true);
            }
        } else { // ArrowLeft
            const currentChapterStartTime = state.chapters[currentIndex];
            if (currentTime > currentChapterStartTime + REWIND_THRESHOLD && currentIndex > 0) {
                state.player.seekTo(currentChapterStartTime, true);
            } else {
                const prevChapterTime = (currentIndex > 0) ? state.chapters[currentIndex - 1] : 0;
                state.player.seekTo(prevChapterTime, true);
            }
        }
    };

    /**
     * Resets state and waits for the player to be ready on a new page/video.
     */
    const initializeForPage = () => {
        log('Initializing for new video...');
        state = { player: null, chapters: null, isReady: false };

        const interval = setInterval(() => {
            const player = document.getElementById('movie_player');
            if (player && typeof player.getDuration === 'function' && player.getDuration() > 0) {
                clearInterval(interval);
                state.player = player;
                state.isReady = true;
                log('Player is ready.');
            }
        }, 300);
    };

    // --- Entry Point ---
    document.addEventListener('keydown', onKeydown, true);
    document.addEventListener('yt-navigate-finish', initializeForPage);
    initializeForPage();
})();