Greasy Fork is available in English.
Locally saves video progress on invidio.us and adds some more functonality such as: link to watch history, links to alternate invious instances, copying invidious links with timestamp, quick copy/open youtube link from list view.
// ==UserScript==
// @name Invidious save video progress
// @namespace http://tampermonkey.net/
// @version 0.2
// @description Locally saves video progress on invidio.us and adds some more functonality such as: link to watch history, links to alternate invious instances, copying invidious links with timestamp, quick copy/open youtube link from list view.
// @author Noruf
// @match https://invidio.us/*
// @match https://invidious.snopyta.org/*
// @match https://invidiou.sh/*
// @match https://yewtu.be/*
// @match https://invidious.toot.koeln/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const timestamps = localStorage.timestamps ? JSON.parse(localStorage.timestamps) : {};
let search = window.location.search;
const isVideoPage = window.location.pathname.includes('watch');
const videoId = isVideoPage? window.location.search.match(/v=(.*?)(&|$)/)[1]: ' ';
const instances = ["invidio.us","invidious.snopyta.org","invidiou.sh","yewtu.be","invidious.toot.koeln"].filter(x => x!=window.location.hostname);
// onReadyEvent(addOtherSources);
var url = new URL(window.location.href);
var time = url.searchParams.get("t");
if(!time&×tamps[videoId]){
search = replaceQueryParam('t', timestamps[videoId], search);
window.location.replace(window.location.pathname + search);
}
onReadyEvent(addHistoryButton);
onReadyEvent(changeLinks);
if(isVideoPage) onReadyEvent(videoProgressMain);
onReadyEvent(addCopyYoutubeLinkButton);
function replaceQueryParam(param, newval, search) {
const regex = new RegExp("([?;&])" + param + "[^&;]*[;&]?");
const query = search.replace(regex, "$1").replace(/&$/, '');
return (query.length > 2 ? query : "?") + (newval ? "&" + param + "=" + newval : '');
}
function onReadyEvent(callback){
// in case the document is already rendered
if (document.readyState!='loading') callback();
// modern browsers
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback);
// IE <= 8
else {document.attachEvent('onreadystatechange', function(){
if (document.readyState=='complete') callback();
});}
}
function altLinks(altP){
const altLink = document.createElement("a");
altLink.appendChild(document.createTextNode("Alternate source"));
altLink.style.cursor = "pointer";
const linkList = document.createElement("div");
linkList.style.display = "none"
for (let inst of instances){
const a = document.createElement("a");
a.href = `https://${inst}/watch?v=${videoId}`;
a.appendChild(document.createTextNode(inst));
linkList.appendChild(a);
linkList.appendChild(document.createElement("br"));
}
altLink.onclick = function(){
linkList.style.display = linkList.style.display=="none"?"block":"none"
}
altP.appendChild(altLink);
altP.appendChild(linkList);
}
function addCopyYoutubeLinkButton(){
if(!isVideoPage)return;
const a = document.querySelector('a[href*="youtube"');
const p = document.createElement("a");
p.appendChild(document.createTextNode(" copy"));
a.parentElement.append(p);
p.onclick = () => {
copyToClipboard(a.href);
};
p.style.cursor = "pointer";
const altP = document.createElement("p");
altLinks(altP)
const ul = p.parentElement.parentElement;
ul.insertBefore(altP,ul.childNodes[2]);
}
function addHistoryButton(){
const userfield = document.querySelector('.user-field');
const newdiv = document.createElement('div');
newdiv.className = 'pure-u-1-4';
userfield.prepend(newdiv);
const anchor = document.createElement('a');
anchor.href = '/feed/history';
anchor.className = 'pure-menu-heading';
newdiv.append(anchor);
const i = document.createElement('i');
i.className = 'icon ion-md-time';
anchor.append(i);
}
function changeLinks(){
const thumbnails = document.querySelectorAll('div.thumbnail');
thumbnails.forEach(t =>{
const a = t.parentElement;
const href = a.href;
if(!href.includes("watch"))return;
const id = href.match(/v=(.*?)(&|$)/)[1];
if(timestamps[id]){
a.href = `${href}&t=${timestamps[id]}s`;
}
if(isVideoPage)return;
const YT = replaceQueryParam('list', '', href).replace(window.location.host,'youtube.com');
const copy = document.createElement("a");
copy.appendChild(document.createTextNode("copy"));
const open = document.createElement("a");
open.href = YT;
open.appendChild(document.createTextNode("open"));
const div = document.createElement('h5');
div.style['text-align'] = 'right';
div.style['margin-top'] = '-5%';
div.append('YT link: ',copy,' ',open);
a.parentElement.append(div);
copy.onclick = () => {
copyToClipboard(YT);
};
});
}
function videoProgressMain (){
const player = document.querySelector('video');
player.onpause = () => {saveProgress(false)};
window.addEventListener('beforeunload', function (e) {
saveProgress(false);
e.returnValue = ''; // Chrome requires returnValue to be set.
});
const saveToClipboard = document.createElement("BUTTON");
saveToClipboard.className = "pure-button";
saveToClipboard.appendChild(document.createTextNode("Save To Clipboard"))
document.querySelector('#subscribe').parentElement.appendChild(saveToClipboard);
const message = document.createElement("span");
document.querySelector('#genre').parentElement.appendChild(message);
saveToClipboard.onclick = () => {
saveProgress(true);
}
function saveProgress(doCopy){
const time = Math.floor(document.querySelector('video').currentTime);
if(isNaN(player.duration))return;
if(doCopy){
copyToClipboard(getURL(time));
}
timestamps[videoId] = time;
if(time < 60 || player.duration - time < 60) {
delete timestamps[videoId];
message.innerHTML = `Timestamp not saved!`;
} else{
message.innerHTML = `Saved at ${convertSeconds(time)}`;
}
history.replaceState( {} , '', replaceQueryParam('t',time,window.location.pathname + window.location.search));
localStorage.timestamps = JSON.stringify(timestamps);
}
function getURL(seconds){
return `${window.location.origin}/watch?v=${videoId}&t=${seconds}s`;
}
function convertSeconds(seconds){
return new Date(seconds * 1000).toISOString().substr(11, 8);
}
}
function copyToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = 0;
textArea.style.left = 0;
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = 0;
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
const msg = successful ? 'successful' : 'unsuccessful';
console.log('Copying text command was ' + msg);
} catch (err) {
console.error('Oops, unable to copy', err);
}
document.body.removeChild(textArea);
}
})();