Youtube Automatic BS Skip

Set outro for any youtube channel and will automatically skip to next video when time is reached.

2021-04-15 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

// ==UserScript==
// @name         Youtube Automatic BS Skip
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Set outro for any youtube channel and will automatically skip to next video when time is reached.
// @author       Daile Alimo
// @match        https://www.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @require      http://code.jquery.com/jquery-latest.js
// ==/UserScript==

/* globals $ whenReady */

const app = "YouTube Automatic BS Skip";
const version = 2.2;
const debug = false;

const log = function(line) {
    if (debug) {
        console.log(line);
    }
}
// whenReady   - 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.
// maxRetries  - The total number of times the whenReady will recur before calling the error function.
const whenReady = function({selectors = [], aliases = [], mutators = {}, callback = (selectors = {}), error, maxRetries = 5}) {
    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);
    }
    setTimeout(function(){
        if (maxRetries >= 1) {
            return whenReady({
                selectors: selectors,
                aliases: aliases,
                mutators: mutators,
                callback: callback,
                maxRetries: --maxRetries
            });
        }
        if (error !== undefined) {
            error("max retries exceeded");
        }
    }, 500);
};
//
let initializationRequired = false;
const destroy = function(afterDetroyed){
    //
    // make sure we have no events binded, in the case fn() was called by interval on URL change
    // this will ensure that we can create clean controls for the current playlist without accidentally
    // having events persisting in the background.
    //
    log("destroying..");
    if ($(".video-stream").unbind()) {log("unbinding .video-stream");}
    if ($("#set-outro").unbind()){log("unbinding #set-outro");}
    if ($("#outro-controls").remove()){log("removed controls");}
    if ($("#outro-bar").remove()){log("removed progressbar");}
    initializationRequired = false;
    afterDetroyed();
}

