Youtube Keyboard Shortcuts: like/dislike, backup/restore position, change speed, picture-in-picture

Adds keyboard shortcuts [ and ] for liking and disliking videos, B and R to Back up and Restore position, H to use picture-in-picture, { and } to change playback speed.

目前为 2022-05-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         Youtube Keyboard Shortcuts: like/dislike, backup/restore position, change speed, picture-in-picture
// @match        https://www.youtube.com/*
// @description  Adds keyboard shortcuts [ and ] for liking and disliking videos, B and R to Back up and Restore position, H to use picture-in-picture, { and } to change playback speed.
// @author       https://gf.qytechs.cn/en/users/728793-keyboard-shortcuts
// @namespace    http://tampermonkey.net/
// @version      1.1
// @grant        none
// @license      MIT
// @icon         https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://youtube.com&size=128
// ==/UserScript==

/* jshint esversion: 6 */

/**
 * Keyboard shortcuts:
 * [ to like a video
 * ] to dislike it
 * h to enable/disable Picture-in-Picture (PiP)
 * b to back up the current time (where you are in the video)
 * r to jump back to the backed up time
 * shift-] – or "}" – to increase playback speed
 * shift-[ – or "{" – to decrease playback speed
 */

/****************************************************************************************************/
/*                                                                                                  */
/*                               Change these constants to configure the script                     */

const PLAYBACK_RATE_STEP = 0.05; // change playback rate by 5% when pressing the shortcut keys
const SHOW_NOTIFICATIONS = true;
const NOTIFICATIONS_INCLUDE_EMOJIS = true;
const NOTIFICATION_DURATION_MILLIS = 2000; // how long – in milliseconds – to keep a like/dislike notification visible.
const REMOVE_FEEDBACK_SHARED_WITH_CREATOR = true; // if true, remove the notification on dislike that says "feedback shared with the creator"

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

var lastToastElement = null;
function showNotification(message) {
    if (!SHOW_NOTIFICATIONS) {
        return;
    }
    if (lastToastElement !== null) { // delete if still visible
        lastToastElement.remove();
        lastToastElement = null;
    }

    const toast = document.createElement('tp-yt-paper-toast');
    toast.innerText = message;
    toast.classList.add('toast-open');

    const styleProps = {
        outline: 'none',
        position: 'fixed',
        left: '0',
        bottom: '12px',
        maxWidth: '297.547px',
        maxHeight: '48px',
        zIndex: '2202',
        opacity: '1',
    };
    for (const prop in styleProps) {
        toast.style[prop] = styleProps[prop];
    }

    document.body.appendChild(toast);
    lastToastElement = toast;

    // needed otherwise the notification won't show
    setTimeout(() => {
        toast.style.display = 'block';
    }, 0);

    // preserves the animation
    setTimeout(() => {
        toast.style.transform = 'none';
    }, 10);

    setTimeout(() => {
        toast.style.transform = 'translateY(200%)';
    }, Math.max(0, NOTIFICATION_DURATION_MILLIS));
}

function removeBuiltInFeedbackShared(feedbackSharedStartTime) {
    const now = Date.now();
    for (const toastElement of Array.from(document.querySelectorAll('tp-yt-paper-toast'))) {
        if (toastElement.textContent.toLowerCase().includes('feedback shared with the creator')) {
            toastElement.remove();
            return;
        }
    }
    if (now - feedbackSharedStartTime < 1000) {
        const intervalMs = (now - feedbackSharedStartTime < 100) ? 10 : 50; // faster at first
        setTimeout(() => removeBuiltInFeedbackShared(feedbackSharedStartTime), intervalMs);
    }
}

function getVideoId() {
    const url = new URL(location.href);
    return url.searchParams.get('v');
}

function findLikeDislikeButtons() {
    if (!/^\/watch/.test(location.pathname)) {
        return { like: null, dislike: null };
    }

    const infoRenderer = document.getElementsByTagName('ytd-video-primary-info-renderer');
    var like = null, dislike = null;
    if (infoRenderer.length == 1) {
        const buttons = Array.from(infoRenderer[0].getElementsByTagName('button'));
        for (var b of buttons) {
            if (b.hasAttribute('aria-label')) {
                if (b.getAttribute('aria-label').toLowerCase().indexOf('dislike this') !== -1) {
                    dislike = b;
                } else if (b.getAttribute('aria-label').toLowerCase().indexOf('like this') !== -1) {
                    like = b;
                }
            }
        }
        if (!like) {
            like = buttons[4];
        }
        if (!dislike) {
            dislike = buttons[5];
        }
    }
    return { like: like, dislike: dislike };
}

