// ==UserScript==
// @name YouTube Focus Enhancer
// @namespace http://tampermonkey.net/
// @version 1.2.1
// @description Hides comments and recommendations until you watch a configurable percentage of the video without skipping; automatically pauses when the player moves out of view, switches between tabs, or clicks away, and automatically resumes the video when you return.
// @match https://www.youtube.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(() => {
'use strict';
/*** CONFIGURATION ***/
const config = {
requiredPercentToUnlockComments: 50, // Prozent (Standard 50). Setzen Sie 100 falls gewünscht.
seekToleranceSeconds: 2, // wenn vorwärts gesprungen > diese Sek., gilt als "geskippt"
intersectionVisibilityThreshold: 0.5, // wieviel vom Player sichtbar sein muss, sonst Pause (0..1)
smoothRevealMs: 400, // Dauer der Fade-In Animation beim Freischalten (ms)
pauseAtEndPlaylistTolerance: 0.5, // Sekunden vor Ende, wo wir bei Playlists anhalten können
initRetryIntervalMs: 800, // interval um video wrapper zu finden
verbose: false // true -> console.debugs
};
/*** UTILITY ***/
const INSTANCE_ID = Math.random().toString(36).slice(2, 9);
const log = (...args) => { if (config.verbose) console.debug(`[YT-SMART ${INSTANCE_ID}]`, ...args); };
const sleep = ms => new Promise(r => setTimeout(r, ms));
/*** ADD STYLES (once per page) ***/
function ensureCSS() {
if (document.head.querySelector('style[data-yt-smart]')) return;
const css = `
ytd-comments#comments, ytd-item-section-renderer#related, ytd-watch-next-secondary-results-renderer {
transition: opacity ${config.smoothRevealMs}ms ease, max-height ${config.smoothRevealMs}ms ease;
}
.tm-hide-comments {
opacity: 0 !important;
max-height: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
}
.tm-hide-related {
opacity: 0 !important;
max-height: 0 !important;
overflow: hidden !important;
pointer-events: none !important;
}
.tm-show-comments {
opacity: 1 !important;
max-height: 2000px !important;
pointer-events: auto !important;
}`;
const s = document.createElement('style');
s.setAttribute('data-yt-smart', INSTANCE_ID);
s.textContent = css;
document.head.appendChild(s);
log('CSS injected');
}
/*** MAIN CLASS - ENCAPSULATES PER-PAGE INSTANCE ***/
class YTGuard {
constructor() {
this.playerVideo = null;
this.videoWrapper = null;
this.currentWatchId = null;
this.watchingState = null;
this.intersectionObserver = null;
this.mutationObserver = null;
this.bound = {}; // bound handlers for removal
this.lastUrl = location.href;
this.urlPoller = null;
this.initialized = false;
this.destroyed = false;
log('YTGuard ctor');
this.setup();
}
setup() {
ensureCSS();
if (window.__ytSmartInstance && !window.__ytSmartInstance.destroyed) {
log('Vorherige Instanz vorhanden — bereinigen');
window.__ytSmartInstance.destroy();
}
window.__ytSmartInstance = this;
this.bound.visibility = this.onVisibilityChange.bind(this);
this.bound.focus = this.onWindowFocus.bind(this);
this.bound.blur = this.onWindowBlur.bind(this);
this.bound.pageshow = this.onPageShow.bind(this);
this.bound.mutation = this.onBodyMutations.bind(this);
this.bound.popstate = this.onHistoryChange.bind(this);
this.bound.pointer = this.onMouseMove.bind(this);
this.bound.click = this.onDocumentClick.bind(this);
document.addEventListener('visibilitychange', this.bound.visibility, true);
window.addEventListener('focus', this.bound.focus, true);
window.addEventListener('blur', this.bound.blur, true);
window.addEventListener('pageshow', this.bound.pageshow);
window.addEventListener('popstate', this.bound.popstate);
document.addEventListener('mousemove', this.bound.pointer, { passive: true });
document.addEventListener('click', this.bound.click, true);
this.patchHistory();
this.mutationObserver = new MutationObserver(this.bound.mutation);
try {
this.mutationObserver.observe(document.documentElement || document.body, { childList: true, subtree: true });
} catch (e) { /* ignore */ }
this.urlPoller = setInterval(() => {
if (location.href !== this.lastUrl) {
log('URL change detected by poller', this.lastUrl, '->', location.href);
this.lastUrl = location.href;
this.onUrlChange();
}
}, config.initRetryIntervalMs);
this.tryAttachLoop();
}
patchHistory() {
try {
const push = history.pushState;
const replace = history.replaceState;
history.pushState = function () {
const r = push.apply(this, arguments);
window.dispatchEvent(new Event('yt-smart-history'));
return r;
};
history.replaceState = function () {
const r = replace.apply(this, arguments);
window.dispatchEvent(new Event('yt-smart-history'));
return r;
};
window.addEventListener('yt-smart-history', () => {
log('history API change -> onUrlChange');
this.onUrlChange();
});
} catch (e) {
log('history patch failed', e);
}
}
async tryAttachLoop() {
for (let i = 0; !this.initialized && i < 50 && !this.destroyed; i++) {
try {
const v = this.findVideoElement();
if (v) {
log('Video gefunden in tryAttachLoop');
this.initForVideo(v);
break;
}
} catch (e) { /* ignore */ }
await sleep(config.initRetryIntervalMs);
}
}
findVideoElement() {
const v = document.querySelector('video.html5-main-video, video.video-stream, #movie_player video');
return v || null;
}
onBodyMutations(muts) {
if (this.destroyed) return;
const v = this.findVideoElement();
if (v && (!this.playerVideo || this.playerVideo !== v)) {
log('MutationObserver detected Video change/appearance');
this.initForVideo(v);
}
this.tryAttachHideClasses();
}
onHistoryChange() {
if (this.destroyed) return;
this.onUrlChange();
}
onUrlChange() {
log('onUrlChange called');
this.cleanupVideoListeners();
this.playerVideo = null;
this.videoWrapper = null;
this.initialized = false;
this.currentWatchId = null;
this.watchingState = null;
this.tryAttachLoop();
this.tryAttachHideClasses();
}
onPageShow() {
log('pageshow');
this.tryAttachLoop();
}
onWindowFocus() {
log('window focus');
this.tryAttachLoop();
if (this.playerVideo) this.resumeIfAllowed('window:focus');
}
onWindowBlur() {
log('window blur');
if (this.playerVideo) this.pauseByScript('window:blur');
}
onVisibilityChange() {
if (document.hidden) {
log('document hidden');
if (this.playerVideo) this.pauseByScript('visibility:hidden');
} else {
log('document visible');
if (this.playerVideo) this.resumeIfAllowed('visibility:visible');
}
}
tryAttachHideClasses() {
const comments = document.querySelector('ytd-comments#comments');
if (comments && !comments.classList.contains('tm-show-comments') && !comments.classList.contains('tm-hide-comments')) {
comments.classList.add('tm-hide-comments');
log('Kommentare initial versteckt');
}
const related = document.querySelector('ytd-watch-next-secondary-results-renderer, ytd-item-section-renderer#related');
if (related && !related.classList.contains('tm-hide-related')) {
related.classList.add('tm-hide-related');
log('Seitenleiste initial versteckt');
}
}
initForVideo(videoEl) {
if (this.destroyed) return;
if (!videoEl) return;
if (this.playerVideo === videoEl && this.initialized) {
log('initForVideo: gleiche Video-Element bereits angebunden');
return;
}
this.cleanupVideoListeners();
this.playerVideo = videoEl;
this.videoWrapper = document.querySelector('.html5-video-player') || document.querySelector('#movie_player') || this.playerVideo.closest('.html5-video-player') || null;
this.currentWatchId = this.getVideoIdFromUrl() || `${Date.now()}`;
this.resetWatchingState();
this.bound.timeupdate = this.onTimeUpdate.bind(this);
this.bound.seeking = this.onSeeking.bind(this);
this.bound.seeked = this.onSeeked.bind(this);
this.bound.pause = this.onPauseEvent.bind(this);
this.bound.play = this.onPlayEvent.bind(this);
this.bound.ended = this.onEndedEvent.bind(this);
this.playerVideo.addEventListener('timeupdate', this.bound.timeupdate);
this.playerVideo.addEventListener('seeking', this.bound.seeking);
this.playerVideo.addEventListener('seeked', this.bound.seeked);
this.playerVideo.addEventListener('pause', this.bound.pause);
this.playerVideo.addEventListener('play', this.bound.play);
this.playerVideo.addEventListener('ended', this.bound.ended);
this.setupIntersectionObserver();
this.initialized = true;
log('initForVideo completed — instance:', INSTANCE_ID, 'videoId:', this.currentWatchId);
this.tryAttachHideClasses();
}
resetWatchingState() {
this.watchingState = {
organic: true,
watchedOrganicSeconds: 0,
lastTimeSeen: this.playerVideo ? this.playerVideo.currentTime : 0,
lastReportedCurrentTime: this.playerVideo ? this.playerVideo.currentTime : 0,
lastSeekedFrom: null,
unlocked: false,
autoPausedByScript: false,
userPaused: false
};
}
getVideoIdFromUrl() {
const m = location.search.match(/[?&]v=([^&]+)/);
return m ? decodeURIComponent(m[1]) : null;
}
onTimeUpdate() {
if (!this.playerVideo || this.destroyed) return;
const t = this.playerVideo.currentTime;
const d = this.playerVideo.duration || 0;
const last = this.watchingState.lastReportedCurrentTime || t;
if (t >= last && !this.playerVideo.seeking) {
const delta = t - last;
if (this.watchingState.organic && delta > 0 && delta < 10) {
this.watchingState.watchedOrganicSeconds += delta;
}
this.watchingState.lastReportedCurrentTime = t;
this.watchingState.lastTimeSeen = t;
} else {
this.watchingState.lastReportedCurrentTime = t;
this.watchingState.lastTimeSeen = t;
}
this.tryUnlockCommentsIfEligible(d);
this.tryPauseAtEndIfPlaylist(d, t);
}
onSeeking() {
if (!this.playerVideo) return;
this.watchingState.lastSeekedFrom = this.watchingState.lastReportedCurrentTime || this.playerVideo.currentTime;
log('seeking from', this.watchingState.lastSeekedFrom);
}
onSeeked() {
if (!this.playerVideo) return;
const from = this.watchingState.lastSeekedFrom != null ? this.watchingState.lastSeekedFrom : this.watchingState.lastReportedCurrentTime;
const to = this.playerVideo.currentTime;
const delta = to - from;
log('seeked', { from, to, delta });
if (delta > config.seekToleranceSeconds) {
this.watchingState.organic = false;
this.watchingState.watchedOrganicSeconds = 0;
log('Detected forward skip -> organic=false; watched reset');
} else {
log('Seek small/backwards -> allowed');
}
this.watchingState.lastReportedCurrentTime = to;
this.watchingState.lastSeekedFrom = null;
}
tryUnlockCommentsIfEligible(duration) {
if (!this.playerVideo || this.watchingState.unlocked) return;
if (!duration || !isFinite(duration) || duration <= 0) return;
const requiredSeconds = (config.requiredPercentToUnlockComments / 100) * duration;
log('watchedOrganicSeconds', this.watchingState.watchedOrganicSeconds, 'requiredSeconds', requiredSeconds);
if (this.watchingState.watchedOrganicSeconds >= requiredSeconds && this.watchingState.organic) {
this.unlockCommentsSmooth();
this.watchingState.unlocked = true;
}
}
unlockCommentsSmooth() {
const comments = document.querySelector('ytd-comments#comments');
if (!comments) {
log('Kommentare DOM nicht gefunden zum Freischalten.');
return;
}
comments.classList.remove('tm-hide-comments');
comments.classList.add('tm-show-comments');
log('Kommentare freigeschaltet (smooth)');
}
isPlaylistActive() {
try {
if (location.search && location.search.includes('list=')) return true;
if (document.querySelector('ytd-playlist-panel-renderer')) return true;
} catch (e) {}
return false;
}
tryPauseAtEndIfPlaylist(duration, currentTime) {
if (!this.isPlaylistActive()) return;
if (config.requiredPercentToUnlockComments < 100) return;
if (!this.playerVideo || !duration || !isFinite(duration)) return;
if (duration - currentTime <= config.pauseAtEndPlaylistTolerance) {
try {
this.playerVideo.pause();
this.watchingState.autoPausedByScript = true;
this.watchingState.watchedOrganicSeconds = duration;
this.watchingState.unlocked = true;
this.unlockCommentsSmooth();
log('Playlist detected: am Ende - Video pausiert und Kommentare freigegeben.');
} catch (e) {
log('Fehler beim Pausieren am Ende der Playlist', e);
}
}
}
onPauseEvent() {
if (this.watchingState.autoPausedByScript) {
log('pause event: von Skript ausgelöst');
} else {
this.watchingState.userPaused = true;
log('pause event: vom Benutzer');
}
}
onPlayEvent() {
this.watchingState.userPaused = false;
this.watchingState.autoPausedByScript = false;
log('play event');
}
onEndedEvent() {
const d = this.playerVideo.duration || 0;
this.watchingState.watchedOrganicSeconds = d;
this.tryUnlockCommentsIfEligible(d);
log('ended event');
}
pauseByScript(reason) {
if (!this.playerVideo) return;
try {
if (!this.playerVideo.paused) {
this.playerVideo.pause();
this.watchingState.autoPausedByScript = true;
log('Video pausiert (script). Grund:', reason);
}
} catch (e) {
log('Fehler beim Pausieren durch Script', e);
}
}
async resumeIfAllowed(reason) {
if (!this.playerVideo) return;
if (!this.watchingState.autoPausedByScript) {
log('resumeIfAllowed: skipping, not auto-paused by script. Grund:', reason);
return;
}
if (this.watchingState.userPaused) {
log('resumeIfAllowed: skipping, Benutzer pausiert aktiv. Grund:', reason);
return;
}
try {
await this.playerVideo.play();
this.watchingState.autoPausedByScript = false;
log('Video automatisch fortgesetzt. Grund:', reason);
} catch (e) {
log('Fehler beim automatischen Fortsetzen (Autoplay-Policy?):', e);
}
}
isClickInsidePlayer(evt) {
const player = document.querySelector('.html5-video-player, #movie_player');
if (!player) return false;
return player.contains(evt.target);
}
onDocumentClick(evt) {
const inside = this.isClickInsidePlayer(evt);
if (!inside && this.playerVideo && !this.playerVideo.paused) {
this.pauseByScript('click:outside-player');
}
}
onMouseMove(evt) {
if (!this.playerVideo) return;
const player = document.querySelector('.html5-video-player, #movie_player');
if (!player) return;
const rect = player.getBoundingClientRect();
const inside = evt.clientX >= rect.left && evt.clientX <= rect.right && evt.clientY >= rect.top && evt.clientY <= rect.bottom;
if (inside) {
this.resumeIfAllowed('pointer:enter-player');
}
}
setupIntersectionObserver() {
try {
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
}
const videoWrapper = this.videoWrapper || document.querySelector('.html5-video-player') || document.querySelector('#movie_player');
if (!videoWrapper) {
log('IntersectionObserver: Video-Wrapper noch nicht gefunden.');
return;
}
this.intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const ratio = entry.intersectionRatio;
log('Intersection ratio', ratio);
if (ratio < config.intersectionVisibilityThreshold) {
this.pauseByScript('intersection:out-of-view');
} else {
this.resumeIfAllowed('intersection:back-in-view');
}
});
}, { threshold: [0, config.intersectionVisibilityThreshold, 1.0] });
this.intersectionObserver.observe(videoWrapper);
log('IntersectionObserver eingerichtet');
} catch (e) {
log('IntersectionObserver Fehler', e);
}
}
cleanupVideoListeners() {
try {
if (!this.playerVideo) return;
const evs = ['timeupdate', 'seeking', 'seeked', 'pause', 'play', 'ended'];
evs.forEach(ev => {
const h = this.bound[ev];
if (h) {
this.playerVideo.removeEventListener(ev, h);
}
});
} catch (e) { /* ignore */ }
try {
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
}
} catch (e) {}
}
destroy() {
if (this.destroyed) return;
this.destroyed = true;
log('destroying instance');
try {
if (this.mutationObserver) this.mutationObserver.disconnect();
if (this.intersectionObserver) this.intersectionObserver.disconnect();
if (this.urlPoller) clearInterval(this.urlPoller);
document.removeEventListener('visibilitychange', this.bound.visibility, true);
window.removeEventListener('focus', this.bound.focus, true);
window.removeEventListener('blur', this.bound.blur, true);
window.removeEventListener('pageshow', this.bound.pageshow);
window.removeEventListener('popstate', this.bound.popstate);
document.removeEventListener('mousemove', this.bound.pointer, { passive: true });
document.removeEventListener('click', this.bound.click, true);
this.cleanupVideoListeners();
} catch (e) { /* ignore */ }
if (window.__ytSmartInstance === this) window.__ytSmartInstance = null;
}
} // end class
/*** STARTUP ***/
try {
if (!window.__ytSmartInstance || window.__ytSmartInstance.destroyed) {
new YTGuard();
log('YTGuard started — instance id', INSTANCE_ID);
} else {
window.__ytSmartInstance.destroy();
new YTGuard();
log('Replaced existing instance with new instance', INSTANCE_ID);
}
} catch (e) {
console.error('YT-SMART init error', e);
}
})();