// validateChannel - ensure we get a channel name out of the channel name element
const validateChannel = function(selector) {
    let channel = selector.first().text();
    log("validating channel: " + channel);
    if (channel === "") {
        return false;
    }
    return channel;
}
//
const progressBar_ID = "progress-bar";
// add indicators to the progress bar.
const setupProgressBar = function(selector) {
    destroyProgressBar();
    // add intro indicator to progress bar
    selector.prepend(
        $("<div id='" + progressBar_ID + "-intro'>").addClass("ytp-load-progress").css({
            "left": "0%",
            "transform": "scaleX(0)",
        })
    );
    // add outro indicator to progress bar
    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() {
    if($("#" + progressBar_ID + "-intro").remove()){log("removed intro bar");}
    if($("#" + progressBar_ID + "-outro").remove()){log("removed outro bar");}
}
// create the indecators on the progressbar.
const createProgressBars = 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 - (outroBar * 100)) + "%",
        "transform": "scaleX(" + outroFraction + ")",
        "background-color": "green",
    });
}
//
const controlUI_ID = "outro-controls";
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";
const apply_ID = "apply";
const logoWidth = 94;
const logoHeight = 50;
const setupControls = function(selector) {
    destroyControls();
    // Its easier to modify if we don't chain jquery.append($()) to build the html components
    //
    let controls = selector.prepend(`
        <div id="${controlUI_ID}">
          <div id="${controlUI_ID}-panel" style="display: none;">
            <h3 id="${controlUI_ID}-title">${app} v${version} - <a href="https://www.buymeacoffee.com/JustDai" target="_blank">Support me &lt;3</a></h3>
            <input type="number" min="0" id="${introLen_ID}" placeholder="loading channel"/>
            <input type="number" min="0" id="${outroLen_ID}" placeholder="loading channel"/>
            <button id="${apply_ID}">apply</button>
            <div><span id="channel_txt">loading</span> intro set: <span id="${introTime_ID}">0</span> seconds outro set: <span id="${outroTime_ID}">0</span> seconds</div>
          </div>
          <div id="${controlUI_ID}-logo">
            <svg width="${logoWidth}" height="${logoHeight}" viewBox="-34 -10 452.5 112.5"><g id="SvgjsG1230" featurekey="symbolFeature-0" transform="matrix(0.2018017853791305,0,0,0.2018017853791305,-13.823422298470438,0.17657770152956154)" fill="#ffffff"><g xmlns="http://www.w3.org/2000/svg"><path d="M136.3,349.2L287,198.4c-7.1-2.6-12.3-9.4-12.3-17.4c0-10.4,8.4-18.8,18.8-18.8c8.1,0,14.8,5.1,17.4,12.3l20.4-20.4   c-3.3-2.8-6.5-5.6-10-8.1l26.1-63.2l-34.7-14.3l-25.2,61.1c-10.1-3-20.7-4.8-31.6-4.8c-10.9,0-21.5,1.8-31.6,4.8l-25.2-61.1   l-34.7,14.3l26.1,63.2c-18.7,13.1-34.3,31.6-45.7,53.7H87.3v37.5h43.5c-3,12-4.8,24.5-5.5,37.5H68.5v37.5h58.4   C128.8,325.2,132,337.5,136.3,349.2z M218.5,162.2c10.4,0,18.8,8.4,18.8,18.8c0,10.4-8.4,18.8-18.8,18.8s-18.8-8.4-18.8-18.8   C199.8,170.6,208.1,162.2,218.5,162.2z"></path><path d="M443.5,95.1L95.1,443.5h53l30.7-30.7c21.7,19.2,48.3,30.7,77.1,30.7c40.1,0,75.9-21.9,100-56.3h68.8v-37.5h-49.2   c4.4-11.8,7.6-24.4,9.6-37.5h58.4v-37.5h-56.8c-0.6-13-2.4-25.5-5.5-37.5h43.5v-37.5h-32.8l51.6-51.6V95.1z"></path></g></g><g id="SvgjsG1231" featurekey="nameFeature-0" transform="matrix(3.452357175253672,0,0,3.452357175253672,96.00000164621218,-37.28546802849764)" fill="#ffffff"><path d="M10.8 11.399999999999999 l1.28 0 l-5.4 10.84 l0 17.76 l-1.28 0 l0 -17.76 l-5.4 -10.84 l1.28 0 l4.76 9.52 z M30.515 40 l-1.12 -4.52 l-10.96 0 l-1.12 4.52 l-1.2 0 l7.08 -28.6 l1.44 0 l7.08 28.6 l-1.2 0 z M18.715 34.28 l10.4 0 l-5.2 -20.88 z M39.63 20.88 c4.08 1.16 7.04 4.92 7.04 9.36 c0 5.4 -4.36 9.76 -9.76 9.76 l-1.16 0 l0 -28.6 l1.16 0 c2.84 0 5.12 2.28 5.12 5.12 c0 1.84 -0.96 3.44 -2.4 4.36 z M36.91 12.559999999999999 l0 7.92 c2.2 0 3.96 -1.76 3.96 -3.96 s-1.76 -3.96 -3.96 -3.96 z M36.91 38.84 c4.76 0 8.6 -3.88 8.6 -8.6 c0 -4.76 -3.84 -8.6 -8.6 -8.6 l0 17.2 l0 0 z M54.705 40.56 c-2.36 0 -4.6 -1.12 -6 -3.04 l0.92 -0.68 c1.2 1.6 3.08 2.56 5.08 2.56 c3.4 0 6.2 -2.8 6.2 -6.24 c0 -3.56 -2.72 -6.16 -5.36 -8.72 c-2.56 -2.4 -5.16 -4.92 -5.16 -8.24 c0 -2.96 2.44 -5.4 5.4 -5.4 c1.72 0 3.36 0.84 4.4 2.24 l-0.96 0.68 c-0.8 -1.08 -2.08 -1.76 -3.44 -1.76 c-2.32 0 -4.24 1.92 -4.24 4.24 c0 2.8 2.32 5.04 4.8 7.4 c2.8 2.72 5.72 5.52 5.72 9.56 c0 4.08 -3.32 7.4 -7.36 7.4 z M72.1 40.56 c-2.36 0 -4.6 -1.12 -6 -3.04 l0.92 -0.68 c1.2 1.6 3.08 2.56 5.08 2.56 c3.4 0 6.2 -2.8 6.2 -6.24 c0 -3.56 -2.72 -6.16 -5.36 -8.72 c-2.56 -2.4 -5.16 -4.92 -5.16 -8.24 c0 -2.96 2.44 -5.4 5.4 -5.4 c1.72 0 3.36 0.84 4.4 2.24 l-0.96 0.68 c-0.8 -1.08 -2.08 -1.76 -3.44 -1.76 c-2.32 0 -4.24 1.92 -4.24 4.24 c0 2.8 2.32 5.04 4.8 7.4 c2.8 2.72 5.72 5.52 5.72 9.56 c0 4.08 -3.32 7.4 -7.36 7.4 z"></path></g></svg>
          </div>
        </div>
    `);
    $(`#${controlUI_ID}-logo`).on("click", function(){
        $(`#${controlUI_ID}-panel`).fadeToggle("fast", "swing");
    });
    return controls;
}
// Write the CSS rules to the DOM
GM_addStyle(`
#${controlUI_ID} {
 display: block;
 margin: 5px;
 text-align: right;
 color: #666666;
}
#${controlUI_ID}-panel {
 margin-right: 1em;
}
#${controlUI_ID} > * {
 display: inline-block;
 max-height: 100%;
}
#${controlUI_ID}-title {
 padding: 2px;
}
#${controlUI_ID}-logo {
 display: inline-block;
 background-color: #c00;
 opacity: .5;
 border-radius: 2px;
 cursor: pointer;
}
#${controlUI_ID}-logo:hover {
 opacity: 1;
}
#${controlUI_ID}-logo svg {
 /* border: 1px solid red; */
}
#${introLen_ID},#${outroLen_ID} {
 margin-right: 2px;
}
`);

