// ==UserScript==
// @name Youtube Automatic BS Skip Edited
// @namespace https://gf.qytechs.cn/en/scripts/392459-youtube-automatic-bs-skip
// @version 2.9.1DaileAlimoMIT_edit20230401
// @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
// ==/UserScript==
//https://gf.qytechs.cn/scripts/392459-youtube-automatic-bs-skip/code/Youtube%20Automatic%20BS%20Skip.user.js
const app = "YouTube Automatic BS Skip";
const version = '2.9.1DaileAlimoMIT_edit20230401';
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";
// ev - Keep checking the DOM until these given selectors are found.
// Invokes a callback function on complete that contains an object containing the JQuery element(s) for the given selectors accessable with aliases if given.
// selectors[] - Each selector to await.
// aliases[] - An alias for each selector/mutator.
// mutators{} - Associative array/object that given alias as key and function as value and selector as arguments returns a calculated result in place of its selector.
// callback - Function that is called when all selectors, containing each selector or its mutators returned value if applicable.
// error - Function that is called when an error such as retries exceeded occurs.
const mutobs = function({selectors = [], aliases = [], mutators = {}, callback = (selectors = {}), error}) {
let ready = {};
let found = 0;
for(let i in selectors){
let $sel = $(selectors[i]);
if ($sel.length) {
let index = aliases[i] ? aliases[i]: i;
if (mutators[index]) {
ready[index] = mutators[index]($sel);
if (ready[index]){
found++;
}
} else {
ready[index] = $sel;
found++;
}
}
}
if (found === selectors.length) {
return callback(ready);
}
};
// add indicators to the progress bar.
const setupProgressBar = function(selector) {
log('called setupProgressBar');
// add intro indicator to progress bar
if (document.getElementById(`${progressBar_ID}-intro`) == null){
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`) == null) {
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`];
};
// destroy the indicators added to the progressbar.
const destroyProgressBar = function() {
log("destroying progressbars");
if($(`#${progressBar_ID}-intro`).remove()){log("removed intro bar");}
if($(`#${progressBar_ID}-outro`).remove()){log("removed outro bar");}
};
// 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
var controls = document.getElementById(controlUI_ID);
if (controls == null) {
log('adding controls to video');
controls = selector.prepend(`
<button id="${controlUI_ID}" class="ytp-button" 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 (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>`
);
}
return controls;
};
const destroyControls = function(){
log("destroying controls");
if($(`#${controlUI_ID}`).remove()){log("removed controls");}
if($(`#${modal_ID}`).remove()){log("removed modal");}
};
const updateControls = ({introPlaceholderTxt, outroPlaceholderTxt, channelTxt, 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);
$(`#${controlUI_ID}`).attr('title', channelTxt);
}
if (actions) { (actions.outro)? $(`#${nextOnOutro}`).attr("checked", "checked") : $(`#${pauseOnOutro}`).attr("checked", "checked"); }
//$(`#${modal_ID}`).removeClass("show");
};
;(_=>{
var selectors= [".video-stream", ".ytp-right-controls", ".ytp-progress-bar", "#meta-contents #text.ytd-channel-name,.ytp-ce-channel-title > a"],
aliases= ["stream", "container", "progressBar", "channel"],
mutators= {
"container": setupControls,
"progressBar": setupProgressBar,
"channel": function(selector) {
let channel = selector.first().text();
log(`validating channel: ${channel}`);
if (channel === "") {
return false;
}
return channel;
},
},
callback= async function(selectors) {
//
var channel = selectors.channel;
var introTargetId = channel.split(" ").join("_") + "-intro";
var outroTargetId = channel.split(" ").join("_") + "-outro";
var outroAction = channel.split(" ").join("_") + "-outro-action";
log("loaded channel: " + channel);
//
var loadedIntroSetInSeconds = await GM.getValue(introTargetId, 0);
var loadedOutroSetInSeconds = await GM.getValue(outroTargetId, 0);
var playNextOnOutro = await GM.getValue(outroAction, true);
//
log("intro set: " + loadedIntroSetInSeconds);
log("outro set: " + loadedOutroSetInSeconds);
log(`outro action: ${(playNextOnOutro)? "skip to next video": "pause"}`);
//
updateControls({
introPlaceholderTxt: (loadedIntroSetInSeconds <= 0)? "unset": loadedIntroSetInSeconds,
outroPlaceholderTxt: (loadedOutroSetInSeconds <= 0)? "unset": loadedOutroSetInSeconds,
channelTxt: channel,
introTxt: loadedIntroSetInSeconds,
outroTxt: loadedOutroSetInSeconds,
actions: {
outro: playNextOnOutro,
},
});
//
const bindToStream = async function(){
// 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;
let paused = false;
let loadedIntroSetInSeconds = await GM.getValue(introTargetId, 0);
let loadedOutroSetInSeconds = await GM.getValue(outroTargetId, 0);
log("intro set: " + loadedIntroSetInSeconds);
log("outro set: " + loadedOutroSetInSeconds);
//
// set duration here and call writeProgressBars
selectors.stream.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 current time less than intro, skip past intro.
if(currentTime < loadedIntroSetInSeconds) {
this.currentTime = loadedIntroSetInSeconds;
}
// If current time greater or equal to outro, click next button or pause the stream.
if(currentTime >= duration - loadedOutroSetInSeconds && loadedOutroSetInSeconds > 0){
if (playNextOnOutro) {
$(".ytp-next-button")[0].click();
} else {
paused = true;
$(".ytp-play-button")[0].click();
}
}
});
};
// handle apply outro in seconds
//
log("bind to click");
// Control popup toggle button click listener
$(`#${controlUI_ID}`).on('click', () => {
log("toggle modal");
$(`#${modal_ID}`).toggleClass("show");
});
$(`#${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
});
bindToStream();
});
// 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);
});
//
bindToStream();
},
error=function(e) {
log(e);
destroyControls();
destroyProgressBar();
}
var l='init'
var cp=t=>{
var d=(_=>new Date())()-t
if(d>500){
if(l!=document.URL){
if(l!=='init'){
destroyProgressBar();
destroyControls();
}
l = document.URL;
}
requestAnimationFrame(_=>cp((_=>new Date())()))
}else{
requestAnimationFrame(_=>cp(t))
}
if(l.includes("watch"))
try {
mutobs({
selectors: selectors,
aliases: aliases,
mutators: mutators,
callback: callback,
error: error,
});
} catch (e) {log(e.message)}
}
requestAnimationFrame(_=>cp((_=>new Date())()))
})();
// 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} {
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;
}
`);