// ==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 // @downloadURL none // ==/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 . */ (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 = '\

Simple Sponsor Skipper













'; 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(); }); } })();