Loop Fix

Fixes youtube video repeating when watching in playlist + refreshing page if video stopped downloading

Versión del día 18/09/2024. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name         Loop Fix
// @description  Fixes youtube video repeating when watching in playlist + refreshing page if video stopped downloading
// @version      0.1.14
// @author       0vC4
// @namespace    https://greasyfork.org/users/670183
// @match        *://*.youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @run-at       document-start
// @license      MIT
// @grant        none
// ==/UserScript==

// refresh if no reply during these actions
const maxLoadingTime = 500;
const maxSeekingTime = 1500;

(() => {
    function blockEvents(condition, ...events) {
        events = events.flat();
        const tag = window.EventTarget.prototype;
        tag._add = tag.addEventListener;
        tag.addEventListener = function (name, callback, options) {
            if (!events.includes(name)) return tag._add.call(this, name, callback, options);

            function cb(e) {
                if (!condition.call(this)) return;
                callback.call(this, e);
            };

            tag._add.call(this, name, cb, options);
        };
    }

    blockEvents(function() {
        return !this.loop; // have loop = block events
    }, 'pause', 'timeupdate', 'waiting');
})();

const log = window.console.log;
window.loopState = localStorage.getItem('loopState') ?? false;
function clicking(e) {
    window.loopState = JSON.parse(this.ariaChecked);
};

let created = false;
let refreshing = false;
let loaded = false;
let oldVid = null;
let seeking = false;
let seekingId = 0;
let loadlock = false;

const maxSeekingDebounce = 500;
const policy = window.trustedTypes && window.trustedTypes.createPolicy ? window.trustedTypes.createPolicy('timeout', {createScriptURL: str => str}) : {createScriptURL: str => str};
const timeout = delay => URL.createObjectURL(new Blob([`setTimeout(() => postMessage(0), ${delay});`]));
const refreshOnLoad = policy.createScriptURL(timeout(maxLoadingTime));
const seekingDebounce = policy.createScriptURL(timeout(maxSeekingDebounce));
window.addEventListener("beforeunload", function(event) {
     loaded = false;
});

window.setInterval(()=>{
    const vid = window.document.querySelector('video.html5-main-video');
    if (vid) {
        // apply loopState after .src or page refresh
        if (vid.loop != window.loopState) vid.loop = window.loopState;

        // attach loop click detector
        const repeat = window.document.querySelectorAll('.ytp-menuitem[tabindex="-1"]')[0];
        if (repeat && repeat.onclick != clicking) {
            repeat.onclick = clicking;
        }

        // to prevent false detecting when changing video
        if (vid != oldVid) {
            loaded = false;
            oldVid = vid;
        }

        if (!created) {
            created = true;
            localStorage.removeItem('loopState');
            vid.addEventListener('seeking', () => {
                clearTimeout(seekingId);
                seeking = true;
                seekingId = setTimeout(() => {
                    seeking = false;
                }, maxSeekingTime);
            });

            // refresh if no data for half sec
            const callback = () => {
                if (!loaded && vid.readyState === window.HTMLMediaElement.HAVE_NOTHING) {
                    localStorage.setItem('loopState', window.loopState);
                    window.location.href = window.location.href;
                }
            };
            new Worker(refreshOnLoad).onmessage = callback;
        }

        // apply loopState after .src or page refresh
        if (vid.loop != window.loopState) vid.loop = window.loopState;

        // to prevent early page refresh
        if (!loaded && (vid.readyState === window.HTMLMediaElement.HAVE_CURRENT_DATA || vid.readyState === window.HTMLMediaElement.HAVE_ENOUGH_DATA)) {
            if (loadlock) return;
            loadlock = true;
            // give a chance to load
            const callback = () => {
                loaded = true;
                loadlock = false;
            };
            new Worker(refreshOnLoad).onmessage = callback;
        }

        const noData = vid.readyState === window.HTMLMediaElement.HAVE_CURRENT_DATA || vid.readyState === window.HTMLMediaElement.HAVE_METADATA;

        if (loaded && !refreshing && noData && !seeking && !vid.paused && +vid.currentTime.toFixed(0) >= +vid.buffered.end(0).toFixed(0) - 2) {
            refreshing = true;
            const callback = () => {
                if (seeking) {
                    refreshing = false;
                    return;
                }
                localStorage.setItem('loopState', window.loopState);
                window.location.href = window.location.href.split('?')[0] + '?t=' + (+vid.currentTime.toFixed(0) + 1) + '&' + window.location.href.split('?')[1];
            };
            new Worker(seekingDebounce).onmessage = callback;
        }
    }
});