Youtube - Resumer

Store video.currentTime locally

  1. // ==UserScript==
  2. // @name Youtube - Resumer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Store video.currentTime locally
  6. // @author You
  7. // @match https://www.youtube.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  9. // @grant GM.setValue
  10. // @grant GM.getValue
  11. // @grant GM_addStyle
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15.  
  16. function l(...args){
  17. console.log('[Resumer]', ...args)
  18. }
  19.  
  20. function videoId(url=document.URL){
  21. return new URL(url).searchParams.get('v')
  22. }
  23.  
  24. let lastTimeInSeconds
  25. function save(video, id){
  26. const seconds = Math.floor(video.currentTime)
  27. if(lastTimeInSeconds != seconds){ // save less often
  28. let completion = video.currentTime / video.duration
  29. GM.setValue(id, video.currentTime)
  30. GM.setValue(id + '-completion', completion)
  31. }
  32. lastTimeInSeconds = seconds
  33. }
  34.  
  35. function findVideo(onVideoFound){
  36. const observer = new MutationObserver((mutations, observer) => {
  37. // Keep trying to find video
  38. let video = document.querySelector('video.video-stream')
  39. if(video){
  40. onVideoFound(video)
  41. observer.disconnect()
  42. }
  43. })
  44. observer.observe(document, {childList:true, subtree:true})
  45. }
  46.  
  47.  
  48. let id = videoId() //if you use the miniplayer the url no longer includes the video id
  49. function listen(video){
  50. let lastSrc
  51.  
  52. function handleTimeUpdate(){
  53. //Video source is '' and duration is NaN when going back to the home page
  54. //When loading a new video, the event is fired with currentTime 0 and duration NaN
  55. if(video.src && !isNaN(video.duration)){
  56. l('timeupdate', id, lastId, video.src, lastSrc)
  57. if(id){
  58. save(video, id)
  59. lastSrc = video.src
  60. }else if(video.src === lastSrc){ //in case you click another video while using the miniplayer
  61. save(video, lastId) //save even if in miniplayer
  62. }
  63. }
  64. }
  65.  
  66. video.addEventListener('timeupdate', handleTimeUpdate)
  67. return () => {
  68. video.removeEventListener('timeupdate', handleTimeUpdate)
  69. }
  70. }
  71.  
  72. async function resume(video){
  73. id = videoId() // set id here because in firefox the url changes before navigate-finish completes
  74. let lastTime = await GM.getValue(id)
  75. if(lastTime){
  76. if(lastTime < video.duration - 1){
  77. l('resuming', id, video.currentTime, lastTime)
  78. video.currentTime = lastTime
  79. }else{
  80. l('nearly complete, skipping resume for', id);
  81. }
  82. }else{
  83. l('new video', video.currentTime)
  84. }
  85. }
  86.  
  87. function cleanUrl(){
  88. //Remove t paramater when opening a video that had a progress bar
  89. let url = new URL(document.URL)
  90. url.searchParams.delete('t')
  91. window.history.replaceState(null, null, url)
  92. }
  93.  
  94. let lastId // don't resume if going back to same page from miniplayer
  95.  
  96. // Event for each page change
  97. document.addEventListener("yt-navigate-finish", () => {
  98. l('navigate-finish', lastId, videoId())
  99. // video page
  100. if(videoId() && lastId !== videoId()) {
  101. lastId = videoId()
  102. cleanUrl()
  103.  
  104. let removeListeners
  105. findVideo(video => {
  106. resume(video)
  107.  
  108. // clean previous listeners
  109. if(removeListeners) removeListeners()
  110. removeListeners = listen(video)
  111. })
  112. }
  113. })
  114.  
  115. /////////////////////
  116.  
  117.  
  118. function addProgressBar(thumbnail, completion){
  119. let overlays = thumbnail.querySelector('#overlays')
  120. let existingProgressBar = thumbnail.querySelector('ytd-thumbnail-overlay-resume-playback-renderer')
  121. if(!existingProgressBar) {
  122. let parent = document.createElement('div')
  123. parent.innerHTML = `
  124. <ytd-thumbnail-overlay-resume-playback-renderer class="style-scope ytd-thumbnail">
  125. <div id="progress" class="style-scope ytd-thumbnail-overlay-resume-playback-renderer" style="width: 100%"></div>
  126. </ytd-thumbnail-overlay-resume-playback-renderer>
  127. `
  128. overlays.appendChild(parent.children[0])
  129. }
  130.  
  131. // style
  132. let progress = overlays.querySelector('#progress')
  133. let width = parseInt(completion * 100)
  134. progress.style.width = `${width}%`
  135. progress.style.backgroundColor = 'blue'
  136. }
  137.  
  138. function progressBars(){
  139. // Add progress bars in the related section
  140. const observer = new MutationObserver(async (mutations, observer) => {
  141. for(let mutation of mutations){
  142. if(mutation.addedNodes.length > 0) {
  143. let thumbnails = mutation.target.querySelectorAll('a.ytd-thumbnail')
  144. for(let thumbnail of thumbnails){
  145. let href = thumbnail.href
  146. if(href) {
  147. let id = videoId(href)
  148. let completion = await GM.getValue(id + '-completion')
  149. if(completion) {
  150. addProgressBar(thumbnail, completion)
  151. }
  152. }
  153. }
  154. }
  155. }
  156. })
  157. observer.observe(document, {childList:true, subtree:true})
  158. }
  159.  
  160. progressBars() // TODO doesn't always work