Last active
August 30, 2026 03:10
-
-
Save MiguelMachado-dev/7e0a999530b78c4d0028f96ca4b0b922 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Twitch Lurk Guard | |
| // @namespace lurk-guard | |
| // @version 1.0.0 | |
| // @description Auto-reloads a Twitch channel page when the stream drops, so lurkers reconnect without touching anything. | |
| // @updateURL https://gist.githubusercontent.com/MiguelMachado-dev/7e0a999530b78c4d0028f96ca4b0b922/raw/lurk-guard.user.js | |
| // @downloadURL https://gist.githubusercontent.com/MiguelMachado-dev/7e0a999530b78c4d0028f96ca4b0b922/raw/lurk-guard.user.js | |
| // @match https://www.twitch.tv/* | |
| // @match https://twitch.tv/* | |
| // @run-at document-idle | |
| // @grant none | |
| // ==/UserScript== | |
| // Same logic as the extension: if the <video> we were watching stops | |
| // advancing (or disappears), reload with backoff until it plays again. | |
| const CHECK_INTERVAL_MS = 5000; | |
| const STALL_MS = 30000; // playing but frozen this long => dead | |
| const FIRST_RELOAD_MS = 25000; // give Twitch's own reconnect a chance | |
| const BACKOFF_STEPS_MS = [60000, 120000, 300000]; // then stay at 300s | |
| const NON_CHANNEL = new Set(['directory', 'downloads', 'settings', 'p', 'store', 'turbo', 'jobs', 'signup', 'login', 'embed', 'popout']); | |
| let lastTime = -1; | |
| let lastAdvance = 0; | |
| let reloadTimer = null; | |
| let attempts = Number(sessionStorage.getItem('lg-attempts') || 0); | |
| function isChannelPage() { | |
| const parts = location.pathname.split('/').filter(Boolean); | |
| return parts.length === 1 && !NON_CHANNEL.has(parts[0].toLowerCase()); | |
| } | |
| function videoEl() { | |
| return ( | |
| document.querySelector('[data-a-target="video-player"] video') || | |
| document.querySelector('.video-player__container video') || | |
| document.querySelector('video') | |
| ); | |
| } | |
| function scheduleReload(reason) { | |
| if (reloadTimer) return; | |
| const delay = | |
| attempts === 0 | |
| ? FIRST_RELOAD_MS | |
| : BACKOFF_STEPS_MS[Math.min(attempts - 1, BACKOFF_STEPS_MS.length - 1)]; | |
| attempts++; | |
| sessionStorage.setItem('lg-attempts', String(attempts)); | |
| console.log(`[lurk-guard] ${reason} — reloading in ${Math.round(delay / 1000)}s (attempt ${attempts})`); | |
| reloadTimer = setTimeout(() => location.reload(), delay); | |
| } | |
| function tick() { | |
| const v = videoEl(); | |
| const playing = v && !v.paused && !v.ended && v.readyState >= 2; | |
| if (playing) { | |
| sessionStorage.setItem('lg-playing', '1'); // survives reloads in this tab | |
| sessionStorage.setItem('lg-attempts', '0'); | |
| if (v.currentTime !== lastTime) { | |
| lastTime = v.currentTime; | |
| lastAdvance = Date.now(); | |
| } else if (Date.now() - lastAdvance > STALL_MS) { | |
| scheduleReload('stalled'); | |
| } | |
| return; | |
| } | |
| // Not playing now. Only act if this tab WAS watching — never loop on a | |
| // channel someone opened while already offline. A paused video might be | |
| // intentional; only reload when it's gone or ended. | |
| const wasWatching = sessionStorage.getItem('lg-playing') === '1'; | |
| if (wasWatching && (!v || v.ended)) scheduleReload('offline'); | |
| } | |
| if (isChannelPage()) setInterval(tick, CHECK_INTERVAL_MS); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment