Skip to content

Instantly share code, notes, and snippets.

@a904guy
Created November 15, 2025 20:18
Show Gist options
  • Select an option

  • Save a904guy/4bab6c92b5a21f28f929c1a956cf380a to your computer and use it in GitHub Desktop.

Select an option

Save a904guy/4bab6c92b5a21f28f929c1a956cf380a to your computer and use it in GitHub Desktop.
Autoplay Youtube Shorts or TikTok (“Brain rot me, scroll feed endlessly, forget to live while I refresh my screen for meaning.”)
function startTikTokAutoplay() {
console.log('[TikTok Autoplay] Initializing...');
if (window._ttAutoNextStop) {
console.log('[TikTok Autoplay] Stopping previous instance.');
window._ttAutoNextStop();
}
const state = {
activeVideo: null,
seen: new WeakSet(),
iObs: null,
mObs: null,
tick: null,
debugMode: true
};
const log = (message, ...args) => {
if (state.debugMode) console.log(`[TikTok Autoplay] ${message}`, ...args);
};
const byTop = (a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top;
const getAllVideos = () => {
const videos = Array.from(document.querySelectorAll('video'))
.filter(v => v.readyState > 0 && v.offsetParent !== null && v.duration && Number.isFinite(v.duration));
log(`Found ${videos.length} eligible video elements.`, videos);
return videos;
};
const mostVisibleVideo = () => {
let best = null;
let bestArea = 0;
const allVideos = getAllVideos();
for (const v of allVideos) {
const r = v.getBoundingClientRect();
const ivw = Math.max(0, Math.min(window.innerWidth, r.right) - Math.max(0, r.left));
const ivh = Math.max(0, Math.min(window.innerHeight, r.bottom) - Math.max(0, r.top));
const area = ivw * ivh;
if (area > bestArea) {
bestArea = area;
best = v;
}
}
log('Most visible video:', best);
return best;
};
const nextVideo = (current) => {
if (!current) {
log('No current video to determine next from, trying most visible.');
return mostVisibleVideo();
}
const curTop = current.getBoundingClientRect().top;
const vids = getAllVideos().sort(byTop);
const next = vids.find(v => v !== current && v.getBoundingClientRect().top > curTop + 10);
log('Next video candidate (below current):', next);
if (!next) {
log('No video found directly below current, falling back to first distinct video.');
return vids.find(v => v !== current);
}
return next;
};
const scrollToVideo = (v) => {
if (!v) {
log('No specific video to scroll to. Attempting to click "Next" button or scroll viewport.');
const nextBtn =
document.querySelector('button[aria-label*="Next" i]') ||
document.querySelector('[data-e2e*="next" i]') ||
document.querySelector('a[href*="next" i]');
if (nextBtn) {
log('Clicking "Next" button:', nextBtn);
nextBtn.click();
} else {
log('No "Next" button found, scrolling viewport down.');
window.scrollBy({ top: window.innerHeight, behavior: 'smooth' });
}
return;
}
const container =
v.closest('[data-e2e*="item" i]') ||
v.closest('[data-e2e*="card" i]') ||
v.closest('article,li,div');
log('Scrolling to target:', container || v, 'Video:', v);
(container || v).scrollIntoView({ behavior: 'smooth', block: 'center' });
};
const handleEnd = (v) => {
log('handleEnd triggered for video:', v);
if (!v || state.seen.has(v)) {
if (v) log('Video already processed or invalid, skipping:', v);
return;
}
state.seen.add(v);
log('Marked video as seen:', v);
setTimeout(() => {
const nxt = nextVideo(v);
log('Calculated next video:', nxt);
scrollToVideo(nxt);
setTimeout(() => {
if (state.activeVideo !== v) {
state.seen.delete(v);
log('Cleared seen status for video:', v);
}
}, 2000);
}, 50);
};
const bindVideo = (v) => {
if (v._ttBound) {
log('Video already bound, skipping:', v);
return;
}
v._ttBound = true;
log('Binding event listeners to video:', v);
v.addEventListener('ended', () => {
log('Video "ended" event fired:', v);
handleEnd(v);
}, { passive: true });
const nearEndCheck = () => {
if (!v.duration || !Number.isFinite(v.duration)) return;
const remaining = v.duration - v.currentTime;
if (remaining > 0 && remaining < 0.25 && !state.seen.has(v)) {
log(`Video near end (${remaining.toFixed(2)}s remaining):`, v);
handleEnd(v);
}
};
v.addEventListener('timeupdate', nearEndCheck, { passive: true });
v.addEventListener('seeked', nearEndCheck, { passive: true });
};
const bindAll = () => {
log('Binding all currently found videos.');
getAllVideos().forEach(bindVideo);
};
state.iObs = new IntersectionObserver((entries) => {
let bestEntry = null;
for (const e of entries) {
if (e.isIntersecting) {
if (!bestEntry || e.intersectionRatio > bestEntry.intersectionRatio) {
bestEntry = e;
}
}
}
if (bestEntry && state.activeVideo !== bestEntry.target) {
log('Active video changed via IntersectionObserver:', bestEntry.target);
state.activeVideo = bestEntry.target;
} else if (!bestEntry && state.activeVideo) {
log('No video currently intersecting significantly, clearing active video.');
state.activeVideo = null;
}
}, { threshold: [0, 0.25, 0.5, 0.75, 0.95] });
const primeObserverTargets = () => {
log('Priming observer targets and binding events for all videos.');
getAllVideos().forEach(v => {
state.iObs.observe(v);
bindVideo(v);
});
};
state.mObs = new MutationObserver((mutations) => {
const relevantChange = mutations.some(mutation =>
mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0
);
if (relevantChange) {
log('DOM mutation detected, re-priming observer targets.');
primeObserverTargets();
}
});
state.mObs.observe(document.documentElement, { childList: true, subtree: true });
state.tick = setInterval(() => {
const v = state.activeVideo || mostVisibleVideo();
if (!v) {
log('Safety net: No active or visible video found.');
return;
}
const endedLike = v.ended || (v.duration && v.currentTime >= v.duration - 0.1);
if (endedLike && (v.paused || v.loop)) {
log('Safety net: Video is ended/paused/looping at end, triggering handleEnd:', v);
handleEnd(v);
} else {
log(`Safety net: Video not ended/paused at end. State: ended=${v.ended}, paused=${v.paused}, loop=${v.loop}, currentTime=${v.currentTime?.toFixed(2)}, duration=${v.duration?.toFixed(2)}`, v);
}
}, 1200);
primeObserverTargets();
bindAll();
window._ttAutoNextStop = () => {
log('Stopping TikTok Autoplay script.');
try { state.iObs?.disconnect(); log('IntersectionObserver disconnected.'); } catch (e) { console.error('Error disconnecting iObs:', e); }
try { state.mObs?.disconnect(); log('MutationObserver disconnected.'); } catch (e) { console.error('Error disconnecting mObs:', e); }
try { clearInterval(state.tick); log('Interval cleared.'); } catch (e) { console.error('Error clearing interval:', e); }
state.activeVideo = null;
log('TikTok Autoplay stopped successfully.');
};
log('TikTok Autoplay script initialized. Call window._ttAutoNextStop() to stop.');
}
startTikTokAutoplay();
(function () {
const LOG = (...args) => console.log("[SHORTS-AUTO]", ...args);
function triggerDownArrow() {
console.log('[DEBUG] Triggering down arrow key press...');
const down = new KeyboardEvent('keydown', {
key: 'ArrowDown',
code: 'ArrowDown',
bubbles: true,
});
document.dispatchEvent(down);
console.log('[DEBUG] Down arrow dispatched.');
}
let boundVideo = null;
let nearEndTimer = null;
let lastTimeSeen = 0;
let stallTimer = null;
function getActiveVideo() {
const vids = Array.from(document.querySelectorAll("video"));
if (vids.length === 0) return null;
const vh = window.innerHeight, vw = window.innerWidth;
const centerY = vh / 2, centerX = vw / 2;
const scored = vids.map(v => {
const r = v.getBoundingClientRect();
const ix = Math.max(0, Math.min(r.right, vw) - Math.max(r.left, 0));
const iy = Math.max(0, Math.min(r.bottom, vh) - Math.max(r.top, 0));
const area = ix * iy;
const cx = Math.min(Math.max(r.left, 0), vw);
const cy = Math.min(Math.max(r.top, 0), vh);
const dy = Math.abs((r.top + r.bottom) / 2 - centerY);
const dx = Math.abs((r.left + r.right) / 2 - centerX);
const distPenalty = dx + dy;
return { v, score: area - distPenalty };
});
scored.sort((a, b) => b.score - a.score);
return scored[0]?.v || null;
}
function scrubLoopAttrs(v) {
if (v.loop) v.loop = false;
if (v.hasAttribute("loop")) v.removeAttribute("loop");
}
function triggerNext() {
LOG("Advancing to next short");
triggerDownArrow();
}
function clearPerVideoTimers() {
if (nearEndTimer) { clearInterval(nearEndTimer); nearEndTimer = null; }
if (stallTimer) { clearInterval(stallTimer); stallTimer = null; }
}
function bindVideo(v) {
if (!v) return;
if (v === boundVideo) {
scrubLoopAttrs(v);
return;
}
if (boundVideo) {
boundVideo.removeEventListener("ended", onEnded, true);
boundVideo.removeEventListener("timeupdate", onTimeupdate, true);
clearPerVideoTimers();
}
boundVideo = v;
scrubLoopAttrs(v);
v.play().catch(() => {});
v.addEventListener("ended", onEnded, true);
let nearEndSeenAt = 0;
clearPerVideoTimers();
nearEndTimer = setInterval(() => {
if (!boundVideo || !Number.isFinite(boundVideo.duration) || boundVideo.duration === 0) return;
const ratio = boundVideo.currentTime / boundVideo.duration;
if (ratio >= 0.985) {
if (nearEndSeenAt === 0) nearEndSeenAt = performance.now();
if (performance.now() - nearEndSeenAt >= 500) {
onEnded();
}
} else {
nearEndSeenAt = 0;
}
}, 100);
lastTimeSeen = -1;
stallTimer = setInterval(() => {
if (!boundVideo) return;
const t = boundVideo.currentTime;
const d = boundVideo.duration;
if (Number.isFinite(d) && d > 0 && t / d > 0.97) {
if (t === lastTimeSeen) {
onEnded();
} else {
lastTimeSeen = t;
}
} else {
lastTimeSeen = t;
}
}, 1500);
let loopKillCount = 0;
const loopKiller = setInterval(() => {
if (!boundVideo || boundVideo !== v) return clearInterval(loopKiller);
scrubLoopAttrs(v);
loopKillCount += 1;
if (loopKillCount >= 20) clearInterval(loopKiller);
}, 500);
LOG("Bound to new video", { duration: v.duration });
}
function onEnded() {
if (!boundVideo) return;
LOG("Detected end");
clearPerVideoTimers();
triggerNext();
setTimeout(() => bindVideo(getActiveVideo()), 600);
}
function onTimeupdate() {}
const mo = new MutationObserver(() => {
const v = getActiveVideo();
if (v) bindVideo(v);
});
mo.observe(document.documentElement, { childList: true, subtree: true });
const loopStripper = setInterval(() => {
document.querySelectorAll("video[loop]").forEach(scrubLoopAttrs);
}, 500);
bindVideo(getActiveVideo());
LOG("Shorts auto-advance armed");
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment