Youtube - Resumer

Store video.currentTime locally

目前为 2023-11-19 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Youtube - Resumer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  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. l('resuming', id, video.currentTime, lastTime)
  77. video.currentTime = lastTime
  78. }else{
  79. l('new', video.currentTime)
  80. }
  81. }
  82.  
  83. function cleanUrl(){
  84. //Remove t paramater when opening a video that had a progress bar
  85. let url = new URL(document.URL)
  86. url.searchParams.delete('t')
  87. window.history.replaceState(null, null, url)
  88. }
  89.  
  90. let lastId // don't resume if going back to same page from miniplayer
  91.  
  92. // Event for each page change
  93. document.addEventListener("yt-navigate-finish", () => {
  94. l('navigate-finish', lastId, videoId())
  95. // video page
  96. if(videoId() && lastId !== videoId()) {
  97. lastId = videoId()
  98. cleanUrl()
  99.  
  100. let removeListeners
  101. findVideo(video => {
  102. resume(video)
  103.  
  104. // clean previous listeners
  105. if(removeListeners) removeListeners()
  106. removeListeners = listen(video)
  107. })
  108. }
  109. })
  110.  
  111. /////////////////////
  112.  
  113.  
  114. function addProgressBar(thumbnail, completion){
  115. let overlays = thumbnail.querySelector('#overlays')
  116. let existingProgressBar = thumbnail.querySelector('ytd-thumbnail-overlay-resume-playback-renderer')
  117. if(!existingProgressBar) {
  118. let parent = document.createElement('div')
  119. parent.innerHTML = `
  120. <ytd-thumbnail-overlay-resume-playback-renderer class="style-scope ytd-thumbnail">
  121. <div id="progress" class="style-scope ytd-thumbnail-overlay-resume-playback-renderer" style="width: 100%"></div>
  122. </ytd-thumbnail-overlay-resume-playback-renderer>
  123. `
  124. overlays.appendChild(parent.children[0])
  125. }
  126.  
  127. // style
  128. let progress = overlays.querySelector('#progress')
  129. let width = parseInt(completion * 100)
  130. progress.style.width = `${width}%`
  131. progress.style.backgroundColor = 'blue'
  132. }
  133.  
  134. function progressBars(){
  135. // Add progress bars in the related section
  136. const observer = new MutationObserver(async (mutations, observer) => {
  137. for(let mutation of mutations){
  138. if(mutation.addedNodes.length > 0) {
  139. let thumbnails = mutation.target.querySelectorAll('a.ytd-thumbnail')
  140. for(let thumbnail of thumbnails){
  141. let href = thumbnail.href
  142. if(href) {
  143. let id = videoId(href)
  144. let completion = await GM.getValue(id + '-completion')
  145. if(completion) {
  146. addProgressBar(thumbnail, completion)
  147. }
  148. }
  149. }
  150. }
  151. }
  152. })
  153. observer.observe(document, {childList:true, subtree:true})
  154. }
  155.  
  156. progressBars() // TODO doesn't always work

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址