Created
November 15, 2025 20:18
-
-
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.”)
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
| 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(); |
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
| (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