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 提交的版本。查看 最新版本

// ==UserScript==
// @license MIT
// @name         YoutubePlayBack
// @namespace    http://tampermonkey.net/
// @version      1.4.4
// @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
// @author       Costin Alexandru Sandu
// @match        https://www.youtube.com/watch*
// @icon         https://tse4.mm.bing.net/th/id/OIG3.UOFNuEtdysdoeX0tMsVU?pid=ImgGn
// @grant        none
// ==/UserScript==

(function () {
  'strict'
  var configData = {
    savedProgressAlreadySet: false,
    savingInterval: 1500,
    currentVideoId: null,
    lastSaveTime: 0
  }


  // ref: https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds
  function fancyTimeFormat(duration) {
    // Hours, minutes and seconds
    const hrs = ~~(duration / 3600);
    const mins = ~~((duration % 3600) / 60);
    const secs = ~~duration % 60;

    // Output like "1:01" or "4:03:59" or "123:03:59"
    let ret = "";

    if (hrs > 0) {
      ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
    }

    ret += "" + mins + ":" + (secs < 10 ? "0" : "");
    ret += "" + secs;

    return ret;
  }

  function executeFnInPageContext(fn) {
    const fnStringified = fn.toString()
    return window.eval('(' + fnStringified + ')' + '()')
  }

  function getVideoCurrentTime() {
    const currentTime = executeFnInPageContext(() => {
      const player = document.querySelector('#movie_player')
      return player.getCurrentTime()
    })
    return currentTime
  }

  function getVideoId() {
    if (configData.currentVideoId) {
      return configData.currentVideoId
    }
    const id = executeFnInPageContext(() => {
      const player = document.querySelector('#movie_player')
      return player.getVideoData().video_id
    })
    return id
  }

  function playerExists() {
    const exists = executeFnInPageContext(() => {
      const player = document.querySelector('#movie_player')
      return Boolean(player)
    })
    return exists
  }

  function setVideoProgress(progress) {
    window.eval('var progress =' + progress)
    executeFnInPageContext(() => {
      const player = document.querySelector('#movie_player')
      player.seekTo(window.progress)
    })
    window.eval('delete progress')
  }

  function updateLastSaved(videoProgress) {
    const lastSaveEl = document.querySelector('.last-save-info')
    if (lastSaveEl) {
      lastSaveEl.innerHTML = "Last saved " + fancyTimeFormat(videoProgress)
    }
  }

  function saveVideoProgress() {
    const videoProgress = getVideoCurrentTime()
    const videoId = getVideoId()

    configData.currentVideoId = videoId
    configData.lastSaveTime = Date.now()
    updateLastSaved(videoProgress)
    window.localStorage.setItem(videoId, videoProgress)
  }

  function getSavedVideoProgress() {
    const videoId = getVideoId()
    const savedVideoProgress = window.localStorage.getItem(videoId)

    return savedVideoProgress
  }

  function videoHasChapters() {
    const chaptersSection = document.querySelector('.ytp-chapter-container[style=""]')
    const chaptersSectionDisplay = getComputedStyle(chaptersSection).display 
    return chaptersSectionDisplay !== 'none'
  }

  function setSavedProgress() {
    const savedProgress = getSavedVideoProgress();
    setVideoProgress(savedProgress)
    configData.savedProgressAlreadySet = true
  }

  // code ref: https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists
  function waitForElm(selector) {
    return new Promise(resolve => {
      if (document.querySelector(selector)) {
        return resolve(document.querySelector(selector));
      }

      const observer = new MutationObserver(mutations => {
        if (document.querySelector(selector)) {
          observer.disconnect();
          resolve(document.querySelector(selector));
        }
      });

      // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
      observer.observe(document.body, {
        childList: true,
        subtree: true
      });
    });
  }

  async function onPlayerElementExist(callback) {
    await waitForElm('#movie_player')
    callback()
  }

  function isReadyToSetSavedProgress() {
    return !configData.savedProgressAlreadySet && playerExists() && getSavedVideoProgress()
  }
  function insertInfoElement(element) {
    const leftControls = document.querySelector('.ytp-left-controls')
    leftControls.appendChild(element)
  }
  function insertInfoElementInChaptersContainer(element) {
    const chaptersContainer = document.querySelector('.ytp-chapter-container[style=""]')
    chaptersContainer.style.display = 'flex'
    chaptersContainer.appendChild(element)
  }
  function createInfoUI() {

    const infoElContainer = document.createElement('div')
    infoElContainer.classList.add('last-save-info-container')

    const infoEl = document.createElement('div')
    infoEl.classList.add('last-save-info')


    infoElContainer.style.all = 'initial'
    infoElContainer.style.fontFamily = 'inherit'
    infoElContainer.style.fontSize = '1.3rem'
    infoElContainer.style.marginLeft = '0.5rem'
    infoElContainer.style.display = 'flex'
    infoElContainer.style.alignItems = 'center'

    infoEl.style.textShadow = 'none'
    infoEl.style.background = 'white'
    infoEl.style.color = 'black'
    infoEl.style.padding = '.5rem'
    infoEl.style.borderRadius = '.5rem'
    
    infoElContainer.appendChild(infoEl)
    
    return infoElContainer
  }
  
  async function onChaptersReadyToMount(callback) {
    const chaptersContainer = await waitForElm('.ytp-chapter-container[style=""]')
    callback()
  }
  
  function initializeUI() {
    const infoEl = createInfoUI()
    insertInfoElement(infoEl)

    onChaptersReadyToMount(() => insertInfoElementInChaptersContainer(infoEl))
  }

  

  function initialize() {
    onPlayerElementExist(() => {
      initializeUI()
      if (isReadyToSetSavedProgress()) {
        setSavedProgress()
      }
    })

    setInterval(saveVideoProgress, configData.savingInterval)
  }

  initialize()
})();