Skip to content

Instantly share code, notes, and snippets.

@MiguelMachado-dev
Last active August 30, 2026 03:10
Show Gist options
  • Select an option

  • Save MiguelMachado-dev/7e0a999530b78c4d0028f96ca4b0b922 to your computer and use it in GitHub Desktop.

Select an option

Save MiguelMachado-dev/7e0a999530b78c4d0028f96ca4b0b922 to your computer and use it in GitHub Desktop.
// ==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