Greasy Fork

Greasy Fork is available in English.

Simple Sponsor Skipper

Skips annoying intros, sponsors and w/e on YouTube and its frontends like Invidious and Piped using the SponsorBlock API.

当前为 2022-10-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.08
// @license     AGPL-3.0-or-later
// @description Skips annoying intros, sponsors and w/e on YouTube and its frontends like Invidious and Piped using the SponsorBlock API.
// @namespace http://greasyfork.icu/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 vidsha256 = await sha256(videoId);
        console.log("SHA256 hash: " + vidsha256);
        let result = [];
        let rBefore = -1;
        let cat = "?category=poi_highlight";
        if (s3settings.categories & categories.sponsor) {
            cat += "&category=sponsor";
        }
        if (s3settings.categories & categories.intro) {
            cat += "&category=intro";
        }
        if (s3settings.categories & categories.outro) {
            cat += "&category=outro";
        }
        if (s3settings.categories & categories.interaction) {
            cat += "&category=interaction";
        }
        if (s3settings.categories & categories.selfpromo) {
            cat += "&category=selfpromo";
        }
        if (s3settings.categories & categories.preview) {
            cat += "&category=preview";
        }
        if (s3settings.categories & categories.music_offtopic) {
            cat += "&category=music_offtopic";
        }
        if (s3settings.categories & categories.filler) {
            cat += "&category=filler";
        }
        const resp = await (function() {
            return new Promise(resolve => {
                GM.xmlHttpRequest({
                    method: 'GET',
                    url: 'https://sponsor.ajay.app/api/skipSegments/' + vidsha256.substring(0,4) + cat,
                    headers: {
                        'Accept': 'application/json'
                    },
                    onload: resolve
                });
            });
        })();
        try {
            const 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;
    }

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