// ==UserScript== // @name YouTube Auto-Resume // @icon https://www.youtube.com/img/favicon_48.png // @author ElectroKnight22 // @namespace electroknight22_youtube_auto_resume_namespace // @version 1.4.1 // @match *://www.youtube.com/* // @match *://www.youtube-nocookie.com/* // @exclude *://www.youtube.com/live_chat* // @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. // @downloadURL https://update.greasyfork.icu/scripts/526798/YouTube%20Auto-Resume.user.js // @updateURL https://update.greasyfork.icu/scripts/526798/YouTube%20Auto-Resume.meta.js // ==/UserScript== /*jshint esversion: 11 */ (function () { "use strict"; let useCompatibilityMode = false; let isIFrame = 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, videoElement) { if (videoElement.inPlaylist && videoElement.playlistType !== 'WL') return; if (videoElement.timeSpecified || videoElement.resumed || videoElement.isLive) return; try { const playerSize = moviePlayer.getPlayerSize(); if (playerSize.width === 0 || playerSize.height === 0) return; const playbackStatus = await GMCustomGetValue(videoElement.videoId); const lastPlaybackTime = playbackStatus?.timestamp; const currentPlaybackTime = moviePlayer.getCurrentTime(); const timeDiff = Math.abs(currentPlaybackTime - lastPlaybackTime); if (!isNaN(timeDiff) && lastPlaybackTime !== 0 && timeDiff > 1) { moviePlayer.seekTo(lastPlaybackTime, true, { skipBufferingCheck: window.location.pathname === '/' ? true : false, }); videoElement.resumed = true; } } catch (error) { console.error("Failed to resume playback due. " + error); } } function handleVideoLoad(event) { try { const moviePlayer = event.target?.player_; const videoElement = event.target?.querySelector('video'); const newId = moviePlayer?.getVideoData().video_id; videoElement.isPreview = event.target?.id === 'inline-player'; videoElement.videoId = newId; videoElement.isLive = moviePlayer?.getVideoData().isLive; videoElement.resumed = videoElement.init; videoElement.timeSpecified = new URL(window.location.href).searchParams.has('t'); videoElement.inPlaylist = new URL(window.location.href).searchParams.has('list'); videoElement.playlistType = new URLSearchParams(window.location.search).get('list'); processVideo(moviePlayer, videoElement); } catch (error) { console.error("Failed to handle video load. ", error); } } function handleInitialLoad() { try { const moviePlayer = document.querySelector('#movie_player'); const videoElement = moviePlayer?.querySelector('video'); videoElement.videoId = new URL(window.location.href).searchParams.get('v') ?? moviePlayer?.getVideoData().video_id; videoElement.isLive = moviePlayer?.getVideoData().isLive; videoElement.timeSpecified = new URL(window.location.href).searchParams.has('t'); videoElement.inPlaylist = new URL(window.location.href).searchParams.has('list'); videoElement.playlistType = new URLSearchParams(window.location.search).get('list'); videoElement.init = true; processVideo(moviePlayer, videoElement); } catch (error) { console.error("Failed to handle initial load. ", error); } } function updatePlaybackStatus(videoElement) { try { const videoDuration = videoElement.duration; const currentPlaybackTime = videoElement.currentTime; const timeLeft = videoDuration - currentPlaybackTime; if (isNaN(timeLeft)) return; if (timeLeft < 1) { GMCustomDeleteValue(videoElement.videoId); } else { const currentPlaybackStatus = { timestamp: currentPlaybackTime, lastUpdated: Date.now() }; GMCustomSetValue(videoElement.videoId, currentPlaybackStatus); } } catch (error) { throw new Error("Failed to update playback status due to this error. Error: " + error); } } function processVideo(moviePlayer, videoElement) { try { if (videoElement.timeSpecified) return; if (videoElement.autoResumeHandler) return; if (window.location.pathname === '/' && !videoElement.isPreview) return; videoElement.autoResumeHandler = async () => { if (!moviePlayer || !videoElement.videoId) return; await resumePlayback(moviePlayer, videoElement); updatePlaybackStatus(videoElement); }; videoElement.addEventListener('timeupdate', videoElement.autoResumeHandler, true); } catch (error) { console.error("Failed to process video elements. ", error); } } async function cleanUpStoredPlaybackStatuses() { try { const videoIds = await GMCustomListValues(); const threshold = daysToRemember * 86400 * 1000; for (const videoId of videoIds) { const storedPlaybackStatus = await GMCustomGetValue(videoId); if (!storedPlaybackStatus || isNaN(storedPlaybackStatus.lastUpdated) || Date.now() - storedPlaybackStatus.lastUpdated > threshold) { await GMCustomDeleteValue(videoId); } } } catch (error) { throw new 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("Running in compatibility mode."); return true; } return false; } async function initialize() { isIFrame = window.top !== window.self; try { if (!hasGreasyMonkeyAPI()) throw new Error("Did not detect valid Grease Monkey API"); await cleanUpStoredPlaybackStatuses(); if (isIFrame || window.location.pathname !== '/') handleInitialLoad(); 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(); })();