YouTube playback progress memory

memory and resume the playback progress

  1. // ==UserScript==
  2. // @name YouTube playback progress memory
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description memory and resume the playback progress
  6. // @author hhst
  7. // @match https://www.youtube.com/watch?v=*
  8. // @match https://m.youtube.com/watch?v=*
  9. // @match https://www.youtube.com/
  10. // @match https://m.youtube.com/
  11. // @run-at document-start
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. const get_page_class = (url) => {
  22. url = url.toLowerCase()
  23. if (url.startsWith('https://m.youtube.com') || url.startsWith('https://www.youtube.com')) {
  24. if (url.includes('shorts')) {
  25. return 'shorts'
  26. }
  27. if (url.includes('watch')) {
  28. return 'watch'
  29. }
  30. if (url.includes('library')) {
  31. return 'library'
  32. }
  33. if (url.includes('subscriptions')) {
  34. return 'subscriptions'
  35. }
  36. if (url.includes('@')) {
  37. return '@'
  38. }
  39. return 'home'
  40. }
  41. return 'unknown'
  42. }
  43.  
  44. // return the youtube video id like 'A9oByH9Ci24'
  45. const get_video_id = (url) => {
  46. try {
  47. const match = url.match(/watch\?v=([^&#]+)/)
  48. return match ? match[1] : null
  49. } catch (error) {
  50. console.error('Error getting video ID:', error)
  51. return null
  52. }
  53. }
  54.  
  55. const observer = new MutationObserver((mutationsList) => {
  56. for (const mutation of mutationsList) {
  57. if (mutation.type === 'childList') {
  58. mutation.addedNodes.forEach((node) => {
  59. if (node.nodeType === 1 && node.classList.contains('video-stream')){
  60. console.log("ready to record...")
  61. // memory progress
  62. node.addEventListener('timeupdate', () => {
  63. if (node.currentTime !== 0){
  64. GM_setValue('progress-' + get_video_id(location.href), node.currentTime.toString())
  65. }
  66. })
  67. }
  68.  
  69. if (node.id === 'movie_player') {
  70. window.last_player_state = -1
  71. node.addEventListener('onStateChange', (data) => {
  72. /* refers to https://developers.google.com/youtube/iframe_api_reference:
  73. onStateChange
  74. This event fires whenever the player's state changes. The data property of the event object that the API passes to your event listener function will specify an integer that corresponds to the new player state. Possible values are:
  75. -1 (unstarted)
  76. 0 (ended)
  77. 1 (playing)
  78. 2 (paused)
  79. 3 (buffering)
  80. 5 (video cued).
  81. */
  82. console.log(get_video_id(location.href), data, window.last_player_state)
  83. if([1, 3].includes(data) && window.last_player_state === -1 && get_page_class(location.href) === 'watch'){
  84. console.log("ready to resume...")
  85. // resume progress
  86. // get the last progress time, default 0
  87. const saved_time = GM_getValue('progress-' + get_video_id(location.href)) || '0'
  88. console.log("resume to", saved_time)
  89. node.seekTo(parseInt(saved_time))
  90. }
  91. window.last_player_state = data
  92. })
  93. }
  94. })
  95. }
  96. }
  97. })
  98.  
  99. observer.observe(document.documentElement, {
  100. childList: true,
  101. subtree: true
  102. })
  103.  
  104. })();