Greasy Fork is available in English.

Youtube outro skip

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

Verze ze dne 13. 04. 2021. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         Youtube outro skip
// @namespace    http://tampermonkey.net/
// @version      2.0
// @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
// @require      http://code.jquery.com/jquery-latest.js
// ==/UserScript==

/* globals $ whenReady */

const version = 2.0;
const debug = true;

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 setupControls = function(selector) {
    destroyControls();
    return selector.prepend(
        $("<div id='" + controlUI_ID + "'>").append(
            $("<h3>Youtube Skip Outro Controller " + version + "</h3>")
        ).append(
            $("<input type='number' min='0' id='" + introLen_ID + "' placeholder='loading channel'/>")
        ).append(
            $("<input type='number' min='0' id='" + outroLen_ID + "' placeholder='loading channel'/>")
        ).append(
            $("<button id='" + apply_ID + "'>apply</button>")
        ).append(
            $("<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>").css({
                "padding": "2px",
            })
        ).css({
            "margin": "2px",
            "textAlign": "right",
            "color": "#666666",
        })
    );
}
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();
        },
    });
});