YoutubePlayBack

Have you ever closed a YouTube video by accident, or have you gone to another one and when you come back the video starts from 0? With this extension it won't happen anymore

目前為 2024-02-19 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @license MIT
  3. // @name YoutubePlayBack
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.4.1
  6. // @description Have you ever closed a YouTube video by accident, or have you gone to another one and when you come back the video starts from 0? With this extension it won't happen anymore
  7. // @author Costin Alexandru Sandu
  8. // @match https://www.youtube.com/watch*
  9. // @icon https://tse4.mm.bing.net/th/id/OIG3.UOFNuEtdysdoeX0tMsVU?pid=ImgGn
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'strict'
  15. var configData = {
  16. savedProgressAlreadySet: false,
  17. savingInterval: 1500,
  18. currentVideoId: null,
  19. lastSaveTime: 0
  20. }
  21.  
  22.  
  23. // ref: https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds
  24. function fancyTimeFormat(duration) {
  25. // Hours, minutes and seconds
  26. const hrs = ~~(duration / 3600);
  27. const mins = ~~((duration % 3600) / 60);
  28. const secs = ~~duration % 60;
  29.  
  30. // Output like "1:01" or "4:03:59" or "123:03:59"
  31. let ret = "";
  32.  
  33. if (hrs > 0) {
  34. ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
  35. }
  36.  
  37. ret += "" + mins + ":" + (secs < 10 ? "0" : "");
  38. ret += "" + secs;
  39.  
  40. return ret;
  41. }
  42.  
  43. function executeFnInPageContext(fn) {
  44. const fnStringified = fn.toString()
  45. return window.eval('(' + fnStringified + ')' + '()')
  46. }
  47.  
  48. function getVideoCurrentTime() {
  49. const currentTime = executeFnInPageContext(() => {
  50. const player = document.querySelector('#movie_player')
  51. return player.getCurrentTime()
  52. })
  53. return currentTime
  54. }
  55.  
  56. function getVideoId() {
  57. if (configData.currentVideoId) {
  58. return configData.currentVideoId
  59. }
  60. const id = executeFnInPageContext(() => {
  61. const player = document.querySelector('#movie_player')
  62. return player.getVideoData().video_id
  63. })
  64. return id
  65. }
  66.  
  67. function playerExists() {
  68. const exists = executeFnInPageContext(() => {
  69. const player = document.querySelector('#movie_player')
  70. return Boolean(player)
  71. })
  72. return exists
  73. }
  74.  
  75. function setVideoProgress(progress) {
  76. window.eval('var progress =' + progress)
  77. executeFnInPageContext(() => {
  78. const player = document.querySelector('#movie_player')
  79. player.seekTo(window.progress)
  80. })
  81. window.eval('delete progress')
  82. }
  83.  
  84. function updateLastSaved(videoProgress) {
  85. const lastSaveEl = document.querySelector('.last-save-info')
  86. if (lastSaveEl) {
  87. lastSaveEl.innerHTML = "Last saved " + fancyTimeFormat(videoProgress)
  88. }
  89. }
  90.  
  91. function saveVideoProgress() {
  92. const videoProgress = getVideoCurrentTime()
  93. const videoId = getVideoId()
  94.  
  95. configData.currentVideoId = videoId
  96. configData.lastSaveTime = Date.now()
  97. updateLastSaved(videoProgress)
  98. window.localStorage.setItem(videoId, videoProgress)
  99. }
  100.  
  101. function getSavedVideoProgress() {
  102. const videoId = getVideoId()
  103. const savedVideoProgress = window.localStorage.getItem(videoId)
  104.  
  105. return savedVideoProgress
  106. }
  107.  
  108. function videoHasChapters() {
  109. const chaptersSection = document.querySelector('.ytp-chapter-container')
  110. const chaptersSectionDisplay = getComputedStyle(chaptersSection).display
  111. return chaptersSectionDisplay !== 'none'
  112. }
  113.  
  114. function setSavedProgress() {
  115. const savedProgress = getSavedVideoProgress();
  116. setVideoProgress(savedProgress)
  117. configData.savedProgressAlreadySet = true
  118. }
  119.  
  120. // code ref: https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
  121. function waitForElm(selector) {
  122. return new Promise(resolve => {
  123. if (document.querySelector(selector)) {
  124. return resolve(document.querySelector(selector));
  125. }
  126.  
  127. const observer = new MutationObserver(mutations => {
  128. if (document.querySelector(selector)) {
  129. observer.disconnect();
  130. resolve(document.querySelector(selector));
  131. }
  132. });
  133.  
  134. // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
  135. observer.observe(document.body, {
  136. childList: true,
  137. subtree: true
  138. });
  139. });
  140. }
  141.  
  142. async function onPlayerElementExist(callback) {
  143. await waitForElm('#movie_player')
  144. callback()
  145. }
  146.  
  147. function isReadyToSetSavedProgress() {
  148. return !configData.savedProgressAlreadySet && playerExists() && getSavedVideoProgress()
  149. }
  150. function insertInfoElement(element) {
  151. const leftControls = document.querySelector('.ytp-left-controls')
  152. leftControls.appendChild(element)
  153. }
  154. function insertInfoElementInChaptersContainer(element) {
  155. const chaptersContainer = document.querySelector('.ytp-chapter-container')
  156. chaptersContainer.appendChild(element)
  157. }
  158. function createInfoUI() {
  159.  
  160. const infoElContainer = document.createElement('div')
  161. infoElContainer.classList.add('last-save-info-container')
  162.  
  163. const infoEl = document.createElement('div')
  164. infoEl.classList.add('last-save-info')
  165.  
  166.  
  167. infoElContainer.style.all = 'initial'
  168. infoElContainer.style.fontFamily = 'inherit'
  169. infoElContainer.style.fontSize = '1.3rem'
  170. infoElContainer.style.display = 'flex'
  171. infoElContainer.style.alignItems = 'center'
  172.  
  173. infoEl.style.textShadow = 'none'
  174. infoEl.style.background = 'white'
  175. infoEl.style.color = 'black'
  176. infoEl.style.padding = '.5rem'
  177. infoEl.style.borderRadius = '.5rem'
  178. infoElContainer.appendChild(infoEl)
  179. return infoElContainer
  180. }
  181. function initializeUI() {
  182. const infoEl = createInfoUI()
  183. if (videoHasChapters()) {
  184. insertInfoElementInChaptersContainer(infoEl)
  185. } else {
  186. insertInfoElement(infoEl)
  187. }
  188. }
  189.  
  190. function initialize() {
  191. onPlayerElementExist(() => {
  192. initializeUI()
  193. if (isReadyToSetSavedProgress()) {
  194. setSavedProgress()
  195. }
  196. })
  197.  
  198. setInterval(saveVideoProgress, configData.savingInterval)
  199. }
  200.  
  201. initialize()
  202. })();