const destroyControls = function(){
    if($("#" + controlUI_ID).remove()){log("removed controls");}
}
const updateControls = ({introPlaceholderTxt, outroPlaceholderTxt, channelTxt, introTxt, outroTxt}) => {
    if (introPlaceholderTxt) {
        $("#" + introLen_ID).attr("placeholder", introPlaceholderTxt);
    }
    if (outroPlaceholderTxt) {
        $("#" + outroLen_ID).attr("placeholder", outroPlaceholderTxt);
    }
    if (channelTxt) {
        $("#" + channelTxt_ID).text(channelTxt);
    }
    if (introTxt) {
        $("#" + introTime_ID).text(introTxt);
    }
    if (outroTxt) {
        $("#" + outroTime_ID).text(outroTxt);
    }
}
(function(setupAndBind) {
    "use strict";
    //
    // detect page change hashchange not working
    // so check every 3 seconds if current URL matches URL we started with.
    // handle appropriately.
    //
    var l = document.URL;
    if (l.includes("watch")) {
        setupAndBind();
    }
    setInterval(function() {
        // check initializationRequired flag and if set, destroy and reinitialize.
        if (initializationRequired) {
            log("forced to destroy");
            destroy(function() {
                log("rebuilding..");
                setupAndBind();
            });
        }
        if (l != document.URL){
            l = document.URL;
            if (l === "https://www.youtube.com/") {
                // ignore home
                destroy(function() {
                   log("complete destruction");
                });
            } else if (l.includes("watch")) {
                log("channel changed");
                initializationRequired = true
            }
        }
    }, 3000);
})(function() {
    // ignore home
    if (document.URL === "https://www.youtube.com/"){
        log("ignoring home");
        return;
    }
    //
    whenReady({ //  .ytp-progress-list
        selectors: [".video-stream", "#primary > #primary-inner", ".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": validateChannel,
        },
        callback: function(selectors) {
            //
            let channel = selectors.channel;
            let introTargetId = channel.split(" ").join("_") + "-intro";
            let outroTargetId = channel.split(" ").join("_") + "-outro";
            log("loaded channel: " + channel);
            //
            var loadedIntroSetInSeconds = GM_getValue(introTargetId) || 0;
            var loadedOutroSetInSeconds = GM_getValue(outroTargetId) || 0;
            log("intro set: " + loadedIntroSetInSeconds);
            log("outro set: " + loadedOutroSetInSeconds);
            //
            updateControls({
                introPlaceholderTxt: (loadedIntroSetInSeconds <= 0)? "Set intro here..": loadedIntroSetInSeconds,
                outroPlaceholderTxt: (loadedOutroSetInSeconds <= 0)? "Set outro here..": loadedOutroSetInSeconds,
                channelTxt: channel,
                introTxt: loadedIntroSetInSeconds,
                outroTxt: loadedOutroSetInSeconds,
            });
            //
            //
            const bindToStream = 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;
                var loadedIntroSetInSeconds = GM_getValue(introTargetId) || 0;
                var loadedOutroSetInSeconds = 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){
                    var currentTime = this.currentTime;
                    var duration = this.duration;
                    //
                    if (duration && !progressBarDone) {
                        progressBarDone = true;
                        createProgressBars(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.
                    if(currentTime >= duration - loadedOutroSetInSeconds){
                        $(".ytp-next-button")[0].click();
                    }
                });
            };
            //
            // handle apply outro in seconds
            //
            log("bind to click");
            $("#" + 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();
            });
            bindToStream();
        },
        error: function(e) {
            log(e);
            destroyControls();
            destroyProgressBar();
        },
    });
});