// ==UserScript==
// @license MIT
// @name Youtube Save/Resume Progress
// @namespace http://tampermonkey.net/
// @version 1.5.5
// @description Have you ever closed a YouTube video by accident, or have you gone to another one and when you come back the video starts from 0? With this extension it won't happen anymore
// @author Costin Alexandru Sandu
// @match https://www.youtube.com/watch*
// @icon https://raw.githubusercontent.com/SaurusLex/YoutubeSaveResumeProgress/refs/heads/master/youtube_save_resume_progress_icon.jpg
// @grant none
// ==/UserScript==
(function () {
"strict";
var configData = {
sanitizer: null,
savedProgressAlreadySet: false,
savingInterval: 2000,
currentVideoId: null,
lastSaveTime: 0,
dependenciesURLs: {
floatingUiCore: "https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]",
floatingUiDom: "https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]",
fontAwesomeIcons:
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css",
},
};
var FontAwesomeIcons = {
trash: ["fa-solid", "fa-trash-can"],
};
function createIcon(iconName, color) {
const icon = document.createElement("i");
const cssClasses = FontAwesomeIcons[iconName];
icon.classList.add(...cssClasses);
icon.style.color = color;
return icon;
}
// ref: https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds
function fancyTimeFormat(duration) {
// Hours, minutes and seconds
const hrs = ~~(duration / 3600);
const mins = ~~((duration % 3600) / 60);
const secs = ~~duration % 60;
// Output like "1:01" or "4:03:59" or "123:03:59"
let ret = "";
if (hrs > 0) {
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
}
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
ret += "" + secs;
return ret;
}
/*function executeFnInPageContext(fn) {
const fnStringified = fn.toString()
return window.eval('(' + fnStringified + ')' + '()')
}*/
function getVideoCurrentTime() {
const player = document.querySelector("#movie_player");
const currentTime = player.getCurrentTime();
return currentTime;
}
function getVideoName() {
const player = document.querySelector("#movie_player");
const videoName = player.getVideoData().title;
return videoName;
}
function getVideoId() {
if (configData.currentVideoId) {
return configData.currentVideoId;
}
const player = document.querySelector("#movie_player");
const id = player.getVideoData().video_id;
return id;
}
function playerExists() {
const player = document.querySelector("#movie_player");
const exists = Boolean(player);
return exists;
}
function setVideoProgress(progress) {
const player = document.querySelector("#movie_player");
player.seekTo(progress);
}
function updateLastSaved(videoProgress) {
const lastSaveEl = document.querySelector(".last-save-info-text");
const lastSaveText = `Last save: ${fancyTimeFormat(videoProgress)}`;
// This is for browsers that support Trusted Types
const lastSaveInnerHtml = configData.sanitizer
? configData.sanitizer.createHTML(lastSaveText)
: lastSaveText;
if (lastSaveEl) {
lastSaveEl.innerHTML = lastSaveInnerHtml;
}
}
function saveVideoProgress() {
const videoProgress = getVideoCurrentTime();
updateLastSaved(videoProgress);
const videoId = getVideoId();
configData.currentVideoId = videoId;
configData.lastSaveTime = Date.now();
const idToStore = "Youtube_SaveResume_Progress-" + videoId;
const progressData = {
videoProgress,
saveDate: Date.now(),
videoName: getVideoName(),
};
window.localStorage.setItem(idToStore, JSON.stringify(progressData));
}
function getSavedVideoList() {
const savedVideoList = Object.entries(window.localStorage).filter(
([key, value]) => key.includes("Youtube_SaveResume_Progress-")
);
return savedVideoList;
}
function getSavedVideoProgress() {
const videoId = getVideoId();
const idToStore = "Youtube_SaveResume_Progress-" + videoId;
const savedVideoData = window.localStorage.getItem(idToStore);
const { videoProgress } = JSON.parse(savedVideoData) || {};
return videoProgress;
}
function videoHasChapters() {
const chaptersSection = document.querySelector(
'.ytp-chapter-container[style=""]'
);
const chaptersSectionDisplay = getComputedStyle(chaptersSection).display;
return chaptersSectionDisplay !== "none";
}
function setSavedProgress() {
const savedProgress = getSavedVideoProgress();
setVideoProgress(savedProgress);
configData.savedProgressAlreadySet = true;
}
// code ref: https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
function waitForElm(selector) {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
// If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}
async function onPlayerElementExist(callback) {
await waitForElm("#movie_player");
callback();
}
function isReadyToSetSavedProgress() {
return (
!configData.savedProgressAlreadySet &&
playerExists() &&
getSavedVideoProgress()
);
}
function insertInfoElement(element) {
const leftControls = document.querySelector(".ytp-left-controls");
leftControls.appendChild(element);
const chaptersContainerElelement = document.querySelector(
".ytp-chapter-container"
);
chaptersContainerElelement.style.flexBasis = "auto";
}
function insertInfoElementInChaptersContainer(element) {
const chaptersContainer = document.querySelector(
'.ytp-chapter-container[style=""]'
);
chaptersContainer.style.display = "flex";
chaptersContainer.appendChild(element);
}
function updateFloatingSettingsUi() {
const settingsButton = document.querySelector(".ysrp-settings-button");
const settingsContainer = document.querySelector(".settings-container");
const { flip, computePosition } = window.FloatingUIDOM;
computePosition(settingsButton, settingsContainer, {
placement: "top",
middleware: [flip()],
}).then(({ x, y }) => {
Object.assign(settingsContainer.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
function setFloatingSettingsUi() {
const settingsButton = document.querySelector(".ysrp-settings-button");
const settingsContainer = document.querySelector(".settings-container");
updateFloatingSettingsUi();
settingsButton.addEventListener("click", () => {
settingsContainer.style.display =
settingsContainer.style.display === "none" ? "flex" : "none";
if (settingsContainer.style.display === "flex") {
updateFloatingSettingsUi();
}
});
}
function createSettingsUI() {
const videos = getSavedVideoList();
const videosCount = videos.length;
const infoElContainer = document.querySelector(".last-save-info-container");
const infoElContainerPosition = infoElContainer.getBoundingClientRect();
const settingsContainer = document.createElement("div");
settingsContainer.classList.add("settings-container");
const settingsContainerHeader = document.createElement("div");
const settingsContainerHeaderTitle = document.createElement("h3");
settingsContainerHeaderTitle.textContent =
"Saved Videos - (" + videosCount + ")";
settingsContainerHeader.style.display = "flex";
settingsContainerHeader.style.justifyContent = "space-between";
const settingsContainerBody = document.createElement("div");
settingsContainerBody.classList.add("settings-container-body");
const settingsContainerBodyStyle = {
display: "flex",
flex: "1",
minHeight: "0",
overflow: "scroll",
};
Object.assign(settingsContainerBody.style, settingsContainerBodyStyle);
const videosList = document.createElement("ul");
videosList.style.display = "flex";
videosList.style.flexDirection = "column";
videosList.style.rowGap = "1rem";
videosList.style.listStyle = "none";
videosList.style.marginTop = "1rem";
videos.forEach((video) => {
const [key, value] = video;
const { videoName } = JSON.parse(value);
const videoEl = document.createElement("li");
const videoElText = document.createElement("span");
videoEl.style.display = "flex";
videoEl.style.alignItems = "center";
videoElText.textContent = videoName;
videoElText.style.flex = "1";
const deleteButton = document.createElement("button");
const trashIcon = createIcon("trash", "#e74c3c");
deleteButton.style.background = "white";
deleteButton.style.border = "rgba(0, 0, 0, 0.3) 1px solid";
deleteButton.style.borderRadius = ".5rem";
deleteButton.style.marginLeft = "1rem";
deleteButton.style.cursor = "pointer";
deleteButton.addEventListener("click", () => {
window.localStorage.removeItem(key);
videosList.removeChild(videoEl);
settingsContainerHeaderTitle.textContent =
"Saved Videos - (" + videosList.children.length + ")";
});
deleteButton.appendChild(trashIcon);
videoEl.appendChild(videoElText);
videoEl.appendChild(deleteButton);
videosList.appendChild(videoEl);
});
const settingsContainerCloseButton = document.createElement("button");
settingsContainerCloseButton.textContent = "x";
settingsContainerCloseButton.addEventListener("click", () => {
settingsContainer.style.display = "none";
});
const settingsContainerStyles = {
all: "initial",
position: "absolute",
fontFamily: "inherit",
flexDirection: "column",
top: "0",
display: "none",
boxShadow: "rgba(0, 0, 0, 0.24) 0px 3px 8px",
border: "1px solid #d5d5d5",
top: infoElContainerPosition.bottom + "px",
left: infoElContainerPosition.left + "px",
padding: "1rem",
width: "50rem",
height: "25rem",
borderRadius: ".5rem",
background: "white",
zIndex: "3000",
};
Object.assign(settingsContainer.style, settingsContainerStyles);
settingsContainerBody.appendChild(videosList);
settingsContainerHeader.appendChild(settingsContainerHeaderTitle);
settingsContainerHeader.appendChild(settingsContainerCloseButton);
settingsContainer.appendChild(settingsContainerHeader);
settingsContainer.appendChild(settingsContainerBody);
document.body.appendChild(settingsContainer);
const savedVideos = getSavedVideoList();
const savedVideosList = document.createElement("ul");
}
function createInfoUI() {
const infoElContainer = document.createElement("div");
infoElContainer.classList.add("last-save-info-container");
const infoElText = document.createElement("span");
const settingsButton = document.createElement("button");
settingsButton.classList.add("ysrp-settings-button");
settingsButton.style.background = "white";
settingsButton.style.border = "rgba(0, 0, 0, 0.3) 1px solid";
settingsButton.style.borderRadius = ".5rem";
settingsButton.style.marginLeft = "1rem";
const infoEl = document.createElement("div");
infoEl.classList.add("last-save-info");
infoElText.textContent = "Last save: Loading...";
infoElText.classList.add("last-save-info-text");
infoEl.appendChild(infoElText);
//infoEl.appendChild(settingsButton)
infoElContainer.style.all = "initial";
infoElContainer.style.fontFamily = "inherit";
infoElContainer.style.fontSize = "1.3rem";
infoElContainer.style.marginLeft = "0.5rem";
infoElContainer.style.display = "flex";
infoElContainer.style.alignItems = "center";
infoEl.style.textShadow = "none";
infoEl.style.background = "white";
infoEl.style.color = "black";
infoEl.style.padding = ".5rem";
infoEl.style.borderRadius = ".5rem";
infoElContainer.appendChild(infoEl);
return infoElContainer;
}
async function onChaptersReadyToMount(callback) {
await waitForElm('.ytp-chapter-container[style=""]');
callback();
}
function addFontawesomeIcons() {
const head = document.getElementsByTagName("HEAD")[0];
const iconsUi = document.createElement("link");
Object.assign(iconsUi, {
rel: "stylesheet",
type: "text/css",
href: configData.dependenciesURLs.fontAwesomeIcons,
});
head.appendChild(iconsUi);
iconsUi.addEventListener("load", () => {
const icon = document.createElement("span");
//const settingsButton = document.querySelector('.ysrp-settings-button')
//settingsButton.appendChild(icon)
//icon.classList.add('fa-solid')
//icon.classList.add('fa-gear')
});
}
function addFloatingUIDependency() {
const floatingUiCore = document.createElement("script");
const floatingUiDom = document.createElement("script");
floatingUiCore.src = configData.dependenciesURLs.floatingUiCore;
floatingUiDom.src = configData.dependenciesURLs.floatingUiDom;
document.body.appendChild(floatingUiCore);
document.body.appendChild(floatingUiDom);
let floatingUiCoreLoaded = false;
let floatingUiDomLoaded = false;
floatingUiCore.addEventListener("load", () => {
floatingUiCoreLoaded = true;
if (floatingUiCoreLoaded && floatingUiDomLoaded) {
setFloatingSettingsUi();
}
});
floatingUiDom.addEventListener("load", () => {
floatingUiDomLoaded = true;
if (floatingUiCoreLoaded && floatingUiDomLoaded) {
setFloatingSettingsUi();
}
});
}
function initializeDependencies() {
addFontawesomeIcons();
// FIXME: floating ui is not working for now
//addFloatingUIDependency()
}
function initializeUI() {
const infoEl = createInfoUI();
insertInfoElement(infoEl);
// createSettingsUI()
initializeDependencies();
onChaptersReadyToMount(() => {
insertInfoElementInChaptersContainer(infoEl);
createSettingsUI();
});
}
function initialize() {
if (
window.trustedTypes &&
window.trustedTypes.createPolicy &&
!window.trustedTypes.defaultPolicy
) {
const sanitizer = window.trustedTypes.createPolicy("default", {
createHTML: (string, sink) => string,
});
configData.sanitizer = sanitizer;
}
onPlayerElementExist(() => {
initializeUI();
if (isReadyToSetSavedProgress()) {
setSavedProgress();
}
});
setInterval(saveVideoProgress, configData.savingInterval);
}
initialize();
})();