YT - Adskip
Automatically skips unwanted video segments on YouTube
This script is marked as "broken" and is likely not working
Details: This service has been closed
Click here to install
Browse More Scripts
// ==UserScript==
// @name YT - Adskip
// @namespace 51e5a7369e49ad0d09cd2f25c6013072b1589fdf
// @version 0.9
// @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 true
// @broken This service has been closed
// @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.9 - Service closed down
// 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';
////This is no longer in use////
return;
////This is no longer in use////
//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.
*/
This script is marked as "expired".
It's likely not going to work the way it should.
If you really need this script,
contact me.
User Script Managers
A userscript manager is the browser extension that injects scripts into websites
to change their behavior to your liking.
Recommendation
All scripts on this site have been developed and tested with Tampermonkey on firefox.
Try other browsers and other script managers at your own risk.
No script should use firefox or chrome specific features,
which means they should also work in other modern browsers.
If you prefer, you can use greasemonkey.
Get Tampermonkey,
Get Greasemonkey (Firefox only)
Script Installation
Once you have obtained a user script manager,
clicking on the install button will pop up an installation prompt.
To allow script manager detection,
you can install this helper script.
It's not necessary but simplifies your future visits to this site.
Script Installation
We detected, that you have a script manager installed and active.
Click the "Install Script" button to obtain the script.