High-performance progress tracking for YouTube with minimal performance impact
// ==UserScript==
// @name YouTube Universal Progress Tracker
// @namespace http://tampermonkey.net/
// @version 3.3
// @description High-performance progress tracking for YouTube with minimal performance impact
// @author ikigaiDH
// @match https://www.youtube.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license GPL-3.0-only
// ==/UserScript==
(function() {
'use strict';
// Add static styles
GM_addStyle(`
.yt-progress-indicator {
position: absolute;
bottom: 4px;
left: 4px;
background-color: #cc0000;
color: white;
padding: 2px 6px;
border-radius: 2px;
font-size: 12px;
font-weight: bold;
z-index: 1000;
font-family: Roboto, Arial, sans-serif;
text-transform: uppercase;
pointer-events: none;
}
`);
// Throttle function with ESLint fix
const throttle = (func, limit) => {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
};
// Storage management with throttled saves
const storage = {
data: GM_getValue('yt_watch_history', {}),
saveThrottled: throttle(function() {
GM_setValue('yt_watch_history', this.data);
}, 100), // Reduced to 100ms for better responsiveness
set: function(key, value) {
this.data[key] = value;
this.saveThrottled();
},
get: function(key) {
return this.data[key] || 0;
}
};
// Video ID extractor
const getVideoId = (element) => {
try {
const link = element.closest('a') || element.querySelector('a');
if (!link) return null;
const url = new URL(link.href);
return url.searchParams.get('v') ||
url.pathname.split('/watch/')[1]?.split('?')[0] ||
url.pathname.split('/')[2];
} catch {
return null;
}
};
// Check if element is visible
const isVisible = (element) => {
const rect = element.getBoundingClientRect();
return rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
};
// Create indicator element
const createIndicator = () => {
const indicator = document.createElement('div');
indicator.className = 'yt-progress-indicator';
return indicator;
};
// Cleanup detached indicators
const cleanupDetachedIndicators = () => {
document.querySelectorAll('.yt-progress-indicator').forEach(indicator => {
if (!document.body.contains(indicator.parentElement)) {
indicator.remove();
}
});
};
// Update thumbnails with requestAnimationFrame
const updateThumbnails = () => {
requestAnimationFrame(() => {
// Only process visible thumbnails
document.querySelectorAll('ytd-thumbnail').forEach(thumbnail => {
if (!isVisible(thumbnail)) return;
const videoId = getVideoId(thumbnail);
if (!videoId) return;
const percentage = storage.get(videoId);
let indicator = thumbnail.querySelector('.yt-progress-indicator');
if (percentage > 0) {
if (!indicator) {
indicator = createIndicator();
const overlays = thumbnail.querySelector('#overlays');
if (overlays) {
overlays.appendChild(indicator);
}
}
indicator.textContent = percentage >= 100 ? '>100%' : `${Math.round(percentage)}%`;
} else if (indicator) {
indicator.remove();
}
});
});
};
// Debounced update function
const debouncedUpdate = throttle(updateThumbnails, 100);
// Video progress tracking
let currentVideo = null;
const trackVideo = () => {
const video = document.querySelector('video');
if (!video) return;
const videoId = new URLSearchParams(window.location.search).get('v');
if (!videoId || videoId === currentVideo?.videoId) return;
// Cleanup previous video listener
if (currentVideo) {
currentVideo.video.removeEventListener('timeupdate', currentVideo.handler);
}
const progressHandler = () => {
if (video.duration > 0) {
const percentage = (video.currentTime / video.duration) * 100;
if (percentage > storage.get(videoId)) {
storage.set(videoId, percentage);
debouncedUpdate();
}
}
};
currentVideo = {
videoId,
video,
handler: progressHandler
};
video.addEventListener('timeupdate', progressHandler);
};
// Selective mutation observer
const observeContent = () => {
const contentContainers = [
document.querySelector('ytd-rich-grid-renderer'),
document.querySelector('ytd-watch-next-secondary-results-renderer')
].filter(Boolean);
const observer = new MutationObserver(() => {
cleanupDetachedIndicators();
debouncedUpdate();
trackVideo();
});
contentContainers.forEach(container => {
observer.observe(container, {
childList: true,
subtree: true,
attributes: false
});
});
return observer;
};
// Initialize
let observer;
window.addEventListener('load', () => {
observer = observeContent();
debouncedUpdate();
trackVideo();
});
// Handle navigation
document.addEventListener('yt-navigate-finish', () => {
if (observer) {
observer.disconnect();
}
observer = observeContent();
debouncedUpdate();
});
// Handle scroll events
window.addEventListener('scroll', debouncedUpdate, { passive: true });
})();