Youtube - Resumer

Store video.currentTime locally

目前为 2023-11-07 提交的版本。查看 最新版本

// ==UserScript==
// @name         Youtube - Resumer
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  Store video.currentTime locally
// @author       You
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==


function l(...args){
    console.log('[Resumer]', ...args)
}

function videoId(url=document.URL){
    return new URL(url).searchParams.get('v')
}

let lastTimeInSeconds
function save(video, id){
    const seconds = Math.floor(video.currentTime)
    if(lastTimeInSeconds != seconds){ //save less often
        let completion = video.currentTime / video.duration
        GM.setValue(id, video.currentTime)
        GM.setValue(id + '-completion', completion)
    }
    lastTimeInSeconds = seconds
}

function findVideo(onVideoFound){
    const observer = new MutationObserver((mutations, observer) => {
        //Keep trying to find video
        let video = document.querySelector('video')
        if(video){
            onVideoFound(video)
            observer.disconnect() //video found
        }
    })

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

let id = videoId() //if you use the miniplayer the url no longer includes the video id
function listen(){
    findVideo(video => {
        let lastSrc

        l('video', video)
        video.addEventListener('timeupdate', () => {
            l('timeupdate')
            //Video source is '' and duration is NaN when going back to the home page
            //When loading a new video, the event is fired with currentTime 0 and duration NaN
            if(video.src && !isNaN(video.duration)){
                l(id, lastId, video.src, lastSrc)
                if(id){
                    save(video, id)
                    lastSrc = video.src
                }else if(video.src === lastSrc){ //in case you click another video while using the miniplayer
                    save(video, lastId) //save even if in miniplayer
                }
            }
        })
    })
}

async function resume(){
    findVideo(async video => {
        id = videoId() // set id here because in firefox the url changes before navigate-finish completes
        let lastTime = await GM.getValue(id)
        if(lastTime){
            l('resuming', id, video.currentTime, lastTime)
            video.currentTime = lastTime
        }else{
            l('new', video.currentTime)
        }
    })
}

function cleanUrl(){
    //Remove t paramater when opening a video that had a progress bar
    let url = new URL(document.URL)
    url.searchParams.delete('t')
    window.history.replaceState(null, null, url)
}

function addProgressBar(overlays, width){
    let parent = document.createElement('div')
    parent.innerHTML = '<ytd-thumbnail-overlay-resume-playback-renderer class="style-scope ytd-thumbnail"><!--css-build:shady--><div id="progress" class="style-scope ytd-thumbnail-overlay-resume-playback-renderer"></div></ytd-thumbnail-overlay-resume-playback-renderer>'
    overlays.appendChild(parent.firstChild)
    let progress = overlays.querySelector('#progress')
    styleProgressBar(progress, width)
}

function styleProgressBar(progress, width){
    progress.style.width = `${width}%`
    progress.style.backgroundColor = 'blue'
}

function progressBars(){
    let related = document.querySelector('#related')
    //Add progress bars in the related section
    const observer = new MutationObserver(async (mutationsList, observer) => {
        for(let mutation of mutationsList){
            if(mutation.target.id === 'overlays'){
                let href = mutation.target.parentElement.parentElement.parentElement.parentElement.querySelector('a').href
                let id = videoId(href)
                let completion = await GM.getValue(id + '-completion')
                if(completion){
                    let width = parseInt(completion * 100)
                    let progress = mutation.target.querySelector('#progress')
                    if(progress){
                        styleProgressBar(progress, width)
                    }else{
                        addProgressBar(mutation.target, width)
                    }
                }
            }
        }
    })
    observer.observe(related, {childList:true, subtree:true})
}

let listening = false //the video element exists even if you go back to the home page, so no need to read event listeners
let lastId //don't resume if going back to same page from miniplayer

//Event for each page change
document.addEventListener("yt-navigate-finish", async () => {
    l('navigate-finish', lastId, videoId())
    //video page
    if(videoId() && lastId !== videoId()) {
        lastId = videoId()
        cleanUrl()
        resume()
        //Add listeners once
        if(!listening){
            l('listening')
            listen()
            progressBars()
            listening = true
        }
    }
})