s Youtube Automatic BS Skip Edited

A script to deal with the automatic skipping of fixed length intros/outros for your favourite Youtube channels.

目前為 2023-04-20 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         s Youtube Automatic BS Skip Edited
// @namespace    https://greasyfork.org/en/scripts/392459-youtube-automatic-bs-skip
// @version      2.9.1.6
// @description  A script to deal with the automatic skipping of fixed length intros/outros for your favourite Youtube channels.
// @license MIT
// @match        https://www.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.addStyle
// @require      http://code.jquery.com/jquery-latest.js
// @run-at       document-start
// ==/UserScript==
console.log(`${GM.info.script.name} run`)
//https://greasyfork.org/scripts/392459-youtube-automatic-bs-skip/code/Youtube%20Automatic%20BS%20Skip.user.js
const app = "YouTube Automatic BS Skip";
const version = '2.9.1_DaileAlimoMIT_edited';
const debug = false;
const log = function(line){if (debug) console.log(line)}
// Elements
const controlUI_ID = "outro-controls";
const modal_ID = "modal";
const progressBar_ID = "progress-bar";
const introTime_ID = "intro-set";
const outroTime_ID = "outro-set";
const introLen_ID = "intro-length";
const outroLen_ID = "outro-length";
const channelTxt_ID = "channel_txt";
// Actions
const pauseOnOutro = "pause-on-outro";
const nextOnOutro = "next-on-outro";
const apply_ID = "apply";
// add indicators to the progress bar.
const setupProgressBar = function(selector) {
    log('called setupProgressBar');
    if($(`#${progressBar_ID}-intro`).remove()){log("removed intro bar");}
    if($(`#${progressBar_ID}-outro`).remove()){log("removed outro bar");}
    // add intro indicator to progress bar
    if (!document.getElementById(`${progressBar_ID}-intro`)){
        log('created intro indicator');
        selector.prepend(
            $(`<div id="${progressBar_ID}-intro">`).addClass("ytp-load-progress").css({
                "left": "0%",
                "transform": "scaleX(0)",
            })
        );
    }
    // add outro indicator to progress bar
    if (!document.getElementById(`${progressBar_ID}-outro`)) {
        log('created outro indicator');
        selector.prepend(
            $(`<div id="${progressBar_ID}-outro">`).addClass("ytp-load-progress").css({
                "left": "100%",
                "transform": "scaleX(0)",
            })
        );
    }
    return [`${progressBar_ID}-intro`, `${progressBar_ID}-outro`];
};
// update the indecators on the progressbar.
const updateProgressbars = function(intro, outro, duration) {
    // update the intro progress bar
    let introBar = $(`#${progressBar_ID}-intro`);
    var introFraction = intro / duration;
    introBar.css({
        "left": "0%",
        "transform": `scaleX(${introFraction})`,
        "background-color": "green",
    });
    // update the outro progress bar
    let outroBar = $(`#${progressBar_ID}-outro`);
    var outroFraction = outro / duration;
    outroBar.css({
        "left": `${100 - (outroFraction * 100)}%`,
        "transform": `scaleX(${outroFraction})`,
        "background-color": "green",
    });
};
const setupControls = function(selector) {
    // Its easier to modify if we don't chain jquery.append($()) to build the html components
    if($(`#${controlUI_ID}`).remove()){log("removed controls");}
    var controls = document.getElementById(controlUI_ID);
    if (controls == null) {
        log('adding controls to video');
        controls = selector.prepend(`
        <button id="${controlUI_ID}" class="ytp-button loading" title="YABSS" aria-label="YABSS">
           <div class="ytp-autonav-toggle-button-container">
              <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path fill="white" d="M19 9l1.25-2.75L23 5l-2.75-1.25L19 1l-1.25 2.75L15 5l2.75 1.25L19 9zm-7.5.5L9 4 6.5 9.5 1 12l5.5 2.5L9 20l2.5-5.5L17 12l-5.5-2.5zM19 15l-1.25 2.75L15 19l2.75 1.25L19 23l1.25-2.75L23 19l-2.75-1.25L19 15z"/></svg>
           </div>
        </button>
        `);
    }
    if($(`#${modal_ID}`).remove()){log("removed modal");}
    if (document.getElementById(modal_ID) == null) {
        log('adding modal to DOM');
        $('body').append(`
         <div id="${modal_ID}">
            <div id="${modal_ID}-escape"></div>
               <div id="${modal_ID}-content">
                  <div id="${channelTxt_ID}">Loading Channel</div>
                      <h2 id="${controlUI_ID}-title" class="d-flex justify-space-between">YouTube Automatic BS Skip ${version}
                         <a href="https://www.buymeacoffee.com/JustDai" target="_blank">
                            <svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24">
                               <g><path d="M0,0h24v24H0V0z" fill="none"></path></g>
                               <g fill="#ffffff"><path d="M18.5,3H6C4.9,3,4,3.9,4,5v5.71c0,3.83,2.95,7.18,6.78,7.29c3.96,0.12,7.22-3.06,7.22-7v-1h0.5c1.93,0,3.5-1.57,3.5-3.5 S20.43,3,18.5,3z M16,5v3H6V5H16z M18.5,8H18V5h0.5C19.33,5,20,5.67,20,6.5S19.33,8,18.5,8z M4,19h16v2H4V19z"></path></g>
                            </svg>
                         </a>
                      </h2>
                     <div id="${controlUI_ID}-control-wrapper">
                        <div class="w-100 d-flex justify-space-around align-center">
                        <label for="${introLen_ID}">Intro</label>
                        <input type="number" min="0" id="${introLen_ID}" placeholder="unset" class="input">
                     </div>
                     <div class="w-100 d-flex justify-space-around align-center">
                        <label for="${outroLen_ID}">Outro</label>
                        <input type="number" min="0" id="${outroLen_ID}" placeholder="unset" class="input">
                     </div>
                     <div class="pa">
                        <div>
                        <label for="${controlUI_ID}-outro-action-group">Action on outro:</label>
                     </div>
                     <fieldset id="${controlUI_ID}-outro-action-group" class="d-flex">
                        <div>
                            <label for="${pauseOnOutro}">Pause Video</label>
                            <input type="radio" name="outro-action-group" id="${pauseOnOutro}">
                        </div>
                        <div>
                            <label for="${nextOnOutro}">Play Next Video</label>
                            <input type="radio" name="outro-action-group" id="${nextOnOutro}" checked="checked">
                        </div>
                     </fieldset>
                  </div>
               <tp-yt-paper-button id="${apply_ID}" class="style-scope ytd-video-secondary-info-renderer d-flex justify-center align-center" style-target="host" role="button" elevation="3" aria-disabled="false">${apply_ID}</tp-yt-paper-button>
            </div>
         </div>
      </div>`
      );
      $(`#${controlUI_ID}`).on('click', () => {
        log("toggle modal");
        $(`#${modal_ID}`).toggleClass("show");
      });
    }
    return controls;
};
const updateControls = ({introPlaceholderTxt, outroPlaceholderTxt, channelTxt, tooltipTxt, introTxt, outroTxt, actions}) => {
    if (introPlaceholderTxt) $(`#${introLen_ID}`).attr("placeholder", introPlaceholderTxt)
    if (outroPlaceholderTxt) $(`#${outroLen_ID}`).attr("placeholder", outroPlaceholderTxt)
    if (introTxt) $(`#${introTime_ID}`).text(introTxt)
    if (outroTxt) $(`#${outroTime_ID}`).text(outroTxt)
    if (channelTxt) $(`#${channelTxt_ID}`).text(channelTxt)
    if (tooltipTxt) $(`#${controlUI_ID}`).attr('title',  tooltipTxt)
    if (actions) {        (actions.outro)? $(`#${nextOnOutro}`).attr("checked", "checked") : $(`#${pauseOnOutro}`).attr("checked", "checked");    }
    //$(`#${modal_ID}`).removeClass("show");
};
const bindToVidStrm = function(vidstrm,loadedIntroSetInSeconds,loadedOutroSetInSeconds){
  // hook video timeupdate, wait for outro and hit next button when time reached
  // if update time less than intro, skip to intro time
  log("binding events");
  let progressBarDone = false;
  var introskipdone
  let paused = false;
  // set duration here and call writeProgressBars
  vidstrm.unbind("timeupdate").on("timeupdate", function(e){
    // use pause to prevent timeupdate after we have clicked pause button
    // there is a slight delay from when pause button is clicked, to when the timeupdates are stopped.
    if (paused)  return setTimeout(1000, () => {paused = false});
    let currentTime = this.currentTime;
    let duration = this.duration;
    if (duration && !progressBarDone) {
      progressBarDone = true;
      updateProgressbars(loadedIntroSetInSeconds, loadedOutroSetInSeconds, duration);
    }
    if(currentTime < loadedIntroSetInSeconds                && !introskipdone )  this.currentTime = loadedIntroSetInSeconds;
    if(              loadedIntroSetInSeconds <= currentTime                   )  introskipdone = true
    // If current time greater or equal to outro, click next button or pause the vidstrm.
    if(currentTime >= duration - loadedOutroSetInSeconds && loadedOutroSetInSeconds > 0){
      if (!playNextOnOutro)   paused = true;
      $(".ytp-play-button")[0].click();
    }
  });
};
const forkJoinJQExistCheck = function({selectors = [], aliases = [], func1 = {}, func2 = async _=>{} }) {
  var $sins=selectors.map(s=>$(s))
  if($sins.filter(x=>x.length /*.length jquery existence check */ ).length===selectors.length){
    var $souts = {};
    $sins.map(($sin,i)=>{
      var dictname = aliases[i] ? aliases[i]: i
      $souts[dictname] = func1[dictname]? func1[dictname]($sin) : $sin
    })
    func2($souts);
  }
};
;(_=>{
  var lasturl
  var cp=_=>{
    if(lasturl!=document.URL && document.URL.includes("watch")){
      $(".video-stream").unbind("timeupdate")
      forkJoinJQExistCheck({
        selectors: [".video-stream", ".ytp-right-controls", ".ytp-progress-bar"],
        aliases: ["vidstrm", "container", "progressBar"],
        func1: {
          "container": setupControls,
          "progressBar": setupProgressBar
        },
        func2: async function($souts) {
          lasturl = document.URL
          updateControls({
            channelTxt: 'N/A',
            tooltipTxt: 'YABSS Loading',
          })
          var thisurl=document.URL
          var channel = await fetch(thisurl).then(a=>a.text()).then(a=>a.match(/"author":"(.*?)"/)[1]);
          //
          var introTargetId = channel.split(" ").join("_") + "-intro";
          var outroTargetId = channel.split(" ").join("_") + "-outro";
          var loadedIntroSetInSeconds = await GM.getValue(introTargetId, 0);
          var loadedOutroSetInSeconds = await GM.getValue(outroTargetId, 0);
          var outroAction = channel.split(" ").join("_") + "-outro-action";
          var playNextOnOutro = await GM.getValue(outroAction, true);
          //
          if(thisurl===document.URL) { // not switched to another video while fetching channel/author
            // a redundant intro skip for the beginning few seconds to tackle 'timeupdate' latency
            var se=$souts.vidstrm.get(0)
            var p=!se.paused
            if(p) se.pause()
            if(se.currentTime < loadedIntroSetInSeconds)  se.currentTime = loadedIntroSetInSeconds;
            if(p) se.play()
            //
            bindToVidStrm($souts.vidstrm, loadedIntroSetInSeconds, loadedOutroSetInSeconds);
            updateControls({
              introPlaceholderTxt: (loadedIntroSetInSeconds <= 0)? "unset": loadedIntroSetInSeconds,
              outroPlaceholderTxt: (loadedOutroSetInSeconds <= 0)? "unset": loadedOutroSetInSeconds,
              channelTxt: channel,
              tooltipTxt: channel,
              introTxt: loadedIntroSetInSeconds,
              outroTxt: loadedOutroSetInSeconds,
              actions: {
                outro: playNextOnOutro,
              },
            })
            $(`#${controlUI_ID}`).removeClass('loading');
            log("loaded channel: " + channel);
            log("intro set: " + loadedIntroSetInSeconds);
            log("outro set: " + loadedOutroSetInSeconds);
            log(`outro action: ${(playNextOnOutro)? "skip to next video": "pause"}`);
            //
            log("bind to click");
            // Control popup toggle button click listener
            $(`#${modal_ID}-escape`).on('click', () => {
              log("clicked outside of modal");
              $(`#${modal_ID}`).removeClass("show");
            });
            // Apply button click listener
            $(`#${apply_ID}`).on("click", function(e) {
              log("updating intro/outro skip");
              var introSeconds = $("#" + introLen_ID).val().toString();
              var outroSeconds = $("#" + outroLen_ID).val().toString();
              if(introSeconds && introSeconds != "" && parseInt(introSeconds) != NaN){
                if (introSeconds < 0) {
                  introSeconds = 0;
                }
                // save outro in local storage
                GM.setValue(introTargetId, introSeconds);
              }
              if(outroSeconds && outroSeconds != "" && parseInt(outroSeconds) != NaN){
                if (outroSeconds < 0) {
                  outroSeconds = 0;
                }
                // save outro in local storage
                GM.setValue(outroTargetId, outroSeconds);
              }
              // update the intro/outro time on the controls
              updateControls({
                introTxt: introSeconds,
                outroTxt: outroSeconds
              });
              bindToVidStrm($souts.vidstrm, introSeconds, outroSeconds);
            });
            // Pause on outro radio button change
            $(`#${pauseOnOutro}`).on("change", function(){
              // pause on outro
              playNextOnOutro = false;
              GM.setValue(outroAction, playNextOnOutro);
            });
            // Next on outro radio button change
            $(`#${nextOnOutro}`).on("change", function(){
              // skip to next on outro
              playNextOnOutro = true;
              GM.setValue(outroAction, playNextOnOutro);
            });
          }
        }
      })
    }
    requestAnimationFrame(cp)
  }
  cp()
})();
// Write the CSS rules to the DOM
GM.addStyle(`
#${modal_ID}-escape {
    position: fixed;
    left: 0;
    top: 0;
    width: 100vw;
    height: 100vh;
    z-index: 1000;
}
#${modal_ID} {
    display: none;
    position: fixed;
    left: 0;
    top: 0;
    width: 100vw;
    height: 100vh;
    z-index: 999;
    background: rgba(0,0,0,.8);
}
#${modal_ID}.show {
    display: flex;
}
#${modal_ID}-content {
    margin: auto;
    width: 30%;
    height: auto;
    background-color: var(--yt-live-chat-action-panel-background-color);
    border-radius: 6px 6px 6px;
    border: 1px solid white;
    padding: 15px;
    color: white;
    z-index: 1001;
}
#${introLen_ID},#${outroLen_ID} {
    font-size: 1.2em;
    padding: .4em;
    border-radius: .5em;
    border: none;
    width: 80%
}
#${apply_ID} {
    position: relative;
    border: 1px solid white;
    transition: background-color .2s ease-in-out
}
#${apply_ID}:hover {
    background-color: rgba(255,255,255,0.3);
}
#${controlUI_ID}.loading {
    opacity:.3
}
#${controlUI_ID} {
    height: 100%;
    padding: 0;
    margin: 0;
    bottom: 45%;
    position: relative;
}
#${controlUI_ID} svg {
    position: relative;
    top: 20%;
    left: 20%;
}
#${controlUI_ID}-panel {
 margin-right: 1em;
 vertical-align:top
}
#${controlUI_ID} > * {
 display: inline-block;
 max-height: 100%;
}
#${controlUI_ID}-title {
 padding: 2px;
}
#${controlUI_ID}-outro-action-group {
    padding: .5em;
}
#${controlUI_ID}-outro-action-group > div {
 display: block;
 margin: auto;
 text-align-last: justify;
}
#${controlUI_ID}-control-wrapper > * {
    padding-top: 1em;
}
#action-radios {
  display: none;
}
#action-radios .actions{
  padding-left: 2px;
  text-align: left;
  background-color: black;
  color: white;
}
#${introLen_ID},#${outroLen_ID} {
 margin-right: 2px;
}
#${channelTxt_ID} {
    position: relative;
    top: -3.5em;
    margin-bottom: -1.5em;
}
.w-100 {
    width: 100% !important;
}
.input {
    padding: .2em;
}
.d-flex {
    display: flex;
}
.justify-center {
    justify-content: center;
}
.justify-space-around {
    justify-content: space-around;
}
.justify-space-between {
    justify-content: space-between;
}
.align-center {
    align-items: center;
}
.pa {
    padding: 1em;
}
`);