Youtube长按倍速脚本

Youtube长按倍速,广告也能倍速/快进跳过。

// ==UserScript==
// @license     AGPL License
// @name        Youtube长按倍速脚本
// @name:en     Youtube long press to speed up
// @namespace   Violentmonkey Scripts
// @match       https://www.youtube.com/*
// @run-at      document-start
// @grant       none
// @version     2.4
// @author      n1nja88888
// @description Youtube长按倍速,广告也能倍速/快进跳过。
// @description:en Youtube long press to speed up, also works for ads.
// ==/UserScript==
'use strict'
const speed = 2
const forward = 5
console.log('n1nja88888 creates this world!')

!function () {
    document._addEventListener = document.addEventListener

    // document中就是keydown控制快进这些
    // keyup存在暂停视频的逻辑
    document.addEventListener = function (type, listener, useCapture = false) {
        if (type === 'keypress' || type === 'keydown')
            listener = keydownOmit(listener, new Set(['ArrowLeft', 'ArrowRight', ' ']))
        else if (type === 'keyup')
            listener = keydownOmit(listener, new Set([' ']))
        this._addEventListener(type, listener, useCapture)
    }

    // 操作同上,对ELement的事件监听器也重写一遍
    Element.prototype._addEventListener = Element.prototype.addEventListener
    Element.prototype.addEventListener = function (type, listener, useCapture = false) {
        if (type === 'keypress' || type === 'keydown')
            listener = keydownOmit(listener, new Set(['ArrowLeft', 'ArrowRight', ' ']))
        else if (type === 'keyup')
            listener = keydownOmit(listener, new Set([' ']))
        this._addEventListener(type, listener, useCapture)
    }
}()
main()

function main() {
    // 倍速定时器
    let timer = null
    //用于记录是否重复按下
    let isPressed = false
    //初始速度,松开按键后是恢复到初始速度
    let initSpeed = -1
    const speedSign = speedSignClosure()
    const forwardSign = forwardSignClosure()
    document._addEventListener('keydown', (e) => {
        if (isCombKey(e))
            return
        const video = getVideo(document.querySelectorAll('video'))
        if (!video)
            return
        // 当前不是在输入框
        if (isActive('input', 'textarea', '#contenteditable-root'))
            return
        switch (e.key) {
            case 'ArrowLeft':
                e.preventDefault()
                video.currentTime -= forward
                forwardSign(false)
                break
            case 'ArrowRight':
                e.preventDefault()
                if (!timer) {
                    isPressed = true
                    timer = setTimeout(() => {
                        if (isPressed) {
                            video.play()
                            initSpeed = video.playbackRate
                            video.playbackRate = speed
                            speedSign()
                        }
                    }, 0.15e3)
                }
                break
            case ' ':
                e.preventDefault()
                video.paused ? video.play() : video.pause()
                break
            // 短视频评论
            case 'c':
            case 'C':
                const comment = document.querySelector('#comments-button button')
                if (!!comment)
                    comment.click()
                break
        }
    })
    document._addEventListener('keyup', (e) => {
        const video = getVideo(document.querySelectorAll('video'))
        if (!!video && e.key === 'ArrowRight'
            && !isActive('input', 'textarea', '#contenteditable-root')) {
            isPressed = false
            clearTimeout(timer)
            timer = null
            if (video.playbackRate === speed) {
                video.playbackRate = initSpeed
                speedSign()
            }
            else {
                video.currentTime += forward
                forwardSign(true)
                const skip = document.querySelector('.ytp-skip-ad-button__text')
                if (!!skip)
                    skip.click()
            }
        }
    })
}
// 判断是否是组合键
function isCombKey(e) {
    return e.ctrlKey || e.shiftkey || e.altKey || e.metaKey
}
// 判断当前页面活动元素
function isActive(...eleSet) {
    for (const ele of eleSet) {
        if (ele instanceof HTMLElement) {
            if (document.activeElement === ele)
                return true
        }
        else {
            switch (ele.charAt(0)) {
                case '.':
                    if (document.activeElement.classList.contains(ele.substring(1)))
                        return true
                    break
                case '#':
                    if (document.activeElement.id.toLowerCase() === ele.substring(1).toLowerCase())
                        return true
                    break
                default:
                    if (document.activeElement.tagName.toLowerCase() === ele.toLowerCase())
                        return true
            }
        }
    }
    return false
}
//改变keydown事件监听器函数 忽略指定key
function keydownOmit(listener, omits) {
    return (...args) => {
        if (!omits.has(args[0].key))
            listener(...args)
    }
}
//获得有src属性的video
function getVideo(list) {
    for (const video of list) {
        if (!!video.getAttribute('src'))
            return video
    }

    return null
}
// 显示倍速
function speedSignClosure() {
    let isModified = false

    return () => {
        const context = document.querySelector('.ytp-speedmaster-overlay')
        if (context) {
            if (!isModified) {
                context.querySelector('.ytp-speedmaster-label').textContent = speed + 'x'
                isModified = true
            }
            context.style.display = context.style.display ? '' : 'none'
        }

    }
}
// 显示快进
function forwardSignClosure() {
    let timeout = null
    let isModified = false
    let height = null
    let width = null
    return (isForward) => {
        const context = document.querySelector('.ytp-doubletap-ui-legacy')
        if (context) {
            clearTimeout(timeout)
            const shadow = context.querySelector('.ytp-doubletap-static-circle')
            if (!isModified) {
                context.querySelector('.ytp-doubletap-tooltip-label').textContent =
                    window.YT_I18N_FORMATTING_DURATION_TIME_SYMBOLS.SECOND.SHORT
                        .replace('#', forward)
                        .match(/{([^{}]*)}/)[1]
                const container = document.querySelector('video').closest('#container')
                height = Math.ceil(container.offsetHeight)
                width = Math.ceil(container.offsetWidth)
                shadow.style.width = '110px'
                shadow.style.height = '110px'
                shadow.style.top = .5 * height + 15 + 'px'
                isModified = true
            }
            const direction = isForward ? 'forward' : 'back'
            // 这里是计算相对位置
            if (isForward)
                shadow.style.left = .8 * width - 30 + 'px'
            else
                shadow.style.left = .1 * width - 15 + 'px'
            context.setAttribute('data-side', direction)

            context.style.display = ''
            context.classList.add('ytp-time-seeking')

            timeout = setTimeout(() => {
                context.style.display = 'none'
                context.classList.remove('ytp-time-seeking')
            }, 0.5e3)
        }
    }
}