Greasy Fork is available in English.

[ARCHIVED]Next Chapter Button for YouTube

Next Chapter Button for YouTube.

// ==UserScript==
// @name        [ARCHIVED]Next Chapter Button for YouTube
// @description	Next Chapter Button for YouTube.
// @version     1.0.1
// @author      look997
// @include     https://www.youtube.com/*
// @homepageURL https://greasyfork.org/pl/users/4353-look997
// @namespace	  https://greasyfork.org/pl/users/4353-look997
// @grant       none
// @run-at      document-end
// @resource    metadata https://greasyfork.org/scripts/437859-next-chapter-button-for-youtube/code/Next%20Chapter%20Button%20for%20YouTube.user.js
// @icon        https://www.google.com/s2/favicons?domain=youtube.com
// @icon64      https://www.google.com/s2/favicons?domain=youtube.com
// ==/UserScript==

(function() {
  'use strict';
  
  
  function getSec (href) {
    return Number(new URLSearchParams(href).get("t").replace("s",""))
  }

  function getOb () {
    const allDescriltionLinksEls = document.querySelectorAll("#description .yt-simple-endpoint");

    //console.log("NCB YT allDescriltionLinksEls", allDescriltionLinksEls);

    const timeDescriptionLinksEls = Array.from(allDescriltionLinksEls)
                                      .filter(el=>/^(([0-9]+:)*([0-5]*[0-9]):[0-5][0-9])$/.test(el.textContent.trim()));

    const chaptersSecs = timeDescriptionLinksEls.map( el=>getSec(el.href) );

    const timeLines = document.querySelector("#description .content").textContent.split("\n")
                      .filter(line=>/(([0-9]+:)*([0-5]*[0-9]):[0-5][0-9])/.test(line))
                      .map(line=>line.replace(/(([0-9]+:)*([0-5]*[0-9]):[0-5][0-9])/,"").replace(/^[\s-(){}\.]+/,"").replace(/[\s-(){}\.]+$/,""));
    //console.log("NCB YT timeDescriptionLinksEls", timeDescriptionLinksEls);

    const currentPlayTime = document.querySelector("#movie_player video").currentTime;
    //console.log("NCB YT currentPlayTime",currentPlayTime);

    const reverseCurrentChapterSec = chaptersSecs.map(el=>el).reverse().find(sec=>currentPlayTime>=sec);


    const sum = timeDescriptionLinksEls.length;

    const currentChapterIndex = chaptersSecs.indexOf(reverseCurrentChapterSec);
    const nextChapterIndex = timeDescriptionLinksEls[currentChapterIndex+1]?currentChapterIndex+1:0;
    const prevChapterIndex = timeDescriptionLinksEls[currentChapterIndex-1]?currentChapterIndex-1:sum-1;

    return {timeDescriptionLinksEls, timeLines, currentChapterIndex, currentPlayTime, sum, nextChapterIndex, prevChapterIndex};
  }

  function jumpToNextChapter () {

    const {timeDescriptionLinksEls, nextChapterIndex} = getOb();

    document.querySelector("#movie_player video").currentTime = getSec(timeDescriptionLinksEls[nextChapterIndex].href);
  }

  function jumpToPrevChapter () {

    const {timeDescriptionLinksEls, prevChapterIndex} = getOb();

    document.querySelector("#movie_player video").currentTime = getSec(timeDescriptionLinksEls[prevChapterIndex].href);
  }

  function repeatChapter () {

    const {timeDescriptionLinksEls, currentChapterIndex} = getOb();

    document.querySelector("#movie_player video").currentTime = getSec(timeDescriptionLinksEls[currentChapterIndex].href);
  }


  function getTitle (buttonText, index) {
    const {timeDescriptionLinksEls, timeLines, sum} = getOb();

    const chapterTitle = timeLines[index].trim();
    const chapterTime = timeDescriptionLinksEls[index].textContent.trim();

    const chapter = window.document.documentElement.lang==="pl-PL"?"rozdział":"chapter";

    const title = `${buttonText}\n\n${chapterTitle}\n${chapterTime} ${index+1}/${sum} ${chapter}`;
    return title;
  }

  function setChapterElTitle () {
    const {timeDescriptionLinksEls, nextChapterIndex} = getOb();
    if (timeDescriptionLinksEls.length===0) { return true; }

    const title = getTitle(window.document.documentElement.lang==="pl-PL"?"Odtwórz następny rozdział":"Jump to next chapter", nextChapterIndex);

    nextChapterButtonEl.title = title;
  }

  function setChapterPrevElTitle () {
    const {timeDescriptionLinksEls, prevChapterIndex} = getOb();
    if (timeDescriptionLinksEls.length===0) { return true; }

    const title = getTitle(window.document.documentElement.lang==="pl-PL"?"Odtwórz poprzedni rozdział":"Jump to prev chapter", prevChapterIndex);

    prevChapterButtonEl.title = title;
  }

  function setChapterCurrentElTitle () {
    const {timeDescriptionLinksEls, currentChapterIndex} = getOb();
    if (timeDescriptionLinksEls.length===0) { return true; }

    const title = getTitle(window.document.documentElement.lang==="pl-PL"?"Powtórz aktualny rozdział":"Repeat current chapter", currentChapterIndex);

    repeatChapterButtonEl.title = title;
    currentChapterTitleEl.title = title;
  }

  let nextChapterButtonEl;
  let currentChapterTitleEl;

  let prevChapterButtonEl;
  let repeatChapterButtonEl;

  function start () {
    
    
    currentChapterTitleEl = document.querySelector("#movie_player .ytp-chapter-title-content");
    if (currentChapterTitleEl.dataset.ncbfyt=="true") { return false; }
    currentChapterTitleEl.dataset.ncbfyt = "true";

    prevChapterButtonEl = document.createElement("span");
    prevChapterButtonEl.textContent = " |< ";
    prevChapterButtonEl.style.cursor = "pointer";
    prevChapterButtonEl.style.marginRight = "10px";
    currentChapterTitleEl.before(prevChapterButtonEl);

    //prevChapterButtonEl.addEventListener("mousemove", setChapterPrevElTitle);

    prevChapterButtonEl.addEventListener("click", jumpToPrevChapter);


    nextChapterButtonEl = document.createElement("span");
    nextChapterButtonEl.textContent = " >| • ";
    nextChapterButtonEl.style.cursor = "pointer";
    nextChapterButtonEl.style.marginRight = "5px";
    currentChapterTitleEl.before(nextChapterButtonEl);

    //nextChapterButtonEl.addEventListener("mousemove", setChapterElTitle);

    nextChapterButtonEl.addEventListener("click", jumpToNextChapter);


    repeatChapterButtonEl = document.createElement("span");
    repeatChapterButtonEl.textContent = " 🔁 ";
    repeatChapterButtonEl.title = "Repeat current chapter";
    repeatChapterButtonEl.style.cursor = "pointer";
    //repeatChapterButtonEl.style.marginLeft = "5px";
    repeatChapterButtonEl.style.marginRight = "5px";
    //currentChapterTitleEl.before(repeatChapterButtonEl);

    //repeatChapterButtonEl.addEventListener("mousemove", setChapterCurrentElTitle);

    //repeatChapterButtonEl.addEventListener("click", repeatChapter);

    currentChapterTitleEl.style.cursor = "pointer";
    //currentChapterTitleEl.addEventListener("mousemove", setChapterCurrentElTitle);
    currentChapterTitleEl.addEventListener("click", repeatChapter);


    const observer = new MutationObserver(()=>{
      setChapterPrevElTitle();
      setChapterElTitle();
      setChapterCurrentElTitle();
    });
    observer.observe(currentChapterTitleEl, { childList: true, subtree: true });
  }

  
  class NavigateFinish {
    constructor (callback) {
      if (typeof callback  !== "function") { throw "Give me callback function in `new WatchPageLoader(callbackFunction);`"; }
      this.cb = callback;
      
      this.init();
    }
    // main UserScript Function after watch page is load
    scriptMainFunction (...args) {
      this.cb(...args);
    }
    
    mainLoad () {
      function getVideoId() {
        const urlParams = new URLSearchParams(window.location.search);
        const videoId = urlParams.get("v");

        return videoId;
      }

      function isVideoLoaded() {
        const videoId = getVideoId();

        return (
          document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null
        );
      }

      const setEventListeners = (evt)=> {
        const checkForJS_Finish = ()=> {
          if (isVideoLoaded()) {
            clearInterval(jsInitChecktimer);
            this.scriptMainFunction();
          }
        }

        if (window.location.href.indexOf("watch?") >= 0) {
          var jsInitChecktimer = setInterval(checkForJS_Finish, 111);
        }
      }

      (function () {
        "use strict";
        window.addEventListener("yt-navigate-finish", setEventListeners, true);
        setEventListeners();
      })();
    }
  
    init () {
      this.mainLoad();
    }
  }
  
  new NavigateFinish(start);
})();