YouTube 自动续播

无缝接续播放任何 YouTube 视频,从您上次离开的地方继续观看。此脚本会自动保存您的播放进度,并拥有智能播放列表处理功能:您在播放列表中的进度会被独立保存,不会影响您在其他地方观看同一视频的记录。此外,它还能以独特规则处理 Shorts 和视频预览,并会自动清理过期数据。

// ==UserScript==
// @name                YouTube Auto-Resume
// @name:zh-TW          YouTube 自動續播
// @name:zh-CN          YouTube 自动续播
// @name:ja             YouTube 自動レジューム
// @icon                https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_auto_resume_namespace
// @version             1.6
// @match               *://www.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @exclude             *://music.youtube.com/*
// @exclude             *://studio.youtube.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         Seamlessly continue any YouTube video exactly where you left off. This script automatically saves your playback position and features intelligent playlist handling: your progress within a playlist is saved separately for that playlist, keeping it distinct from your progress on the same video watched elsewhere. It also handles Shorts and video previews with unique rules, and automatically cleans up old data.
// @description:zh-TW   無縫接續播放任何 YouTube 影片,從您上次離開的地方繼續觀看。此腳本會自動儲存您的播放進度,並擁有智慧型播放清單處理功能:您在播放清單中的進度會被獨立儲存,不會影響您在其他地方觀看同部影片的紀錄。此外,它還能以獨特規則處理 Shorts 和影片預覽,並會自動清理過期資料。
// @description:zh-CN   无缝接续播放任何 YouTube 视频,从您上次离开的地方继续观看。此脚本会自动保存您的播放进度,并拥有智能播放列表处理功能:您在播放列表中的进度会被独立保存,不会影响您在其他地方观看同一视频的记录。此外,它还能以独特规则处理 Shorts 和视频预览,并会自动清理过期数据。
// @description:ja      あらゆるYouTube動画を、中断したその場所からシームレスに再生を再開します。このスクリプトは再生位置を自動的に保存し、スマートなプレイリスト処理機能を搭載。プレイリスト内での視聴進捗はそのプレイリスト専用に別途保存され、他の場所で同じ動画を視聴した際の進捗に影響を与えません。また、ショート動画やプレビューも独自のルールで処理し、古いデータは自動でクリーンアップします。
// @homepage            https://gf.qytechs.cn/scripts/526798-youtube-auto-resume
// ==/UserScript==

/*jshint esversion: 11 */

