// ==UserScript==
// @name Simple Sponsor Skipper
// @author SkauOfArcadia
// @match *://m.youtube.com/*
// @match *://youtu.be/*
// @match *://www.youtube.com/*
// @match *://www.youtube-nocookie.com/embed/*
// @match *://inv.riverside.rocks/*
// @match *://invidio.xamh.de/*
// @match *://invidious.esmailelbob.xyz/*
// @match *://invidious.flokinet.to/*
// @match *://invidious-jp.kavin.rocks/*
// @match *://invidious-us.kavin.rocks/*
// @match *://invidious.kavin.rocks/*
// @match *://invidious.lunar.icu/*
// @match *://inv.bp.mutahar.rocks/*
// @match *://invidious.mutahar.rocks/*
// @match *://invidious.namazso.eu/*
// @match *://invidious.osi.kr/*
// @match *://invidious.privacy.gd/*
// @match *://invidious.snopyta.org/*
// @match *://invidious.weblibre.org/*
// @match *://tube.cthd.icu/*
// @match *://vid.mint.lgbt/*
// @match *://vid.puffyan.us/*
// @match *://yewtu.be/*
// @match *://youtube.076.ne.jp/*
// @match *://yt.artemislena.eu/*
// @match *://tube.cadence.moe/*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.notification
// @grant GM.openInTab
// @grant GM.registerMenuCommand
// @grant GM.xmlHttpRequest
// @connect sponsor.ajay.app
// @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @run-at document-start
// @version 2022.10
// @license AGPL-3.0-or-later
// @description Skips annoying intros, sponsors and w/e on YouTube and its frontends like Invidious and CloudTube using the SponsorBlock API.
// @namespace https://greasyfork.org/users/751327
// ==/UserScript==
/**
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(async function() {
"use strict";
async function go(videoId) {
console.log("New video ID: " + videoId);
let segurl = "";
let result = [];
let rBefore = -1;
let cat = ["poi_highlight"];
if (s3settings.categories & categories.sponsor) {
cat.push("sponsor");
}
if (s3settings.categories & categories.intro) {
cat.push("intro");
}
if (s3settings.categories & categories.outro) {
cat.push("outro");
}
if (s3settings.categories & categories.interaction) {
cat.push("interaction");
}
if (s3settings.categories & categories.selfpromo) {
cat.push("selfpromo");
}
if (s3settings.categories & categories.preview) {
cat.push("preview");
}
if (s3settings.categories & categories.music_offtopic) {
cat.push("music_offtopic");
}
if (s3settings.categories & categories.filler) {
cat.push("filler");
}
if (s3settings.disable_hashing)
{
segurl = 'https://sponsor.ajay.app/api/skipSegments?videoID=' + videoId + "&categories=" + encodeURIComponent(JSON.stringify(shuffle(cat)));
}
else
{
let vidsha256 = await sha256(videoId);
console.log("SHA256 hash: " + vidsha256);
segurl = 'https://sponsor.ajay.app/api/skipSegments/' + vidsha256.substring(0,4) + "?categories=" + encodeURIComponent(JSON.stringify(shuffle(cat)));
}
console.log(segurl);
const resp = await (function() {
return new Promise(resolve => {
GM.xmlHttpRequest({
method: 'GET',
url: segurl,
headers: {
'Accept': 'application/json'
},
onload: resolve
});
});
})();
try {
let response;
if (s3settings.disable_hashing)
response = JSON.parse("[{\"videoID\":\"" + videoId + "\",\"segments\":" + resp.responseText + "}]");
else
response = JSON.parse(resp.responseText);
for (let x = 0; x < response.length; x++)
{
if (response[x].videoID === videoId)
{
rBefore = response[x].segments.length;
result = processSegments(response[x].segments);
break;
}
}
} catch (e) { result = []; }
let x = 0;
let prevTime = -1;
let player;
let favicon = document.querySelector('link[rel=icon]');
if (favicon && favicon.hasAttribute('href')){
favicon = favicon.href;
} else {
favicon = null;
}
if (result.length > 0) {
if (s3settings.notifications && window.self === window.top) {
let ntxt = "";
if (result.length === rBefore) {
ntxt = "Received " + result.length;
if (result.length > 1) {
ntxt += " segments."
} else {
ntxt += " segment."
}
} else {
ntxt = "Received " + rBefore + " segments, " + result.length + " after processed.";
}
setTimeout(() => { GM.notification({
title: "Skippable segments found!",
text: ntxt + "\n\u00AD\n" + document.title + " (Video ID: " + videoId + ")",
silent: true,
timeout: 5000,
image: favicon,
})}, 600);
}
const tfunc = function() {
if (location.pathname.indexOf(videoId) === -1 && location.search.indexOf('v=' + videoId) === -1) {
window.clearInterval(timer);
document.removeEventListener("visibilitychange", efunc);
console.log('Disposing of timer for video ID ' + videoId);
} //Dispose of the timer once we no longer need it.
else if ((location.hostname.endsWith(".youtube.com") || location.hostname === 'www.youtube-nocookie.com' || location.hostname === 'youtu.be') && !!document.getElementById("movie_player")) //Youtube
{
player = unsafeWindow.document.getElementById("movie_player");
if (player.baseURI.indexOf(videoId) !== -1 && player.getPlayerState() === 1 && x < result.length && player.getCurrentTime() >= result[x].segment[0]) {
if (player.getCurrentTime() < result[x].segment[1]) {
player.seekTo(result[x].segment[1]);
if (s3settings.notifications)
{
GM.notification({
title: "Skipped " + result[x].category.replace('music_offtopic','non-music').replace('selfpromo', 'self-promotion') + " segment.",
text: "Segment " + (x + 1) + " out of " + result.length + "\n\u00AD\n" + document.title + " (Video ID: " + videoId + ")",
silent: true,
timeout: 5000,
image: favicon,
});
}
console.log("Skipping " + result[x].category + " segment (" + (x + 1) + " out of " + result.length + ") from " + result[x].segment[0] + " to " + result[x].segment[1]);
}
x++;
} else if (player.getCurrentTime() < prevTime) {
for (let s = 0; s < result.length; s++) {
if (player.getCurrentTime() < result[s].segment[1]) {
x = s;
console.log("Next segment is " + s);
break;
}
}
}
prevTime = player.getCurrentTime();
} else if (!!document.getElementById("player_html5_api") || !!document.getElementById("player") || !!document.getElementById("video")) //Invidious and CloudTube
{
player = document.getElementById("player_html5_api") || document.getElementById("player") || document.getElementById("video");
if (!player.paused && x < result.length && player.currentTime >= result[x].segment[0]) {
if (player.currentTime < result[x].segment[1]) {
player.currentTime = result[x].segment[1];
if (s3settings.notifications) {
GM.notification({
title: "Skipped " + result[x].category.replace('music_offtopic','non-music').replace('selfpromo', 'self-promotion') + " segment.",
text: "Segment " + (x + 1) + " out of " + result.length + "\n\u00AD\n" + document.title + " (Video ID: " + videoId + ")",
silent: true,
timeout: 5000,
image: favicon,
});
}
console.log("Skipping " + result[x].category + " segment (" + (x + 1) + " out of " + result.length + ") from " + result[x].segment[0] + " to " + result[x].segment[1]);
}
x++;
} else if (player.currentTime < prevTime) {
for (let s = 0; s < result.length; s++) {
if (player.currentTime < result[s].segment[1]) {
x = s;
console.log("Next segment is " + s);
break;
}
}
}
prevTime = player.currentTime;
}
};
var timer = window.setInterval(tfunc, 333);
const efunc = function() { //prevents the interval from being killed after switching tabs
window.clearInterval(timer);
timer = window.setInterval(tfunc, 333);
};
document.addEventListener("visibilitychange", efunc);
}
}
function processSegments(segments) {
if (typeof segments === 'object') {
let newSegments = [];
let newKey = 0;
for (let x = 0; x < segments.length; x++) {
if (x > 0 && Math.ceil(newSegments[newKey - 1].segment[1]) >= Math.floor(segments[x].segment[0]) && newSegments[newKey - 1].segment[1] < segments[x].segment[1] && segments[x].votes >= s3settings.upvotes) {
newSegments[newKey - 1].segment[1] = segments[x].segment[1];
newSegments[newKey - 1].category = "combined";
console.log(x + " combined with " + (newKey - 1));
} else if (segments[x].votes < s3settings.upvotes || (x > 0 && Math.ceil(newSegments[newKey - 1].segment[1]) >= Math.floor(segments[x].segment[0]) && newSegments[newKey - 1].segment[1] >= segments[x].segment[1])) {
console.log("Ignoring segment " + x);
} else {
newSegments[newKey] = segments[x];
console.log(newKey + " added");
newKey++;
}
}
return newSegments;
} else {
return [];
}
}
async function sha256(message) {
// encode as UTF-8
const msgBuffer = new TextEncoder().encode(message);
// hash the message
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
// convert ArrayBuffer to Array
const hashArray = Array.from(new Uint8Array(hashBuffer));
// convert bytes to hex string
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
function shuffle(array) {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
const categories = {
sponsor: 1,
intro: 2,
outro: 4,
interaction: 8,
selfpromo: 16,
preview: 32,
music_offtopic: 64,
filler: 128
}
let s3settings;
s3settings = await GM.getValue('s3settings');
if(!!s3settings && Object.keys(s3settings).length > 0){
console.log((new Date()).toTimeString().split(' ')[0] + ' - Simple Sponsor Skipper: Settings loaded!');
} else {
s3settings = JSON.parse('{ "categories":127, "upvotes":-2, "notifications":true, "disable_hashing":false }');
if(navigator.userAgent.toLowerCase().indexOf('pale moon') !== -1
|| navigator.userAgent.toLowerCase().indexOf('mypal') !== -1
|| navigator.userAgent.toLowerCase().indexOf('male poon') !== -1)
{
s3settings.disable_hashing = true;
}
await GM.setValue('s3settings', s3settings);
console.log((new Date()).toTimeString().split(' ')[0] + ' - Simple Sponsor Skipper: Default settings saved!');
GM.notification({
title: "Simple Sponsor Skipper",
text: "It looks like this is your first time using Simple Sponsor Skipper.\n\u00AD\nClick here to open the configuration menu!",
timeout: 10000,
silent: true,
onclick: function() { GM.openInTab(document.location.protocol + "//" + document.location.host.replace('youtube-nocookie.com', 'youtube.com') + document.location.pathname.replace('/embed/','/watch?v=').replace('/v/','/watch?v=') + document.location.search.replace('?','&').replace('&v=','?v=') + "#s3config"); },
});
}
if (location.hash.toLowerCase() === '#s3config') {
window.addEventListener("DOMContentLoaded", function() {
const docHtml = document.getElementsByTagName('html')[0];
docHtml.innerHTML = '\<center><h1>Simple Sponsor Skipper</h1><br><form><div><input type="checkbox" id="sponsor"><label for="sponsor">Skip sponsor segments</label><br><input type="checkbox" id="intro"><label for="intro">Skip intro segments</label><br><input type="checkbox" id="outro"><label for="outro">Skip outro segments</label><br><input type="checkbox" id="interaction"><label for="interaction">Skip interaction reminder segments</label><br><input type="checkbox" id="selfpromo"><label for="selfpromo">Skip self-promotion segments</label><br><input type="checkbox" id="preview"><label for="preview">Skip preview segments</label><br><input type="checkbox" id="music_offtopic"><label for="music_offtopic">Skip non-music segments in music videos</label><br><input type="checkbox" id="filler"><label for="filler">Skip filler segments (WARNING: very aggressive!)</label><br><label for="upvotes">Minimum segment upvotes:</label><input type="number" id="upvotes"><br><input type="checkbox" id="notifications"><label for="notifications">Enable Desktop Notifications</label><br><input type="checkbox" id="disable_hashing"><label for="disable_hashing">Disable Video ID Hashing (Pale Moon Compatibility Fix)</label></div><br><div><button type="button" id="btnsave">Save settings</button><button type="button" id="btnclose">Close</button></div></form></center>';
docHtml.style = "";
document.title = 'Simple Sponsor Skipper Configuration';
document.getElementById('sponsor').checked = (s3settings.categories & categories.sponsor);
document.getElementById('intro').checked = (s3settings.categories & categories.intro);
document.getElementById('outro').checked = (s3settings.categories & categories.outro);
document.getElementById('interaction').checked = (s3settings.categories & categories.interaction);
document.getElementById('selfpromo').checked = (s3settings.categories & categories.selfpromo);
document.getElementById('preview').checked = (s3settings.categories & categories.preview);
document.getElementById('music_offtopic').checked = (s3settings.categories & categories.music_offtopic);
document.getElementById('filler').checked = (s3settings.categories & categories.filler);
document.getElementById('upvotes').value = s3settings.upvotes;
document.getElementById('notifications').checked = s3settings.notifications;
document.getElementById('disable_hashing').checked = s3settings.disable_hashing;
const btnSave = document.getElementById('btnsave');
btnSave.addEventListener("click", async function() {
s3settings.categories = 0;
if (document.getElementById('sponsor').checked) {
s3settings.categories += categories.sponsor;
}
if (document.getElementById('intro').checked) {
s3settings.categories += categories.intro;
}
if (document.getElementById('outro').checked) {
s3settings.categories += categories.outro;
}
if (document.getElementById('interaction').checked) {
s3settings.categories += categories.interaction;
}
if (document.getElementById('selfpromo').checked) {
s3settings.categories += categories.selfpromo;
}
if (document.getElementById('preview').checked) {
s3settings.categories += categories.preview;
}
if (document.getElementById('music_offtopic').checked) {
s3settings.categories += categories.music_offtopic;
}
if (document.getElementById('filler').checked) {
s3settings.categories += categories.filler;
} else if (s3settings.categories === 0) {
s3settings.categories = 1;
}
s3settings.upvotes = parseInt(document.getElementById('upvotes').value, 10) || -2;
s3settings.notifications = document.getElementById('notifications').checked;
s3settings.disable_hashing = document.getElementById('disable_hashing').checked;
await GM.setValue('s3settings', s3settings);
console.log((new Date()).toTimeString().split(' ')[0] + ' - Simple Sponsor Skipper: Settings saved!');
btnSave.textContent = "Saved!";
btnSave.disabled = true;
setTimeout(() => { btnSave.textContent = "Save settings"; btnSave.disabled = false; }, 3000);
});
document.getElementById('btnclose').addEventListener("click", function() {
location.replace(location.protocol + "//" + location.host + location.pathname + location.search)
});
});
} else {
var oldVidId = "";
var params = new URLSearchParams(location.search);
if (params.has('v')) {
oldVidId = params.get('v');
go(oldVidId);
} else if (location.pathname.indexOf('/embed/') === 0 || location.pathname.indexOf('/v/') === 0) {
oldVidId = location.pathname.replace('/v/', '').replace('/embed/', '').split('/')[0];
go(oldVidId);
}
window.addEventListener("load", function() {
let observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
params = new URLSearchParams(location.search);
if (params.has('v') && params.get('v') !== oldVidId) {
oldVidId = params.get('v');
go(oldVidId);
} else if ((location.pathname.indexOf('/embed/') === 0 || location.pathname.indexOf('/v/') === 0) && location.pathname.indexOf(oldVidId) === -1) {
oldVidId = location.pathname.replace('/v/', '').replace('/embed/', '').split('/')[0];
go(oldVidId);
} else if (!params.has('v') && location.pathname.indexOf('/embed/') === -1 && location.pathname.indexOf('/v/') === -1) {
oldVidId = "";
}
});
});
let config = {
childList: true,
subtree: true
};
observer.observe(document.body, config);
});
}
if (window.self === window.top) {
GM.registerMenuCommand("Configuration", function() { window.location.replace(window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.search + "#s3config"); window.location.reload(); });
}
})();