Invidious save video progress

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.

  1. // ==UserScript==
  2. // @name Invidious save video progress
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2
  5. // @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.
  6. // @author Noruf
  7. // @match https://invidio.us/*
  8. // @match https://invidious.snopyta.org/*
  9. // @match https://invidiou.sh/*
  10. // @match https://yewtu.be/*
  11. // @match https://invidious.toot.koeln/*
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15.  
  16.  
  17. (function() {
  18. 'use strict';
  19. const timestamps = localStorage.timestamps ? JSON.parse(localStorage.timestamps) : {};
  20. let search = window.location.search;
  21. const isVideoPage = window.location.pathname.includes('watch');
  22. const videoId = isVideoPage? window.location.search.match(/v=(.*?)(&|$)/)[1]: ' ';
  23. const instances = ["invidio.us","invidious.snopyta.org","invidiou.sh","yewtu.be","invidious.toot.koeln"].filter(x => x!=window.location.hostname);
  24.  
  25. // onReadyEvent(addOtherSources);
  26. var url = new URL(window.location.href);
  27. var time = url.searchParams.get("t");
  28. if(!time&&timestamps[videoId]){
  29. search = replaceQueryParam('t', timestamps[videoId], search);
  30. window.location.replace(window.location.pathname + search);
  31. }
  32. onReadyEvent(addHistoryButton);
  33. onReadyEvent(changeLinks);
  34. if(isVideoPage) onReadyEvent(videoProgressMain);
  35. onReadyEvent(addCopyYoutubeLinkButton);
  36.  
  37. function replaceQueryParam(param, newval, search) {
  38. const regex = new RegExp("([?;&])" + param + "[^&;]*[;&]?");
  39. const query = search.replace(regex, "$1").replace(/&$/, '');
  40. return (query.length > 2 ? query : "?") + (newval ? "&" + param + "=" + newval : '');
  41. }
  42.  
  43. function onReadyEvent(callback){
  44. // in case the document is already rendered
  45. if (document.readyState!='loading') callback();
  46. // modern browsers
  47. else if (document.addEventListener) document.addEventListener('DOMContentLoaded', callback);
  48. // IE <= 8
  49. else {document.attachEvent('onreadystatechange', function(){
  50. if (document.readyState=='complete') callback();
  51. });}
  52. }
  53.  
  54. function altLinks(altP){
  55. const altLink = document.createElement("a");
  56. altLink.appendChild(document.createTextNode("Alternate source"));
  57. altLink.style.cursor = "pointer";
  58.  
  59. const linkList = document.createElement("div");
  60. linkList.style.display = "none"
  61. for (let inst of instances){
  62. const a = document.createElement("a");
  63. a.href = `https://${inst}/watch?v=${videoId}`;
  64. a.appendChild(document.createTextNode(inst));
  65. linkList.appendChild(a);
  66. linkList.appendChild(document.createElement("br"));
  67. }
  68. altLink.onclick = function(){
  69. linkList.style.display = linkList.style.display=="none"?"block":"none"
  70. }
  71. altP.appendChild(altLink);
  72. altP.appendChild(linkList);
  73.  
  74. }
  75.  
  76. function addCopyYoutubeLinkButton(){
  77. if(!isVideoPage)return;
  78. const a = document.querySelector('a[href*="youtube"');
  79. const p = document.createElement("a");
  80. p.appendChild(document.createTextNode(" copy"));
  81. a.parentElement.append(p);
  82. p.onclick = () => {
  83. copyToClipboard(a.href);
  84. };
  85. p.style.cursor = "pointer";
  86. const altP = document.createElement("p");
  87. altLinks(altP)
  88. const ul = p.parentElement.parentElement;
  89. ul.insertBefore(altP,ul.childNodes[2]);
  90. }
  91.  
  92.  
  93. function addHistoryButton(){
  94. const userfield = document.querySelector('.user-field');
  95. const newdiv = document.createElement('div');
  96. newdiv.className = 'pure-u-1-4';
  97. userfield.prepend(newdiv);
  98. const anchor = document.createElement('a');
  99. anchor.href = '/feed/history';
  100. anchor.className = 'pure-menu-heading';
  101. newdiv.append(anchor);
  102. const i = document.createElement('i');
  103. i.className = 'icon ion-md-time';
  104. anchor.append(i);
  105. }
  106. function changeLinks(){
  107. const thumbnails = document.querySelectorAll('div.thumbnail');
  108. thumbnails.forEach(t =>{
  109. const a = t.parentElement;
  110. const href = a.href;
  111. if(!href.includes("watch"))return;
  112. const id = href.match(/v=(.*?)(&|$)/)[1];
  113. if(timestamps[id]){
  114. a.href = `${href}&t=${timestamps[id]}s`;
  115. }
  116. if(isVideoPage)return;
  117. const YT = replaceQueryParam('list', '', href).replace(window.location.host,'youtube.com');
  118. const copy = document.createElement("a");
  119. copy.appendChild(document.createTextNode("copy"));
  120. const open = document.createElement("a");
  121. open.href = YT;
  122. open.appendChild(document.createTextNode("open"));
  123. const div = document.createElement('h5');
  124. div.style['text-align'] = 'right';
  125. div.style['margin-top'] = '-5%';
  126. div.append('YT link: ',copy,' ',open);
  127. a.parentElement.append(div);
  128. copy.onclick = () => {
  129. copyToClipboard(YT);
  130. };
  131. });
  132. }
  133.  
  134. function videoProgressMain (){
  135. const player = document.querySelector('video');
  136. player.onpause = () => {saveProgress(false)};
  137. window.addEventListener('beforeunload', function (e) {
  138. saveProgress(false);
  139. e.returnValue = ''; // Chrome requires returnValue to be set.
  140. });
  141. const saveToClipboard = document.createElement("BUTTON");
  142. saveToClipboard.className = "pure-button";
  143. saveToClipboard.appendChild(document.createTextNode("Save To Clipboard"))
  144. document.querySelector('#subscribe').parentElement.appendChild(saveToClipboard);
  145. const message = document.createElement("span");
  146. document.querySelector('#genre').parentElement.appendChild(message);
  147. saveToClipboard.onclick = () => {
  148. saveProgress(true);
  149. }
  150. function saveProgress(doCopy){
  151. const time = Math.floor(document.querySelector('video').currentTime);
  152. if(isNaN(player.duration))return;
  153. if(doCopy){
  154. copyToClipboard(getURL(time));
  155. }
  156. timestamps[videoId] = time;
  157. if(time < 60 || player.duration - time < 60) {
  158. delete timestamps[videoId];
  159. message.innerHTML = `Timestamp not saved!`;
  160. } else{
  161. message.innerHTML = `Saved at ${convertSeconds(time)}`;
  162. }
  163. history.replaceState( {} , '', replaceQueryParam('t',time,window.location.pathname + window.location.search));
  164. localStorage.timestamps = JSON.stringify(timestamps);
  165. }
  166.  
  167. function getURL(seconds){
  168. return `${window.location.origin}/watch?v=${videoId}&t=${seconds}s`;
  169. }
  170. function convertSeconds(seconds){
  171. return new Date(seconds * 1000).toISOString().substr(11, 8);
  172. }
  173. }
  174.  
  175.  
  176. function copyToClipboard(text) {
  177. const textArea = document.createElement("textarea");
  178. textArea.value = text;
  179. textArea.style.position = 'fixed';
  180. textArea.style.top = 0;
  181. textArea.style.left = 0;
  182. textArea.style.width = '2em';
  183. textArea.style.height = '2em';
  184. textArea.style.padding = 0;
  185. textArea.style.border = 'none';
  186. textArea.style.outline = 'none';
  187. textArea.style.boxShadow = 'none';
  188. textArea.style.background = 'transparent';
  189. document.body.appendChild(textArea);
  190. textArea.focus();
  191. textArea.select();
  192. try {
  193. const successful = document.execCommand('copy');
  194. const msg = successful ? 'successful' : 'unsuccessful';
  195. console.log('Copying text command was ' + msg);
  196. } catch (err) {
  197. console.error('Oops, unable to copy', err);
  198. }
  199. document.body.removeChild(textArea);
  200. }
  201. })();