Navigation for YouTube Shorts

Navigate YouTube Shorts using your device's next/prev media keys (including headset).

Version au 06/05/2025. Voir la dernière version.

// ==UserScript==
// @name         Navigation for YouTube Shorts
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Navigate YouTube Shorts using your device's next/prev media keys (including headset).
// @author       CHJ85
// @match        https://www.youtube.com/shorts/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Inject a function into the page context
    function inject(fn) {
        const s = document.createElement('script');
        s.textContent = '(' + fn.toString() + ')()';
        document.documentElement.appendChild(s);
        s.remove();
    }

    // The logic to run inside the page
    function pageScript() {
        // 1) Simulate ArrowUp/ArrowDown keypress
        function simulateArrowKey(direction) {
            const key = direction === 'next' ? 'ArrowDown' : 'ArrowUp';
            document.dispatchEvent(new KeyboardEvent('keydown', { key, code: key, bubbles: true }));
            console.log(`[YS Nav] dispatched ${key}`);
        }

        // 2) Clear & re-apply mediaSession action handlers
        function applyHandlers() {
            if (!navigator.mediaSession || !location.pathname.includes('/shorts/')) return;
            navigator.mediaSession.setActionHandler('nexttrack', null);
            navigator.mediaSession.setActionHandler('previoustrack', null);
            navigator.mediaSession.setActionHandler('nexttrack',  () => simulateArrowKey('next'));
            navigator.mediaSession.setActionHandler('previoustrack', () => simulateArrowKey('previous'));
            console.log('[YS Nav] handlers applied');
        }

        // 3) Hook the metadata setter so every time YouTube sets metadata, we re-apply handlers
        const proto = Object.getPrototypeOf(navigator.mediaSession);
        const desc  = Object.getOwnPropertyDescriptor(proto, 'metadata');
        if (desc && desc.set) {
            const origSet = desc.set;
            desc.set = function(meta) {
                origSet.call(this, meta);
                applyHandlers();
            };
            Object.defineProperty(proto, 'metadata', desc);
            console.log('[YS Nav] metadata setter hooked');
        }

        // 4) Kick things off by assigning dummy metadata
        try {
            navigator.mediaSession.metadata = new MediaMetadata({
                title:  'YouTube Short',
                artist: 'Unknown',
                album:  'YouTube Shorts'
            });
        } catch (e) {
            console.warn('[YS Nav] could not assign dummy metadata', e);
        }

        // 5) Listen for the video’s native 'ended' event so loops re-apply handlers
        function hookVideoEnd() {
            const vid = document.querySelector('video');
            if (vid && !vid.__ysNavHooked) {
                vid.__ysNavHooked = true;
                vid.addEventListener('ended', () => {
                    console.log('[YS Nav] video ended—reapplying handlers');
                    setTimeout(applyHandlers, 100);
                });
            }
        }
        // Run once and whenever the DOM changes
        hookVideoEnd();
        new MutationObserver(hookVideoEnd).observe(document.documentElement, { childList: true, subtree: true });

        // 6) SPA navigation hooks (pushState/replaceState + YouTube's event)
        (function hookHistory() {
            const push = history.pushState, replace = history.replaceState;
            const emit  = () => window.dispatchEvent(new Event('locationchange'));
            history.pushState  = function() { push.apply(this, arguments); emit(); };
            history.replaceState = function() { replace.apply(this, arguments); emit(); };
            window.addEventListener('popstate', emit);
        })();
        window.addEventListener('locationchange',   () => setTimeout(applyHandlers, 500));
        window.addEventListener('yt-navigate-finish',() => setTimeout(applyHandlers, 500));

        console.log('[YS Nav] pageScript initialized (v1.7)');
    }

    inject(pageScript);
})();