YouTube播放进度记忆

记录和恢复播放进度

// ==UserScript==
// @name         YouTube播放进度记忆
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  记录和恢复播放进度
// @author       hhst
// @match        https://www.youtube.com/watch?v=*
// @match        https://m.youtube.com/watch?v=*
// @match        https://www.youtube.com/
// @match        https://m.youtube.com/
// @run-at       document-start
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 判断是否处在watch页
    const get_page_class = (url) => {
        url = url.toLowerCase()
        if (url.startsWith('https://m.youtube.com') || url.startsWith('https://www.youtube.com')) {
            if (url.includes('shorts')) {
                return 'shorts'
            }
            if (url.includes('watch')) {
                return 'watch'
            }
            if (url.includes('library')) {
                return 'library'
            }
            if (url.includes('subscriptions')) {
                return 'subscriptions'
            }
            if (url.includes('@')) {
                return '@'
            }
            return 'home'
        }
            return 'unknown'
    }

    // return the youtube video id like 'A9oByH9Ci24'
    const get_video_id = (url) => {
        try {
            const match = url.match(/watch\?v=([^&#]+)/)
            return match ? match[1] : null
        } catch (error) {
            console.error('Error getting video ID:', error)
            return null
        }
    }

    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1 && node.classList.contains('video-stream')){
                        console.log("准备记录...")
                        // memory progress
                        node.addEventListener('timeupdate', () => {
                            if (node.currentTime !== 0){
                                GM_setValue('progress-' + get_video_id(location.href), node.currentTime.toString())
                            }
                        })
                    }

                    if (node.id === 'movie_player') {
                        window.last_player_state = -1
                        node.addEventListener('onStateChange', (data) => {
                            /* 根据youtube iframe开发文档: https://developers.google.com/youtube/iframe_api_reference:
                            onStateChange
                            此事件在每次播放器的状态改变时触发。 API传递给事件监听器函数的事件对象的data属性会指定一个与新播放器状态相对应的整数。 可能的值包括:
                            -1(未开始)
                            0(已结束)
                            1(正在播放)
                            2(已暂停)
                            3(正在缓冲)
                            5(视频已插入)
                            */ 
                            if([1, 3].includes(data) && window.last_player_state === -1 && get_page_class(location.href) === 'watch'){
                                console.log("准备恢复...")
                                // resume progress
                                // get the last progress time, default 0
                                const saved_time = GM_getValue('progress-' + get_video_id(location.href)) || '0'
                                console.log("恢复到", saved_time)
                                node.seekTo(parseInt(saved_time))
                            }
                            window.last_player_state = data
                        })
                    }
                })
            }
        }
    })

    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    })

})();