Netflix keyboard controls

Use similar controls as on YouTube when watching Netflix (`f` for full screen, `k` to play/pause, `c` for captions, `m` to mute/unmute, and a LOT more)

Устаревшая версия за 24.01.2021. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Netflix keyboard controls
// @namespace    netflix.keyboard
// @version      1.0
// @description  Use similar controls as on YouTube when watching Netflix (`f` for full screen, `k` to play/pause, `c` for captions, `m` to mute/unmute, and a LOT more)
// @match        https://netflix.com/*
// @match        https://www.netflix.com/*
// @grant        none
// @author       https://github.com/nicolasff
// ==/UserScript==

(function() {
    /* global netflix */
    'use strict';
    const debug = false;

    // change these constants if you prefer to use different keys (or change the letter to uppercase if you want the shortcut to require the use of Shift)
    // to disable a particular feature, set the value to null.
    const PLAY_PAUSE_KEY = 'k';
    const PICTURE_IN_PICTURE_KEY = 'p'; // turns picture-in-picture on or off
    const SCRUB_FORWARD_KEY = 'l'; // skips ahead by `SCRUB_DELTA_SECONDS` (10s by default)
    const SCRUB_BACKWARD_KEY = 'j'; // goes back `SCRUB_DELTA_SECONDS` (10s by default)
    const SUBTITLES_ON_OFF_KEY = 'c'; // turns subtitles on or off. see `DEFAULT_SUBTITLES_LANGUAGE` below for a way to pick the language of your choice
    const SUBTITLES_SIZE_KEY = 's';
    const SUBTITLES_NEXT_LANGUAGE_KEY = 'v'; // selects the next subtitles track
    const NEXT_AUDIO_TRACK_KEY = 'a'; // switches audio to the next track
    const MUTE_UNMUTE_KEY = null; // Netflix sets mute/unmute to 'm'. You can use a different key here.
    const VOLUME_UP_KEY = 'ArrowUp';
    const VOLUME_DOWN_KEY = 'ArrowDown';
    const NUMBER_KEYS_ENABLED = true; // press key 0 to jump to 0% (start of the video), 1 for 10%… up to 9 for 90%

    // these constants control the behavior of the shortcut keys above
    const SCRUB_DELTA_SECONDS = 10; // how much to skip forward/backward when using the forward/backward keys
    const VOLUME_DELTA = 0.05; // how much to increase/decrease the volume by (range is 0.0 to 1.0 so 0.05 is 5%)
    const DEFAULT_SUBTITLES_LANGUAGE = 'English'; // change this to have the subtitles key pick a different language. Example values you can use: 'en', 'fr', 'es', 'zh-Hans', 'zh-Hant'...

    /***************************************************************************************************************************************************************************************************/

    /**
     * Gets a nested property inside an object
     */
    function getDeepProperty(obj, props) {
        var cur = obj;
        for (var key of props.split('.')) {
            if (!cur[key]) {
                return null;
            }
            cur = cur[key];
        }
        return cur;
    }

    /**
     * Returns the "Player" object used by the Netflix web app to control video playback.
     */
    function getPlayer() {
        // either through React...
        const ctrl = document.querySelector('.PlayerControls--control-element.progress-control');
        for (var key in ctrl) {
            if (key.startsWith('__reactInternalInstance$')) { // this is how to access the associated React instance
                const player = getDeepProperty(ctrl[key], 'memoizedProps.children.props.player');
                if (player) {
                    return player;
                }
            }
        }
        // or if not found that way, let's try the `netflix` object.
        const apiFunction = getDeepProperty(netflix, 'appContext.state.playerApp.getAPI');
        if (apiFunction) {
            const api = apiFunction();
            return api.videoPlayer.getVideoPlayerBySessionId(api.videoPlayer.getAllPlayerSessionIds()[0]);
        }
    }

    function isBoolean(b) {
        return b === true || b === false;
    }

    /**
     * Returns the subtitles track for a given language.
     * Matches full name (e.g. "English") or a BCP 47 language code (e.g. "en")
     */
    function findSubtitlesTrack(player, language) {
        const tracks = player.getTimedTextTrackList();
        var selectedTrack = null;
        for (var i = 0; i < tracks.length; i++) {
            if ((tracks[i].displayName === language || tracks[i].bcp47 === language) && tracks[i].trackType === 'PRIMARY') {
                return tracks[i];
            }
        }
        return null;
    }

    /**
     * Returns the next size for subtitles
     */
    function nextSubtitlesSize(currentSize) {
        switch(currentSize) {
            case 'SMALL': return 'MEDIUM';
            case 'MEDIUM': return 'LARGE';
            case 'LARGE': return 'SMALL';
            default: // not found somehow
                return 'MEDIUM';
        }
    }

    function switchSubtitles(player, debug) {
        // first, get current track to see if subtitles are currently visible
        const currentTrack = player.getTimedTextTrack();
        var disabledTrack = findSubtitlesTrack(player, 'Off');

        // select language
        var chosenTrack = findSubtitlesTrack(player, DEFAULT_SUBTITLES_LANGUAGE);
        if (!chosenTrack) {
            console.warn('Could not find subtitles in ' + DEFAULT_SUBTITLES_LANGUAGE + (DEFAULT_SUBTITLES_LANGUAGE !== 'English' ? ', defaulting to English' : ''));
            chosenTrack = findSubtitlesTrack(player, 'English');
            if (!chosenTrack) {
                DEFAULT_SUBTITLES_LANGUAGE !== 'English' && console.warn('Could not find subtitles in English either :-/');
                return;
            }
        }
        const currentlyDisabled = (currentTrack.displayName === disabledTrack.displayName);
        debug && console.log('chosen language subtitles track:', chosenTrack, 'currently disabled:', currentlyDisabled);

        // switch
        player.setTimedTextTrack(currentlyDisabled ? chosenTrack : disabledTrack);
    }

    function selectNextAudioTrack(player, debug) {
        const trackList = player.getAudioTrackList();
        const currentTrack = player.getAudioTrack();
        if (!trackList || !currentTrack) {
            console.warn('Could not find the current audio track or the list of audio tracks');
        }

        for (var i = 0; i < trackList.length; i++) {
            if (currentTrack.displayName === trackList[i].displayName) { // found!
                const nextTrack = trackList[(i+1) % trackList.length];
                debug && console.log('Switching audio track to ' + nextTrack.displayName);
                player.setAudioTrack(nextTrack);
                return;
            }
        }
    }

    function selectNextSubtitlesTrack(player, debug) {
        const trackList = player.getTimedTextTrackList();
        const currentTrack = player.getTimedTextTrack();
        if (!trackList || !currentTrack) {
            console.warn('Could not find the current subtitles track or the list of subtitles tracks');
        }

        for (var i = 0; i < trackList.length; i++) {
            if (currentTrack.trackId === trackList[i].trackId) { // found!
                const nextTrack = trackList[(i+1) % trackList.length];
                debug && console.log('Switching subtitles track to ' + nextTrack.displayName);
                player.setTimedTextTrack(nextTrack);
                return;
            }
        }
    }

    addEventListener("keydown", function(e) { // we need `keydown` instead of `keypress` to catch arrow presses
        if (e.ctrlKey || e.altKey) { // return early if any modifier key like Control or Alt is part of the key press
            return;
        }
        const KEYCODE_ZERO = 48; // keycode for character '0'
        const videos = document.getElementsByTagName('video');
        const video = videos && videos.length === 1 ? videos[0] : null;
        const player = getPlayer();
        debug && console.log('Key press:', e);

        if (e.key === PICTURE_IN_PICTURE_KEY) {
            if (document.pictureInPictureElement) {
                document.exitPictureInPicture();
            } else {
                video && video.requestPictureInPicture();
            }
        } else if (!player) {
            console.error('No player object found, please update this script');
            return;
        }
        // from now own, we know we have a `player` instance
        if (e.key === PLAY_PAUSE_KEY) {
            player.getPaused() ? player.play() : player.pause();
        } else if (e.key === SCRUB_FORWARD_KEY) {
            player.seek(Math.min(player.getDuration(), player.getCurrentTime() + SCRUB_DELTA_SECONDS));
        } else if (e.key === SCRUB_BACKWARD_KEY) {
            player.seek(Math.max(0, player.getCurrentTime() - SCRUB_DELTA_SECONDS));
        } else if (e.key === VOLUME_UP_KEY) {
            player.setVolume(Math.min(1.0, player.getVolume() + VOLUME_DELTA));
        } else if (e.key === VOLUME_DOWN_KEY) {
            player.setVolume(Math.max(0.0, player.getVolume() - VOLUME_DELTA));
        } else if (e.key === MUTE_UNMUTE_KEY) {
            if (MUTE_UNMUTE_KEY === 'm') {
                console.warn('Netflix already mutes with "m"');
            } else {
                const muteState = player.getMuted();
                if (isBoolean(muteState)) { // make sure we got a valid state back
                    player.setMuted(!muteState);
                }
            }
        } else if (e.key === NEXT_AUDIO_TRACK_KEY) {
            selectNextAudioTrack(player, debug);
        } else if (e.key === SUBTITLES_NEXT_LANGUAGE_KEY) {
            selectNextSubtitlesTrack(player, debug);
        } else if (NUMBER_KEYS_ENABLED && e.keyCode >= KEYCODE_ZERO && e.keyCode <= KEYCODE_ZERO + 9) {
            player.seek((e.keyCode - KEYCODE_ZERO) * (player.getDuration() / 10.0));
        } else if (e.key === SUBTITLES_ON_OFF_KEY) {
            switchSubtitles(player, debug); // extracted for readability
        } else if (e.key === SUBTITLES_SIZE_KEY) {
            const currentSettings = player.getTimedTextSettings();
            if (currentSettings && currentSettings.size) {
                player.setTimedTextSettings({size: nextSubtitlesSize(currentSettings.size)});
            } else {
                console.warn('Unable to find current subtitles size');
            }
        }
    });
})();