YouTube Chapter Navigation (Shift + Arrows)

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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 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();
})();