YT - Adskip

Automatically skips unwanted video segments on YouTube

Click here to install Browse More Scripts
// ==UserScript==
// @name         YT - Adskip
// @namespace    51e5a7369e49ad0d09cd2f25c6013072b1589fdf
// @version      0.8
// @description  Automatically skips unwanted video segments on YouTube
// @author       /u/AyrA_ch
// @match        http*://www.youtube.com/*
// @match        http*://youtube.com/*
// @match        http*://cable.ayra.ch/*
// @external     false
// @expired      false
// @supportURL   https://cable.ayra.ch/ytas/contact
// @homepage     https://cable.ayra.ch/ytas/
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_notification
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

// Version History
// 0.8 - Change when range updates are acquired
// 0.7 - Update video player menu item
// 0.6 - Better Id regular expression
// 0.5 - Make API requests only when Id was found
// 0.4 - Support for custom settings
// 0.3 - Run at earliest possible time
// 0.2 - Add YT Adskip detection
// 0.1 - Initial Version

(function ($, window) {
    'use strict';

    //Default settings
    const defaultSettings = Object.freeze({
        0: {
            block: true,
            min: 50
        },
        1: {
            block: true,
            min: 50
        },
        2: {
            block: true,
            min: 50
        },
        3: {
            block: true,
            min: 100
        },
        4: {
            block: true,
            min: 100
        },
        5: {
            block: true,
            min: 50
        },
        6: {
            block: true,
            min: 50
        },
        7: {
            block: false,
            min: 300
        }
    });

    //Checks if a setting is valid
    var isValidSetting = function (x) {
        //Must be object
        return typeof(x) === typeof({}) &&
        //must have two keys
        Object.keys(x).length === 2 &&
        //Block key must be boolean
        typeof(x["block"]) === typeof(true) &&
        //Duration must be number
        typeof(x["min"]) === typeof(1) &&
        //positive number
        x["min"] >= 0 &&
        //no fractions
        (x["min"] | 0) === x["min"];
    };

    //Gets current settings or defaults if none have been made
    var getSettings = function () {
        var v = GM_getValue('settings', null);
        if (v) {
            return JSON.parse(v);
        }
        //Defaults for missing settings
        return defaultSettings;
    };

    //Saves new settings (if valid)
    var setSettings = function (x) {
        var settings = {};
        var keys = Object.keys(defaultSettings);
        for (var i = 0; i < keys.length; i++) {
            var k = keys[i];
            settings[k] = isValidSetting(x[k]) ? x[k] : defaultSettings[k];
        }
        GM_setValue('settings', JSON.stringify(settings));
        return settings;
    };

    //Provide integration on cable.ayra.ch
    if (location.hostname.toLowerCase() === 'cable.ayra.ch') {
        Object.defineProperty(window, 'ytasSettings', {
            get: getSettings,
            set: setSettings
        });
        document.addEventListener("DOMContentLoaded", function () {
            //Report YTas script
            if (typeof(window.hasYTas) === typeof(function () {})) {
                window.hasYTas();
            }
            //Update API key if available
            if (typeof(window.apiKey) === typeof("")) {
                setInterval(function () {
                    var key = GM_getValue("apiKey", "");
                    if (key !== window.apiKey) {
                        GM_setValue("apiKey", window.apiKey);
                        GM_notification({
                            text: "The YT Adskip API key was updated",
                            title: "API key update",
                            timeout: 3000
                        }, function () {});
                    }
                }, 500);
            }
        });
        return false;
    } else {
        document.addEventListener("DOMContentLoaded", function () {
            //Endpoint for Ad list
            var API_AD_ENDPOINT = "https://cable.ayra.ch/ytas/get/";
            //Endpoint for Ad type info
            var API_INFO_ENDPOINT = "https://cable.ayra.ch/ytas/info";
            //Regular expressions to find a valid video Id.
            //For details on the ID pattern, see https://cable.ayra.ch/help/fs.php?help=youtube_id
            var regex = [
                /(?:youtu\.be\/|youtube(?:-nocookie)?.com\/(?:v\/|e\/|.*u\/\w+\/|embed\/|.*v=))([\w-]{10}[AEIMQUYcgkosw048])/i,
                /(?:youtu\.be\/|youtube(?:-nocookie)?.com\/(?:attribution_link\?.*))([\w-]{10}[AEIMQUYcgkosw048])/i
            ];
            //Factor to convert from video time to API time
            var TIME_FACTOR = 10;

            //Current Video ad segments
            var segments = [];
            //Current Video Id, to stop repeated requests
            var currentId = null;
            //Possible types of ranges
            var rangeTypes = [];

            //DOM Menu item for YTas access
            var menuItem = null;

            //Gets if the menu item is present and in the DOM
            var hasMenuItem = function () {
                return menuItem && menuItem.parentNode;
            };
            //Adds the YTas Menu item to the settings menu of a video
            var addMenuItem = function () {
                var menu = $(".ytp-settings-menu .ytp-panel-menu");
                if (!menu) {
                    console.warn("Menu not found");
                    return false;
                }
                if (!hasMenuItem()) {
                    var item = document.createElement("div");
                    item.setAttribute("class", "ytp-menuitem");
                    item.setAttribute("role", "menuitem");
                    item.setAttribute("tabindex", "0");
                    item.innerHTML = '<div class="ytp-menuitem-icon"></div>';
                    item.innerHTML += '<div class="ytp-menuitem-label">Range Editor</div>';
                    item.innerHTML += '<div class="ytp-menuitem-content">YT Adskip</div>';
                    item.addEventListener("click", function () {
                        console.warn('Unimplemented Feature');
                        alert('Unimplemented Feature. Contact /u/AyrA_ch if you are interested in making this');
                    });
                    menu.appendChild(item);
                    menuItem = item;
                }
            };

            //JSON.tryParse
            var toJSON = function (x) {
                try {
                    return JSON.parse(x);
                } catch (e) {
                    console.warn("YTAS: Attempted to parse invalid JSON.", {
                        data: x,
                        error: e
                    });
                }
                //undefined instead of null because "null" can be a valid JSON input
                return undefined;
            };

            //Gets the current video element
            var getVideo = function () {
                return $("video");
            };

            //Gets the current video id
            var getVideoId = function () {
                for (var i = 0; i < regex.length; i++) {
                    var match = location.href.match(regex[i]);
                    if (match) {
                        return match[1];
                    }
                }
                return null;
            };

            //Obtains all ranges from the Adskip API
            var getRanges = function (cb) {
                var id = getVideoId();
                if (id) {
                    var xhr = new XMLHttpRequest();
                    xhr.open("GET", API_AD_ENDPOINT + getVideoId());
                    xhr.addEventListener("load", function () {
                        if (typeof(cb) === typeof(function () {})) {
                            cb(toJSON(xhr.responseText));
                        }
                    });
                    xhr.send();
                } else {
                    if (typeof(cb) === typeof(function () {})) {
                        cb(null);
                    }
                }
            };

            //Obtains Adskip info from the API
            var getRangeInfo = function (cb, force) {
                //Try to load from cache first
                var types = GM_getValue("rangeTypes", "");
                if (force || types == "" || types == null) {
                    var xhr = new XMLHttpRequest();
                    xhr.open("GET", API_INFO_ENDPOINT);
                    xhr.addEventListener("load", function () {
                        rangeTypes = toJSON(xhr.responseText) || [];
                        //Cache range types
                        GM_setValue("rangeTypes", rangeTypes.length > 0 ? JSON.stringify(rangeTypes) : "");
                        if (typeof(cb) === typeof(function () {})) {
                            cb(rangeTypes);
                        }
                    });
                    xhr.send();
                } else {
                    rangeTypes = toJSON(types);
                    if (typeof(cb) === typeof(function () {})) {
                        cb(rangeTypes);
                    }
                }
            };

			var next=function(){
				return setTimeout(initAdskip,500);
			};

            //Initializes the Adskip algorithm for the current page
            var initAdskip = function () {
                var id = getVideoId();
                var video = getVideo();
                //Don't reload ad list if we did not navigate
                if (id && video && id !== currentId) {
                    currentId = id;
                    segments = [];
                    getRanges(function (result) {
                        if (result.success) {
                            if (result.data && result.data.length > 0) {
                                //Function that processes the ranges
                                var cont = function () {
                                    console.debug("YTAS: API success:", result.data);
                                    segments = filterSegments(result.data);
                                    console.debug("YTAS: Filtered List:", segments);
                                };

                                if (result.data.filter(function (v) {
                                        //return true, if a mentioned range type doesn't exists.
                                        return !rangeTypes[v.type];
                                    }).length > 0) {
                                    //There is at least one type we don't know.
                                    //Reload types, then process the current ranges
                                    getRangeInfo(cont, true);
                                } else {
                                    cont();
                                }
                            } else {
                                console.debug("YTAS: Video", id, "has no adskip ranges");
                                segments = [];
                            }
                        } else {
                            console.warn("YTAS: API failed:", result);
                        }
						next();
                    });
                }
				else{
					next();
				}
            };
            getRangeInfo(initAdskip);

            //Filter segment types according to settings
            var filterSegments = function (s) {
                var settings = getSettings();
                var ret = [];
                for (var i in s) {
                    var segment = s[i];
                    //Support unknown types by mapping them to the "Unspecified" entry.
                    var type = settings[segment.type] || settings[0];
                    //Add entry if we want to block these and it's long enough
                    if (type.block && segment.duration >= type.min) {
                        ret.push(segment);
                    }
                }
                return ret;
            };

            //Gets the first segment that contains the given time
            var getSegment = function (time) {
                return segments.filter(function (v) {
                    return v.start <= time && v.start + v.duration > time && !v.used;
                })[0];
            };

            //Removes a segment from the list
            var removeUsedSegment = function (s) {
                segments = segments.filter(function (v) {
                    return v.start !== s.start || v.end !== s.end || v.type !== s.type;
                });
            };

            //Function responsible for skipping ranges
            var rangeSkipper = function () {
                var video = getVideo();
                var timer = 200;
                //Must have video element
                //Must not be paused
                //Must have working media (duration is NaN or 0 if not)
                //Must have segments
                if (video && !video.paused && video.duration && segments) {
                    var current = getSegment(video.currentTime * TIME_FACTOR);
                    if (current && !current.used) {
                        var rangeType = rangeTypes[current.type];
                        var newTime = (current.start + current.duration) / TIME_FACTOR;
                        //Don't go beyond the video
                        video.currentTime = newTime < video.duration ? newTime : video.duration;
                        timer = 2000;
                        console.info("YTAS: Skipped over a range:", current);
                        console.log("YTAS: Skip reason:", rangeType.name);
                        console.debug("YTAS:", rangeType.desc);
                        //Don't skip over the same range twice
                        removeUsedSegment(current);
                    } else {
                        timer = 200;
                    }
                } else {
                    //Wait longer if video and/or segments are not available
                    timer = 1000;
                }
                setTimeout(rangeSkipper, timer);
            };
            rangeSkipper();

			/* This no longer works
            //Check Id ane menu entry on each change to the site
            //"subtree:true" is not needed because YT changes the main body content constantly.
            //We do this because YT no longer navigates properly on the site but uses the history state.
            var observer = new MutationObserver(function () {
                initAdskip();
                addMenuItem();
            });
            var config = {
                attributes: true,
                childList: true,
                characterData: false,
                subtree: true
            };
            observer.observe(document.body, config);
			//*/
        });
    }
    return true;
})(document.querySelector.bind(document), unsafeWindow);

/*
LICENSE:
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
The full license text can be found here: http://creativecommons.org/licenses/by-nc-sa/4.0/
The link has an easy to understand version of the license and the full license text.

DISCLAIMER:
THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/

Copyright © 2018 by Kevin Gut 📧 | More services | Generated for 3.232.129.123