(function () {
    "use strict";

    const MIN_SEEK_DIFFERENCE_SECONDS = 1.5;
    const daysToRemember = 30;
    const daysToRememberShorts = 1;
    const daysToRememberPreviews = 30 / (24 * 60); // 30 minutes

    let useCompatibilityMode = false;
    let isIFrame = false;

    let g_activeCleanup = null;
    let g_wasManualNavigation = false;

    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;

    function getPlaylistWhenReady(playerApi) {
        return new Promise((resolve, reject) => {
            const initialPlaylist = playerApi.getPlaylist();
            if (initialPlaylist && initialPlaylist.length > 0) {
                return resolve(initialPlaylist);
            }
            let hasResolved = false, pollerInterval = null;
            const cleanup = () => {
                window.removeEventListener('yt-playlist-data-updated', startPolling);
                if (pollerInterval) clearInterval(pollerInterval);
            };
            const startPolling = () => {
                if (hasResolved) return;
                let attempts = 0;
                const maxAttempts = 50;
                pollerInterval = setInterval(() => {
                    const playlist = playerApi.getPlaylist();
                    if (playlist && playlist.length > 0) {
                        hasResolved = true; cleanup(); resolve(playlist);
                    } else if (attempts >= maxAttempts) {
                        hasResolved = true; cleanup(); reject(new Error("Playlist not found after event and 5s of polling."));
                    }
                    attempts++;
                }, 100);
            };
            window.addEventListener('yt-playlist-data-updated', startPolling, { once: true });
            setTimeout(() => { if (!hasResolved) startPolling(); }, 1000);
        });
    }

    async function resumePlayback(playerApi, videoId, videoElement, inPlaylist = false, playlistId = '', isManualNav = false) {
        try {
            const keyToFetch = (inPlaylist && !isManualNav) ? playlistId : videoId;
            const playbackStatus = await GMCustomGetValue(keyToFetch);

            if (!playbackStatus?.timestamp) return;

            const storedVideoId = playbackStatus.videoId || videoId;
            const lastPlaybackTime = playbackStatus.timestamp;
            const currentPlaybackTime = playerApi.getCurrentTime();

            if (Math.abs(currentPlaybackTime - lastPlaybackTime) > MIN_SEEK_DIFFERENCE_SECONDS) {
                await new Promise(async (resolve) => {
                    const onSeeked = () => { clearTimeout(seekTimeout); videoElement.removeEventListener('seeked', onSeeked); resolve(); };
                    const seekTimeout = setTimeout(() => { videoElement.removeEventListener('seeked', onSeeked); resolve(); }, 1500);
                    videoElement.addEventListener('seeked', onSeeked, { once: true });

                    if (inPlaylist && !isManualNav && videoId !== storedVideoId) {
                        try {
                            const playlist = await getPlaylistWhenReady(playerApi);
                            const index = playlist.indexOf(storedVideoId);
                            if (index !== -1) {
                                playerApi.playVideoAt(index);
                            }
                        } catch (error) {
                            console.error(`YouTube Auto-Resume: Could not get playlist.`, error);
                        }
                    }

                    playerApi.seekTo(lastPlaybackTime, true);
                    console.log(`%cYouTube Auto-Resume:%c Resuming video %c${storedVideoId}%c to ${Math.round(lastPlaybackTime)}s`, 'font-weight: bold;', '', 'font-weight: bold;', '');
                });
            }
        } catch (error) {
            console.error(`Failed to resume playback: ${error}`);
        }
    }

    function updatePlaybackStatus(playerApi, videoElement, videoType, playlistId = '') {
        try {
            const liveVideoId = playerApi.getVideoData()?.video_id;
            if (!liveVideoId) return;

            const videoDuration = videoElement.duration;
            const currentPlaybackTime = videoElement.currentTime;
            if (isNaN(videoDuration) || isNaN(currentPlaybackTime) || currentPlaybackTime === 0) return;

            const currentPlaybackStatus = {
                timestamp: currentPlaybackTime,
                lastUpdated: Date.now(),
                videoType: videoType,
                videoId: liveVideoId,
            };

            if (playlistId) {
                GMCustomSetValue(playlistId, currentPlaybackStatus);
            } else {
                const isFinished = currentPlaybackTime / videoDuration > 0.99;
                if (isFinished) {
                    GMCustomDeleteValue(liveVideoId);
                } else {
                    GMCustomSetValue(liveVideoId, currentPlaybackStatus);
                }
            }
        } catch (error) {
            console.error(`Failed to update playback status: ${error}`);
        }
    }

    function processVideo(playerContainer, playerApi, videoElement) {
        try {
            if (g_activeCleanup) {
                g_activeCleanup();
            }

            const isManualNav = g_wasManualNavigation;
            g_wasManualNavigation = false;

            const pageUrl = new URL(window.location.href);
            const searchParams = pageUrl.searchParams;
            const initialVideoId = searchParams.get('v') || playerApi.getVideoData()?.video_id;
            if (!initialVideoId) return;

            const isPreview = playerContainer.id === 'inline-player';
            const isLive = playerApi.getVideoData().isLive;
            const timeSpecified = searchParams.has('t');
            if (isLive || timeSpecified) return;

            const inPlaylist = searchParams.has('list');
            const playlistId = searchParams.get('list');

            let videoType = 'regular';
            if (isPreview) {
                videoType = 'preview';
            } else if (inPlaylist) {
                videoType = 'playlist';
            } else if (pageUrl.pathname.startsWith('/shorts/')) {
                videoType = 'short';
            }

            let hasAttemptedResume = false;
            const timeUpdateHandler = async () => {
                if (!hasAttemptedResume) {
                    hasAttemptedResume = true;
                    await resumePlayback(playerApi, initialVideoId, videoElement, inPlaylist, playlistId, isManualNav);
                } else {
                    updatePlaybackStatus(playerApi, videoElement, videoType, playlistId);
                }
            };

            videoElement.addEventListener('timeupdate', timeUpdateHandler, true);

            g_activeCleanup = () => {
                videoElement.removeEventListener('timeupdate', timeUpdateHandler, true);
            };

        } catch (error) {
            console.error(`Failed to process video elements: ${error}`);
        }
    }

    function handleVideoLoad(event) {
        const playerContainer = event.target;
        const playerApi = playerContainer?.player_;
        const videoElement = playerContainer?.querySelector('video');
        if (playerContainer && playerApi && videoElement) {
            processVideo(playerContainer, playerApi, videoElement);
        }
    }

    function handleInitialLoad() {
        const playerContainer = document.querySelector('#movie_player');
        if (playerContainer) {
            const videoElement = playerContainer.querySelector('video');
            if (!videoElement) return;
            processVideo(playerContainer, playerContainer, videoElement);
        }
    }

    async function cleanUpStoredPlaybackStatuses() {
        try {
            const keys = await GMCustomListValues();
            for (const key of keys) {
                const storedPlaybackStatus = await GMCustomGetValue(key);
                if (!storedPlaybackStatus || isNaN(storedPlaybackStatus.lastUpdated)) {
                    await GMCustomDeleteValue(key);
                    continue;
                }

                if (!storedPlaybackStatus.videoId && key.length === 11) {
                    storedPlaybackStatus.videoId = key;
                    await GMCustomSetValue(key, storedPlaybackStatus);
                }

                const videoType = storedPlaybackStatus.videoType || 'regular';
                let daysToExpire;
                switch (videoType) {
                    case 'short': daysToExpire = daysToRememberShorts; break;
                    case 'preview': daysToExpire = daysToRememberPreviews; break;
                    default: daysToExpire = daysToRemember; break;
                }
                const threshold = daysToExpire * 86400 * 1000;
                if (Date.now() - storedPlaybackStatus.lastUpdated > threshold) {
                    await GMCustomDeleteValue(key);
                }
            }
        } catch (error) {
            console.error(`Failed to clean up stored playback statuses: ${error}`);
        }
    }

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

    async function initialize() {
        isIFrame = window.top !== window.self;
        try {
            if (!hasGreasyMonkeyAPI() || isIFrame) return;

            window.addEventListener('pagehide', () => {
                if (g_activeCleanup) {
                    g_activeCleanup();
                    g_activeCleanup = null;
                }
            }, true);

            window.addEventListener('yt-navigate-start', () => {
                g_wasManualNavigation = true;
            }, true);

            await cleanUpStoredPlaybackStatuses();
            setInterval(cleanUpStoredPlaybackStatuses, 300000);
            window.addEventListener('pageshow', handleInitialLoad, true);
            window.addEventListener('yt-player-updated', handleVideoLoad, true);
            window.addEventListener('yt-autonav-pause-player-ended', (event) => {
                const videoId = event.target?.player?.getVideoData()?.video_id;
                if (videoId) GMCustomDeleteValue(videoId);
            }, true);
        } catch (error) {
            console.error(`Error when initializing script: ${error}.`);
        }
    }

    initialize();
})();

QingJ © 2025

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