function applyScrubSeconds(event, delta) {
    const video = getVideoElement();
    if (video) {
        video.currentTime += delta;
        event.preventDefault();
    }
}

function getPressedAttribute(button) {
    return button.hasAttribute('aria-pressed') ? button.getAttribute('aria-pressed') : null;
}

function formatTime(timeSec) {
    const hours = Math.floor(timeSec / 3600.0);
    const minutes = Math.floor((timeSec % 3600) / 60);
    const seconds = Math.floor((timeSec % 60));

    return (hours > 0 ? (('' + hours).padStart(2, '0') + ':') : '') +
        ('' + minutes).padStart(2, '0') + ':' +
        ('' + seconds).padStart(2, '0');
}

function formatPlaybackRate(rate) {
    if (rate == Math.floor(rate)) { // integer
        return rate + 'x';
    } else if (rate.toFixed(2).endsWith('0')) { // float, 1 decimal place
        return rate.toFixed(1) + 'x';
    } else { // float, 2 decimal places max
        return rate.toFixed(2) + 'x';
    }
}

function maybePrefixWithEmoji(emoji, message) {
    return NOTIFICATIONS_INCLUDE_EMOJIS ? emoji + ' ' + message : message;
}

function getVideoElement() {
    const videos = Array.from(document.querySelectorAll('video')).filter(v => ! isNaN(v.duration)); // filter the invalid ones
    return videos.length === 0 ? null : videos[0];
}

var pipEnabled = false; // initial value
const observer = new MutationObserver(findLikeDislikeButtons); // find buttons on DOM changes
observer.observe(document.documentElement, { childList: true, subtree: true });

var savedPosition = {};

// add keybindings
addEventListener('keypress', function (e) {
    const tag = e.target.tagName.toLowerCase();
    if (e.target.getAttribute('contenteditable') == 'true' || tag == 'input' || tag == 'textarea') {
        return;
    }

    const player = document.getElementById('movie_player');
    const buttons = findLikeDislikeButtons();
    if (e.code == 'BracketLeft' && (!e.ctrlKey) && (!e.shiftKey) && buttons.like) {
        const likePressed = getPressedAttribute(buttons.like);
        if (likePressed === 'true') {
            showNotification(maybePrefixWithEmoji('😭', 'Removed like from video'));
        } else if (likePressed === 'false') {
            showNotification(maybePrefixWithEmoji('❤️', 'Liked video'));
        }
        buttons.like.click();
    } else if (e.code == 'BracketRight' && (!e.ctrlKey) && (!e.shiftKey) && buttons.dislike) {
        const dislikePressed = getPressedAttribute(buttons.dislike);
        if (dislikePressed === 'true') {
            showNotification(maybePrefixWithEmoji('😐', 'Removed dislike from video'));
        } else if (dislikePressed === 'false') {
            showNotification(maybePrefixWithEmoji('💔', 'Disliked video'));
            if (REMOVE_FEEDBACK_SHARED_WITH_CREATOR) {
                removeBuiltInFeedbackShared(Date.now());
            }
        }
        buttons.dislike.click();
    } else if (e.code == 'KeyH') {
        if (pipEnabled) {
            document.exitPictureInPicture();
        } else {
            const video = getVideoElement();
            video && video.requestPictureInPicture();
        }
        pipEnabled = !pipEnabled;
    } else if (e.code == 'KeyB') { // back up current position
        const video = getVideoElement();
        if (video) {
            const videoId = getVideoId();
            savedPosition = { videoId: videoId, time: video.currentTime };
            showNotification('Saved position: ' + formatTime(savedPosition.time));
        }
    } else if (e.code == 'KeyR') { // restore saved position
        const video = getVideoElement();
        if (video) {
            const videoId = getVideoId();
            if (videoId && savedPosition.videoId && savedPosition.videoId === videoId) {
                video.currentTime = savedPosition.time;
            } else {
                showNotification('No saved timestamp found');
            }
        }
    } else if (player && e.shiftKey && e.code === 'BracketRight') {
        player.setPlaybackRate(player.getPlaybackRate() + PLAYBACK_RATE_STEP);
        showNotification('Playback rate:' + formatPlaybackRate(player.getPlaybackRate()));
    } else if (player && e.shiftKey && e.code === 'BracketLeft') {
        player.setPlaybackRate(player.getPlaybackRate() - PLAYBACK_RATE_STEP);
        showNotification('Playback rate:' + formatPlaybackRate(player.getPlaybackRate()));
    }
});

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址