Greasy Fork

Greasy Fork is available in English.

YouTube Auto-Resume

This script automatically tracks and restores your YouTube playback position. This user script remembers where you left off in any video—allowing you to seamlessly continue watching even after navigating away or reloading the page. It saves your progress for each video for up to 3 days (configurable) and automatically cleans up outdated entries, ensuring a smooth and uninterrupted viewing experience every time.

当前为 2025-02-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                YouTube Auto-Resume
// @icon                https://www.youtube.com/img/favicon_48.png
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_auto_resume_namespace
// @version             1.2.7
// @match               *://*.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @grant               GM.getValue
// @grant               GM.setValue
// @grant               GM.deleteValue
// @grant               GM.listValues
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_deleteValue
// @grant               GM_listValues
// @license             MIT
// @description         This script automatically tracks and restores your YouTube playback position. This user script remembers where you left off in any video—allowing you to seamlessly continue watching even after navigating away or reloading the page. It saves your progress for each video for up to 3 days (configurable) and automatically cleans up outdated entries, ensuring a smooth and uninterrupted viewing experience every time.
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    "use strict";

    let useCompatibilityMode = false;

    // script will save playback time for 3 days by default, edit this value to change. Old data is only cleared on script load. This value will be reset to 3 if the script is updated.
    const daysToRemember = 3;

    const GMCustomGetValue = useCompatibilityMode ? GM_getValue : GM.getValue;
    const GMCustomSetValue = useCompatibilityMode ? GM_setValue : GM.setValue;
    const GMCustomDeleteValue = useCompatibilityMode ? GM_deleteValue : GM.deleteValue;
    const GMCustomListValues = useCompatibilityMode ? GM_listValues : GM.listValues;

    async function resumePlayback(moviePlayer, videoId) {
        try {
            const playbackStatus = await GMCustomGetValue(videoId);
            if (!playbackStatus) return;
            const lastPlaybackTime = playbackStatus.timestamp;
            const timeDiff = Math.abs(moviePlayer.getCurrentTime() - lastPlaybackTime) || 99999999;
            if (!isNaN(lastPlaybackTime) && lastPlaybackTime !== 0 && timeDiff > 1) {
                moviePlayer?.seekTo(lastPlaybackTime);
            }
        } catch (error) {
            throw ("Failed to resume playback due to this error. Error: " + error);
        }
    }

    function handleVideoLoad(event) {
        const moviePlayer = event.target?.player_;
        const videoElement = event.target?.querySelector('video');
        videoElement.videoId = moviePlayer?.getVideoData().video_id;
        videoElement.isLive = moviePlayer?.getVideoData().isLive;
        videoElement.resumed = false;
        console.log('resumed1', videoElement.resumed);
        if (!videoElement.videoId || videoElement.autoResumeHandler) return;
        videoElement.autoResumeHandler = async () => {
            if (!videoElement.resumed && !videoElement.isLive) {
                const playerSize = moviePlayer.getPlayerSize();
                if (playerSize.width === 0 || playerSize.height === 0) return;
                videoElement.resumed = true;
                await resumePlayback(moviePlayer, videoElement.videoId);
            }
            updatePlaybackStatus(videoElement, videoElement.videoId);
        };
        videoElement.addEventListener('timeupdate', videoElement.autoResumeHandler, true);
    }

    function updatePlaybackStatus(videoElement, videoId) {
        if (!videoId) return;
        try {
            const currentPlaybackTime = videoElement.currentTime;
            if (currentPlaybackTime > 0) {
                const currentPlaybackStatus = {
                    timestamp: currentPlaybackTime,
                    lastUpdated: Date.now()
                };
                GMCustomSetValue(videoId, currentPlaybackStatus);
            }
        } catch (error) {
            throw ("Failed to update playback status due to this error. Error: " + error);
        }
    }

    async function cleanUpStoredPlaybackStatuses() {
        try {
            const keys = await GMCustomListValues();
            const threshold = daysToRemember * 24 * 60 * 60 * 1000;
            for (const key of keys) {
                const storedStatus = await GMCustomGetValue(key);
                if (!storedStatus ||
                    typeof storedStatus.lastUpdated !== 'number' ||
                    Date.now() - storedStatus.lastUpdated > threshold ) {
                    await GMCustomDeleteValue(key);
                }
            }
        } catch (error) {
            throw ("Failed to clean up stored playback statuses due to this error. Error: " + error);
        }
    }

    function hasGreasyMonkeyAPI() {
        if (typeof GM !== 'undefined') return true;
        if (typeof GM_info !== 'undefined') {
            useCompatibilityMode = true;
            console.warn("Running in compatibility mode.");
            return true;
        }
        return false;
    }

    async function initialize() {
        try {
            if (!hasGreasyMonkeyAPI()) throw "Did not detect valid Grease Monkey API";
            await cleanUpStoredPlaybackStatuses();
            window.addEventListener('yt-player-updated', (event) => {
                handleVideoLoad(event);
            }, true);
            window.addEventListener('yt-autonav-pause-player-ended', (event) => {
                const videoId = event.target?.player?.getVideoData()?.video_id;
                GMCustomDeleteValue(videoId);
            }, true);
        } catch (error) {
            console.error(`Error when initializing script: ${error}. Aborting script.`);
        }
    }

    initialize();
})();