// ==UserScript==
// @name WayBackTube (Hyper-Optimized + Search Terms)
// @namespace http://tampermonkey.net/
// @license MIT
// @version 80
// @description Travel back in time on YouTube with subscriptions, search terms, AND simple 2011 theme! Complete time travel experience with custom recommendation algorithm and clean vintage styling.
// @author You
// @match https://www.youtube.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect youtube.com
// @connect googleapis.com
// @connect worldtimeapi.org
// @connect ipgeolocation.io
// @connect worldclockapi.com
// @connect *
// @connect httpbin.org
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// === HYPER-OPTIMIZED CONFIG ===
const CONFIG = {
updateInterval: 50,
debugMode: true,
videosPerChannel: 30,
maxHomepageVideos: 60,
maxVideoPageVideos: 30,
channelPageVideosPerMonth: 50,
watchNextVideosCount: 30,
seriesMatchVideosCount: 3,
cacheExpiry: {
videos: 7200000, // 2 hours
channelVideos: 3600000, // 1 hour
searchResults: 1800000 // 30 minutes
},
maxConcurrentRequests: 2,
batchSize: 3,
apiCooldown: 200,
autoLoadOnHomepage: false,
autoLoadDelay: 1500,
autoAdvanceDays: true,
autoRefreshInterval: 21600000, // 6 hours
refreshVideoPercentage: 0.5,
homepageLoadMoreSize: 12,
aggressiveNukingInterval: 10,
viralVideoPercentage: 0.15,
viralVideosCount: 20,
RECOMMENDATION_COUNT: 20,
SAME_CHANNEL_RATIO: 0.6,
OTHER_CHANNELS_RATIO: 0.4,
KEYWORD_RATIO: 0.5,
FRESH_VIDEOS_COUNT: 15,
KEYWORD_MATCH_RATIO: 0.5,
// Video weighting configuration - UPDATED: 60/40 subscription-search split
searchTermVideoPercentage: 0.40, // 40% search term videos (user requested more subscriptions)
sameChannelVideoPercentage: 0.00, // 0% same channel videos - ONLY next episode goes to top
subscriptionVideoPercentage: 0.60, // 60% subscription videos (user requested increase)
// Pre-compiled selectors for performance
SHORTS_SELECTORS: [
'ytd-reel-shelf-renderer',
'ytd-rich-shelf-renderer[is-shorts]',
'[aria-label*="Shorts"]',
'[title*="Shorts"]',
'ytd-video-renderer[is-shorts]',
'.ytd-reel-shelf-renderer',
'.shorts-shelf',
'.reel-shelf-renderer',
'.shortsLockupViewModelHost',
'.ytGridShelfViewModelHost',
'[overlay-style="SHORTS"]',
'[href*="/shorts/"]'
],
CHANNEL_PAGE_SELECTORS: [
'ytd-browse[page-subtype="channels"] ytd-video-renderer',
'ytd-browse[page-subtype="channel"] ytd-video-renderer',
'ytd-browse[page-subtype="channels"] ytd-grid-video-renderer',
'ytd-browse[page-subtype="channel"] ytd-grid-video-renderer',
'ytd-browse[page-subtype="channels"] ytd-rich-item-renderer',
'ytd-browse[page-subtype="channel"] ytd-rich-item-renderer',
'ytd-c4-tabbed-header-renderer ytd-video-renderer',
'ytd-channel-video-player-renderer ytd-video-renderer',
'#contents ytd-video-renderer',
'#contents ytd-grid-video-renderer',
'#contents ytd-rich-item-renderer',
'ytd-browse[page-subtype="channel"] ytd-shelf-renderer',
'ytd-browse[page-subtype="channel"] ytd-rich-shelf-renderer',
'ytd-browse[page-subtype="channel"] ytd-item-section-renderer',
'ytd-browse[page-subtype="channel"] ytd-section-list-renderer',
'ytd-browse[page-subtype="channel"] ytd-horizontal-card-list-renderer',
'ytd-browse[page-subtype="channel"] ytd-playlist-renderer',
'ytd-browse[page-subtype="channel"] ytd-compact-playlist-renderer',
'ytd-browse[page-subtype="channel"] ytd-grid-playlist-renderer',
'#contents ytd-shelf-renderer',
'#contents ytd-rich-shelf-renderer',
'#contents ytd-item-section-renderer',
'#contents ytd-section-list-renderer',
'#contents ytd-horizontal-card-list-renderer',
'#contents ytd-playlist-renderer',
'#contents ytd-compact-playlist-renderer',
'#contents ytd-grid-playlist-renderer',
'ytd-browse[page-subtype="channel"] #contents > *',
'ytd-browse[page-subtype="channel"] #primary-inner > *:not(ytd-c4-tabbed-header-renderer)',
'[data-target-id="browse-feed-tab"]',
'ytd-browse[page-subtype="channel"] ytd-browse-feed-actions-renderer'
],
// Feed filter chip selectors for hiding "All", "Music", "Gaming", etc.
FEED_CHIP_SELECTORS: [
'ytd-feed-filter-chip-bar-renderer',
'ytd-chip-cloud-renderer',
'ytd-chip-cloud-chip-renderer',
'ytd-feed-filter-renderer',
'#chips',
'.ytd-feed-filter-chip-bar-renderer',
'[role="tablist"]',
'iron-selector[role="tablist"]'
]
};
// === OPTIMIZED UTILITIES ===
class OptimizedUtils {
static cache = new Map();
static domCache = new WeakMap();
static selectorCache = new Map();
static log(message, ...args) {
if (CONFIG.debugMode) {
console.log(`[WayBackTube] ${message}`, ...args);
}
}
static memoize(fn, keyFn) {
const cache = new Map();
return function(...args) {
const key = keyFn ? keyFn(...args) : JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
static throttle(func, wait) {
let timeout;
let previous = 0;
return function(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
return func.apply(this, args);
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now();
timeout = null;
func.apply(this, args);
}, remaining);
}
};
}
static debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
static $(selector, context = document) {
const key = `${selector}-${context === document ? 'doc' : 'ctx'}`;
if (this.selectorCache.has(key)) {
return this.selectorCache.get(key);
}
const element = context.querySelector(selector);
if (element) this.selectorCache.set(key, element);
return element;
}
static $$(selector, context = document) {
return Array.from(context.querySelectorAll(selector));
}
static parseDate(dateStr) {
if (!dateStr) return null;
const formats = [
/^\d{4}-\d{2}-\d{2}$/,
/^\d{2}\/\d{2}\/\d{4}$/,
/^\d{4}\/\d{2}\/\d{2}$/
];
if (formats[0].test(dateStr)) {
return new Date(dateStr + 'T00:00:00');
} else if (formats[1].test(dateStr)) {
const [month, day, year] = dateStr.split('/');
return new Date(year, month - 1, day);
} else if (formats[2].test(dateStr)) {
const [year, month, day] = dateStr.split('/');
return new Date(year, month - 1, day);
}
return new Date(dateStr);
}
static formatDate(date, format = 'YYYY-MM-DD') {
if (!date) return '';
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day);
}
static addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
static getRandomElement(array) {
return array[Math.floor(Math.random() * array.length)];
}
static shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
static removeDuplicates(videos) {
const seenIds = new Set();
const uniqueVideos = [];
for (const video of videos) {
const videoId = video.id?.videoId || video.id || video.snippet?.resourceId?.videoId;
if (videoId && !seenIds.has(videoId)) {
seenIds.add(videoId);
uniqueVideos.push(video);
}
}
return uniqueVideos;
}
static weightedShuffleByDate(videos, maxDate) {
if (!videos || videos.length === 0) return [];
// Sort videos by publish date first - newest to oldest
const sortedVideos = [...videos].sort((a, b) => {
const dateA = new Date(a.snippet?.publishedAt || a.publishedAt || '2005-01-01');
const dateB = new Date(b.snippet?.publishedAt || b.publishedAt || '2005-01-01');
return dateB - dateA; // Newest first
});
// Create weighted array based on video publish dates with HEAVY bias towards recent content
const weightedVideos = sortedVideos.map((video, index) => {
const publishDate = new Date(video.snippet?.publishedAt || video.publishedAt || '2005-01-01');
const hoursDiff = Math.max(1, Math.floor((maxDate - publishDate) / (1000 * 60 * 60)));
const daysDiff = Math.max(1, Math.floor(hoursDiff / 24));
// HEAVY weight bias towards very recent content
let weight;
if (hoursDiff <= 6) {
weight = 100; // Last 6 hours - MAXIMUM priority
} else if (hoursDiff <= 24) {
weight = 50; // Last 24 hours - VERY high priority
} else if (daysDiff <= 3) {
weight = 25; // Last 3 days - High priority
} else if (daysDiff <= 7) {
weight = 15; // Last week - Good priority
} else if (daysDiff <= 30) {
weight = 8; // Last month - Medium priority
} else if (daysDiff <= 90) {
weight = 4; // Last 3 months - Lower priority
} else if (daysDiff <= 365) {
weight = 2; // Last year - Low priority
} else {
weight = 1; // Older than 1 year - Minimal priority
}
// Position bonus: videos already sorted newest first get additional weight
// This ensures the newest videos stay at the top
const positionBonus = Math.max(0, 20 - Math.floor(index / 5)); // Top 100 videos get position bonus
weight += positionBonus;
return { video, weight, publishDate, hoursDiff };
});
// Separate videos into tiers for better control
const recentVideos = weightedVideos.filter(v => v.hoursDiff <= 24); // Last 24 hours
const newVideos = weightedVideos.filter(v => v.hoursDiff > 24 && v.hoursDiff <= 168); // Last week
const olderVideos = weightedVideos.filter(v => v.hoursDiff > 168); // Older than a week
// Create weighted selection with heavy bias towards recent content
const weightedSelection = [];
// Add recent videos with maximum representation
recentVideos.forEach(({ video, weight }) => {
for (let i = 0; i < weight; i++) {
weightedSelection.push(video);
}
});
// Add new videos with good representation
newVideos.forEach(({ video, weight }) => {
for (let i = 0; i < weight; i++) {
weightedSelection.push(video);
}
});
// Add older videos with minimal representation
olderVideos.forEach(({ video, weight }) => {
for (let i = 0; i < weight; i++) {
weightedSelection.push(video);
}
});
// Shuffle the weighted array and remove duplicates while preserving heavy recent bias
const shuffled = this.shuffleArray(weightedSelection);
const uniqueVideos = [];
const seenIds = new Set();
for (const video of shuffled) {
const videoId = video.id?.videoId || video.id || video.snippet?.resourceId?.videoId;
if (videoId && !seenIds.has(videoId)) {
seenIds.add(videoId);
uniqueVideos.push(video);
}
}
return uniqueVideos;
}
static waitFor(condition, timeout = 10000, interval = 100) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
if (condition()) {
resolve();
} else if (Date.now() - startTime >= timeout) {
reject(new Error('Timeout waiting for condition'));
} else {
setTimeout(check, interval);
}
};
check();
});
}
static sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
static isValidVideoId(videoId) {
return /^[a-zA-Z0-9_-]{11}$/.test(videoId);
}
static extractVideoId(url) {
if (!url) return null;
const match = url.match(/(?:v=|\/embed\/|\/watch\?v=|\/v\/|youtu\.be\/)([^&\n?#]+)/);
return match ? match[1] : null;
}
static extractChannelId(url) {
if (!url) return null;
const patterns = [
/\/channel\/([a-zA-Z0-9_-]+)/,
/\/c\/([a-zA-Z0-9_-]+)/,
/\/user\/([a-zA-Z0-9_-]+)/,
/\/@([a-zA-Z0-9_-]+)/
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match) return match[1];
}
return null;
}
static cleanTitle(title) {
if (!title) return '';
return title
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/\s+/g, ' ')
.trim();
}
static extractKeywords(title) {
if (!title) return [];
return title
.toLowerCase()
.replace(/[^\w\s]/g, '')
.split(/\s+/)
.filter(word => word.length > 2)
.slice(0, 5);
}
static getCurrentPage() {
const path = window.location.pathname;
if (path === '/') return 'home';
if (path.startsWith('/watch')) return 'video';
if (path.startsWith('/channel') || path.startsWith('/c/') || path.startsWith('/user/') || path.startsWith('/@')) return 'channel';
if (path.startsWith('/results')) return 'search';
return 'other';
}
static getPageContext() {
return {
page: this.getCurrentPage(),
url: window.location.href,
videoId: this.extractVideoId(window.location.href),
channelId: this.extractChannelId(window.location.href)
};
}
static calculateRelativeDate(uploadDate, maxDate) {
if (!uploadDate || !maxDate) return '';
const upload = new Date(uploadDate);
const max = new Date(maxDate);
if (upload > max) return 'In the future';
// Calculate proper date differences
let years = max.getFullYear() - upload.getFullYear();
let months = max.getMonth() - upload.getMonth();
let days = max.getDate() - upload.getDate();
// Adjust for negative days
if (days < 0) {
months--;
const prevMonth = new Date(max.getFullYear(), max.getMonth(), 0);
days += prevMonth.getDate();
}
// Adjust for negative months
if (months < 0) {
years--;
months += 12;
}
// Calculate total days for smaller units
const totalMilliseconds = max - upload;
const totalDays = Math.floor(totalMilliseconds / (1000 * 60 * 60 * 24));
const totalHours = Math.floor(totalMilliseconds / (1000 * 60 * 60));
const totalMinutes = Math.floor(totalMilliseconds / (1000 * 60));
// Return the most appropriate unit
if (years > 0) {
return `${years} year${years > 1 ? 's' : ''} ago`;
}
if (months > 0) {
return `${months} month${months > 1 ? 's' : ''} ago`;
}
if (totalDays >= 7) {
const weeks = Math.floor(totalDays / 7);
return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
}
if (totalDays > 0) {
return `${totalDays} day${totalDays > 1 ? 's' : ''} ago`;
}
if (totalHours > 0) {
return `${totalHours} hour${totalHours > 1 ? 's' : ''} ago`;
}
if (totalMinutes > 0) {
return `${totalMinutes} minute${totalMinutes > 1 ? 's' : ''} ago`;
}
return 'Just now';
}
// Filter relative date text elements to show time relative to selected date
static filterRelativeDates(maxDate) {
if (!maxDate) return;
const relativePatterns = [
/(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago/gi,
/(\d+)\s+(sec|min|hr|hrs|d|w|mo|yr)s?\s+ago/gi
];
// Find all text nodes with relative dates in search results ONLY
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
if (!node.parentElement) return NodeFilter.FILTER_REJECT;
// Skip our own UI elements
if (node.parentElement.closest('.wayback-container, .yt-time-machine-ui, #wayback-channel-content, .wayback-channel-video-card, .tm-video-card')) {
return NodeFilter.FILTER_REJECT;
}
// Skip homepage content that we've replaced
if (node.parentElement.closest('ytd-browse[page-subtype="home"]')) {
return NodeFilter.FILTER_REJECT;
}
// Skip watch next sidebar (our content)
if (node.parentElement.closest('#secondary #related')) {
return NodeFilter.FILTER_REJECT;
}
// ONLY process search results - look for search result containers
const isInSearchResults = node.parentElement.closest('#contents ytd-search, ytd-search-section-renderer, ytd-video-renderer, ytd-shelf-renderer');
if (!isInSearchResults) {
return NodeFilter.FILTER_REJECT;
}
const text = node.textContent.trim();
return relativePatterns.some(pattern => pattern.test(text)) ?
NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
}
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
// Track processed nodes to avoid double-processing
const processedNodes = new Set();
// Update each text node with corrected relative date
textNodes.forEach(textNode => {
if (processedNodes.has(textNode)) return;
processedNodes.add(textNode);
let originalText = textNode.textContent;
let newText = originalText;
relativePatterns.forEach(pattern => {
newText = newText.replace(pattern, (match, amount, unit) => {
// Convert original upload date to be relative to our selected date
const normalizedUnit = this.normalizeTimeUnit(unit);
const originalUploadDate = this.calculateOriginalDate(amount, normalizedUnit, new Date());
// Apply 1-year leniency for future date detection
const maxDateWithLeniency = new Date(maxDate);
maxDateWithLeniency.setFullYear(maxDateWithLeniency.getFullYear() + 1);
if (originalUploadDate <= maxDate) {
// Video is within time machine date - update normally
const relativeToSelected = this.calculateRelativeDate(originalUploadDate, maxDate);
return relativeToSelected || match;
} else if (originalUploadDate <= maxDateWithLeniency) {
// Video is within 1-year leniency - show random months from 6-11
const randomMonths = Math.floor(Math.random() * 6) + 6; // 6-11 months
return `${randomMonths} month${randomMonths > 1 ? 's' : ''} ago`;
} else {
// Video is more than 1 year in the future - mark for removal
const videoElement = textNode.parentElement.closest('ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer');
if (videoElement) {
setTimeout(() => {
OptimizedUtils.log(`Removing future video from search: ${match} (uploaded after ${OptimizedUtils.formatDate(maxDateWithLeniency)})`);
videoElement.style.display = 'none';
videoElement.remove();
}, 0);
}
return match; // Return original temporarily before removal
}
});
});
if (newText !== originalText) {
if (textNode) textNode.textContent = newText;
}
});
}
static normalizeTimeUnit(unit) {
const unitMap = {
'sec': 'second', 'min': 'minute', 'hr': 'hour', 'hrs': 'hour',
'd': 'day', 'w': 'week', 'mo': 'month', 'yr': 'year'
};
return unitMap[unit.toLowerCase()] || unit.toLowerCase();
}
static calculateOriginalDate(amount, unit, fromDate) {
const date = new Date(fromDate);
const value = parseInt(amount);
switch (unit) {
case 'second':
date.setSeconds(date.getSeconds() - value);
break;
case 'minute':
date.setMinutes(date.getMinutes() - value);
break;
case 'hour':
date.setHours(date.getHours() - value);
break;
case 'day':
date.setDate(date.getDate() - value);
break;
case 'week':
date.setDate(date.getDate() - (value * 7));
break;
case 'month':
date.setMonth(date.getMonth() - value);
break;
case 'year':
date.setFullYear(date.getFullYear() - value);
break;
}
return date;
}
}
// === OPTIMIZED API MANAGER ===
class OptimizedAPIManager {
constructor() {
// Make API keys persistent across versions - use wayback_persistent prefix
this.keys = GM_getValue('wayback_persistent_api_keys', []);
this.currentKeyIndex = GM_getValue('wayback_persistent_current_key_index', 0);
this.keyStats = GM_getValue('wayback_persistent_key_stats', {});
this.baseUrl = 'https://www.googleapis.com/youtube/v3';
this.viralVideoCache = new Map();
this.requestQueue = [];
this.activeRequests = 0;
this.rateLimiter = new Map();
this.init();
}
init() {
const now = Date.now();
const oneDayAgo = now - 86400000; // 24 hours
// Reset failed keys older than 24 hours
Object.keys(this.keyStats).forEach(key => {
const stats = this.keyStats[key];
if (stats.lastFailed && stats.lastFailed < oneDayAgo) {
stats.failed = false;
stats.quotaExceeded = false;
}
});
this.validateKeyIndex();
OptimizedUtils.log(`API Manager initialized with ${this.keys.length} keys`);
}
validateKeyIndex() {
if (this.currentKeyIndex >= this.keys.length) {
this.currentKeyIndex = 0;
}
}
get currentKey() {
return this.keys.length > 0 ? this.keys[this.currentKeyIndex] : null;
}
addKey(apiKey) {
if (!apiKey || apiKey.length < 35 || this.keys.includes(apiKey)) {
return false;
}
this.keys.push(apiKey);
this.keyStats[apiKey] = {
failed: false,
quotaExceeded: false,
lastUsed: 0,
requestCount: 0,
successCount: 0
};
this.saveKeys();
OptimizedUtils.log(`Added API key: ${apiKey.substring(0, 8)}...`);
return true;
}
removeKey(apiKey) {
const index = this.keys.indexOf(apiKey);
if (index === -1) return false;
this.keys.splice(index, 1);
delete this.keyStats[apiKey];
// Adjust current index
if (this.currentKeyIndex >= this.keys.length) {
this.currentKeyIndex = Math.max(0, this.keys.length - 1);
} else if (index <= this.currentKeyIndex && this.currentKeyIndex > 0) {
this.currentKeyIndex--;
}
this.saveKeys();
OptimizedUtils.log(`Removed API key: ${apiKey.substring(0, 8)}...`);
return true;
}
rotateToNextKey() {
if (this.keys.length <= 1) return false;
const startIndex = this.currentKeyIndex;
let attempts = 0;
while (attempts < this.keys.length) {
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.keys.length;
attempts++;
const currentKey = this.currentKey;
const stats = this.keyStats[currentKey];
if (!stats || (!stats.quotaExceeded && !stats.failed)) {
this.saveKeys();
OptimizedUtils.log(`Rotated to key ${this.currentKeyIndex + 1}/${this.keys.length}`);
return true;
}
}
this.currentKeyIndex = 0;
this.saveKeys();
OptimizedUtils.log('All keys have issues, reset to first key');
return false;
}
markKeySuccess(apiKey) {
const stats = this.keyStats[apiKey] || {};
Object.assign(stats, {
lastUsed: Date.now(),
requestCount: (stats.requestCount || 0) + 1,
successCount: (stats.successCount || 0) + 1,
failed: false
});
this.keyStats[apiKey] = stats;
this.saveKeys();
}
markKeyFailed(apiKey, errorMessage) {
const stats = this.keyStats[apiKey] || {};
stats.failed = true;
stats.lastFailed = Date.now();
const quotaErrors = ['quota', 'exceeded', 'dailylimitexceeded', 'ratelimitexceeded'];
if (quotaErrors.some(error => errorMessage.toLowerCase().includes(error))) {
stats.quotaExceeded = true;
}
this.keyStats[apiKey] = stats;
this.saveKeys();
OptimizedUtils.log(`Key failed: ${apiKey.substring(0, 8)}... - ${errorMessage}`);
}
saveKeys() {
// Use persistent storage for API keys across versions
GM_setValue('wayback_persistent_api_keys', this.keys);
GM_setValue('wayback_persistent_current_key_index', this.currentKeyIndex);
GM_setValue('wayback_persistent_key_stats', this.keyStats);
}
getRandomUserAgent() {
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0'
];
return userAgents[Math.floor(Math.random() * userAgents.length)];
}
async testAPIKey(apiKey) {
try {
const response = await this.makeRequest(`https://www.googleapis.com/youtube/v3/search?part=snippet&maxResults=1&q=test&key=${apiKey}`);
if (response && response.items) {
return { success: true, message: 'Working perfectly' };
} else {
return { success: false, message: 'Invalid response format' };
}
} catch (error) {
const errorMsg = error.message || error.toString();
if (errorMsg.includes('quotaExceeded') || errorMsg.includes('dailyLimitExceeded')) {
return { success: false, message: 'quotaExceeded - Daily quota exceeded' };
} else if (errorMsg.includes('keyInvalid') || errorMsg.includes('invalid')) {
return { success: false, message: 'keyInvalid - API key is invalid' };
} else if (errorMsg.includes('accessNotConfigured')) {
return { success: false, message: 'accessNotConfigured - YouTube Data API not enabled' };
} else {
return { success: false, message: errorMsg };
}
}
}
async testAllKeys() {
const results = [];
for (let i = 0; i < this.keys.length; i++) {
const key = this.keys[i];
const result = await this.testAPIKey(key);
results.push({
keyIndex: i,
keyPreview: `${key.substring(0, 8)}...`,
result: result.success ? 'Working perfectly' : result.message
});
// Add delay between requests to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500));
}
return results;
}
getCache(key, forceRefresh = false) {
if (forceRefresh) return null;
const cached = GM_getValue(`cache_${key}`, null);
if (cached) {
try {
const data = JSON.parse(cached);
if (Date.now() - data.timestamp < CONFIG.cacheExpiry.videos) {
return data.value;
}
} catch (e) {
// Invalid cache entry
}
}
return null;
}
setCache(key, value, forceRefresh = false) {
const cacheData = {
timestamp: Date.now(),
value: value
};
GM_setValue(`cache_${key}`, JSON.stringify(cacheData));
}
clearCache() {
const keys = GM_listValues();
keys.forEach(key => {
if (key.startsWith('cache_')) {
GM_deleteValue(key);
}
});
}
generateRealisticViewCount(publishedAt, referenceDate) {
const videoDate = new Date(publishedAt);
const refDate = new Date(referenceDate);
const daysSinceUpload = Math.floor((refDate - videoDate) / (1000 * 60 * 60 * 24));
let minViews, maxViews;
if (daysSinceUpload <= 1) {
minViews = 50;
maxViews = 10000;
} else if (daysSinceUpload <= 7) {
minViews = 500;
maxViews = 100000;
} else if (daysSinceUpload <= 30) {
minViews = 2000;
maxViews = 500000;
} else if (daysSinceUpload <= 365) {
minViews = 5000;
maxViews = 2000000;
} else {
minViews = 10000;
maxViews = 5000000;
}
const randomViews = Math.floor(Math.random() * (maxViews - minViews) + minViews);
return this.formatViewCount(randomViews);
}
formatViewCount(count) {
if (count >= 1000000) {
return (count / 1000000).toFixed(1) + 'M';
} else if (count >= 1000) {
return (count / 1000).toFixed(1) + 'K';
}
return count.toString();
}
async makeRequest(endpoint, params) {
return new Promise((resolve, reject) => {
if (this.keys.length === 0) {
reject(new Error('No API keys available'));
return;
}
this.requestQueue.push({ endpoint, params, resolve, reject, attempts: 0 });
this.processQueue();
});
}
async processQueue() {
if (this.activeRequests >= CONFIG.maxConcurrentRequests || this.requestQueue.length === 0) {
return;
}
const request = this.requestQueue.shift();
this.activeRequests++;
try {
const result = await this.executeRequest(request);
request.resolve(result);
} catch (error) {
request.reject(error);
} finally {
this.activeRequests--;
setTimeout(() => this.processQueue(), CONFIG.apiCooldown);
}
}
async executeRequest(request) {
const maxAttempts = Math.min(this.keys.length, 10);
if (request.attempts >= maxAttempts) {
throw new Error('All API keys exhausted');
}
const currentKey = this.currentKey;
if (!currentKey) {
throw new Error('No valid API key');
}
// Rate limiting per key
const now = Date.now();
const keyRateLimit = this.rateLimiter.get(currentKey) || 0;
if (now < keyRateLimit) {
await OptimizedUtils.sleep(keyRateLimit - now);
}
const urlParams = new URLSearchParams({ ...request.params, key: currentKey });
const url = `${request.endpoint}?${urlParams}`;
OptimizedUtils.log(`Request attempt ${request.attempts + 1} with key ${this.currentKeyIndex + 1}`);
return new Promise((resolve, reject) => {
// Get proxy configuration if enabled
const requestConfig = {
method: 'GET',
url: url,
timeout: 10000,
headers: {
'Accept': 'application/json',
'User-Agent': this.getRandomUserAgent()
}
};
GM_xmlhttpRequest({
...requestConfig,
onload: (response) => {
this.rateLimiter.set(currentKey, Date.now() + 100); // 100ms rate limit per key
// Debug logging for API responses
OptimizedUtils.log(`📥 API Response: ${response.status} for ${url.split('?')[0]}`);
if (requestConfig.proxy) {
OptimizedUtils.log(`🔄 Response came through proxy: ${requestConfig.proxy.host}:${requestConfig.proxy.port}`);
}
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
this.markKeySuccess(currentKey);
// Log success stats
const itemCount = data.items ? data.items.length : 0;
OptimizedUtils.log(`✅ API Success: Retrieved ${itemCount} items`);
resolve(data);
} catch (e) {
OptimizedUtils.log(`❌ JSON Parse Error: ${e.message}`);
reject(new Error('Invalid JSON response'));
}
} else if (response.status === 403) {
// Specific handling for quota exceeded
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = JSON.parse(response.responseText);
if (errorData.error?.message) {
errorMessage = errorData.error.message;
if (errorMessage.includes('quota') || errorMessage.includes('limit')) {
OptimizedUtils.log(`🚫 Quota exceeded for key ${this.currentKeyIndex + 1}: ${errorMessage}`);
this.keyStats[currentKey].quotaExceeded = true;
}
}
} catch (e) {
// Use default error message
}
this.markKeyFailed(currentKey, errorMessage);
if (this.rotateToNextKey()) {
request.attempts++;
OptimizedUtils.log(`🔄 Rotating to next key after quota error, attempt ${request.attempts}`);
setTimeout(() => {
this.executeRequest(request).then(resolve).catch(reject);
}, 500);
} else {
reject(new Error(`API Error: ${errorMessage}`));
}
} else {
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = JSON.parse(response.responseText);
if (errorData.error?.message) {
errorMessage = errorData.error.message;
}
} catch (e) {
// Use default error message
}
OptimizedUtils.log(`❌ API Error ${response.status}: ${errorMessage}`);
this.markKeyFailed(currentKey, errorMessage);
if (this.rotateToNextKey()) {
request.attempts++;
setTimeout(() => {
this.executeRequest(request).then(resolve).catch(reject);
}, 500);
} else {
reject(new Error(`API Error: ${errorMessage}`));
}
}
},
onerror: (error) => {
OptimizedUtils.log(`🔥 Network Error for ${url.split('?')[0]}:`, error);
this.markKeyFailed(currentKey, 'Network error');
if (this.rotateToNextKey()) {
request.attempts++;
OptimizedUtils.log(`🔄 Retrying after network error, attempt ${request.attempts}`);
setTimeout(() => {
this.executeRequest(request).then(resolve).catch(reject);
}, 1000);
} else {
reject(new Error(`Network error: ${error.error || 'Connection failed'}`));
}
}
});
});
}
async searchVideos(query, options = {}) {
const params = {
part: 'snippet',
type: 'video',
order: options.order || 'relevance',
maxResults: options.maxResults || 50,
q: query,
...options.additionalParams
};
if (options.publishedAfter) {
params.publishedAfter = options.publishedAfter.toISOString();
}
if (options.publishedBefore) {
params.publishedBefore = options.publishedBefore.toISOString();
}
try {
const data = await this.makeRequest(`${this.baseUrl}/search`, params);
// Normalize search results to match video format expected by other parts of the code
const items = data.items || [];
return items.map(item => ({
id: item.id?.videoId || item.id,
snippet: item.snippet
}));
} catch (error) {
OptimizedUtils.log('Search videos error:', error.message);
return [];
}
}
async getChannelVideos(channelId, options = {}) {
const params = {
part: 'snippet',
channelId: channelId,
type: 'video',
order: options.order || 'date',
maxResults: options.maxResults || CONFIG.videosPerChannel,
...options.additionalParams
};
if (options.publishedAfter) {
params.publishedAfter = options.publishedAfter.toISOString();
}
if (options.publishedBefore) {
params.publishedBefore = options.publishedBefore.toISOString();
}
try {
const data = await this.makeRequest(`${this.baseUrl}/search`, params);
// Normalize search results to match video format expected by other parts of the code
const items = data.items || [];
return items.map(item => ({
id: item.id?.videoId || item.id,
snippet: item.snippet
}));
} catch (error) {
OptimizedUtils.log('Channel videos error:', error.message);
return [];
}
}
async getVideoDetails(videoIds) {
if (!Array.isArray(videoIds)) videoIds = [videoIds];
const params = {
part: 'snippet,statistics',
id: videoIds.join(',')
};
try {
const data = await this.makeRequest(`${this.baseUrl}/videos`, params);
return data.items || [];
} catch (error) {
OptimizedUtils.log('Video details error:', error.message);
return [];
}
}
async getChannelDetails(channelId) {
const params = {
part: 'snippet,statistics',
id: channelId
};
try {
const data = await this.makeRequest(`${this.baseUrl}/channels`, params);
return data.items?.[0] || null;
} catch (error) {
OptimizedUtils.log('Channel details error:', error.message);
return null;
}
}
async getViralVideos(timeframe) {
const cacheKey = `viral_${timeframe.start}_${timeframe.end}`;
if (this.viralVideoCache.has(cacheKey)) {
const cached = this.viralVideoCache.get(cacheKey);
if (Date.now() - cached.timestamp < CONFIG.cacheExpiry.videos) {
return cached.data;
}
}
try {
const params = {
part: 'snippet,statistics',
chart: 'mostPopular',
maxResults: CONFIG.viralVideosCount,
publishedAfter: timeframe.start.toISOString(),
publishedBefore: timeframe.end.toISOString()
};
const data = await this.makeRequest(`${this.baseUrl}/videos`, params);
const videos = data.items || [];
this.viralVideoCache.set(cacheKey, {
data: videos,
timestamp: Date.now()
});
return videos;
} catch (error) {
OptimizedUtils.log('Viral videos error:', error.message);
return [];
}
}
getKeyStats() {
return {
totalKeys: this.keys.length,
currentKey: this.currentKeyIndex + 1,
stats: this.keyStats
};
}
clearCache() {
this.viralVideoCache.clear();
OptimizedUtils.log('API cache cleared');
}
}
// === PERSISTENT VIDEO CACHE ===
class PersistentVideoCache {
constructor() {
this.prefix = 'wayback_video_cache_';
this.indexKey = 'wayback_video_cache_index';
this.loadIndex();
}
loadIndex() {
this.index = GM_getValue(this.indexKey, {});
}
saveIndex() {
GM_setValue(this.indexKey, this.index);
}
get(key) {
const fullKey = this.prefix + key;
const cached = GM_getValue(fullKey, null);
if (!cached) {
OptimizedUtils.log(`Cache MISS: ${key}`);
return null;
}
try {
const data = typeof cached === 'string' ? JSON.parse(cached) : cached;
// Check if cache has expired
if (data.expiry && Date.now() > data.expiry) {
OptimizedUtils.log(`Cache EXPIRED: ${key}`);
this.delete(key);
return null;
}
OptimizedUtils.log(`Cache HIT: ${key} - ${data.value ? data.value.length : 0} videos`);
return data.value;
} catch (e) {
OptimizedUtils.log('Cache parse error:', e);
this.delete(key);
return null;
}
}
set(key, value, expiryMs = CONFIG.cacheExpiry.videos) {
const fullKey = this.prefix + key;
const data = {
value: value,
timestamp: Date.now(),
expiry: Date.now() + expiryMs
};
GM_setValue(fullKey, JSON.stringify(data));
// Update index
this.index[key] = {
timestamp: data.timestamp,
expiry: data.expiry,
size: value ? value.length : 0
};
this.saveIndex();
OptimizedUtils.log(`Cache SET: ${key} - ${value ? value.length : 0} videos (expires in ${Math.round(expiryMs/1000/60)} minutes)`);
}
has(key) {
return this.get(key) !== null;
}
delete(key) {
const fullKey = this.prefix + key;
GM_deleteValue(fullKey);
delete this.index[key];
this.saveIndex();
OptimizedUtils.log(`Cache DELETE: ${key}`);
}
clear() {
// Clear all cached entries
Object.keys(this.index).forEach(key => {
const fullKey = this.prefix + key;
GM_deleteValue(fullKey);
});
// Clear index
this.index = {};
this.saveIndex();
OptimizedUtils.log('Video cache CLEARED');
}
size() {
return Object.keys(this.index).length;
}
// For debugging - show all cache entries
debug() {
console.log('=== Video Cache Debug ===');
console.log(`Total entries: ${this.size()}`);
Object.entries(this.index).forEach(([key, info]) => {
const remaining = info.expiry - Date.now();
const minutes = Math.round(remaining / 1000 / 60);
console.log(`${key}: ${info.size} videos, expires in ${minutes} minutes`);
});
}
}
// === OPTIMIZED CACHE MANAGER ===
class OptimizedCacheManager {
constructor() {
this.prefix = 'ytCache_';
this.memoryCache = new Map();
this.maxMemoryCacheSize = 1000;
this.cacheHitCount = 0;
this.cacheMissCount = 0;
}
generateKey(type, ...params) {
return `${this.prefix}${type}_${params.join('_')}`;
}
set(type, data, expiry = CONFIG.cacheExpiry.videos, ...params) {
const key = this.generateKey(type, ...params);
const cacheData = {
data,
timestamp: Date.now(),
expiry
};
// Store in memory cache with size limit
if (this.memoryCache.size >= this.maxMemoryCacheSize) {
const firstKey = this.memoryCache.keys().next().value;
this.memoryCache.delete(firstKey);
}
this.memoryCache.set(key, cacheData);
// Store in GM storage for persistence
try {
GM_setValue(key, JSON.stringify(cacheData));
} catch (error) {
OptimizedUtils.log('Cache set error:', error.message);
}
}
get(type, ...params) {
const key = this.generateKey(type, ...params);
// Check memory cache first
if (this.memoryCache.has(key)) {
const cached = this.memoryCache.get(key);
if (Date.now() - cached.timestamp < cached.expiry) {
this.cacheHitCount++;
return cached.data;
} else {
this.memoryCache.delete(key);
}
}
// Check GM storage
try {
const cached = GM_getValue(key);
if (cached) {
const parsedCache = JSON.parse(cached);
if (Date.now() - parsedCache.timestamp < parsedCache.expiry) {
// Add back to memory cache
this.memoryCache.set(key, parsedCache);
this.cacheHitCount++;
return parsedCache.data;
} else {
GM_deleteValue(key);
}
}
} catch (error) {
OptimizedUtils.log('Cache get error:', error.message);
}
this.cacheMissCount++;
return null;
}
delete(type, ...params) {
const key = this.generateKey(type, ...params);
this.memoryCache.delete(key);
try {
GM_deleteValue(key);
} catch (error) {
OptimizedUtils.log('Cache delete error:', error.message);
}
}
clear(pattern = null) {
if (!pattern) {
this.memoryCache.clear();
try {
const keys = GM_listValues().filter(key => key.startsWith(this.prefix));
keys.forEach(key => GM_deleteValue(key));
OptimizedUtils.log('All cache cleared');
} catch (error) {
OptimizedUtils.log('Cache clear error:', error.message);
}
return;
}
// Clear by pattern
const keysToDelete = [];
for (const [key] of this.memoryCache) {
if (key.includes(pattern)) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(key => {
this.memoryCache.delete(key);
try {
GM_deleteValue(key);
} catch (error) {
OptimizedUtils.log('Cache pattern delete error:', error.message);
}
});
OptimizedUtils.log(`Cache cleared for pattern: ${pattern}`);
}
getStats() {
const hitRate = this.cacheHitCount + this.cacheMissCount > 0
? (this.cacheHitCount / (this.cacheHitCount + this.cacheMissCount) * 100).toFixed(2)
: 0;
return {
memorySize: this.memoryCache.size,
hitRate: `${hitRate}%`,
hits: this.cacheHitCount,
misses: this.cacheMissCount
};
}
cleanup() {
const now = Date.now();
const expiredKeys = [];
for (const [key, cached] of this.memoryCache) {
if (now - cached.timestamp >= cached.expiry) {
expiredKeys.push(key);
}
}
expiredKeys.forEach(key => {
this.memoryCache.delete(key);
try {
GM_deleteValue(key);
} catch (error) {
OptimizedUtils.log('Cache cleanup error:', error.message);
}
});
if (expiredKeys.length > 0) {
OptimizedUtils.log(`Cleaned up ${expiredKeys.length} expired cache entries`);
}
}
}
// === OPTIMIZED VIDEO MANAGER ===
class OptimizedVideoManager {
constructor(apiManager, cacheManager) {
this.api = apiManager;
this.cache = cacheManager;
this.currentTimeframe = this.getCurrentTimeframe();
this.processingQueue = [];
this.isProcessing = false;
this.videoPool = new Map();
this.channelVideoCache = new Map();
}
getCurrentTimeframe() {
const selectedDate = GM_getValue('ytSelectedDate', this.getDefaultDate());
const date = OptimizedUtils.parseDate(selectedDate);
return {
start: new Date(date.getFullYear(), date.getMonth(), 1),
end: new Date(date.getFullYear(), date.getMonth() + 1, 0),
selectedDate: date
};
}
getDefaultDate() {
const now = new Date();
return OptimizedUtils.formatDate(new Date(now.getFullYear() - 10, now.getMonth(), now.getDate()));
}
setTimeframe(dateStr) {
const date = OptimizedUtils.parseDate(dateStr);
if (!date) return false;
this.currentTimeframe = {
start: new Date(date.getFullYear(), date.getMonth(), 1),
end: new Date(date.getFullYear(), date.getMonth() + 1, 0),
selectedDate: date
};
GM_setValue('ytSelectedDate', OptimizedUtils.formatDate(date));
OptimizedUtils.log(`Timeframe set to: ${OptimizedUtils.formatDate(date)}`);
return true;
}
async getVideosForChannel(channelId, count = CONFIG.videosPerChannel) {
const cacheKey = `channel_${channelId}_${this.currentTimeframe.start.getTime()}`;
// Check cache first
let videos = this.cache.get('channelVideos', cacheKey);
if (videos) {
return OptimizedUtils.shuffleArray(videos).slice(0, count);
}
// Check memory pool
if (this.channelVideoCache.has(channelId)) {
const cached = this.channelVideoCache.get(channelId);
if (Date.now() - cached.timestamp < CONFIG.cacheExpiry.channelVideos) {
return OptimizedUtils.shuffleArray(cached.videos).slice(0, count);
}
}
try {
videos = await this.api.getChannelVideos(channelId, {
publishedAfter: this.currentTimeframe.start,
publishedBefore: this.currentTimeframe.end,
maxResults: count * 2, // Get more for better randomization
order: 'relevance'
});
if (videos.length > 0) {
this.cache.set('channelVideos', videos, CONFIG.cacheExpiry.channelVideos, cacheKey);
this.channelVideoCache.set(channelId, {
videos,
timestamp: Date.now()
});
}
return OptimizedUtils.shuffleArray(videos).slice(0, count);
} catch (error) {
OptimizedUtils.log(`Error getting videos for channel ${channelId}:`, error.message);
return [];
}
}
async getHomepageVideos(count = CONFIG.maxHomepageVideos) {
const cacheKey = `homepage_${this.currentTimeframe.selectedDate.getTime()}`;
// Check cache
let videos = this.cache.get('homepage', cacheKey);
if (videos && videos.length >= count) {
return OptimizedUtils.shuffleArray(videos).slice(0, count);
}
try {
// Combine multiple search strategies
const searchTerms = [
'', // General popular videos
'music', 'gaming', 'sports', 'news', 'entertainment',
'tutorial', 'review', 'funny', 'technology', 'science'
];
const videoBatches = await Promise.allSettled(
searchTerms.slice(0, 5).map(term =>
this.api.searchVideos(term, {
publishedAfter: this.currentTimeframe.start,
publishedBefore: this.currentTimeframe.end,
order: 'relevance',
maxResults: Math.ceil(count / 5)
})
)
);
videos = videoBatches
.filter(result => result.status === 'fulfilled')
.flatMap(result => result.value)
.filter(video => this.isValidVideo(video));
// Add some viral videos for the period
const viralVideos = await this.api.getViralVideos(this.currentTimeframe);
const viralCount = Math.floor(count * CONFIG.viralVideoPercentage);
videos = videos.concat(viralVideos.slice(0, viralCount));
// Remove duplicates and cache
videos = this.removeDuplicateVideos(videos);
if (videos.length > 0) {
this.cache.set('homepage', videos, CONFIG.cacheExpiry.videos, cacheKey);
}
return OptimizedUtils.shuffleArray(videos).slice(0, count);
} catch (error) {
OptimizedUtils.log('Error getting homepage videos:', error.message);
return [];
}
}
async getRecommendationVideos(currentVideoId, count = CONFIG.RECOMMENDATION_COUNT) {
const cacheKey = `recommendations_${currentVideoId}_${this.currentTimeframe.selectedDate.getTime()}`;
// Check cache
let videos = this.cache.get('recommendations', cacheKey);
if (videos && videos.length >= count) {
return OptimizedUtils.shuffleArray(videos).slice(0, count);
}
try {
const currentVideo = (await this.api.getVideoDetails([currentVideoId]))[0];
if (!currentVideo) return [];
const channelId = currentVideo.snippet.channelId;
const title = currentVideo.snippet.title;
const keywords = OptimizedUtils.extractKeywords(title);
// Get videos from same channel
const sameChannelCount = Math.floor(count * CONFIG.SAME_CHANNEL_RATIO);
const sameChannelVideos = await this.getVideosForChannel(channelId, sameChannelCount);
// Get videos from other channels using keywords
const otherChannelCount = count - sameChannelCount;
const keywordVideos = await this.getKeywordBasedVideos(keywords, otherChannelCount, channelId);
videos = [...sameChannelVideos, ...keywordVideos];
videos = this.removeDuplicateVideos(videos);
if (videos.length > 0) {
this.cache.set('recommendations', videos, CONFIG.cacheExpiry.videos, cacheKey);
}
return OptimizedUtils.shuffleArray(videos).slice(0, count);
} catch (error) {
OptimizedUtils.log('Error getting recommendation videos:', error.message);
return [];
}
}
async getKeywordBasedVideos(keywords, count, excludeChannelId = null) {
if (!keywords.length) return [];
const searchPromises = keywords.slice(0, 3).map(keyword =>
this.api.searchVideos(keyword, {
publishedAfter: this.currentTimeframe.start,
publishedBefore: this.currentTimeframe.end,
maxResults: Math.ceil(count / 3),
order: 'relevance'
})
);
try {
const results = await Promise.allSettled(searchPromises);
let videos = results
.filter(result => result.status === 'fulfilled')
.flatMap(result => result.value)
.filter(video => this.isValidVideo(video));
if (excludeChannelId) {
videos = videos.filter(video => video.snippet.channelId !== excludeChannelId);
}
return this.removeDuplicateVideos(videos).slice(0, count);
} catch (error) {
OptimizedUtils.log('Error getting keyword-based videos:', error.message);
return [];
}
}
async getChannelPageVideos(channelId, count = CONFIG.channelPageVideosPerMonth) {
const cacheKey = `channelPage_${channelId}_${this.currentTimeframe.selectedDate.getTime()}`;
let videos = this.cache.get('channelPage', cacheKey);
if (videos && videos.length >= count) {
return videos.slice(0, count);
}
try {
videos = await this.api.getChannelVideos(channelId, {
publishedAfter: this.currentTimeframe.start,
publishedBefore: this.currentTimeframe.end,
maxResults: count,
order: 'date'
});
videos = videos.filter(video => this.isValidVideo(video));
if (videos.length > 0) {
this.cache.set('channelPage', videos, CONFIG.cacheExpiry.channelVideos, cacheKey);
}
return videos;
} catch (error) {
OptimizedUtils.log(`Error getting channel page videos for ${channelId}:`, error.message);
return [];
}
}
async searchVideos(query, count = 50) {
const cacheKey = `search_${query}_${this.currentTimeframe.selectedDate.getTime()}`;
let videos = this.cache.get('search', cacheKey);
if (videos && videos.length >= count) {
return videos.slice(0, count);
}
try {
videos = await this.api.searchVideos(query, {
publishedAfter: this.currentTimeframe.start,
publishedBefore: this.currentTimeframe.end,
maxResults: count,
order: 'relevance'
});
videos = videos.filter(video => this.isValidVideo(video));
if (videos.length > 0) {
this.cache.set('search', videos, CONFIG.cacheExpiry.searchResults, cacheKey);
}
return videos;
} catch (error) {
OptimizedUtils.log(`Error searching videos for "${query}":`, error.message);
return [];
}
}
isValidVideo(video) {
if (!video || !video.snippet) return false;
const publishedAt = new Date(video.snippet.publishedAt);
// Check if video is within timeframe
if (publishedAt < this.currentTimeframe.start || publishedAt > this.currentTimeframe.end) {
return false;
}
return true;
}
removeDuplicateVideos(videos) {
const seen = new Set();
return videos.filter(video => {
const id = video.id?.videoId || video.id;
if (seen.has(id)) return false;
seen.add(id);
return true;
});
}
addToProcessingQueue(task) {
this.processingQueue.push(task);
if (!this.isProcessing) {
this.processQueue();
}
}
async processQueue() {
if (this.processingQueue.length === 0) {
this.isProcessing = false;
return;
}
this.isProcessing = true;
const batch = this.processingQueue.splice(0, CONFIG.batchSize);
const promises = batch.map(task => this.executeTask(task));
try {
await Promise.allSettled(promises);
await OptimizedUtils.sleep(CONFIG.apiCooldown);
this.processQueue(); // Process next batch
} catch (error) {
OptimizedUtils.log('Queue processing error:', error.message);
this.isProcessing = false;
}
}
async executeTask(task) {
try {
const result = await task.execute();
if (task.callback) {
task.callback(result);
}
return result;
} catch (error) {
OptimizedUtils.log('Task execution error:', error.message);
if (task.errorCallback) {
task.errorCallback(error);
}
throw error;
}
}
clearCache() {
this.cache.clear('homepage');
this.cache.clear('channelPage');
this.cache.clear('recommendations');
this.cache.clear('search');
this.channelVideoCache.clear();
this.videoPool.clear();
OptimizedUtils.log('Video manager cache cleared');
}
getStats() {
return {
currentTimeframe: this.currentTimeframe,
queueSize: this.processingQueue.length,
isProcessing: this.isProcessing,
videoPoolSize: this.videoPool.size,
channelCacheSize: this.channelVideoCache.size
};
}
}
// === OPTIMIZED UI MANAGER ===
class OptimizedUIManager {
constructor(videoManager, apiManager, cacheManager) {
this.videoManager = videoManager;
this.apiManager = apiManager;
this.cacheManager = cacheManager;
this.elements = new Map();
this.observers = new Map();
this.eventListeners = new Map();
this.isInitialized = false;
this.cssInjected = false;
}
async init() {
if (this.isInitialized) return;
await this.waitForPageLoad();
this.injectStyles();
this.createMainInterface();
this.setupEventListeners();
this.startObservers();
this.isInitialized = true;
OptimizedUtils.log('UI Manager initialized');
}
async waitForPageLoad() {
await OptimizedUtils.waitFor(() => document.body && document.head, 10000);
await OptimizedUtils.sleep(100); // Small delay for page stabilization
}
injectStyles() {
if (this.cssInjected) return;
const styles = `
/* WayBackTube Optimized Styles */
#wayback-interface {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
background: rgba(0, 0, 0, 0.9);
border-radius: 8px;
padding: 15px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
font-size: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
min-width: 280px;
transition: all 0.3s ease;
}
#wayback-interface.minimized {
padding: 10px;
min-width: auto;
}
#wayback-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 10px;
}
#wayback-title {
font-weight: bold;
font-size: 16px;
color: #ff6b6b;
}
.wayback-controls {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.wayback-input {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
padding: 8px;
color: white;
font-size: 13px;
transition: all 0.2s ease;
}
.wayback-input:focus {
outline: none;
border-color: #ff6b6b;
background: rgba(255, 255, 255, 0.15);
}
.wayback-button {
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
border: none;
border-radius: 4px;
color: white;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
.wayback-button:hover {
background: linear-gradient(135deg, #ee5a52, #dd4a41);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(255, 107, 107, 0.3);
}
.wayback-button:active {
transform: translateY(0);
}
.wayback-button.small {
padding: 6px 10px;
font-size: 11px;
}
.wayback-stats {
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
font-size: 11px;
line-height: 1.4;
}
.wayback-stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.wayback-status {
display: inline-block;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
}
.wayback-status.active {
background: rgba(76, 175, 80, 0.3);
color: #4CAF50;
}
.wayback-status.loading {
background: rgba(255, 193, 7, 0.3);
color: #FFC107;
}
.wayback-status.error {
background: rgba(244, 67, 54, 0.3);
color: #F44336;
}
.wayback-hidden {
display: none !important;
}
.wayback-loading {
opacity: 0.7;
position: relative;
}
.wayback-loading::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: wayback-shimmer 1.5s infinite;
}
@keyframes wayback-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* Hide modern YouTube elements */
${CONFIG.SHORTS_SELECTORS.map(selector => `${selector}`).join(', ')} {
display: none !important;
}
${CONFIG.CHANNEL_PAGE_SELECTORS.map(selector => `${selector}`).join(', ')} {
visibility: hidden !important;
height: 0 !important;
overflow: hidden !important;
}
/* Modern content warning */
.wayback-warning {
background: rgba(255, 152, 0, 0.1);
border: 1px solid rgba(255, 152, 0, 0.3);
border-radius: 4px;
padding: 8px;
margin-top: 10px;
font-size: 11px;
color: #FF9800;
}
/* New sections styles */
.wayback-section {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
.wayback-section-header {
margin-bottom: 10px;
}
.wayback-section-header h4 {
margin: 0 0 4px 0;
font-size: 14px;
color: #ff6b6b;
font-weight: bold;
}
.wayback-section-desc {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
line-height: 1.3;
}
.wayback-list {
max-height: 120px;
overflow-y: auto;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
margin: 8px 0;
padding: 4px;
}
.wayback-list::-webkit-scrollbar {
width: 6px;
}
.wayback-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.wayback-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.wayback-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
margin: 2px 0;
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
font-size: 12px;
}
.wayback-list-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.wayback-item-text {
flex: 1;
color: white;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
margin-right: 8px;
}
.wayback-remove-btn {
background: rgba(244, 67, 54, 0.8);
border: none;
border-radius: 50%;
color: white;
width: 20px;
height: 20px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.wayback-remove-btn:hover {
background: rgba(244, 67, 54, 1);
transform: scale(1.1);
}
.wayback-button.secondary {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.9);
}
.wayback-button.secondary:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-1px);
}
/* Responsive design */
@media (max-width: 768px) {
#wayback-interface {
position: fixed;
top: auto;
bottom: 20px;
right: 20px;
left: 20px;
min-width: auto;
}
}
`;
// Simple 2011 Theme: Blue text + Square corners
const vintage2011CSS = `
/* Simple 2011 Theme - VERY VISIBLE TEST */
body.wayback-2011-theme {
/* Blue text for all links and video titles */
--yt-spec-text-primary: #0066cc !important;
--yt-spec-text-secondary: #0066cc !important;
/* Normal background - remove testing colors */
}
/* Video titles - blue text with more specific selectors */
body.wayback-2011-theme h3 a,
body.wayback-2011-theme #video-title,
body.wayback-2011-theme ytd-video-renderer h3 a,
body.wayback-2011-theme .ytd-video-renderer h3 a,
body.wayback-2011-theme a[id="video-title-link"],
body.wayback-2011-theme ytd-video-primary-info-renderer h1 a {
color: #0066cc !important;
}
/* Channel names - blue text */
body.wayback-2011-theme ytd-channel-name a,
body.wayback-2011-theme .ytd-channel-name a,
body.wayback-2011-theme #channel-name a,
body.wayback-2011-theme .ytd-video-secondary-info-renderer a {
color: #0066cc !important;
}
/* All links should be blue */
body.wayback-2011-theme a {
color: #0066cc !important;
}
/* Remove ALL rounded corners - make everything square */
body.wayback-2011-theme *,
body.wayback-2011-theme *:before,
body.wayback-2011-theme *:after {
border-radius: 0 !important;
-webkit-border-radius: 0 !important;
-moz-border-radius: 0 !important;
}
/* Square thumbnails */
body.wayback-2011-theme ytd-thumbnail img,
body.wayback-2011-theme .ytd-thumbnail img,
body.wayback-2011-theme img {
border-radius: 0 !important;
}
/* Square buttons */
body.wayback-2011-theme button,
body.wayback-2011-theme .yt-spec-button-shape-next,
body.wayback-2011-theme .yt-spec-button-shape-next__button {
border-radius: 0 !important;
}
/* Square search box */
body.wayback-2011-theme input,
body.wayback-2011-theme #search-form,
body.wayback-2011-theme ytd-searchbox,
body.wayback-2011-theme #search-form input {
border-radius: 0 !important;
}
/* Square video containers */
body.wayback-2011-theme ytd-video-renderer,
body.wayback-2011-theme ytd-rich-item-renderer,
body.wayback-2011-theme .ytd-video-renderer,
body.wayback-2011-theme .ytd-rich-item-renderer {
border-radius: 0 !important;
}
/* Remove ALL modern animations and hover effects */
body.wayback-2011-theme *,
body.wayback-2011-theme *:before,
body.wayback-2011-theme *:after {
transition: none !important;
animation: none !important;
transform: none !important;
-webkit-transition: none !important;
-webkit-animation: none !important;
-webkit-transform: none !important;
-moz-transition: none !important;
-moz-animation: none !important;
-moz-transform: none !important;
}
/* Disable hover animations on video thumbnails and containers */
body.wayback-2011-theme ytd-video-renderer:hover,
body.wayback-2011-theme ytd-rich-item-renderer:hover,
body.wayback-2011-theme .ytd-video-renderer:hover,
body.wayback-2011-theme .ytd-rich-item-renderer:hover {
transform: none !important;
box-shadow: none !important;
transition: none !important;
}
/* Disable thumbnail hover effects */
body.wayback-2011-theme ytd-thumbnail:hover,
body.wayback-2011-theme .ytd-thumbnail:hover {
transform: none !important;
transition: none !important;
}
/* Disable button hover animations */
body.wayback-2011-theme button:hover,
body.wayback-2011-theme .yt-spec-button-shape-next:hover {
transform: none !important;
transition: none !important;
}
/* Remove elevation/shadow effects */
body.wayback-2011-theme * {
box-shadow: none !important;
filter: none !important;
backdrop-filter: none !important;
}
`;
// Inject styles into page head for better CSS priority
const styleElement = document.createElement('style');
styleElement.textContent = styles + '\n' + vintage2011CSS;
document.head.appendChild(styleElement);
// Also use GM_addStyle as backup
GM_addStyle(styles);
GM_addStyle(vintage2011CSS);
this.cssInjected = true;
OptimizedUtils.log('Styles injected successfully (including 2011 vintage theme)');
}
createMainInterface() {
if (this.elements.has('main')) return;
const mainDiv = document.createElement('div');
mainDiv.id = 'wayback-interface';
const selectedDate = GM_getValue('ytSelectedDate', this.videoManager.getDefaultDate());
const isActive = GM_getValue('ytActive', false);
mainDiv.innerHTML = `
<div id="wayback-header">
<div id="wayback-title">⏰ WayBackTube</div>
<button id="wayback-minimize" class="wayback-button small">–</button>
</div>
<div id="wayback-content">
<div class="wayback-controls">
<input type="date" id="wayback-date-input" class="wayback-input" value="${selectedDate}">
<button id="wayback-toggle" class="wayback-button">${isActive ? 'Disable' : 'Enable'}</button>
</div>
<div class="wayback-controls">
<button id="wayback-refresh" class="wayback-button">🔄 Refresh</button>
<button id="wayback-advance-date" class="wayback-button">📅 Advance Date</button>
<button id="wayback-vintage-toggle" class="wayback-button wayback-vintage-toggle">🕰️ 2011 Theme</button>
<button id="wayback-settings" class="wayback-button">⚙️</button>
</div>
<div id="wayback-stats" class="wayback-stats">
<div class="wayback-stat-row">
<span>Status:</span>
<span class="wayback-status ${isActive ? 'active' : 'error'}" id="wayback-status">
${isActive ? 'Active' : 'Inactive'}
</span>
</div>
<div class="wayback-stat-row">
<span>Date:</span>
<span id="wayback-current-date">${selectedDate}</span>
</div>
<div class="wayback-stat-row">
<span>Page:</span>
<span id="wayback-current-page">${OptimizedUtils.getCurrentPage()}</span>
</div>
<div class="wayback-stat-row">
<span>Clock:</span>
<span id="wayback-clock-display" style="font-family: monospace; color: #00ff00;">
${GM_getValue('ytTestClockEnabled', false) ? 'Day 0, 00:00:00' : 'Not running'}
</span>
</div>
</div>
<div id="wayback-api-section" style="display: none;">
<div class="wayback-controls">
<input type="password" id="wayback-api-key" class="wayback-input" placeholder="Enter YouTube API Key">
<button id="wayback-add-key" class="wayback-button">Add</button>
</div>
<div class="wayback-controls">
<button id="wayback-test-keys" class="wayback-button" style="width: 100%;">Test All API Keys</button>
</div>
<div id="wayback-key-stats"></div>
</div>
<!-- Subscriptions Management Section -->
<div class="wayback-section">
<div class="wayback-section-header">
<h4>📺 Channel Subscriptions</h4>
<span class="wayback-section-desc">Add channel names to load videos from</span>
</div>
<div class="wayback-controls">
<input type="text" id="wayback-subscription-input" class="wayback-input" placeholder="Enter channel name..." maxlength="50">
<button id="wayback-add-subscription" class="wayback-button">Add</button>
</div>
<div id="wayback-subscriptions-list" class="wayback-list">
<!-- Subscriptions will be populated here -->
</div>
<div class="wayback-controls">
<button id="wayback-clear-subscriptions" class="wayback-button secondary">Clear All</button>
<button id="wayback-load-homepage" class="wayback-button">Load Videos</button>
</div>
</div>
<!-- Search Terms Management Section -->
<div class="wayback-section">
<div class="wayback-section-header">
<h4>🔍 Search Terms</h4>
<span class="wayback-section-desc">Add search terms to discover content (memes, gaming, etc.)</span>
</div>
<div class="wayback-controls">
<input type="text" id="wayback-search-term-input" class="wayback-input" placeholder="Enter search term..." maxlength="50">
<button id="wayback-add-search-term" class="wayback-button">Add</button>
</div>
<div id="wayback-search-terms-list" class="wayback-list">
<!-- Search terms will be populated here -->
</div>
<div class="wayback-controls">
<button id="wayback-clear-search-terms" class="wayback-button secondary">Clear All</button>
<button id="wayback-load-search-videos" class="wayback-button">Load Videos</button>
</div>
</div>
</div>
`;
document.body.appendChild(mainDiv);
this.elements.set('main', mainDiv);
// Setup element references
this.elements.set('dateInput', OptimizedUtils.$('#wayback-date-input'));
this.elements.set('toggleButton', OptimizedUtils.$('#wayback-toggle'));
this.elements.set('refreshButton', OptimizedUtils.$('#wayback-refresh'));
this.elements.set('advanceDateButton', OptimizedUtils.$('#wayback-advance-date'));
this.elements.set('vintageToggleButton', OptimizedUtils.$('#wayback-vintage-toggle'));
this.elements.set('settingsButton', OptimizedUtils.$('#wayback-settings'));
this.elements.set('minimizeButton', OptimizedUtils.$('#wayback-minimize'));
this.elements.set('statusElement', OptimizedUtils.$('#wayback-status'));
this.elements.set('currentDate', OptimizedUtils.$('#wayback-current-date'));
this.elements.set('currentPage', OptimizedUtils.$('#wayback-current-page'));
this.elements.set('apiSection', OptimizedUtils.$('#wayback-api-section'));
this.elements.set('apiKeyInput', OptimizedUtils.$('#wayback-api-key'));
this.elements.set('addKeyButton', OptimizedUtils.$('#wayback-add-key'));
this.elements.set('testKeysButton', OptimizedUtils.$('#wayback-test-keys'));
this.elements.set('keyStatsDiv', OptimizedUtils.$('#wayback-key-stats'));
// Subscription management elements
this.elements.set('subscriptionInput', OptimizedUtils.$('#wayback-subscription-input'));
this.elements.set('addSubscriptionButton', OptimizedUtils.$('#wayback-add-subscription'));
this.elements.set('subscriptionsList', OptimizedUtils.$('#wayback-subscriptions-list'));
this.elements.set('clearSubscriptionsButton', OptimizedUtils.$('#wayback-clear-subscriptions'));
this.elements.set('loadHomepageButton', OptimizedUtils.$('#wayback-load-homepage'));
// Search terms management elements
this.elements.set('searchTermInput', OptimizedUtils.$('#wayback-search-term-input'));
this.elements.set('addSearchTermButton', OptimizedUtils.$('#wayback-add-search-term'));
this.elements.set('searchTermsList', OptimizedUtils.$('#wayback-search-terms-list'));
this.elements.set('clearSearchTermsButton', OptimizedUtils.$('#wayback-clear-search-terms'));
this.elements.set('loadSearchVideosButton', OptimizedUtils.$('#wayback-load-search-videos'));
// Initialize the UI with existing data
this.initializeUI();
OptimizedUtils.log('Main interface created');
}
setupEventListeners() {
const throttledRefresh = OptimizedUtils.throttle(() => this.handleRefresh(), 1000);
const debouncedDateChange = OptimizedUtils.debounce((date) => this.handleDateChange(date), 500);
// Main controls
this.addEventListenerSafe('dateInput', 'change', (e) => {
debouncedDateChange(e.target.value);
});
this.addEventListenerSafe('toggleButton', 'click', () => this.handleToggle());
this.addEventListenerSafe('refreshButton', 'click', throttledRefresh);
this.addEventListenerSafe('advanceDateButton', 'click', () => this.handleAdvanceDate());
this.addEventListenerSafe('vintageToggleButton', 'click', () => this.handleVintageToggle());
this.addEventListenerSafe('settingsButton', 'click', () => this.toggleApiSection());
this.addEventListenerSafe('minimizeButton', 'click', () => this.toggleMinimize());
// API management
this.addEventListenerSafe('addKeyButton', 'click', () => this.handleAddApiKey());
this.addEventListenerSafe('testKeysButton', 'click', () => this.handleTestApiKeys());
this.addEventListenerSafe('apiKeyInput', 'keypress', (e) => {
if (e.key === 'Enter') this.handleAddApiKey();
});
// Subscription management
this.addEventListenerSafe('addSubscriptionButton', 'click', () => this.handleAddSubscription());
this.addEventListenerSafe('subscriptionInput', 'keypress', (e) => {
if (e.key === 'Enter') this.handleAddSubscription();
});
this.addEventListenerSafe('clearSubscriptionsButton', 'click', () => this.handleClearSubscriptions());
this.addEventListenerSafe('loadHomepageButton', 'click', () => this.handleLoadHomepage());
// Search terms management
this.addEventListenerSafe('addSearchTermButton', 'click', () => this.handleAddSearchTerm());
this.addEventListenerSafe('searchTermInput', 'keypress', (e) => {
if (e.key === 'Enter') this.handleAddSearchTerm();
});
this.addEventListenerSafe('clearSearchTermsButton', 'click', () => this.handleClearSearchTerms());
this.addEventListenerSafe('loadSearchVideosButton', 'click', () => this.handleLoadSearchVideos());
// Event delegation for remove buttons
this.elements.get('main').addEventListener('click', (e) => {
if (e.target.classList.contains('wayback-remove-btn')) {
if (e.target.dataset.subscription) {
this.handleRemoveSubscription(e.target.dataset.subscription);
} else if (e.target.dataset.searchTerm) {
this.handleRemoveSearchTerm(e.target.dataset.searchTerm);
}
}
});
// URL change detection
let lastUrl = location.href;
const urlObserver = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
this.handleUrlChange();
}
});
urlObserver.observe(document, { subtree: true, childList: true });
this.observers.set('url', urlObserver);
OptimizedUtils.log('Event listeners setup complete');
}
addEventListenerSafe(elementKey, event, handler) {
const element = this.elements.get(elementKey);
if (element) {
element.addEventListener(event, handler);
// Store for cleanup
const key = `${elementKey}_${event}`;
if (!this.eventListeners.has(key)) {
this.eventListeners.set(key, []);
}
this.eventListeners.get(key).push({ element, event, handler });
}
}
handleDateChange(dateStr) {
if (this.videoManager.setTimeframe(dateStr)) {
this.updateInterface();
this.handleRefresh();
OptimizedUtils.log(`Date changed to: ${dateStr}`);
}
}
handleToggle() {
const isActive = GM_getValue('ytActive', false);
const newState = !isActive;
GM_setValue('ytActive', newState);
this.updateInterface();
if (newState) {
this.handleRefresh();
}
OptimizedUtils.log(`WayBackTube ${newState ? 'enabled' : 'disabled'}`);
}
async handleRefresh() {
if (!GM_getValue('ytActive', false)) return;
this.setLoadingState(true);
try {
// Clear caches
this.videoManager.clearCache();
this.cacheManager.cleanup();
// Wait a bit for page to stabilize
await OptimizedUtils.sleep(500);
// Refresh current page content
await this.refreshCurrentPage();
this.setLoadingState(false);
OptimizedUtils.log('Refresh completed');
} catch (error) {
this.setLoadingState(false);
OptimizedUtils.log('Refresh error:', error.message);
this.showError('Refresh failed. Please try again.');
}
}
handleAdvanceDate() {
// Force advance date by one day
const currentDate = new Date(this.videoManager.settings.date);
currentDate.setDate(currentDate.getDate() + 1);
const newDateString = currentDate.toISOString().split('T')[0];
this.videoManager.setDate(newDateString);
// Update the date input field
const dateInput = this.elements.get('dateInput');
if (dateInput) {
dateInput.value = newDateString;
}
// Update last rotation time to now
this.videoManager.settings.lastDateRotation = Date.now();
GM_setValue('ytLastDateRotation', Date.now());
this.showSuccess(`Date advanced to: ${newDateString}`);
OptimizedUtils.log(`🗓️ Date manually advanced to: ${newDateString}`);
// Refresh content with new date
if (GM_getValue('ytActive', false)) {
setTimeout(() => this.handleRefresh(), 500);
}
}
handleVintageToggle() {
// Let TimeMachineUI handle all vintage toggle logic to avoid conflicts
// This method is kept for compatibility but delegates to global function
if (window.handleGlobalVintageToggle) {
window.handleGlobalVintageToggle();
}
}
async refreshCurrentPage() {
const context = OptimizedUtils.getPageContext();
switch (context.page) {
case 'home':
await this.refreshHomepage();
break;
case 'video':
await this.refreshVideoPage(context.videoId);
break;
case 'channel':
await this.refreshChannelPage(context.channelId);
break;
case 'search':
await this.refreshSearchPage();
break;
default:
OptimizedUtils.log('Page type not supported for refresh:', context.page);
}
}
async refreshHomepage() {
OptimizedUtils.log('Refreshing homepage...');
// Get new videos
const videos = await this.videoManager.getHomepageVideos();
if (videos.length === 0) {
this.showWarning('No videos found for selected date range');
return;
}
// Replace homepage content
this.replaceHomepageVideos(videos);
}
async refreshVideoPage(videoId) {
if (!videoId) return;
OptimizedUtils.log('Refreshing video page recommendations...');
const videos = await this.videoManager.getRecommendationVideos(videoId);
this.replaceVideoPageRecommendations(videos);
}
async refreshChannelPage(channelId) {
if (!channelId) return;
OptimizedUtils.log('Refreshing channel page...');
// Get all cached videos for this channel, sorted by date
const videos = this.getAllCachedChannelVideos(channelId);
this.replaceChannelPageVideos(videos, channelId);
}
getAllCachedChannelVideos(channelId) {
// Get videos from all cached sources for this channel
const allVideos = [];
// Get from subscription cache
const subscriptionVideos = this.videoManager.videoCache.get('subscription_videos_only') || [];
const channelSubVideos = subscriptionVideos.filter(video => video.channelId === channelId);
allVideos.push(...channelSubVideos);
// Get from search term cache
const searchTermVideos = this.videoManager.videoCache.get('search_term_videos_only') || [];
const channelSearchVideos = searchTermVideos.filter(video => video.channelId === channelId);
allVideos.push(...channelSearchVideos);
// Get from watched videos cache
const watchedVideos = GM_getValue('wayback_watched_videos_cache', []);
const channelWatchedVideos = watchedVideos.filter(video => video.channelId === channelId);
allVideos.push(...channelWatchedVideos);
// Remove duplicates and sort by upload date (newest first)
const uniqueVideos = OptimizedUtils.removeDuplicates(allVideos);
return uniqueVideos.sort((a, b) => {
const dateA = new Date(a.publishedAt || a.snippet?.publishedAt || '2005-01-01');
const dateB = new Date(b.publishedAt || b.snippet?.publishedAt || '2005-01-01');
return dateB - dateA; // Newest first
});
}
async refreshSearchPage() {
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('search_query');
if (!query) return;
OptimizedUtils.log('Refreshing search results...');
const videos = await this.videoManager.searchVideos(query);
this.replaceSearchResults(videos);
}
replaceHomepageVideos(videos) {
// Implementation for replacing homepage videos
const containers = OptimizedUtils.$$('#contents ytd-rich-item-renderer, #contents ytd-video-renderer');
let videoIndex = 0;
containers.forEach((container, index) => {
if (videoIndex >= videos.length) return;
const video = videos[videoIndex];
this.replaceVideoElement(container, video);
videoIndex++;
});
OptimizedUtils.log(`Replaced ${Math.min(videoIndex, videos.length)} homepage videos`);
}
replaceVideoPageRecommendations(videos) {
const containers = OptimizedUtils.$$('#secondary ytd-compact-video-renderer');
videos.slice(0, containers.length).forEach((video, index) => {
if (containers[index]) {
this.replaceVideoElement(containers[index], video);
}
});
OptimizedUtils.log(`Replaced ${Math.min(videos.length, containers.length)} recommendation videos`);
}
replaceChannelPageVideos(videos, channelId) {
// Hide all channel content first (home and videos sections)
CONFIG.CHANNEL_PAGE_SELECTORS.forEach(selector => {
OptimizedUtils.$$(selector).forEach(element => {
element.style.display = 'none';
});
});
// Completely replace with our cached videos and search button
this.showChannelVideosWithSearchButton(videos, channelId);
}
showChannelVideosWithSearchButton(videos, channelId) {
// Create a container for cached videos from all sources
const channelContent = OptimizedUtils.$('#contents, #primary-inner');
if (!channelContent) return;
// Remove existing content
const existingContent = OptimizedUtils.$('#wayback-channel-content');
if (existingContent) {
existingContent.remove();
}
// Get channel name from first video or fallback
const channelName = videos.length > 0 ? (videos[0].channel || videos[0].snippet?.channelTitle || 'This Channel') : 'This Channel';
// Create new channel content with search button
const channelContainer = document.createElement('div');
channelContainer.id = 'wayback-channel-content';
channelContainer.innerHTML = `
<div style="padding: 20px; background: #f9f9f9; border-radius: 8px; margin: 20px 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0; color: #333;">
${channelName} Videos (${videos.length} cached)
</h3>
<button id="wayback-search-more-channel" class="wayback-button" style="
background: #ff4444; color: white; border: none; padding: 8px 16px;
border-radius: 4px; cursor: pointer; font-size: 14px;
">
🔍 Search More Videos
</button>
</div>
<div id="channel-video-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;">
${videos.length > 0 ? videos.map(video => this.createChannelVideoCard(video)).join('') : '<p style="color: #666; grid-column: 1/-1; text-align: center;">No cached videos found for this channel.</p>'}
</div>
</div>
`;
channelContent.insertBefore(channelContainer, channelContent.firstChild);
// Add click handler for search button
const searchButton = OptimizedUtils.$('#wayback-search-more-channel');
if (searchButton) {
searchButton.addEventListener('click', () => this.searchMoreChannelVideos(channelId, channelName));
}
}
createChannelVideoCard(video) {
const videoId = video.id?.videoId || video.id;
const snippet = video.snippet || video;
if (!videoId || !snippet) return '';
const thumbnailUrl = snippet.thumbnails?.medium?.url || snippet.thumbnails?.high?.url || snippet.thumbnails?.default?.url || snippet.thumbnail;
const publishedDate = new Date(snippet.publishedAt || video.publishedAt || '2005-01-01');
const title = snippet.title || video.title || 'Unknown Title';
const description = snippet.description || video.description || '';
const viewCount = video.statistics?.viewCount || video.viewCount || 'Unknown views';
return `
<div class="wayback-channel-video-card" style="background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s;"
onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
<a href="/watch?v=${videoId}" style="display: block; text-decoration: none; color: inherit;" onclick="window.waybackTubeManager.cacheWatchedVideo('${videoId}')">
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<img src="${thumbnailUrl}" alt="${title}"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;">
</div>
<div style="padding: 12px;">
<h4 style="margin: 0 0 8px 0; font-size: 14px; line-height: 1.3; color: #333; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
${OptimizedUtils.cleanTitle(title)}
</h4>
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">
${this.formatRelativeTime(publishedDate)}
</div>
<div style="font-size: 11px; color: #888;">
${typeof viewCount === 'number' ? this.formatViewCount(viewCount) : viewCount}
</div>
${description ? `<p style="font-size: 11px; color: #999; margin: 8px 0 0 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">${description.substring(0, 100)}...</p>` : ''}
</div>
</a>
</div>
`;
}
async searchMoreChannelVideos(channelId, channelName) {
const searchButton = OptimizedUtils.$('#wayback-search-more-channel');
if (searchButton) {
if (searchButton) searchButton.textContent = '🔍 Searching...';
searchButton.disabled = true;
}
try {
// Search for more videos from this channel using current date constraints
const maxDate = new Date(this.settings.date);
const newVideos = await this.apiManager.getChannelVideos(channelId, {
publishedBefore: maxDate,
maxResults: 50,
order: 'date'
});
if (newVideos && newVideos.length > 0) {
// Cache the new videos for this channel
this.cacheChannelVideos(channelId, newVideos);
// Refresh the channel page with updated cache
const allVideos = this.getAllCachedChannelVideos(channelId);
this.showChannelVideosWithSearchButton(allVideos, channelId);
this.showSuccess(`Found ${newVideos.length} more videos for ${channelName}!`);
} else {
this.showWarning(`No additional videos found for ${channelName} before ${maxDate.toDateString()}`);
}
} catch (error) {
OptimizedUtils.log('Error searching for more channel videos:', error);
this.showError('Failed to search for more videos. Check your API keys.');
}
if (searchButton) {
if (searchButton) searchButton.textContent = '🔍 Search More Videos';
searchButton.disabled = false;
}
}
cacheChannelVideos(channelId, videos) {
// Add videos to watched cache for persistence
const existingWatchedVideos = GM_getValue('wayback_watched_videos_cache', []);
const videosToCache = videos.map(video => ({
id: video.id?.videoId || video.id,
title: video.snippet?.title || 'Unknown Title',
channel: video.snippet?.channelTitle || 'Unknown Channel',
channelId: video.snippet?.channelId || channelId,
thumbnail: video.snippet?.thumbnails?.medium?.url || video.snippet?.thumbnails?.high?.url || video.snippet?.thumbnails?.default?.url || '',
publishedAt: video.snippet?.publishedAt || new Date().toISOString(),
description: video.snippet?.description || '',
viewCount: video.statistics?.viewCount || 'Unknown',
cachedAt: new Date().toISOString(),
source: 'channel_search'
}));
// Merge with existing and remove duplicates
const allVideos = [...existingWatchedVideos, ...videosToCache];
const uniqueVideos = OptimizedUtils.removeDuplicates(allVideos);
// Limit cache size to prevent storage issues
const maxCacheSize = 5000;
if (uniqueVideos.length > maxCacheSize) {
uniqueVideos.sort((a, b) => new Date(b.cachedAt) - new Date(a.cachedAt));
uniqueVideos.splice(maxCacheSize);
}
GM_setValue('wayback_watched_videos_cache', uniqueVideos);
OptimizedUtils.log(`Cached ${videosToCache.length} new videos for channel ${channelId}`);
}
replaceSearchResults(videos) {
const containers = OptimizedUtils.$$('#contents ytd-video-renderer');
videos.slice(0, containers.length).forEach((video, index) => {
if (containers[index]) {
this.replaceVideoElement(containers[index], video);
}
});
OptimizedUtils.log(`Replaced ${Math.min(videos.length, containers.length)} search results`);
}
replaceVideoElement(container, video) {
try {
const videoId = video.id?.videoId || video.id;
const snippet = video.snippet;
if (!videoId || !snippet) return;
// Update thumbnail
const thumbnailImg = container.querySelector('img');
if (thumbnailImg) {
const thumbnailUrl = snippet.thumbnails?.high?.url ||
snippet.thumbnails?.medium?.url ||
snippet.thumbnails?.default?.url;
if (thumbnailUrl) {
thumbnailImg.src = thumbnailUrl;
thumbnailImg.alt = snippet.title;
}
}
// Update title
const titleElement = container.querySelector('#video-title, .ytd-video-meta-block #video-title, a[aria-describedby]');
if (titleElement) {
if (titleElement) titleElement.textContent = OptimizedUtils.cleanTitle(snippet.title);
titleElement.title = snippet.title;
// Update href
if (titleElement.tagName === 'A') {
titleElement.href = `/watch?v=${videoId}`;
}
}
// Update channel name
const channelElement = container.querySelector('.ytd-channel-name a, #channel-name a, #text.ytd-channel-name');
if (channelElement) {
if (channelElement) channelElement.textContent = snippet.channelTitle;
if (channelElement.href) {
channelElement.href = `/channel/${snippet.channelId}`;
}
}
// Update published date
const dateElement = container.querySelector('#published-time-text, .ytd-video-meta-block #published-time-text');
if (dateElement) {
const publishedDate = new Date(snippet.publishedAt);
if (dateElement) dateElement.textContent = this.formatRelativeTime(publishedDate);
}
// Update view count if available
const viewCountElement = container.querySelector('#metadata-line span:first-child, .ytd-video-meta-block span:first-child');
if (viewCountElement && video.statistics?.viewCount) {
if (viewCountElement) viewCountElement.textContent = this.formatViewCount(video.statistics.viewCount);
}
// Update links
const linkElements = container.querySelectorAll('a[href*="/watch"]');
linkElements.forEach(link => {
link.href = `/watch?v=${videoId}`;
});
// Mark as replaced
container.setAttribute('data-wayback-replaced', 'true');
container.setAttribute('data-wayback-video-id', videoId);
} catch (error) {
OptimizedUtils.log('Error replacing video element:', error.message);
}
}
showChannelVideosForPeriod(videos) {
// Create a container for period videos
const channelContent = OptimizedUtils.$('#contents, #primary-inner');
if (!channelContent) return;
// Remove existing period content
const existingPeriodContent = OptimizedUtils.$('#wayback-period-content');
if (existingPeriodContent) {
existingPeriodContent.remove();
}
if (videos.length === 0) {
this.showNoVideosMessage(channelContent);
return;
}
// Create new period content
const periodContainer = document.createElement('div');
periodContainer.id = 'wayback-period-content';
periodContainer.innerHTML = `
<div style="padding: 20px; background: #f9f9f9; border-radius: 8px; margin: 20px 0;">
<h3 style="margin: 0 0 15px 0; color: #333;">
Videos from ${OptimizedUtils.formatDate(this.videoManager.currentTimeframe.selectedDate, 'MMMM YYYY')}
</h3>
<div id="period-video-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 15px;">
${videos.map(video => this.createVideoCard(video)).join('')}
</div>
</div>
`;
channelContent.insertBefore(periodContainer, channelContent.firstChild);
}
createVideoCard(video) {
const videoId = video.id?.videoId || video.id;
const snippet = video.snippet;
if (!videoId || !snippet) return '';
const thumbnailUrl = snippet.thumbnails?.medium?.url || snippet.thumbnails?.default?.url;
const publishedDate = new Date(snippet.publishedAt);
return `
<div class="wayback-video-card" style="background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
<a href="/watch?v=${videoId}" style="display: block; text-decoration: none; color: inherit;">
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<img src="${thumbnailUrl}" alt="${snippet.title}"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;">
</div>
<div style="padding: 12px;">
<h4 style="margin: 0 0 8px 0; font-size: 14px; line-height: 1.3; color: #333; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">
${OptimizedUtils.cleanTitle(snippet.title)}
</h4>
<div style="font-size: 12px; color: #666;">
${this.formatRelativeTime(publishedDate)}
</div>
</div>
</a>
</div>
`;
}
showNoVideosMessage(container) {
const messageDiv = document.createElement('div');
messageDiv.id = 'wayback-period-content';
messageDiv.innerHTML = `
<div style="padding: 40px; text-align: center; background: #f9f9f9; border-radius: 8px; margin: 20px 0;">
<h3 style="margin: 0 0 10px 0; color: #666;">No Videos Found</h3>
<p style="margin: 0; color: #888;">
This channel had no videos published in ${OptimizedUtils.formatDate(this.videoManager.currentTimeframe.selectedDate, 'MMMM YYYY')}.
<br>Try selecting a different date.
</p>
</div>
`;
container.insertBefore(messageDiv, container.firstChild);
}
formatRelativeTime(date) {
const now = new Date();
const diffMs = now - date;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const diffMonths = Math.floor(diffDays / 30);
const diffYears = Math.floor(diffDays / 365);
if (diffYears > 0) return `${diffYears} year${diffYears > 1 ? 's' : ''} ago`;
if (diffMonths > 0) return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
if (diffDays > 0) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
return 'Today';
}
formatViewCount(count) {
const num = parseInt(count);
if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M views`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K views`;
return `${num} views`;
}
handleAddApiKey() {
const input = this.elements.get('apiKeyInput');
const key = input.value.trim();
if (this.apiManager.addKey(key)) {
input.value = '';
this.updateApiStats();
this.showSuccess('API key added successfully!');
} else {
this.showError('Invalid API key or key already exists');
}
}
async handleTestApiKeys() {
const testButton = this.elements.get('testKeysButton');
const keyStatsDiv = this.elements.get('keyStatsDiv');
if (!testButton || !keyStatsDiv) return;
if (this.apiManager.keys.length === 0) {
this.showError('No API keys to test. Add some first.');
return;
}
// Disable button and show loading
testButton.disabled = true;
if (testButton) testButton.textContent = 'Testing...';
if (keyStatsDiv) keyStatsDiv.textContent = '';
const testingDiv = document.createElement('div');
testingDiv.style.color = '#ffa500';
if (testingDiv) testingDiv.textContent = 'Testing API keys...';
keyStatsDiv.appendChild(testingDiv);
try {
const results = await this.apiManager.testAllKeys();
// Display results
let resultHtml = '<div style="margin-top: 10px;"><h4>API Key Test Results:</h4>';
results.forEach((result, index) => {
const statusColor = result.result === 'Working perfectly' ? '#4CAF50' : '#f44336';
resultHtml += `
<div style="margin: 5px 0; padding: 5px; background: rgba(255,255,255,0.1); border-radius: 4px;">
<div style="font-size: 12px;">${result.keyPreview}</div>
<div style="color: ${statusColor}; font-size: 11px;">${result.result}</div>
</div>
`;
});
resultHtml += '</div>';
if (keyStatsDiv) keyStatsDiv.textContent = '';
const resultDiv = document.createElement('div');
if (resultDiv) resultDiv.textContent = resultHtml.replace(/<[^>]*>/g, ''); // Strip HTML tags
keyStatsDiv.appendChild(resultDiv);
this.showSuccess(`Tested ${results.length} API keys successfully!`);
} catch (error) {
if (keyStatsDiv) keyStatsDiv.textContent = '';
const errorDiv = document.createElement('div');
errorDiv.style.color = '#f44336';
if (errorDiv) errorDiv.textContent = `Error testing keys: ${error.message}`;
keyStatsDiv.appendChild(errorDiv);
this.showError('Failed to test API keys');
} finally {
// Re-enable button
testButton.disabled = false;
testButton.textContent = 'Test All API Keys';
}
}
toggleApiSection() {
const section = this.elements.get('apiSection');
const isVisible = section.style.display !== 'none';
section.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
this.updateApiStats();
}
}
toggleMinimize() {
const mainInterface = this.elements.get('main');
const content = OptimizedUtils.$('#wayback-content');
const minimizeButton = this.elements.get('minimizeButton');
const isMinimized = mainInterface.classList.contains('minimized');
if (isMinimized) {
mainInterface.classList.remove('minimized');
content.style.display = 'block';
minimizeButton.textContent = '–';
} else {
mainInterface.classList.add('minimized');
content.style.display = 'none';
minimizeButton.textContent = '+';
}
}
updateInterface() {
const isActive = GM_getValue('ytActive', false);
const selectedDate = GM_getValue('ytSelectedDate', this.videoManager.getDefaultDate());
const currentPage = OptimizedUtils.getCurrentPage();
// Update status
const statusElement = this.elements.get('statusElement');
const toggleButton = this.elements.get('toggleButton');
if (statusElement) {
statusElement.className = `wayback-status ${isActive ? 'active' : 'error'}`;
statusElement.textContent = isActive ? 'Active' : 'Inactive';
}
if (toggleButton) {
toggleButton.textContent = isActive ? 'Disable' : 'Enable';
}
// Update date display
const currentDateElement = this.elements.get('currentDate');
if (currentDateElement) {
currentDateElement.textContent = selectedDate;
}
// Update current page
const currentPageElement = this.elements.get('currentPage');
if (currentPageElement) {
currentPageElement.textContent = currentPage;
}
// Update date input
const dateInput = this.elements.get('dateInput');
if (dateInput && dateInput.value !== selectedDate) {
dateInput.value = selectedDate;
}
}
updateApiStats() {
const statsDiv = this.elements.get('keyStatsDiv');
if (!statsDiv) return;
const stats = this.apiManager.getKeyStats();
const cacheStats = this.cacheManager.getStats();
statsDiv.innerHTML = `
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.2); font-size: 11px;">
<div class="wayback-stat-row">
<span>API Keys:</span>
<span>${stats.totalKeys} (Current: ${stats.currentKey}/${stats.totalKeys})</span>
</div>
<div class="wayback-stat-row">
<span>Cache Hit Rate:</span>
<span>${cacheStats.hitRate}</span>
</div>
<div class="wayback-stat-row">
<span>Memory Cache:</span>
<span>${cacheStats.memorySize} items</span>
</div>
</div>
`;
}
setLoadingState(isLoading) {
const refreshButton = this.elements.get('refreshButton');
const statusElement = this.elements.get('statusElement');
if (refreshButton) {
refreshButton.disabled = isLoading;
refreshButton.textContent = isLoading ? '🔄 Loading...' : '🔄 Refresh';
}
if (statusElement && isLoading) {
statusElement.className = 'wayback-status loading';
statusElement.textContent = 'Loading';
} else if (statusElement && !isLoading) {
const isActive = GM_getValue('ytActive', false);
statusElement.className = `wayback-status ${isActive ? 'active' : 'error'}`;
statusElement.textContent = isActive ? 'Active' : 'Inactive';
}
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showError(message) {
this.showNotification(message, 'error');
}
showWarning(message) {
this.showNotification(message, 'warning');
}
showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `wayback-notification wayback-notification-${type}`;
notification.innerHTML = `
<div style="
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10001;
background: ${type === 'error' ? '#f44336' : type === 'warning' ? '#ff9800' : '#4caf50'};
color: white;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
font-size: 14px;
max-width: 400px;
text-align: center;
animation: wayback-notification-slide 0.3s ease;
">
${message}
</div>
`;
document.body.appendChild(notification);
// Auto remove after 3 seconds
setTimeout(() => {
notification.remove();
}, 3000);
}
startObservers() {
// Content observer for dynamic page changes
const contentObserver = new MutationObserver(OptimizedUtils.throttle(() => {
if (GM_getValue('ytActive', false)) {
this.handleContentChange();
}
}, 1000));
const targetNode = document.body;
if (targetNode) {
contentObserver.observe(targetNode, {
childList: true,
subtree: true,
attributes: false
});
this.observers.set('content', contentObserver);
}
OptimizedUtils.log('Content observers started');
}
handleContentChange() {
// Aggressive content blocking
this.blockModernContent();
this.updateCurrentPageContent();
}
blockModernContent() {
// Block Shorts
CONFIG.SHORTS_SELECTORS.forEach(selector => {
OptimizedUtils.$$(selector).forEach(element => {
if (!element.hasAttribute('data-wayback-blocked')) {
element.style.display = 'none';
element.setAttribute('data-wayback-blocked', 'true');
}
});
});
// Block modern channel content and replace with cached videos
if (OptimizedUtils.getCurrentPage() === 'channel') {
CONFIG.CHANNEL_PAGE_SELECTORS.forEach(selector => {
OptimizedUtils.$$(selector).forEach(element => {
if (!element.hasAttribute('data-wayback-blocked')) {
element.style.visibility = 'hidden';
element.style.height = '0';
element.style.overflow = 'hidden';
element.setAttribute('data-wayback-blocked', 'true');
}
});
});
// Replace with cached channel videos
this.replaceChannelPageWithCache();
}
}
async updateCurrentPageContent() {
const context = OptimizedUtils.getPageContext();
// Update page info in UI
const currentPageElement = this.elements.get('currentPage');
if (currentPageElement) {
currentPageElement.textContent = context.page;
}
}
handleUrlChange() {
OptimizedUtils.log('URL changed, updating content...');
this.updateInterface();
if (GM_getValue('ytActive', false)) {
// Small delay to let page load
setTimeout(() => {
this.handleContentChange();
}, 1000);
}
}
cleanup() {
// Remove event listeners
this.eventListeners.forEach((listeners, key) => {
listeners.forEach(({ element, event, handler }) => {
element.removeEventListener(event, handler);
});
});
this.eventListeners.clear();
// Disconnect observers
this.observers.forEach(observer => {
observer.disconnect();
});
this.observers.clear();
// Remove elements
this.elements.forEach((element, key) => {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
});
this.elements.clear();
OptimizedUtils.log('UI Manager cleaned up');
}
// === SUBSCRIPTION MANAGEMENT HANDLERS ===
handleAddSubscription() {
const input = this.elements.get('subscriptionInput');
const channelName = input.value.trim();
if (!channelName) return;
const subscriptions = this.loadSubscriptions();
if (!subscriptions.find(sub => sub.name.toLowerCase() === channelName.toLowerCase())) {
subscriptions.push({ name: channelName, id: null });
this.saveSubscriptions(subscriptions);
this.updateSubscriptionsList();
input.value = '';
OptimizedUtils.log(`Added subscription: ${channelName}`);
} else {
OptimizedUtils.log(`Subscription already exists: ${channelName}`);
}
}
handleRemoveSubscription(channelName) {
const subscriptions = this.loadSubscriptions();
const index = subscriptions.findIndex(sub => sub.name === channelName);
if (index > -1) {
subscriptions.splice(index, 1);
this.saveSubscriptions(subscriptions);
this.updateSubscriptionsList();
OptimizedUtils.log(`Removed subscription: ${channelName}`);
}
}
handleClearSubscriptions() {
this.saveSubscriptions([]);
this.updateSubscriptionsList();
OptimizedUtils.log('Cleared all subscriptions');
}
async handleLoadHomepage() {
if (!GM_getValue('ytActive', false)) {
OptimizedUtils.log('WayBackTube is not active');
return;
}
const subscriptions = this.loadSubscriptions();
const searchTerms = this.loadSearchTerms();
if (subscriptions.length === 0 && searchTerms.length === 0) {
OptimizedUtils.log('No subscriptions or search terms to load from');
return;
}
try {
this.setLoadingState(true);
await this.refreshHomepage();
this.setLoadingState(false);
OptimizedUtils.log('Homepage videos loaded successfully');
} catch (error) {
this.setLoadingState(false);
OptimizedUtils.log('Failed to load homepage videos:', error);
}
}
// === SEARCH TERMS MANAGEMENT HANDLERS ===
handleAddSearchTerm() {
const input = this.elements.get('searchTermInput');
const searchTerm = input.value.trim();
if (!searchTerm) return;
const searchTerms = this.loadSearchTerms();
if (!searchTerms.includes(searchTerm.toLowerCase())) {
searchTerms.push(searchTerm.toLowerCase());
this.saveSearchTerms(searchTerms);
this.updateSearchTermsList();
input.value = '';
OptimizedUtils.log(`Added search term: ${searchTerm}`);
} else {
OptimizedUtils.log(`Search term already exists: ${searchTerm}`);
}
}
handleRemoveSearchTerm(searchTerm) {
const searchTerms = this.loadSearchTerms();
const index = searchTerms.indexOf(searchTerm);
if (index > -1) {
searchTerms.splice(index, 1);
this.saveSearchTerms(searchTerms);
this.updateSearchTermsList();
OptimizedUtils.log(`Removed search term: ${searchTerm}`);
}
}
handleClearSearchTerms() {
this.saveSearchTerms([]);
this.updateSearchTermsList();
OptimizedUtils.log('Cleared all search terms');
}
async handleLoadSearchVideos() {
if (!GM_getValue('ytActive', false)) {
OptimizedUtils.log('WayBackTube is not active');
return;
}
const searchTerms = this.loadSearchTerms();
if (searchTerms.length === 0) {
OptimizedUtils.log('No search terms to load videos from');
return;
}
try {
this.setLoadingState(true);
await this.refreshHomepage();
this.setLoadingState(false);
OptimizedUtils.log('Search term videos loaded successfully');
} catch (error) {
this.setLoadingState(false);
OptimizedUtils.log('Failed to load search term videos:', error);
}
}
// === DATA MANAGEMENT HELPERS ===
loadSubscriptions() {
const saved = GM_getValue('wayback_subscriptions', '[]');
try {
const subscriptions = JSON.parse(saved);
return subscriptions.length > 0 ? subscriptions : this.getDefaultSubscriptions();
} catch {
return this.getDefaultSubscriptions();
}
}
saveSubscriptions(subscriptions) {
GM_setValue('wayback_subscriptions', JSON.stringify(subscriptions));
}
getDefaultSubscriptions() {
return [
{ name: 'PewDiePie', id: 'UC-lHJZR3Gqxm24_Vd_AJ5Yw' },
{ name: 'Markiplier', id: 'UC7_YxT-KID8kRbqZo7MyscQ' },
{ name: 'Jacksepticeye', id: 'UCYzPXprvl5Y-Sf0g4vX-m6g' }
];
}
loadSearchTerms() {
const saved = GM_getValue('wayback_search_terms', '[]');
try {
const searchTerms = JSON.parse(saved);
return searchTerms.length > 0 ? searchTerms : this.getDefaultSearchTerms();
} catch {
return this.getDefaultSearchTerms();
}
}
saveSearchTerms(searchTerms) {
GM_setValue('wayback_search_terms', JSON.stringify(searchTerms));
}
getDefaultSearchTerms() {
return ['memes', 'gaming', 'funny', 'music', 'tutorial'];
}
// === UI UPDATE HELPERS ===
updateSubscriptionsList() {
const container = this.elements.get('subscriptionsList');
if (!container) return;
const subscriptions = this.loadSubscriptions();
container.innerHTML = subscriptions.map(sub => `
<div class="wayback-list-item">
<span class="wayback-item-text">${sub.name}</span>
<button class="wayback-remove-btn" data-subscription="${sub.name}">×</button>
</div>
`).join('');
}
updateSearchTermsList() {
const container = this.elements.get('searchTermsList');
if (!container) return;
const searchTerms = this.loadSearchTerms();
container.innerHTML = searchTerms.map(term => `
<div class="wayback-list-item">
<span class="wayback-item-text">${term}</span>
<button class="wayback-remove-btn" data-search-term="${term}">×</button>
</div>
`).join('');
}
// Initialize the UI with existing data
initializeUI() {
this.updateSubscriptionsList();
this.updateSearchTermsList();
this.initializeVintageTheme();
}
initializeVintageTheme() {
const isVintageActive = GM_getValue('ytVintage2011Theme', false);
const button = this.elements.get('vintageToggleButton');
// Apply saved theme state
if (isVintageActive) {
document.body.classList.add('wayback-2011-theme');
}
// Update button state
if (button) {
button.textContent = isVintageActive ? '🕰️ Modern Theme' : '🕰️ 2011 Theme';
if (isVintageActive) {
button.classList.add('wayback-vintage-active');
}
}
OptimizedUtils.log(`2011 Vintage Theme initialized: ${isVintageActive ? 'active' : 'inactive'}`);
}
}
// === COMPLETE MAIN APPLICATION CLASS ===
class WayBackTubeOptimized {
constructor() {
// Settings and state MUST be initialized first
const now = Date.now();
this.settings = {
active: GM_getValue('ytActive', false),
uiVisible: GM_getValue('ytTimeMachineUIVisible', true),
date: GM_getValue('ytSelectedDate', '2012-01-01'),
lastDateRotation: GM_getValue('ytLastDateRotation', now - (25 * 60 * 60 * 1000)), // Default to 25 hours ago to trigger rotation
dateRotationEnabled: GM_getValue('ytDateRotationEnabled', true)
};
// Initialize all managers with complete functionality
this.apiManager = new OptimizedAPIManager();
this.subscriptionManager = new SubscriptionManager();
this.feedChipHider = new FeedChipHider();
this.searchEnhancement = new OptimizedSearchEnhancement(this);
this.tabHider = new TabHider();
this.maxDate = new Date(this.settings.date);
// FIXED: Use persistent cache manager instead of Map()
this.videoCache = new PersistentVideoCache();
this.stats = {
processed: 0,
filtered: 0,
apiCalls: 0,
cacheHits: 0
};
// Initialize components
this.searchManager = new SearchManager(this.apiManager, this.maxDate);
this.uiManager = new EnhancedUIManager(this);
this.isInitialized = false;
this.autoRefreshTimer = null;
this.filterTimer = null;
this.isProcessing = false;
this.init();
}
async init() {
if (this.isInitialized) return;
try {
OptimizedUtils.log('Initializing Complete WayBackTube...');
// Check for date rotation on startup
this.checkDateRotation();
// Wait for page to be ready
await OptimizedUtils.waitFor(() => document.readyState === 'complete', 15000);
// Set up channel page monitoring
this.setupChannelPageMonitoring();
// Start filtering and content replacement
this.startFiltering();
this.startHomepageReplacement();
this.startVideoPageEnhancement();
this.tabHider.init();
// Auto-load if enabled and on homepage
if (CONFIG.autoLoadOnHomepage && this.isHomePage() && this.settings.active) {
setTimeout(() => {
this.loadAllVideos(false); // Don't refresh, use cache if available
}, CONFIG.autoLoadDelay);
}
// Setup auto-refresh
this.setupAutoRefresh();
// Setup date rotation check (every hour)
this.setupDateRotationTimer();
this.isInitialized = true;
OptimizedUtils.log('Complete WayBackTube initialized successfully');
} catch (error) {
OptimizedUtils.log('Initialization error:', error.message);
console.error('WayBackTube initialization failed:', error);
}
}
// === TIME MACHINE CONTROLS ===
setDate(dateString) {
this.settings.date = dateString;
this.maxDate = new Date(dateString);
this.searchManager.updateMaxDate(this.maxDate);
GM_setValue('ytSelectedDate', dateString);
OptimizedUtils.log(`Date set to: ${dateString}`);
// Clear caches and reload content
this.apiManager.clearCache();
this.videoCache.clear(); // Also clear persistent video cache
if (this.settings.active) {
this.loadAllVideos(true); // Force refresh after clearing cache
}
}
checkDateRotation() {
if (!this.settings.dateRotationEnabled) {
OptimizedUtils.log('Date rotation is disabled');
return;
}
const now = Date.now();
const lastRotation = this.settings.lastDateRotation;
const timeSinceLastRotation = now - lastRotation;
const oneDayMs = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const hoursElapsed = timeSinceLastRotation / (60 * 60 * 1000);
OptimizedUtils.log(`Date rotation check: ${hoursElapsed.toFixed(1)} hours since last rotation (need 24+)`);
if (timeSinceLastRotation >= oneDayMs) {
// Advance the date by one day
const currentDate = new Date(this.settings.date);
currentDate.setDate(currentDate.getDate() + 1);
const newDateString = currentDate.toISOString().split('T')[0];
this.setDate(newDateString);
// Update last rotation time
this.settings.lastDateRotation = now;
GM_setValue('ytLastDateRotation', now);
OptimizedUtils.log(`🗓️ Date automatically rotated to: ${newDateString} (simulating daily uploads)`);
// Clear cache and refresh content to show "new" videos for the day
this.apiManager.clearCache();
this.videoCache.clear(); // Also clear persistent video cache
if (this.settings.active) {
setTimeout(() => {
this.loadAllVideos(true); // Force refresh for new daily content
}, 1000);
}
}
}
setupDateRotationTimer() {
// Check for date rotation every hour
setInterval(() => {
this.checkDateRotation();
}, 60 * 60 * 1000); // 1 hour
// Setup fast test clock that simulates 24 hours in 2 minutes for testing
this.setupTestClock();
}
setupTestClock() {
// Real-time clock: actual 24 hours = 24 hours, new videos every 4 real hours
const testClockEnabled = GM_getValue('wayback_persistent_clock_enabled', false);
if (!testClockEnabled) return;
// Store clock data with version-independent keys to persist across updates
const startTime = GM_getValue('wayback_persistent_clock_start', Date.now());
const lastVideoCheck = GM_getValue('wayback_persistent_last_video_check', 0);
// Save initial start time to version-independent storage if not already set
if (!GM_getValue('wayback_persistent_clock_start')) {
GM_setValue('wayback_persistent_clock_start', startTime);
}
this.testClockTimer = setInterval(() => {
const now = Date.now();
const timeElapsed = now - startTime;
// Calculate hours elapsed since start
const hoursElapsed = timeElapsed / (1000 * 60 * 60); // Real hours
const currentHour = Math.floor(hoursElapsed % 24); // 0-23 hour format
// Check for 4-hour intervals for new videos (every 4 real hours)
const currentInterval = Math.floor(hoursElapsed / 4);
const lastInterval = GM_getValue('wayback_persistent_last_interval', -1);
if (currentInterval !== lastInterval && currentInterval > 0) {
GM_setValue('wayback_persistent_last_interval', currentInterval);
OptimizedUtils.log(`🕐 4-hour mark reached (${hoursElapsed.toFixed(1)}h total) - New videos available!`);
// Clear video cache to simulate new uploads - but only when clock advances
this.apiManager.clearCache();
this.videoCache.clear(); // Clear persistent video cache
if (this.settings.active) {
OptimizedUtils.log('Clock advanced - reloading videos with fresh API calls');
this.loadAllVideos(true); // Force refresh for new content
}
}
// Check for 24-hour date rotation (every 24 real hours)
const daysSinceStart = Math.floor(hoursElapsed / 24);
const lastRotationDay = GM_getValue('wayback_persistent_last_rotation_day', -1);
if (daysSinceStart > lastRotationDay && daysSinceStart > 0) {
GM_setValue('wayback_persistent_last_rotation_day', daysSinceStart);
// Advance the date by one day
const currentDate = new Date(this.settings.date);
currentDate.setDate(currentDate.getDate() + 1);
const newDateString = currentDate.toISOString().split('T')[0];
this.setDate(newDateString);
OptimizedUtils.log(`🗓️ REAL CLOCK: Date advanced to ${newDateString} (Day ${daysSinceStart})`);
}
// Update UI with real time
this.updateTestClockDisplay(hoursElapsed);
}, 1000); // Update every second for seconds display
}
updateTestClockDisplay(totalHours) {
const days = Math.floor(totalHours / 24);
const hours = Math.floor(totalHours % 24);
const minutes = Math.floor((totalHours - Math.floor(totalHours)) * 60);
const seconds = Math.floor(((totalHours - Math.floor(totalHours)) * 60 - minutes) * 60);
const timeString = `Day ${days}, ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
// Update UI clock display instead of floating overlay
const clockElement = document.getElementById('wayback-clock-display');
if (clockElement) {
clockElement.textContent = timeString;
}
// Also update the alternative UI if it exists
const tmClockElement = document.getElementById('tm-clock-display');
if (tmClockElement) {
tmClockElement.textContent = timeString;
}
}
toggleTestClock(enabled) {
GM_setValue('wayback_persistent_clock_enabled', enabled);
if (enabled) {
// Initialize start time if not set
if (!GM_getValue('wayback_persistent_clock_start')) {
GM_setValue('wayback_persistent_clock_start', Date.now());
}
this.setupTestClock();
OptimizedUtils.log('🕐 Real-time clock enabled: 24 real hours = 1 day, new videos every 4 real hours');
} else {
if (this.testClockTimer) {
clearInterval(this.testClockTimer);
this.testClockTimer = null;
}
// Clear clock display in UI
const clockElement = document.getElementById('wayback-clock-display');
if (clockElement) {
clockElement.textContent = 'Clock stopped';
}
OptimizedUtils.log('🕐 Real-time clock disabled');
}
}
enableDateRotation(enabled) {
this.settings.dateRotationEnabled = enabled;
GM_setValue('ytDateRotationEnabled', enabled);
OptimizedUtils.log(`Date rotation ${enabled ? 'enabled' : 'disabled'}`);
}
setRandomDate() {
const startDate = new Date('2005-04-23'); // YouTube founded
const endDate = new Date('2018-12-31'); // Pre-modern era
const randomTime = startDate.getTime() + Math.random() * (endDate.getTime() - startDate.getTime());
const randomDate = new Date(randomTime);
this.setDate(randomDate.toISOString().split('T')[0]);
}
getDateString() {
return this.settings.date;
}
toggle() {
this.settings.active = !this.settings.active;
GM_setValue('ytActive', this.settings.active);
OptimizedUtils.log(`Time Machine ${this.settings.active ? 'enabled' : 'disabled'}`);
if (this.settings.active) {
// Only load if we don't have cached videos
const hasSubscriptionCache = (this.videoCache.get('subscription_videos_only') || []).length > 0;
const hasSearchTermCache = (this.videoCache.get('search_term_videos_only') || []).length > 0;
const searchTerms = this.uiManager.getSearchTerms();
const subscriptions = this.subscriptionManager.getSubscriptions();
const needsLoading = (subscriptions.length > 0 && !hasSubscriptionCache) ||
(searchTerms.length > 0 && !hasSearchTermCache);
if (needsLoading) {
OptimizedUtils.log('Loading videos for first time activation...');
this.loadAllVideos(false); // Don't refresh, use cache if available
} else {
OptimizedUtils.log('Using existing cached videos - no API calls needed');
}
}
}
// === SERIES ADVANCEMENT FUNCTIONALITY ===
async findNextEpisodeInSeries(currentVideoTitle, currentChannelName) {
// Extract episode patterns from current video title
const episodePatterns = [
/episode\s*(\d+)/i,
/ep\.?\s*(\d+)/i,
/part\s*(\d+)/i,
/#(\d+)/i,
/(\d+)(?=\s*[-–—]\s*)/i, // Number followed by dash
/(\d+)(?=\s*$)/i // Number at end of title
];
let episodeNumber = null;
let patternUsed = null;
let baseTitle = currentVideoTitle;
// Try to find episode number
for (const pattern of episodePatterns) {
const match = currentVideoTitle.match(pattern);
if (match) {
episodeNumber = parseInt(match[1]);
patternUsed = pattern;
// Create base title by removing the episode number
baseTitle = currentVideoTitle.replace(pattern, '').trim();
break;
}
}
if (!episodeNumber) {
OptimizedUtils.log('No episode pattern found in:', currentVideoTitle);
return null;
}
const nextEpisodeNumber = episodeNumber + 1;
OptimizedUtils.log(`Found episode ${episodeNumber}, searching for episode ${nextEpisodeNumber}`);
// Generate search query for next episode
const searchQueries = [
`${currentChannelName} ${baseTitle} episode ${nextEpisodeNumber}`,
`${currentChannelName} ${baseTitle} ep ${nextEpisodeNumber}`,
`${currentChannelName} ${baseTitle} part ${nextEpisodeNumber}`,
`${currentChannelName} ${baseTitle} #${nextEpisodeNumber}`,
`${currentChannelName} ${baseTitle} ${nextEpisodeNumber}`
];
// Try each search query to find the next episode
for (const query of searchQueries) {
try {
OptimizedUtils.log(`Searching for next episode: "${query}"`);
const results = await this.apiManager.searchVideos(query.trim(), {
publishedBefore: this.maxDate,
maxResults: 10,
order: 'relevance'
});
if (results && results.length > 0) {
// Look for exact episode match in results
const nextEpisode = results.find(video => {
const title = video.snippet?.title || '';
const channel = video.snippet?.channelTitle || '';
// Check if it's from same channel and contains next episode number
if (channel.toLowerCase() !== currentChannelName.toLowerCase()) {
return false;
}
// Check if title contains the next episode number
return episodePatterns.some(pattern => {
const match = title.match(pattern);
return match && parseInt(match[1]) === nextEpisodeNumber;
});
});
if (nextEpisode) {
OptimizedUtils.log(`✓ Found next episode: "${nextEpisode.snippet.title}"`);
return {
id: nextEpisode.id,
title: nextEpisode.snippet?.title || 'Next Episode',
channel: nextEpisode.snippet?.channelTitle || currentChannelName,
channelId: nextEpisode.snippet?.channelId || '',
thumbnail: nextEpisode.snippet?.thumbnails?.medium?.url ||
nextEpisode.snippet?.thumbnails?.high?.url ||
nextEpisode.snippet?.thumbnails?.default?.url || '',
publishedAt: nextEpisode.snippet?.publishedAt || new Date().toISOString(),
description: nextEpisode.snippet?.description || '',
viewCount: this.apiManager.generateRealisticViewCount(
nextEpisode.snippet?.publishedAt || new Date().toISOString(),
this.maxDate
),
relativeDate: OptimizedUtils.calculateRelativeDate(
nextEpisode.snippet?.publishedAt || new Date().toISOString(),
this.maxDate
),
isNextEpisode: true // Mark as series advancement
};
}
}
} catch (error) {
OptimizedUtils.log(`Error searching for "${query}":`, error);
}
}
OptimizedUtils.log('No next episode found');
return null;
}
// === VIDEO LOADING AND MANAGEMENT ===
async loadVideosFromSearchTerms(isRefresh = false) {
const searchTerms = this.uiManager.getSearchTerms();
if (searchTerms.length === 0) {
return [];
}
// Check cache first - same behavior as subscriptions
const cacheKey = `search_videos_${JSON.stringify(searchTerms.sort())}_${this.maxDate.toDateString()}`;
const cachedVideos = this.videoCache.get(cacheKey);
if (cachedVideos && cachedVideos.length > 0 && !isRefresh) {
OptimizedUtils.log(`✓ Using cached search term videos (${cachedVideos.length} videos) - SAVED API QUOTA`);
return cachedVideos;
}
OptimizedUtils.log(`⚠️ Loading search term videos from API (cache miss or refresh) - using API quota`);
OptimizedUtils.log(`Loading videos from ${searchTerms.length} search terms...`);
let allVideos = [];
const videosPerTerm = Math.ceil(CONFIG.maxHomepageVideos / searchTerms.length);
for (const term of searchTerms) {
try {
const endDate = new Date(this.maxDate);
const videos = await this.apiManager.searchVideos(term, {
publishedBefore: endDate,
maxResults: videosPerTerm,
order: 'relevance'
});
if (videos && videos.length > 0) {
// Flatten video structure to match subscription videos format
const flattenedVideos = videos.map(video => ({
id: video.id,
title: video.snippet?.title || 'Unknown Title',
channel: video.snippet?.channelTitle || 'Unknown Channel',
channelId: video.snippet?.channelId || '',
thumbnail: video.snippet?.thumbnails?.medium?.url ||
video.snippet?.thumbnails?.high?.url ||
video.snippet?.thumbnails?.default?.url || '',
publishedAt: video.snippet?.publishedAt || new Date().toISOString(),
description: video.snippet?.description || '',
viewCount: this.apiManager.generateRealisticViewCount(
video.snippet?.publishedAt || new Date().toISOString(),
endDate
),
relativeDate: OptimizedUtils.calculateRelativeDate(video.snippet?.publishedAt || new Date().toISOString(), endDate)
}));
OptimizedUtils.log(`Flattened ${flattenedVideos.length} search videos for "${term}"`);
allVideos = allVideos.concat(flattenedVideos);
}
OptimizedUtils.log(`Found ${videos?.length || 0} videos for search term: ${term}`);
} catch (error) {
OptimizedUtils.log(`Error loading videos for search term "${term}":`, error);
}
}
// Remove duplicates and limit total
const uniqueVideos = OptimizedUtils.removeDuplicates(allVideos);
const limitedVideos = uniqueVideos.slice(0, CONFIG.maxHomepageVideos);
// Cache the results with same expiry as subscription videos
this.videoCache.set(cacheKey, limitedVideos, CONFIG.cacheExpiry.videos);
OptimizedUtils.log(`Loaded ${limitedVideos.length} unique videos from search terms`);
return limitedVideos;
}
async loadAllVideos(isRefresh = false) {
const subscriptions = this.uiManager.getSubscriptions(); // FIX: Use uiManager, not subscriptionManager
const searchTerms = this.uiManager.getSearchTerms();
OptimizedUtils.log(`🚀 LOADING ALL VIDEOS DEBUG:`);
OptimizedUtils.log(` Subscriptions: ${subscriptions.length} (${subscriptions.map(s => s.name).join(', ')})`);
OptimizedUtils.log(` Search terms: ${searchTerms.length} (${searchTerms.join(', ')})`);
OptimizedUtils.log(` Refresh: ${isRefresh}`);
if (subscriptions.length === 0 && searchTerms.length === 0) {
OptimizedUtils.log('❌ No subscriptions or search terms found - cannot load videos');
throw new Error('No subscriptions or search terms to load from');
}
// Use SINGLE unified cache key for all videos
const cacheKey = `all_videos_unified_${this.maxDate.toDateString()}`;
let cachedVideos = this.videoCache.get(cacheKey) || [];
if (cachedVideos.length > 0 && !isRefresh) {
OptimizedUtils.log(`✓ Using ${cachedVideos.length} cached unified videos - NO API CALLS`);
this.videoCache.set('all_videos', cachedVideos); // Also set legacy cache
return cachedVideos;
}
OptimizedUtils.log('Loading all videos from subscriptions and search terms...');
let allVideos = [];
// Load subscription videos if we have subscriptions
let subscriptionVideos = [];
if (subscriptions.length > 0) {
OptimizedUtils.log(`📥 Loading ${subscriptions.length} subscription channels...`);
subscriptionVideos = await this.loadVideosFromSubscriptions(isRefresh);
OptimizedUtils.log(`✓ Loaded ${subscriptionVideos.length} subscription videos`);
allVideos.push(...subscriptionVideos);
}
// Load search term videos if we have search terms
let searchTermVideos = [];
if (searchTerms.length > 0) {
OptimizedUtils.log(`🔍 Loading ${searchTerms.length} search terms...`);
searchTermVideos = await this.loadVideosFromSearchTerms(isRefresh);
OptimizedUtils.log(`✓ Loaded ${searchTermVideos.length} search term videos`);
allVideos.push(...searchTermVideos);
}
// Apply weighted mixing and remove duplicates
const uniqueVideos = OptimizedUtils.removeDuplicates(allVideos);
const finalVideos = this.mixVideosByWeight(
allVideos.filter(v => subscriptions.some(sub => sub.name === v.channel || sub.id === v.channelId)),
allVideos.filter(v => !subscriptions.some(sub => sub.name === v.channel || sub.id === v.channelId))
);
// Store in SINGLE unified cache
this.videoCache.set(cacheKey, finalVideos, CONFIG.cacheExpiry.videos);
this.videoCache.set('all_videos', finalVideos); // Legacy cache for compatibility
OptimizedUtils.log(`✓ Stored ${finalVideos.length} videos in unified cache`);
const searchInFinal = finalVideos.filter(v => searchTermVideos.some(s => s.id === v.id)).length;
const subInFinal = finalVideos.filter(v => subscriptionVideos.some(s => s.id === v.id)).length;
OptimizedUtils.log(` Final homepage mix: ${searchInFinal} search + ${subInFinal} subscription = ${finalVideos.length} total`);
return finalVideos;
}
mixVideosByWeight(subscriptionVideos, searchTermVideos) {
// Debug logging to see what's happening
OptimizedUtils.log(`🎯 HOMEPAGE MIXING DEBUG:`);
OptimizedUtils.log(` Subscription videos: ${subscriptionVideos.length}`);
OptimizedUtils.log(` Search term videos: ${searchTermVideos.length}`);
OptimizedUtils.log(` Search percentage: ${CONFIG.searchTermVideoPercentage * 100}%`);
OptimizedUtils.log(` Subscription percentage: ${CONFIG.subscriptionVideoPercentage * 100}%`);
// Don't limit total count - let users load more via pagination
const totalVideos = subscriptionVideos.length + searchTermVideos.length;
const searchCount = Math.floor(totalVideos * CONFIG.searchTermVideoPercentage);
const subscriptionCount = totalVideos - searchCount;
OptimizedUtils.log(` Target search videos: ${searchCount}`);
OptimizedUtils.log(` Target subscription videos: ${subscriptionCount}`);
const selectedSearch = OptimizedUtils.shuffleArray([...searchTermVideos]).slice(0, searchCount);
const selectedSubscription = OptimizedUtils.shuffleArray([...subscriptionVideos]).slice(0, subscriptionCount);
OptimizedUtils.log(` Actually selected search: ${selectedSearch.length}`);
OptimizedUtils.log(` Actually selected subscription: ${selectedSubscription.length}`);
const combined = [...selectedSearch, ...selectedSubscription];
const uniqueVideos = OptimizedUtils.removeDuplicates(combined);
// Apply heavy recent bias to the mixed results
const recentlyBiasedVideos = OptimizedUtils.weightedShuffleByDate(uniqueVideos, this.maxDate);
OptimizedUtils.log(`Video weighting: ${selectedSearch.length} search (${CONFIG.searchTermVideoPercentage * 100}%) + ${selectedSubscription.length} subscription = ${recentlyBiasedVideos.length} total with HEAVY recent bias`);
return recentlyBiasedVideos;
}
getCurrentVideoId() {
const url = window.location.href;
const match = url.match(/[?&]v=([^&]+)/);
return match ? match[1] : null;
}
async getCurrentChannelId(videoId) {
if (!videoId) return null;
try {
const videoDetails = await this.apiManager.getVideoDetails([videoId]);
if (videoDetails && videoDetails.length > 0) {
return videoDetails[0].snippet?.channelId;
}
} catch (error) {
OptimizedUtils.log('Error getting current channel ID:', error);
}
return null;
}
getCurrentVideoTitle() {
// Try multiple selectors to get the video title
const titleSelectors = [
'h1.title.style-scope.ytd-video-primary-info-renderer',
'h1.ytd-video-primary-info-renderer',
'h1[class*="title"]',
'.title.style-scope.ytd-video-primary-info-renderer',
'ytd-video-primary-info-renderer h1',
'#container h1'
];
for (const selector of titleSelectors) {
const titleElement = document.querySelector(selector);
if (titleElement && titleElement.textContent.trim()) {
const title = titleElement.textContent.trim();
OptimizedUtils.log(`Found video title: "${title}"`);
return title;
}
}
OptimizedUtils.log('Could not find video title on page');
return null;
}
getCurrentChannelName() {
// Try multiple selectors to get the channel name
const channelSelectors = [
'ytd-video-owner-renderer .ytd-channel-name a',
'ytd-channel-name #text a',
'#owner-text a',
'.ytd-channel-name a',
'a.yt-simple-endpoint.style-scope.yt-formatted-string',
'#upload-info #channel-name a'
];
for (const selector of channelSelectors) {
const channelElement = document.querySelector(selector);
if (channelElement && channelElement.textContent.trim()) {
const channelName = channelElement.textContent.trim();
OptimizedUtils.log(`Found channel name: "${channelName}"`);
return channelName;
}
}
OptimizedUtils.log('Could not find channel name on page');
return null;
}
async createWatchNextWeightedVideos(allVideos, currentChannelId, currentVideoTitle = null, currentChannelName = null) {
let nextEpisode = null;
// First, try to find the next episode in the series
if (currentVideoTitle && currentChannelName) {
try {
nextEpisode = await this.findNextEpisodeInSeries(currentVideoTitle, currentChannelName);
} catch (error) {
OptimizedUtils.log('Error finding next episode:', error);
}
}
// Use SINGLE unified cache for weighting - no separate caches
const cacheKey = `all_videos_unified_${this.maxDate.toDateString()}`;
const allCachedVideos = this.videoCache.get(cacheKey) || this.videoCache.get('all_videos') || [];
// Separate cached videos by type for weighting
const subscriptions = this.subscriptionManager.getSubscriptions();
const subscriptionVideos = allCachedVideos.filter(v => subscriptions.some(sub => sub.name === v.channel || sub.id === v.channelId));
const searchTermVideos = allCachedVideos.filter(v => !subscriptions.some(sub => sub.name === v.channel || sub.id === v.channelId));
// Separate same channel videos if we have a channel ID
let sameChannelVideos = [];
let otherVideos = allCachedVideos;
if (currentChannelId) {
sameChannelVideos = allCachedVideos.filter(video => video.channelId === currentChannelId);
otherVideos = allCachedVideos.filter(video => video.channelId !== currentChannelId);
}
const totalCount = CONFIG.maxVideoPageVideos;
// Reserve slot for next episode if found
const reservedSlots = nextEpisode ? 1 : 0;
const availableSlots = totalCount - reservedSlots;
const sameChannelCount = Math.floor(availableSlots * CONFIG.sameChannelVideoPercentage);
const searchTermCount = Math.floor((availableSlots - sameChannelCount) * CONFIG.searchTermVideoPercentage);
const subscriptionCount = availableSlots - sameChannelCount - searchTermCount;
// Select videos with weighting
const selectedSameChannel = OptimizedUtils.shuffleArray(sameChannelVideos).slice(0, sameChannelCount);
const selectedSearch = OptimizedUtils.shuffleArray(searchTermVideos).slice(0, searchTermCount);
const selectedSubscription = OptimizedUtils.shuffleArray(subscriptionVideos).slice(0, subscriptionCount);
// Fill remaining slots if any category doesn't have enough videos
let combinedVideos = [...selectedSameChannel, ...selectedSearch, ...selectedSubscription];
if (combinedVideos.length < availableSlots) {
const remaining = availableSlots - combinedVideos.length;
const additionalVideos = OptimizedUtils.shuffleArray(otherVideos).slice(0, remaining);
combinedVideos = [...combinedVideos, ...additionalVideos];
}
const finalVideos = OptimizedUtils.removeDuplicates(combinedVideos);
// Add next episode at the VERY TOP if found
if (nextEpisode) {
finalVideos.unshift(nextEpisode);
OptimizedUtils.log(`🎬 Next episode added to top: "${nextEpisode.title}"`);
}
OptimizedUtils.log(`Watch Next weighting: ${nextEpisode ? '1 next episode + ' : ''}${selectedSameChannel.length} same channel (${CONFIG.sameChannelVideoPercentage * 100}%) + ${selectedSearch.length} search (${CONFIG.searchTermVideoPercentage * 100}%) + ${selectedSubscription.length} subscription`);
return finalVideos; // Don't shuffle to keep next episode at top
}
async loadVideosFromSubscriptions(isRefresh = false) {
const subscriptions = this.subscriptionManager.getSubscriptions();
if (subscriptions.length === 0) {
return [];
}
OptimizedUtils.log('Loading videos from subscriptions' + (isRefresh ? ' (refresh mode)' : '') + '...');
const existingVideos = this.videoCache.get('subscription_videos') || [];
const allVideos = [];
const endDate = new Date(this.maxDate);
endDate.setHours(23, 59, 59, 999);
// Load viral videos
const viralVideos = await this.apiManager.getViralVideos(endDate, isRefresh);
OptimizedUtils.log('Loaded ' + viralVideos.length + ' viral videos');
// Load from subscriptions in batches
for (let i = 0; i < subscriptions.length; i += CONFIG.batchSize) {
const batch = subscriptions.slice(i, i + CONFIG.batchSize);
const batchPromises = batch.map(async (sub) => {
try {
let channelId = sub.id;
if (!channelId) {
channelId = await this.getChannelIdByName(sub.name);
}
if (channelId) {
const videos = await this.getChannelVideos(channelId, sub.name, endDate, isRefresh);
return videos;
}
return [];
} catch (error) {
OptimizedUtils.log('Failed to load videos for ' + sub.name + ':', error);
return [];
}
});
const batchResults = await Promise.all(batchPromises);
batchResults.forEach(videos => allVideos.push(...videos));
if (i + CONFIG.batchSize < subscriptions.length) {
await OptimizedUtils.sleep(CONFIG.apiCooldown);
}
}
// Mix videos for refresh mode
let finalVideos = allVideos;
if (isRefresh && existingVideos.length > 0) {
const existingViralVideos = this.videoCache.get('viral_videos') || [];
finalVideos = this.mixVideosForRefresh(finalVideos, existingVideos, viralVideos, existingViralVideos);
OptimizedUtils.log('Mixed ' + finalVideos.length + ' new videos with ' + existingVideos.length + ' existing videos');
} else {
finalVideos = finalVideos.concat(viralVideos);
}
// Remove duplicates and shuffle final result
finalVideos = OptimizedUtils.removeDuplicates(finalVideos);
finalVideos = OptimizedUtils.shuffleArray(finalVideos);
// Cache results
this.videoCache.set('viral_videos', viralVideos);
this.videoCache.set('subscription_videos', finalVideos);
OptimizedUtils.log('Loaded ' + finalVideos.length + ' total videos from subscriptions');
return finalVideos;
}
mixVideosForRefresh(newVideos, existingVideos, newViralVideos = [], existingViralVideos = []) {
const totalDesired = Math.max(CONFIG.maxHomepageVideos, existingVideos.length);
const viralVideoCount = Math.floor(totalDesired * CONFIG.viralVideoPercentage);
const remainingSlots = totalDesired - viralVideoCount;
const newVideoCount = Math.floor(remainingSlots * CONFIG.refreshVideoPercentage);
const existingVideoCount = remainingSlots - newVideoCount;
OptimizedUtils.log('Mixing videos: ' + newVideoCount + ' new + ' + existingVideoCount + ' existing + ' + viralVideoCount + ' viral');
const selectedNew = OptimizedUtils.shuffleArray(newVideos).slice(0, newVideoCount);
const selectedExisting = OptimizedUtils.shuffleArray(existingVideos).slice(0, existingVideoCount);
const allViralVideos = newViralVideos.concat(existingViralVideos);
const selectedViral = OptimizedUtils.shuffleArray(allViralVideos).slice(0, viralVideoCount);
const mixedVideos = selectedNew.concat(selectedExisting).concat(selectedViral);
return OptimizedUtils.shuffleArray(mixedVideos);
}
async getChannelIdByName(channelName) {
const cacheKey = 'channel_id_' + channelName;
let channelId = this.apiManager.getCache(cacheKey);
if (!channelId) {
try {
const response = await this.apiManager.makeRequest(`${this.apiManager.baseUrl}/search`, {
part: 'snippet',
q: channelName,
type: 'channel',
maxResults: 1
});
if (response.items && response.items.length > 0) {
channelId = response.items[0].snippet.channelId;
this.apiManager.setCache(cacheKey, channelId);
this.stats.apiCalls++;
}
} catch (error) {
OptimizedUtils.log('Failed to find channel ID for ' + channelName + ':', error);
}
} else {
this.stats.cacheHits++;
}
return channelId;
}
async getChannelVideos(channelId, channelName, endDate, forceRefresh = false) {
const cacheKey = 'channel_videos_' + channelId + '_' + this.settings.date;
let videos = this.apiManager.getCache(cacheKey, forceRefresh);
if (!videos) {
try {
const response = await this.apiManager.makeRequest(`${this.apiManager.baseUrl}/search`, {
part: 'snippet',
channelId: channelId,
type: 'video',
order: 'date',
publishedBefore: endDate.toISOString(),
maxResults: CONFIG.videosPerChannel
});
videos = response.items ? response.items.map(item => ({
id: item.id.videoId,
title: item.snippet.title,
channel: item.snippet.channelTitle || channelName,
channelId: item.snippet.channelId,
thumbnail: item.snippet.thumbnails?.medium?.url || item.snippet.thumbnails?.default?.url || '',
publishedAt: item.snippet.publishedAt,
description: item.snippet.description || '',
viewCount: this.apiManager.generateRealisticViewCount(item.snippet.publishedAt, endDate),
relativeDate: OptimizedUtils.calculateRelativeDate(item.snippet.publishedAt, endDate)
})) : [];
this.apiManager.setCache(cacheKey, videos, forceRefresh);
this.stats.apiCalls++;
} catch (error) {
OptimizedUtils.log('Failed to get videos for channel ' + channelName + ':', error);
videos = [];
}
} else {
this.stats.cacheHits++;
}
return videos;
}
// === PAGE HANDLING ===
startFiltering() {
OptimizedUtils.log('Starting ultra-aggressive video filtering...');
const filterVideos = () => {
if (this.isProcessing || !this.settings.active) return;
this.isProcessing = true;
const videoSelectors = [
'ytd-video-renderer',
'ytd-grid-video-renderer',
'ytd-rich-item-renderer',
'ytd-compact-video-renderer',
'ytd-movie-renderer',
'ytd-playlist-renderer'
];
let processed = 0;
let filtered = 0;
videoSelectors.forEach(selector => {
const videos = document.querySelectorAll(selector);
videos.forEach(video => {
if (video.dataset.tmProcessed) return;
video.dataset.tmProcessed = 'true';
processed++;
if (this.shouldHideVideo(video)) {
video.style.display = 'none';
filtered++;
}
});
});
if (processed > 0) {
this.stats.processed += processed;
this.stats.filtered += filtered;
}
this.isProcessing = false;
};
setInterval(filterVideos, CONFIG.updateInterval);
}
shouldHideVideo(videoElement) {
// Check for Shorts
if (this.isShorts(videoElement)) {
return true;
}
// Check video date
const videoDate = this.extractVideoDate(videoElement);
if (videoDate && videoDate > this.maxDate) {
return true;
}
return false;
}
isShorts(element) {
const shortsIndicators = CONFIG.SHORTS_SELECTORS;
return shortsIndicators.some(selector => {
try {
return element.matches(selector) || element.querySelector(selector);
} catch (e) {
return false;
}
});
}
extractVideoDate(videoElement) {
const dateSelectors = [
'#metadata-line span:last-child',
'#published-time-text',
'[aria-label*="ago"]'
];
for (const selector of dateSelectors) {
const element = videoElement.querySelector(selector);
if (element) {
const dateText = element.textContent?.trim();
if (dateText) {
return this.parseRelativeDate(dateText);
}
}
}
return null;
}
parseRelativeDate(dateText) {
const now = this.maxDate;
const text = dateText.toLowerCase();
const minuteMatch = text.match(/(\d+)\s*minutes?\s*ago/);
if (minuteMatch) {
return new Date(now.getTime() - parseInt(minuteMatch[1]) * 60 * 1000);
}
const hourMatch = text.match(/(\d+)\s*hours?\s*ago/);
if (hourMatch) {
return new Date(now.getTime() - parseInt(hourMatch[1]) * 60 * 60 * 1000);
}
const dayMatch = text.match(/(\d+)\s*days?\s*ago/);
if (dayMatch) {
return new Date(now.getTime() - parseInt(dayMatch[1]) * 24 * 60 * 60 * 1000);
}
const weekMatch = text.match(/(\d+)\s*weeks?\s*ago/);
if (weekMatch) {
return new Date(now.getTime() - parseInt(weekMatch[1]) * 7 * 24 * 60 * 60 * 1000);
}
const monthMatch = text.match(/(\d+)\s*months?\s*ago/);
if (monthMatch) {
return new Date(now.getTime() - parseInt(monthMatch[1]) * 30 * 24 * 60 * 60 * 1000);
}
const yearMatch = text.match(/(\d+)\s*years?\s*ago/);
if (yearMatch) {
return new Date(now.getTime() - parseInt(yearMatch[1]) * 365 * 24 * 60 * 60 * 1000);
}
return null;
}
startHomepageReplacement() {
// More aggressive immediate hiding of original homepage
const nukeOriginalContent = () => {
if (!this.isHomePage()) return;
const selectors = [
'ytd-rich-grid-renderer',
'ytd-rich-section-renderer',
'#contents ytd-rich-item-renderer',
'ytd-browse[page-subtype="home"] #contents',
'.ytd-rich-grid-renderer'
];
selectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
if (!el.dataset.tmNuked && !el.dataset.tmReplaced) {
el.style.opacity = '0';
el.style.visibility = 'hidden';
el.dataset.tmNuked = 'true';
}
});
});
};
const replaceHomepage = () => {
if (this.isHomePage() && this.settings.active) {
const container = document.querySelector('ytd-browse[page-subtype="home"] ytd-rich-grid-renderer');
if (container && !container.dataset.tmReplaced) {
container.dataset.tmReplaced = 'true';
container.style.opacity = '1';
container.style.visibility = 'visible';
this.replaceHomepage(container);
}
}
};
// Immediately nuke original content
nukeOriginalContent();
// Continuous nuking and replacement
setInterval(() => {
nukeOriginalContent();
replaceHomepage();
}, 100); // More frequent to catch dynamic content
}
isHomePage() {
return location.pathname === '/' || location.pathname === '';
}
async replaceHomepage(container, isRefresh = false) {
OptimizedUtils.log('Replacing homepage' + (isRefresh ? ' (refresh)' : '') + '...');
container.innerHTML = '<div class="tm-loading">Loading...</div>';
try {
// Use SINGLE unified cache - no separate checking
const cacheKey = `all_videos_unified_${this.maxDate.toDateString()}`;
let videos = this.videoCache.get(cacheKey) || this.videoCache.get('all_videos');
OptimizedUtils.log('Homepage cache check:', videos ? `${videos.length} cached videos found` : 'No cached videos');
if (!videos || videos.length === 0) {
// Keep trying without timeout - let it load as long as needed
try {
videos = await this.loadAllVideos(isRefresh);
} catch (error) {
OptimizedUtils.log('Failed to load videos:', error);
videos = [];
}
}
if (videos && videos.length > 0) {
// Completely randomize videos each time
// Use weighted shuffle to favor newer videos while keeping older ones visible
this.allHomepageVideos = OptimizedUtils.weightedShuffleByDate(videos, this.maxDate);
this.currentVideoIndex = 0;
// Show initial batch - use smaller initial count to ensure "Load more" appears
const initialBatchSize = Math.min(CONFIG.homepageLoadMoreSize || 20, this.allHomepageVideos.length);
const initialVideos = this.allHomepageVideos.slice(0, initialBatchSize);
this.currentVideoIndex = initialBatchSize;
const videoCards = initialVideos.map(video => `
<div class="tm-video-card" data-video-id="${video.id}">
<img src="${video.thumbnail}" alt="${video.title}">
<div class="tm-video-info">
<div class="tm-video-title">${video.title}</div>
<div class="tm-video-channel">${video.channel}</div>
<div class="tm-video-meta">
<span>${video.viewCount} views</span>
<span>•</span>
<span>${video.relativeDate}</span>
</div>
</div>
</div>
`).join('');
const hasMoreVideos = this.currentVideoIndex < this.allHomepageVideos.length;
container.innerHTML = `
<div style="margin-bottom: 20px; text-align: center;">
<button id="tm-new-videos-btn" style="
background: #ff0000;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
">New videos</button>
</div>
<div class="tm-homepage-grid" id="tm-video-grid">${videoCards}</div>
${hasMoreVideos ? `
<div style="margin: 30px 0; text-align: center;">
<button id="tm-load-more-btn" style="
background: #333;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
">Load more videos</button>
</div>
` : ''}
`;
this.attachHomepageClickHandlers();
this.attachNewVideosButton();
this.attachLoadMoreButton();
} else {
// Show helpful message when no videos are available
container.innerHTML = `
<div class="tm-no-content" style="text-align: center; padding: 40px;">
<h3>No videos found for ${this.maxDate.toLocaleDateString()}</h3>
<p>This could mean:</p>
<ul style="text-align: left; display: inline-block;">
<li>No API keys are configured</li>
<li>No subscriptions are set up</li>
<li>No videos were uploaded before this date</li>
</ul>
<p>Click the ⚙️ button to add API keys and configure subscriptions.</p>
</div>
`;
}
} catch (error) {
OptimizedUtils.log('Homepage replacement failed:', error);
container.innerHTML = `
<div class="tm-error" style="text-align: center; padding: 40px; color: #f44336;">
<h3>Failed to load content</h3>
<p>Error: ${error.message}</p>
<p>Check your API keys and try again.</p>
</div>
`;
}
}
attachHomepageClickHandlers() {
document.querySelectorAll('.tm-video-card').forEach(card => {
card.addEventListener('click', () => {
const videoId = card.dataset.videoId;
if (videoId) {
window.location.href = '/watch?v=' + videoId;
}
});
});
}
attachNewVideosButton() {
const button = document.getElementById('tm-new-videos-btn');
if (button) {
button.addEventListener('click', async () => {
button.textContent = 'Loading...';
button.disabled = true;
try {
await this.loadAllVideos(true);
const container = document.querySelector('ytd-browse[page-subtype="home"] ytd-rich-grid-renderer');
if (container) {
container.dataset.tmReplaced = '';
this.replaceHomepage(container, true);
}
} catch (error) {
OptimizedUtils.log('Failed to load new videos:', error);
button.textContent = 'New videos';
button.disabled = false;
}
});
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = '#d50000';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = '#ff0000';
});
}
}
attachLoadMoreButton() {
const button = document.getElementById('tm-load-more-btn');
if (!button) return;
button.addEventListener('click', () => {
const videoGrid = document.getElementById('tm-video-grid');
if (!videoGrid || !this.allHomepageVideos || this.currentVideoIndex >= this.allHomepageVideos.length) return;
const nextBatch = this.allHomepageVideos.slice(
this.currentVideoIndex,
this.currentVideoIndex + CONFIG.homepageLoadMoreSize
);
if (nextBatch.length === 0) return;
const videoCards = nextBatch.map(video => `
<div class="tm-video-card" data-video-id="${video.id}">
<img src="${video.thumbnail}" alt="${video.title}">
<div class="tm-video-info">
<div class="tm-video-title">${video.title}</div>
<div class="tm-video-channel">${video.channel}</div>
<div class="tm-video-meta">
<span>${video.viewCount} views</span>
<span>•</span>
<span>${video.relativeDate}</span>
</div>
</div>
</div>
`).join('');
videoGrid.innerHTML += videoCards;
this.currentVideoIndex += nextBatch.length;
// Attach click handlers to new cards - use event delegation
this.attachHomepageClickHandlers();
// Hide button if no more videos
if (this.currentVideoIndex >= this.allHomepageVideos.length) {
button.style.display = 'none';
}
});
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = '#555';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = '#333';
});
}
startVideoPageEnhancement() {
// Watch for URL changes to detect new video clicks
let currentVideoId = new URL(location.href).searchParams.get('v');
const checkVideoPage = () => {
if (location.pathname === '/watch' && this.settings.active) {
const newVideoId = new URL(location.href).searchParams.get('v');
// More aggressive and frequent content nuking
this.nukeOriginalWatchNextContent();
const sidebar = document.querySelector('#secondary #secondary-inner');
if (sidebar) {
if (!sidebar.dataset.tmEnhanced || newVideoId !== currentVideoId) {
sidebar.dataset.tmEnhanced = 'true';
currentVideoId = newVideoId;
this.enhanceVideoPage(sidebar);
}
// Continuously nuke original content to prevent it from reappearing
setTimeout(() => this.nukeOriginalWatchNextContent(), 100);
setTimeout(() => this.nukeOriginalWatchNextContent(), 500);
setTimeout(() => this.nukeOriginalWatchNextContent(), 1000);
}
}
};
// Check more frequently
setInterval(checkVideoPage, 200);
// Listen for popstate events (back/forward navigation)
window.addEventListener('popstate', () => setTimeout(checkVideoPage, 50));
// Listen for pushstate events (clicking on videos)
const originalPushState = history.pushState;
history.pushState = function() {
originalPushState.apply(this, arguments);
setTimeout(checkVideoPage, 50);
};
// Also listen for replaceState
const originalReplaceState = history.replaceState;
history.replaceState = function() {
originalReplaceState.apply(this, arguments);
setTimeout(checkVideoPage, 50);
};
}
nukeOriginalWatchNextContent() {
const selectors = [
'#related',
'ytd-watch-next-secondary-results-renderer',
'#secondary #secondary-inner > *:not(.tm-watch-next):not([data-tm-enhanced])',
'ytd-compact-video-renderer:not([data-tm-enhanced])',
'ytd-continuation-item-renderer',
'#secondary-inner ytd-item-section-renderer:not([data-tm-enhanced])',
'#secondary-inner ytd-shelf-renderer:not([data-tm-enhanced])',
'ytd-watch-next-secondary-results-renderer *',
'#secondary-inner > ytd-watch-next-secondary-results-renderer'
];
selectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
if (!el.classList.contains('tm-watch-next') && !el.dataset.tmEnhanced) {
el.style.display = 'none !important';
el.style.visibility = 'hidden';
el.style.height = '0';
el.style.overflow = 'hidden';
el.setAttribute('data-tm-nuked', 'true');
}
});
});
}
async enhanceVideoPage(sidebar) {
if (!this.settings.active) return;
OptimizedUtils.log('Enhancing video page...');
// Clear everything first
this.clearSidebar(sidebar);
// Create permanent container
const permanentContainer = document.createElement('div');
permanentContainer.id = 'tm-permanent-sidebar';
permanentContainer.dataset.tmEnhanced = 'true';
permanentContainer.style.cssText = `
position: relative !important;
z-index: 999999 !important;
background: #0f0f0f !important;
min-height: 400px !important;
`;
// Silent loading - no visible indicator
permanentContainer.innerHTML = '<div style="height: 20px;"></div>';
sidebar.innerHTML = '';
sidebar.appendChild(permanentContainer);
try {
// Get current video information for same-channel weighting and series advancement
const currentVideoId = this.getCurrentVideoId();
const currentChannelId = await this.getCurrentChannelId(currentVideoId);
// Get current video title and channel name from page DOM
const currentVideoTitle = this.getCurrentVideoTitle();
const currentChannelName = this.getCurrentChannelName();
// FIXED: Use SINGLE unified cache - no separate checking or combining
const cacheKey = `all_videos_unified_${this.maxDate.toDateString()}`;
let videos = this.videoCache.get(cacheKey) || this.videoCache.get('all_videos');
if (!videos || videos.length === 0) {
// Only load if absolutely no cached videos exist
OptimizedUtils.log('No cached videos found, loading initial set...');
videos = await this.loadAllVideos(false);
} else {
OptimizedUtils.log(`Using ${videos.length} cached videos for watch next (no reload)`);
}
if (videos && videos.length > 0) {
// Apply watch next weighting with same channel priority and series advancement
const weightedVideos = await this.createWatchNextWeightedVideos(videos, currentChannelId, currentVideoTitle, currentChannelName);
// Store all videos for load more functionality
this.allWatchNextVideos = weightedVideos;
this.currentWatchNextIndex = 0;
// Use smaller initial batch to ensure "Load more" appears
const initialBatchSize = Math.min(CONFIG.homepageLoadMoreSize || 12, this.allWatchNextVideos.length);
const initialBatch = this.allWatchNextVideos.slice(0, initialBatchSize);
this.currentWatchNextIndex = initialBatch.length;
this.createWatchNextSectionInContainer(permanentContainer, initialBatch);
} else {
permanentContainer.innerHTML = `
<div style="padding: 20px; text-align: center; border: 1px solid #f44336; margin: 10px 0;">
<div style="color: #f44336; font-weight: bold;">No videos found for ${this.maxDate.toLocaleDateString()}</div>
<div style="font-size: 12px; color: #666; margin-top: 5px;">Check your API keys in settings</div>
</div>
`;
}
} catch (error) {
OptimizedUtils.log('Video page enhancement failed:', error);
permanentContainer.innerHTML = `
<div style="padding: 20px; text-align: center; border: 1px solid #f44336; margin: 10px 0;">
<div style="color: #f44336; font-weight: bold;">Error loading content</div>
<div style="font-size: 12px; color: #666; margin-top: 5px;">${error.message}</div>
</div>
`;
}
// Protect our container from being removed
this.protectSidebarContent(permanentContainer);
}
clearSidebar(sidebar) {
// Completely clear sidebar
const children = [...sidebar.children];
children.forEach(child => {
if (!child.dataset.tmEnhanced) {
child.remove();
}
});
}
protectSidebarContent(container) {
// Set up mutation observer to protect our content
if (this.sidebarProtector) {
this.sidebarProtector.disconnect();
}
this.sidebarProtector = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// If our container was removed, re-add it
if (!document.getElementById('tm-permanent-sidebar')) {
const sidebar = document.querySelector('#secondary #secondary-inner');
if (sidebar && container.parentNode !== sidebar) {
sidebar.innerHTML = '';
sidebar.appendChild(container);
}
}
// Remove any new content that's not ours
const sidebar = document.querySelector('#secondary #secondary-inner');
if (sidebar) {
const children = [...sidebar.children];
children.forEach(child => {
if (child.id !== 'tm-permanent-sidebar' && !child.dataset.tmEnhanced) {
child.style.display = 'none';
}
});
}
}
});
});
const sidebar = document.querySelector('#secondary #secondary-inner');
if (sidebar) {
this.sidebarProtector.observe(sidebar, {
childList: true,
subtree: true
});
}
}
createWatchNextSectionInContainer(container, videos) {
const videoCards = videos.map(video => {
// Remove the "NEXT EPISODE" badge as requested
const nextEpisodeBadge = '';
return `
<div class="tm-watch-next-card" data-video-id="${video.id}" style="position: relative;">
${nextEpisodeBadge}
<img src="${video.thumbnail}" alt="${video.title}">
<div class="tm-watch-next-info">
<div class="tm-watch-next-title">${video.title}</div>
<div class="tm-watch-next-channel">${video.channel}</div>
<div class="tm-watch-next-meta">${video.viewCount} views • ${video.relativeDate}</div>
</div>
</div>
`;
}).join('');
container.innerHTML = `
<div class="tm-watch-next">
<h3>Watch next</h3>
<div class="tm-watch-next-grid" id="tm-watch-next-grid">${videoCards}</div>
</div>
`;
this.attachWatchNextClickHandlers();
this.setupWatchNextLoadMore(container);
}
setupWatchNextLoadMore(container) {
// Add load more button if there are more videos
if (this.allWatchNextVideos && this.currentWatchNextIndex < this.allWatchNextVideos.length) {
const loadMoreBtn = document.createElement('button');
loadMoreBtn.id = 'tm-watch-next-load-more';
loadMoreBtn.style.cssText = `
width: 100%;
background: #333;
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
margin-top: 16px;
transition: background-color 0.2s;
`;
loadMoreBtn.textContent = 'Load more suggestions';
const watchNextSection = container.querySelector('.tm-watch-next');
watchNextSection.appendChild(loadMoreBtn);
loadMoreBtn.addEventListener('click', () => {
const watchNextGrid = document.getElementById('tm-watch-next-grid');
if (!watchNextGrid || !this.allWatchNextVideos || this.currentWatchNextIndex >= this.allWatchNextVideos.length) return;
const nextBatch = this.allWatchNextVideos.slice(
this.currentWatchNextIndex,
this.currentWatchNextIndex + CONFIG.homepageLoadMoreSize
);
if (nextBatch.length === 0) return;
const videoCards = nextBatch.map(video => `
<div class="tm-watch-next-card" data-video-id="${video.id}">
<img src="${video.thumbnail}" alt="${video.title}">
<div class="tm-watch-next-info">
<div class="tm-watch-next-title">${video.title}</div>
<div class="tm-watch-next-channel">${video.channel}</div>
<div class="tm-watch-next-meta">${video.viewCount} views • ${video.relativeDate}</div>
</div>
</div>
`).join('');
watchNextGrid.innerHTML += videoCards;
this.currentWatchNextIndex += nextBatch.length;
// Reattach click handlers
this.attachWatchNextClickHandlers();
// Hide button if no more videos
if (this.currentWatchNextIndex >= this.allWatchNextVideos.length) {
loadMoreBtn.style.display = 'none';
}
});
loadMoreBtn.addEventListener('mouseenter', () => {
loadMoreBtn.style.backgroundColor = '#555';
});
loadMoreBtn.addEventListener('mouseleave', () => {
loadMoreBtn.style.backgroundColor = '#333';
});
}
}
attachWatchNextClickHandlers() {
document.querySelectorAll('.tm-watch-next-card').forEach(card => {
card.addEventListener('click', () => {
const videoId = card.dataset.videoId;
if (videoId) {
window.location.href = '/watch?v=' + videoId;
}
});
});
}
setupAutoRefresh() {
if (CONFIG.autoRefreshInterval) {
OptimizedUtils.log('Setting up auto-refresh every ' + (CONFIG.autoRefreshInterval / 60000) + ' minutes');
setInterval(() => {
if (this.isHomePage() && this.settings.active) {
OptimizedUtils.log('Auto-refresh triggered');
this.loadAllVideos(true).then(() => {
const container = document.querySelector('ytd-browse[page-subtype="home"] ytd-rich-grid-renderer');
if (container) {
this.replaceHomepage(container, true);
}
});
}
}, CONFIG.autoRefreshInterval);
}
}
// === PUBLIC API METHODS ===
removeApiKey(index) {
if (this.apiManager.removeKey(this.apiManager.keys[index])) {
this.uiManager.updateUI();
}
}
removeSubscription(index) {
if (this.subscriptionManager.removeSubscription(index)) {
this.uiManager.updateUI();
}
}
getStats() {
return {
apiCalls: this.stats.apiCalls,
processed: this.stats.processed,
cacheHits: this.stats.cacheHits,
filtered: this.stats.filtered,
isActive: this.settings.active,
currentDate: this.settings.date
};
}
cleanup() {
if (this.autoRefreshTimer) {
clearInterval(this.autoRefreshTimer);
}
if (this.filterTimer) {
clearInterval(this.filterTimer);
}
OptimizedUtils.log('Application cleaned up');
}
getMaxDate() {
return new Date(this.settings.date);
}
// Cache watched videos for channel page and general use
cacheWatchedVideo(videoId) {
if (!videoId) return;
// Get current video details from page if possible
const videoData = this.extractCurrentVideoData(videoId);
if (videoData) {
const existingWatchedVideos = GM_getValue('wayback_watched_videos_cache', []);
const videoToCache = {
id: videoId,
title: videoData.title || 'Unknown Title',
channel: videoData.channel || 'Unknown Channel',
channelId: videoData.channelId || '',
thumbnail: videoData.thumbnail || '',
publishedAt: videoData.publishedAt || new Date().toISOString(),
description: videoData.description || '',
viewCount: videoData.viewCount || 'Unknown',
cachedAt: new Date().toISOString(),
source: 'watched_video'
};
// Check if already cached
const existingIndex = existingWatchedVideos.findIndex(v => v.id === videoId);
if (existingIndex >= 0) {
// Update existing entry
existingWatchedVideos[existingIndex] = videoToCache;
} else {
// Add new entry
existingWatchedVideos.push(videoToCache);
}
// Limit cache size
if (existingWatchedVideos.length > 5000) {
existingWatchedVideos.sort((a, b) => new Date(b.cachedAt) - new Date(a.cachedAt));
existingWatchedVideos.splice(5000);
}
GM_setValue('wayback_watched_videos_cache', existingWatchedVideos);
OptimizedUtils.log(`Cached watched video: ${videoToCache.title}`);
}
}
extractCurrentVideoData(videoId) {
// Try to extract video data from current page
try {
const titleElement = document.querySelector('h1.title yt-formatted-string, h1.ytd-video-primary-info-renderer yt-formatted-string');
const channelElement = document.querySelector('#owner-name a, #upload-info #channel-name a, ytd-channel-name a');
const publishElement = document.querySelector('#info-strings yt-formatted-string, #date yt-formatted-string');
const descElement = document.querySelector('#description yt-formatted-string, #meta-contents yt-formatted-string');
const thumbnailElement = document.querySelector('video');
return {
title: titleElement?.textContent?.trim() || 'Unknown Title',
channel: channelElement?.textContent?.trim() || 'Unknown Channel',
channelId: OptimizedUtils.extractChannelId(channelElement?.href || '') || '',
thumbnail: thumbnailElement?.poster || '',
publishedAt: this.parsePublishDate(publishElement?.textContent?.trim()) || new Date().toISOString(),
description: descElement?.textContent?.trim()?.substring(0, 200) || '',
viewCount: this.extractViewCount() || 'Unknown'
};
} catch (error) {
OptimizedUtils.log('Error extracting video data:', error);
return null;
}
}
parsePublishDate(dateText) {
if (!dateText) return null;
// Handle "X years ago", "X months ago", etc.
const relativeMatch = dateText.match(/(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago/i);
if (relativeMatch) {
const amount = parseInt(relativeMatch[1]);
const unit = relativeMatch[2].toLowerCase();
const date = new Date();
switch (unit) {
case 'second':
date.setSeconds(date.getSeconds() - amount);
break;
case 'minute':
date.setMinutes(date.getMinutes() - amount);
break;
case 'hour':
date.setHours(date.getHours() - amount);
break;
case 'day':
date.setDate(date.getDate() - amount);
break;
case 'week':
date.setDate(date.getDate() - (amount * 7));
break;
case 'month':
date.setMonth(date.getMonth() - amount);
break;
case 'year':
date.setFullYear(date.getFullYear() - amount);
break;
}
return date.toISOString();
}
return null;
}
extractViewCount() {
const viewElement = document.querySelector('#count .view-count, #info-strings yt-formatted-string');
if (viewElement) {
const viewText = viewElement.textContent.trim();
const viewMatch = viewText.match(/([\d,]+)\s*views?/i);
return viewMatch ? viewMatch[1].replace(/,/g, '') : null;
}
return null;
}
replaceChannelPageWithCache() {
const channelId = OptimizedUtils.extractChannelId(window.location.href);
if (!channelId) return;
const cachedVideos = this.getAllCachedChannelVideos(channelId);
if (cachedVideos.length > 0) {
this.replaceChannelPageVideos(cachedVideos, channelId);
} else {
// Show placeholder for empty cache
this.showEmptyChannelCache(channelId);
}
}
getAllCachedChannelVideos(channelId) {
// Check multiple sources for channel videos
const watchedVideos = GM_getValue('wayback_watched_videos_cache', []);
const subscriptionVideos = this.videoCache.get('subscription_videos_only') || [];
const searchTermVideos = this.videoCache.get('search_term_videos_only') || [];
const allCachedVideos = this.videoCache.get('all_videos') || [];
// Combine all sources and filter by channel
const allVideoSources = [...watchedVideos, ...subscriptionVideos, ...searchTermVideos, ...allCachedVideos];
OptimizedUtils.log(`Searching for channel videos with ID: "${channelId}"`);
OptimizedUtils.log(`Total videos to search: ${allVideoSources.length}`);
// Filter videos for this channel and sort by date (newest first)
const channelVideos = allVideoSources
.filter(video => video.channelId === channelId)
.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt));
OptimizedUtils.log(`Found ${channelVideos.length} cached videos for channel ${channelId}`);
return channelVideos;
}
showEmptyChannelCache(channelId) {
// More aggressive channel content clearing
const channelSelectors = [
'#contents',
'#primary-inner',
'div[page-subtype="channels"]',
'.page-container',
'#page-manager',
'ytd-browse[page-subtype="channels"]'
];
let channelContent = null;
for (const selector of channelSelectors) {
channelContent = document.querySelector(selector);
if (channelContent) break;
}
if (!channelContent) {
// Fallback: use body if we can't find specific containers
channelContent = document.body;
}
// Remove ALL existing YouTube channel content more aggressively
const contentToRemove = channelContent.querySelectorAll(`
ytd-c4-tabbed-header-renderer,
ytd-channel-video-player-renderer,
ytd-section-list-renderer,
ytd-item-section-renderer,
ytd-grid-renderer,
ytd-shelf-renderer,
.ytd-browse,
#contents > *:not(#wayback-channel-content),
.channel-header,
.branded-page-v2-container
`);
contentToRemove.forEach(element => {
if (element.id !== 'wayback-channel-content') {
element.style.display = 'none';
}
});
// Remove existing wayback content
const existingContent = document.querySelector('#wayback-channel-content');
if (existingContent) {
existingContent.remove();
}
// Create our replacement content
const channelContainer = document.createElement('div');
channelContainer.id = 'wayback-channel-content';
channelContainer.style.cssText = `
position: relative;
z-index: 9999;
background: white;
min-height: 400px;
width: 100%;
padding: 20px;
`;
channelContainer.innerHTML = `
<div style="padding: 40px; background: #f9f9f9; border-radius: 8px; margin: 20px 0; text-align: center;">
<h3 style="margin: 0 0 16px 0; color: #333;">No Cached Videos Found</h3>
<p style="margin: 0 0 20px 0; color: #666;">
No videos have been cached for this channel yet. Watch some videos from this channel
or use the search button below to find historical content.
</p>
<button id="wayback-search-more-channel" class="wayback-button" style="
background: #ff4444; color: white; border: none; padding: 12px 24px;
border-radius: 4px; cursor: pointer; font-size: 14px;
">
🔍 Search Channel Videos
</button>
</div>
`;
// Insert at the very beginning
if (channelContent.firstChild) {
channelContent.insertBefore(channelContainer, channelContent.firstChild);
} else {
channelContent.appendChild(channelContainer);
}
// Fix the function reference - get the correct context
const searchButton = document.querySelector('#wayback-search-more-channel');
if (searchButton) {
searchButton.addEventListener('click', () => {
// Access through the global waybackTubeManager reference
if (window.waybackTubeManager && window.waybackTubeManager.searchMoreChannelVideos) {
window.waybackTubeManager.searchMoreChannelVideos(channelId, 'This Channel');
} else {
console.error('WayBackTube: searchMoreChannelVideos function not available');
}
});
}
}
setupChannelPageMonitoring() {
// Monitor for URL changes to detect channel page navigation
let currentUrl = window.location.href;
const checkUrlChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
if (OptimizedUtils.getCurrentPage() === 'channel' && this.settings.active) {
setTimeout(() => {
this.replaceChannelPageWithCache();
}, 1500); // Give page time to load
}
}
};
// Check URL changes frequently
setInterval(checkUrlChange, 1000);
// Also monitor DOM changes for channel page content
const observer = new MutationObserver(() => {
if (OptimizedUtils.getCurrentPage() === 'channel' && this.settings.active) {
// Debounce the replacement to avoid excessive calls
clearTimeout(this.channelReplaceTimeout);
this.channelReplaceTimeout = setTimeout(() => {
this.replaceChannelPageWithCache();
}, 2000);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
// === WORKING SEARCH ENHANCEMENT (COPIED VERBATIM) ===
class OptimizedSearchEnhancement {
constructor(timeMachine) {
this.timeMachine = timeMachine;
this.maxDate = timeMachine.getMaxDate();
this.interceptedElements = new Set();
this.init();
}
init() {
this.log('Search enhancement starting...');
this.setupSearchInterception();
this.cleanupSearchResults();
this.hideSearchSuggestions();
}
log(message, ...args) {
if (CONFIG.debugMode) {
console.log('[WayBackTube Search]', message, ...args);
}
}
setupSearchInterception() {
// Monitor for new elements being added to the page
const observer = new MutationObserver(() => {
this.interceptSearchInputs();
this.interceptSearchButtons();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Initial setup
this.interceptSearchInputs();
this.interceptSearchButtons();
}
interceptSearchInputs() {
const interceptInput = (input) => {
if (this.interceptedElements.has(input)) return;
this.interceptedElements.add(input);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
this.performSearch(input.value.trim(), input);
}
});
// Also intercept form submissions
const form = input.closest('form');
if (form && !this.interceptedElements.has(form)) {
this.interceptedElements.add(form);
form.addEventListener('submit', (e) => {
e.preventDefault();
e.stopPropagation();
this.performSearch(input.value.trim(), input);
});
}
};
// Find all search inputs
const searchInputs = document.querySelectorAll('input#search, input[name="search_query"]');
searchInputs.forEach(interceptInput);
}
interceptSearchButtons() {
const interceptButton = (btn) => {
if (this.interceptedElements.has(btn)) return;
this.interceptedElements.add(btn);
btn.addEventListener('click', (e) => {
const input = document.querySelector('input#search, input[name="search_query"]');
if (input && input.value.trim()) {
e.preventDefault();
e.stopPropagation();
this.performSearch(input.value.trim(), input);
}
});
};
// Find all search buttons
const searchButtons = document.querySelectorAll(
'#search-icon-legacy button, ' +
'button[aria-label="Search"], ' +
'ytd-searchbox button, ' +
'.search-button'
);
searchButtons.forEach(interceptButton);
}
performSearch(query, inputElement) {
this.log('Intercepting search for:', query);
// Don't add before: if it already exists
if (query.includes('before:')) {
this.log('Search already contains before: filter, proceeding normally');
this.executeSearch(query, inputElement);
return;
}
// Add the before: filter to the actual search
const beforeDate = this.maxDate.toISOString().split('T')[0];
const modifiedQuery = query + ' before:' + beforeDate;
this.log('Modified search query:', modifiedQuery);
// Execute the search with the modified query
this.executeSearch(modifiedQuery, inputElement);
// Keep the original query visible in the input - multiple attempts
const restoreOriginalQuery = () => {
const searchInputs = document.querySelectorAll('input#search, input[name="search_query"]');
searchInputs.forEach(input => {
if (input.value.includes('before:')) {
input.value = query;
}
});
};
// Restore immediately and with delays
restoreOriginalQuery();
setTimeout(restoreOriginalQuery, 50);
setTimeout(restoreOriginalQuery, 100);
setTimeout(restoreOriginalQuery, 200);
setTimeout(restoreOriginalQuery, 500);
setTimeout(restoreOriginalQuery, 1000);
}
executeSearch(searchQuery, inputElement) {
// Method 1: Try to use YouTube's search API directly
if (this.tryYouTubeSearch(searchQuery)) {
return;
}
// Method 2: Modify the URL and navigate
const searchParams = new URLSearchParams();
searchParams.set('search_query', searchQuery);
const searchUrl = '/results?' + searchParams.toString();
this.log('Navigating to search URL:', searchUrl);
// Show loading indicator to prevent "offline" message
this.showSearchLoading();
window.location.href = searchUrl;
}
showSearchLoading() {
// Create a temporary loading overlay to prevent offline message
const loadingOverlay = document.createElement('div');
loadingOverlay.id = 'tm-search-loading';
loadingOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 15, 15, 0.95);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-family: Roboto, Arial, sans-serif;
font-size: 16px;
`;
loadingOverlay.innerHTML = `
<div style="text-align: center;">
<div style="border: 2px solid #333; border-top: 2px solid #ff0000; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto 16px;"></div>
<div>Searching...</div>
</div>
<style>
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
`;
document.body.appendChild(loadingOverlay);
// Remove loading overlay after a delay (in case search doesn't redirect)
setTimeout(() => {
const overlay = document.getElementById('tm-search-loading');
if (overlay) overlay.remove();
}, 5000);
}
tryYouTubeSearch(query) {
try {
// Try to trigger YouTube's internal search mechanism
if (typeof window !== 'undefined' && window.ytplayer && window.ytplayer.config) {
// Use YouTube's internal router if available
if (typeof window.yt !== 'undefined' && window.yt && window.yt.www && window.yt.www.routing) {
window.yt.www.routing.navigate('/results?search_query=' + encodeURIComponent(query));
return true;
}
}
return false;
} catch (e) {
OptimizedUtils.log('YouTube internal search failed:', e);
return false;
}
}
cleanupSearchResults() {
// Hide search filter chips that show the before: date
const hideBeforeChips = () => {
// Hide all elements that contain "before:" text
const allElements = document.querySelectorAll('*');
allElements.forEach(element => {
if (element.dataset.tmProcessedForBefore) return;
const text = element.textContent || element.innerText || '';
const ariaLabel = element.getAttribute('aria-label') || '';
const title = element.getAttribute('title') || '';
if (text.includes('before:') || ariaLabel.includes('before:') || title.includes('before:')) {
// Check if it's a search filter chip or similar element
if (element.matches('ytd-search-filter-renderer, ytd-toggle-button-renderer, [role="button"], .ytd-search-filter-renderer, .filter-chip')) {
element.style.display = 'none';
element.setAttribute('data-hidden-by-time-machine', 'true');
element.dataset.tmProcessedForBefore = 'true';
}
}
});
};
// Run cleanup immediately and periodically
hideBeforeChips();
setInterval(hideBeforeChips, 200); // More frequent cleanup
// Also run cleanup on page mutations
const observer = new MutationObserver(() => {
setTimeout(hideBeforeChips, 50);
// Also restore search input if it shows before:
setTimeout(() => {
const searchInputs = document.querySelectorAll('input#search, input[name="search_query"]');
searchInputs.forEach(input => {
if (input.value.includes('before:')) {
const originalQuery = input.value.replace(/\s*before:\d{4}-\d{2}-\d{2}/, '').trim();
if (originalQuery) {
input.value = originalQuery;
}
}
});
}, 10);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Continuous monitoring of search input
const self = this;
setInterval(() => {
const searchInputs = document.querySelectorAll('input#search, input[name="search_query"]');
searchInputs.forEach(input => {
if (input.value.includes('before:')) {
const originalQuery = input.value.replace(/\s*before:\d{4}-\d{2}-\d{2}/, '').trim();
if (originalQuery) {
input.value = originalQuery;
}
}
});
// Note: Date modification is handled by SearchManager, not FeedChipHider
}, 100);
}
hideBeforeTags() {
const beforeTags = document.querySelectorAll('span, div');
beforeTags.forEach(element => {
const text = element.textContent;
if (text && text.includes('before:') && text.includes(this.maxDate.toISOString().split('T')[0])) {
element.style.display = 'none';
element.setAttribute('data-hidden-by-time-machine', 'true');
}
});
const filterChips = document.querySelectorAll('ytd-search-filter-renderer');
filterChips.forEach(chip => {
const text = chip.textContent;
if (text && text.includes('before:')) {
chip.style.display = 'none';
chip.setAttribute('data-hidden-by-time-machine', 'true');
}
});
}
hideSearchSuggestions() {
// Hide search suggestions dropdown/autocomplete
const hideSearchDropdown = () => {
const selectors = [
'ytd-search-suggestions-renderer',
'.sbsb_a', // Google search suggestions
'#suggestions-wrapper',
'#suggestions',
'.search-suggestions',
'ytd-searchbox #suggestions',
'tp-yt-iron-dropdown',
'.ytp-suggestions-container',
'#masthead-search-suggestions',
'.search-autocomplete'
];
selectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
if (!el.dataset.tmSuggestionHidden) {
el.style.display = 'none !important';
el.style.visibility = 'hidden !important';
el.style.opacity = '0 !important';
el.style.height = '0 !important';
el.style.overflow = 'hidden !important';
el.setAttribute('data-tm-suggestion-hidden', 'true');
}
});
});
};
// Hide immediately and continuously
hideSearchDropdown();
setInterval(hideSearchDropdown, 100);
// Set up mutation observer for dynamic suggestions
const observer = new MutationObserver(() => {
hideSearchDropdown();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Also disable autocomplete attributes on search inputs
const disableAutocomplete = () => {
const searchInputs = document.querySelectorAll('input#search, input[name="search_query"]');
searchInputs.forEach(input => {
if (!input.dataset.tmAutocompleteDisabled) {
input.setAttribute('autocomplete', 'off');
input.setAttribute('spellcheck', 'false');
input.dataset.tmAutocompleteDisabled = 'true';
}
});
};
disableAutocomplete();
setInterval(disableAutocomplete, 1000);
}
}
// === TAB HIDING FUNCTIONALITY ===
class TabHider {
constructor() {
this.tabsToHide = ['All', 'Music', 'Gaming', 'News', 'Live', 'Sports', 'Learning', 'Fashion & Beauty', 'Podcasts'];
this.isActive = false;
}
init() {
this.isActive = true;
this.startHiding();
OptimizedUtils.log('Tab hider initialized');
}
startHiding() {
// Hide tabs immediately and then continuously
this.hideTabs();
setInterval(() => this.hideTabs(), 500);
}
hideTabs() {
if (!this.isActive) return;
// Hide YouTube navigation tabs
const tabSelectors = [
'tp-yt-paper-tab',
'.ytd-feed-filter-chip-bar-renderer',
'ytd-feed-filter-chip-bar-renderer yt-chip-cloud-chip-renderer',
'[role="tab"]'
];
tabSelectors.forEach(selector => {
const tabs = OptimizedUtils.$$(selector);
tabs.forEach(tab => {
const text = tab.textContent?.trim();
if (text && this.tabsToHide.includes(text)) {
tab.style.display = 'none';
tab.setAttribute('data-hidden-by-wayback', 'true');
}
});
});
// Hide feed filter chips
const chipSelectors = [
'yt-chip-cloud-chip-renderer',
'.ytd-feed-filter-chip-bar-renderer yt-chip-cloud-chip-renderer',
'ytd-feed-filter-chip-bar-renderer [role="tab"]'
];
chipSelectors.forEach(selector => {
const chips = OptimizedUtils.$$(selector);
chips.forEach(chip => {
const text = chip.textContent?.trim();
if (text && this.tabsToHide.includes(text)) {
chip.style.display = 'none';
chip.setAttribute('data-hidden-by-wayback', 'true');
}
});
});
// Hide entire filter bar if needed
const filterBars = OptimizedUtils.$$('ytd-feed-filter-chip-bar-renderer');
filterBars.forEach(bar => {
const visibleChips = bar.querySelectorAll('yt-chip-cloud-chip-renderer:not([data-hidden-by-wayback])');
if (visibleChips.length === 0) {
bar.style.display = 'none';
}
});
}
stop() {
this.isActive = false;
// Restore hidden tabs
const hiddenElements = OptimizedUtils.$$('[data-hidden-by-wayback]');
hiddenElements.forEach(element => {
element.style.display = '';
element.removeAttribute('data-hidden-by-wayback');
});
}
}
// === SUBSCRIPTION MANAGER ===
class SubscriptionManager {
constructor() {
this.subscriptions = this.loadSubscriptions();
}
loadSubscriptions() {
const saved = GM_getValue('ytSubscriptions', '[]');
try {
return JSON.parse(saved);
} catch (e) {
OptimizedUtils.log('Failed to parse subscriptions, using defaults');
return this.getDefaultSubscriptions();
}
}
getDefaultSubscriptions() {
return [
{ name: 'PewDiePie', id: 'UC-lHJZR3Gqxm24_Vd_AJ5Yw' },
{ name: 'Markiplier', id: 'UC7_YxT-KID8kRbqZo7MyscQ' },
{ name: 'Jacksepticeye', id: 'UCYzPXprvl5Y-Sf0g4vX-m6g' },
{ name: 'VanossGaming', id: 'UCKqH_9mk1waLgBiL2vT5b9g' },
{ name: 'nigahiga', id: 'UCSAUGyc_xA8uYzaIVG6MESQ' },
{ name: 'Smosh', id: 'UCY30JRSgfhYXA6i6xX1erWg' },
{ name: 'RayWilliamJohnson', id: 'UCGt7X90Au6BV8rf49BiM6Dg' },
{ name: 'Fred', id: 'UCJKjhgPJcPlW0WV_YQCXq_A' },
{ name: 'Shane', id: 'UCtinbF-Q-fVthA0qrFQTgXQ' },
{ name: 'TheFineBros', id: 'UC0v-tlzsn0QZwJnkiaUSJVQ' }
];
}
saveSubscriptions() {
GM_setValue('ytSubscriptions', JSON.stringify(this.subscriptions));
}
getSubscriptions() {
return this.subscriptions;
}
addSubscription(name, id = null) {
if (!name.trim()) return false;
const subscription = {
name: name.trim(),
id: id
};
// Check for duplicates
const exists = this.subscriptions.some(sub =>
sub.name.toLowerCase() === subscription.name.toLowerCase()
);
if (!exists) {
this.subscriptions.push(subscription);
this.saveSubscriptions();
OptimizedUtils.log(`Added subscription: ${subscription.name}`);
return true;
}
return false;
}
removeSubscription(index) {
if (index >= 0 && index < this.subscriptions.length) {
const removed = this.subscriptions.splice(index, 1)[0];
this.saveSubscriptions();
OptimizedUtils.log(`Removed subscription: ${removed.name}`);
return true;
}
return false;
}
clearAllSubscriptions() {
this.subscriptions = [];
this.saveSubscriptions();
OptimizedUtils.log('Cleared all subscriptions');
}
}
// === FEED CHIP HIDER ===
class FeedChipHider {
constructor() {
this.hiddenChips = new Set();
this.initializeHiding();
}
initializeHiding() {
setInterval(() => {
this.hideTopTabs();
}, 200);
}
hideTopTabs() {
// Hide feed filter chips (All, Music, Gaming, etc.)
CONFIG.FEED_CHIP_SELECTORS.forEach(selector => {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
if (!element.dataset.tmHidden) {
element.style.display = 'none !important';
element.style.visibility = 'hidden !important';
element.style.opacity = '0 !important';
element.style.height = '0 !important';
element.style.maxHeight = '0 !important';
element.style.overflow = 'hidden !important';
element.dataset.tmHidden = 'true';
this.hiddenChips.add(element);
}
});
});
// Hide specific tab-like elements by text content
const tabSelectors = [
'yt-chip-cloud-chip-renderer',
'ytd-feed-filter-chip-renderer',
'paper-tab[role="tab"]',
'[role="tab"]'
];
tabSelectors.forEach(selector => {
const tabs = document.querySelectorAll(selector);
tabs.forEach(tab => {
const text = tab.textContent?.toLowerCase().trim();
if (text && ['all', 'music', 'gaming', 'news', 'live', 'sports', 'learning', 'fashion & beauty', 'gaming'].includes(text)) {
if (!tab.dataset.tmHidden) {
tab.style.display = 'none !important';
tab.style.visibility = 'hidden !important';
tab.dataset.tmHidden = 'true';
this.hiddenChips.add(tab);
}
}
});
});
}
hideSearchFilters() {
const elementsToCheck = document.querySelectorAll('*:not([data-tm-processed-for-before])');
elementsToCheck.forEach(element => {
const text = element.textContent || '';
const ariaLabel = element.getAttribute('aria-label') || '';
const title = element.getAttribute('title') || '';
if (text.includes('before:') || ariaLabel.includes('before:') || title.includes('before:')) {
if (element.matches('ytd-search-filter-renderer, .search-filter-chip, ytd-chip-cloud-chip-renderer, ytd-search-sub-menu-renderer *')) {
element.style.display = 'none !important';
element.style.visibility = 'hidden !important';
element.style.opacity = '0 !important';
element.style.height = '0 !important';
element.style.width = '0 !important';
element.style.position = 'absolute !important';
element.style.left = '-9999px !important';
element.setAttribute('data-hidden-by-time-machine', 'true');
}
let parent = element.parentElement;
while (parent && parent !== document.body) {
if (parent.matches('ytd-search-filter-renderer, ytd-chip-cloud-chip-renderer, ytd-search-sub-menu-renderer')) {
parent.style.display = 'none !important';
parent.setAttribute('data-hidden-by-time-machine', 'true');
break;
}
parent = parent.parentElement;
}
}
element.dataset.tmProcessedForBefore = 'true';
});
const specificSelectors = [
'ytd-search-filter-renderer',
'ytd-chip-cloud-chip-renderer',
'ytd-search-sub-menu-renderer',
'.search-filter-chip',
'[data-text*="before:"]',
'[aria-label*="before:"]',
'[title*="before:"]'
];
specificSelectors.forEach(selector => {
try {
const elements = document.querySelectorAll(selector);
elements.forEach(element => {
element.style.display = 'none !important';
element.setAttribute('data-hidden-by-time-machine', 'true');
});
} catch (e) {
// Ignore selector errors
}
});
}
}
// === ENHANCED SEARCH MANAGER ===
class SearchManager {
constructor(apiManager, maxDate) {
this.apiManager = apiManager;
this.maxDate = maxDate;
this.feedChipHider = new FeedChipHider();
this.initializeSearchHiding();
}
initializeSearchHiding() {
setInterval(() => {
this.feedChipHider.hideSearchFilters();
// Also modify dates in search results from the correct context
if (OptimizedUtils.getCurrentPage() === 'search') {
this.modifyDatesInSearchResults();
}
// Process comments on watch pages
if (OptimizedUtils.getCurrentPage() === 'watch') {
this.modifyCommentDates();
}
}, 500); // Increased interval to reduce excessive logging
const observer = new MutationObserver(() => {
setTimeout(() => {
this.feedChipHider.hideSearchFilters();
if (OptimizedUtils.getCurrentPage() === 'search') {
this.modifyDatesInSearchResults();
}
if (OptimizedUtils.getCurrentPage() === 'watch') {
this.modifyCommentDates();
}
}, 2000); // Increased delay to prevent excessive processing
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
modifyDatesInSearchResults() {
if (OptimizedUtils.getCurrentPage() !== 'search') return;
// Find all video duration/date elements in search results
const videoElements = document.querySelectorAll('ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer');
videoElements.forEach(videoElement => {
if (videoElement.dataset.tmDateModified) return;
// Look for upload date elements - using valid CSS selectors only
const dateSelectors = [
'.ytd-video-meta-block .ytd-video-meta-block span:nth-child(2)',
'#metadata .ytd-video-meta-block span:nth-child(2)',
'.ytd-video-meta-block div:nth-child(2)',
'.ytd-video-meta-block [aria-label*="ago"]',
'#metadata-line .ytd-video-meta-block span',
'#metadata .published-time-text',
'.video-time',
'.ytd-video-meta-block span:last-child'
];
// Find elements containing "ago" text manually
const metadataSpans = videoElement.querySelectorAll('[id*="metadata"] span');
metadataSpans.forEach(span => {
if (span.textContent && span.textContent.includes('ago')) {
if (this.isRelativeDateElement(span)) {
this.updateRelativeDate(span);
videoElement.dataset.tmDateModified = 'true';
}
}
});
// Process other selectors
for (const selector of dateSelectors) {
try {
const dateElement = videoElement.querySelector(selector);
if (dateElement && this.isRelativeDateElement(dateElement)) {
this.updateRelativeDate(dateElement);
videoElement.dataset.tmDateModified = 'true';
break;
}
} catch (error) {
// Skip invalid selectors
continue;
}
}
});
}
isRelativeDateElement(element) {
const text = element.textContent || element.innerText || '';
const relativePatterns = [
/\d+\s+(year|month|week|day|hour|minute)s?\s+ago/i,
/\d+\s+(yr|mo|wk|day|hr|min)s?\s+ago/i,
/(yesterday|today)/i
];
return relativePatterns.some(pattern => pattern.test(text));
}
updateRelativeDate(element) {
const originalText = element.textContent || element.innerText || '';
const maxDate = this.maxDate;
if (!maxDate) return;
// Parse the original relative date
const match = originalText.match(/(\d+)\s+(year|month|week|day|hour|minute)s?\s+ago/i);
if (match) {
const value = parseInt(match[1]);
const unit = match[2].toLowerCase();
// Calculate original upload date from current time (not time machine date)
const now = new Date();
let uploadDate = new Date(now);
// Use proper date arithmetic for accurate calculations
switch (unit) {
case 'year':
uploadDate.setFullYear(uploadDate.getFullYear() - value);
break;
case 'month':
uploadDate.setMonth(uploadDate.getMonth() - value);
break;
case 'week':
uploadDate.setDate(uploadDate.getDate() - (value * 7));
break;
case 'day':
uploadDate.setDate(uploadDate.getDate() - value);
break;
case 'hour':
uploadDate.setHours(uploadDate.getHours() - value);
break;
case 'minute':
uploadDate.setMinutes(uploadDate.getMinutes() - value);
break;
}
// Apply 1-year leniency for future date detection
const maxDateWithLeniency = new Date(maxDate);
maxDateWithLeniency.setFullYear(maxDateWithLeniency.getFullYear() + 1);
if (uploadDate <= maxDate) {
// Video is within the time machine date - update normally
const newRelativeDate = OptimizedUtils.calculateRelativeDate(uploadDate, maxDate);
if (newRelativeDate && newRelativeDate !== originalText) {
element.textContent = newRelativeDate;
element.title = `Original: ${originalText} | Adjusted for ${OptimizedUtils.formatDate(maxDate)}`;
}
} else if (uploadDate <= maxDateWithLeniency) {
// Video is within 1-year leniency - show random months from 6-11
const randomMonths = Math.floor(Math.random() * 6) + 6; // 6-11 months
element.textContent = `${randomMonths} month${randomMonths > 1 ? 's' : ''} ago`;
element.title = `Original: ${originalText} | Within grace period, showing random months`;
return;
} else {
// Video is more than 1 year in the future - remove completely
const videoElement = element.closest('ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer');
if (videoElement) {
OptimizedUtils.log(`Removing future video from search: ${originalText} (uploaded after ${OptimizedUtils.formatDate(maxDateWithLeniency)})`);
videoElement.style.display = 'none';
videoElement.remove();
}
}
}
}
modifyCommentDates() {
const currentPage = OptimizedUtils.getCurrentPage();
if (currentPage !== 'watch') {
// OptimizedUtils.log(`Not on watch page, current page: ${currentPage}`);
return;
}
// Find all comment date elements with multiple selectors
const commentSelectors = [
'ytd-comment-view-model',
'ytd-comment-replies-renderer',
'ytd-comment-thread-renderer',
'#comments #contents',
'ytd-comments',
'#comment'
];
let commentElements = [];
commentSelectors.forEach(selector => {
const elements = document.querySelectorAll(selector);
commentElements = [...commentElements, ...Array.from(elements)];
});
// Also try to find comment date links directly with more selectors
const allDateLinks = document.querySelectorAll(`
a.yt-simple-endpoint[href*="lc="],
a[href*="lc="],
.published-time-text,
[aria-label*="ago"],
#published-time-text,
.style-scope.ytd-comment-view-model a
`);
// Debug elements found
OptimizedUtils.log(`Found ${commentElements.length} comment containers and ${allDateLinks.length} date links`);
// Process direct date links first
allDateLinks.forEach(dateLink => {
if (dateLink.dataset.tmCommentDateModified) return;
const dateText = dateLink.textContent.trim();
OptimizedUtils.log(`Processing comment date link: "${dateText}"`);
// Check if this looks like a relative date
if (this.isRelativeDateElement({ textContent: dateText })) {
this.updateCommentDate(dateLink, dateText);
dateLink.dataset.tmCommentDateModified = 'true';
}
});
// Also process within comment containers
commentElements.forEach(commentElement => {
if (commentElement.dataset.tmCommentDateModified) return;
// Look for comment date links - using broader selectors
const dateLinks = commentElement.querySelectorAll('a[href*="lc="], .published-time-text, [class*="published"], [class*="time"]');
dateLinks.forEach(dateLink => {
if (dateLink.dataset.tmCommentDateModified) return;
const dateText = dateLink.textContent.trim();
// Check if this looks like a relative date
if (this.isRelativeDateElement({ textContent: dateText })) {
OptimizedUtils.log(`Processing comment in container: "${dateText}"`);
this.updateCommentDate(dateLink, dateText);
dateLink.dataset.tmCommentDateModified = 'true';
}
});
commentElement.dataset.tmCommentDateModified = 'true';
});
}
updateCommentDate(element, originalText) {
const maxDate = this.maxDate;
if (!maxDate) {
OptimizedUtils.log('No maxDate available for comment processing');
return;
}
OptimizedUtils.log(`Updating comment date: "${originalText}" with maxDate: ${OptimizedUtils.formatDate(maxDate)}`);
// Parse the original relative date
const match = originalText.match(/(\d+)\s+(year|month|week|day|hour|minute)s?\s+ago/i);
if (match) {
OptimizedUtils.log(`Matched comment date pattern: ${match[1]} ${match[2]}`);
const value = parseInt(match[1]);
const unit = match[2].toLowerCase();
// Calculate original comment date from current time
const now = new Date();
let commentDate = new Date(now);
// Use proper date arithmetic for accurate calculations
switch (unit) {
case 'year':
commentDate.setFullYear(commentDate.getFullYear() - value);
break;
case 'month':
commentDate.setMonth(commentDate.getMonth() - value);
break;
case 'week':
commentDate.setDate(commentDate.getDate() - (value * 7));
break;
case 'day':
commentDate.setDate(commentDate.getDate() - value);
break;
case 'hour':
commentDate.setHours(commentDate.getHours() - value);
break;
case 'minute':
commentDate.setMinutes(commentDate.getMinutes() - value);
break;
}
// Apply 1-year leniency for future date detection
const maxDateWithLeniency = new Date(maxDate);
maxDateWithLeniency.setFullYear(maxDateWithLeniency.getFullYear() + 1);
if (commentDate <= maxDate) {
// Comment is within time machine date - update normally
const newRelativeDate = OptimizedUtils.calculateRelativeDate(commentDate, maxDate);
if (newRelativeDate && newRelativeDate !== originalText) {
element.textContent = newRelativeDate;
element.title = `Original: ${originalText} | Adjusted for ${OptimizedUtils.formatDate(maxDate)}`;
}
} else if (commentDate <= maxDateWithLeniency) {
// Comment is within 1-year leniency - show random months from 6-11
const randomMonths = Math.floor(Math.random() * 6) + 6; // 6-11 months
element.textContent = `${randomMonths} month${randomMonths > 1 ? 's' : ''} ago`;
element.title = `Original: ${originalText} | Within grace period, showing random months`;
} else {
// Comment is more than 1 year in the future - remove completely
const commentContainer = element.closest('ytd-comment-view-model, ytd-comment-replies-renderer, ytd-comment-thread-renderer');
if (commentContainer) {
OptimizedUtils.log(`Removing future comment: ${originalText} (posted after ${OptimizedUtils.formatDate(maxDateWithLeniency)})`);
commentContainer.style.display = 'none';
commentContainer.remove();
}
}
}
}
injectSearchDateFilter() {
if (location.pathname !== '/results') return;
const urlParams = new URLSearchParams(location.search);
let searchQuery = urlParams.get('search_query') || urlParams.get('q') || '';
if (!searchQuery.includes('before:') && this.maxDate) {
const beforeDate = this.maxDate.toISOString().split('T')[0];
const newQuery = `${searchQuery} before:${beforeDate}`;
urlParams.set('search_query', newQuery);
const newUrl = `${location.pathname}?${urlParams.toString()}`;
if (newUrl !== location.href) {
OptimizedUtils.log(`Injecting date filter: ${beforeDate}`);
window.history.replaceState({}, '', newUrl);
}
}
}
updateMaxDate(newMaxDate) {
this.maxDate = newMaxDate;
}
}
// === ENHANCED UI MANAGER ===
class EnhancedUIManager {
constructor(timeMachine) {
this.timeMachine = timeMachine;
this.isVisible = GM_getValue('ytTimeMachineUIVisible', true);
this.init();
}
init() {
// Wait for DOM to be ready before creating UI
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.createUI();
this.setupKeyboardShortcuts();
});
} else {
this.createUI();
this.setupKeyboardShortcuts();
}
}
createUI() {
GM_addStyle(`
.yt-time-machine-ui {
position: fixed;
top: 10px;
right: 10px;
width: 320px;
max-height: 80vh;
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
border: 1px solid #444;
border-radius: 12px;
padding: 16px;
font-family: 'Roboto', sans-serif;
font-size: 13px;
color: #fff;
z-index: 10000;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
transition: opacity 0.3s ease;
overflow-y: auto;
overflow-x: hidden;
}
.yt-time-machine-ui::-webkit-scrollbar {
width: 8px;
}
.yt-time-machine-ui::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.yt-time-machine-ui::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.yt-time-machine-ui::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.yt-time-machine-ui.hidden {
opacity: 0;
pointer-events: none;
}
.yt-time-machine-toggle {
position: fixed;
top: 10px;
right: 10px;
width: 40px;
height: 40px;
background: #c41e3a;
border: none;
border-radius: 50%;
color: white;
font-size: 16px;
cursor: pointer;
z-index: 10001;
display: none;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(196, 30, 58, 0.4);
}
.yt-time-machine-toggle.visible {
display: flex;
}
.tm-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
color: #ff6b6b;
text-align: center;
position: relative;
}
.tm-minimize-btn {
position: absolute;
right: 0;
top: -2px;
background: #666;
color: white;
border: none;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.tm-minimize-btn:hover {
background: #888;
}
.tm-input {
width: 100%;
padding: 8px 10px;
margin: 4px 0;
border: 1px solid #555;
border-radius: 6px;
background: #333;
color: #fff;
font-size: 12px;
}
.tm-button {
padding: 8px 12px;
margin: 4px 2px;
border: none;
border-radius: 6px;
background: #4a90e2;
color: white;
cursor: pointer;
font-size: 11px;
transition: background 0.2s ease;
}
.tm-button:hover {
background: #357abd;
}
.tm-button:disabled {
background: #666;
cursor: not-allowed;
}
.tm-button.tm-danger {
background: #dc3545;
}
.tm-button.tm-danger:hover {
background: #c82333;
}
.tm-section {
margin: 12px 0;
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
.tm-section h3 {
margin: 0 0 8px 0;
font-size: 12px;
color: #4a90e2;
}
.tm-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
font-size: 11px;
}
.tm-stat {
background: rgba(0, 0, 0, 0.3);
padding: 6px;
border-radius: 4px;
text-align: center;
}
.tm-subscription-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
border-bottom: 1px solid #444;
}
.tm-subscription-item:last-child {
border-bottom: none;
}
.tm-subscription-name {
flex: 1;
font-size: 11px;
}
.tm-remove-btn {
background: #e74c3c;
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
cursor: pointer;
}
.tm-loading {
text-align: center;
padding: 20px;
color: #4a90e2;
}
/* Homepage Video Cards */
.tm-homepage-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 16px;
}
.tm-video-card {
background: #0f0f0f;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.tm-video-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.tm-video-card img {
width: 100%;
height: 135px;
object-fit: cover;
border-radius: 0;
}
.tm-video-info {
padding: 12px;
}
.tm-video-title {
font-size: 14px;
font-weight: 500;
color: #fff;
line-height: 1.3;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tm-video-channel {
font-size: 12px;
color: #aaa;
margin-bottom: 4px;
}
.tm-video-meta {
font-size: 12px;
color: #aaa;
}
.tm-video-meta span {
margin-right: 8px;
}
/* Vintage theme toggle button */
.tm-vintage-toggle {
background: linear-gradient(135deg, #8b4513, #a0522d) !important;
color: white !important;
}
.tm-vintage-toggle:hover {
background: linear-gradient(135deg, #a0522d, #d2b48c) !important;
color: #333 !important;
}
.tm-vintage-toggle.active {
background: linear-gradient(135deg, #228b22, #32cd32) !important;
color: white !important;
}
.tm-vintage-toggle.active:hover {
background: linear-gradient(135deg, #32cd32, #90ee90) !important;
color: #333 !important;
}
/* Watch Next Cards - Enhanced and bigger */
.tm-watch-next {
background: #0f0f0f;
padding: 16px;
margin-bottom: 16px;
position: relative;
z-index: 999999 !important;
min-height: 400px;
}
.tm-watch-next h3 {
color: #fff;
font-size: 16px;
margin: 0 0 16px 0;
font-weight: normal;
}
.tm-watch-next-grid {
display: flex;
flex-direction: column;
gap: 0;
}
.tm-watch-next-card {
display: flex;
cursor: pointer;
padding: 12px 0;
border-bottom: 1px solid #333;
}
.tm-watch-next-card:hover {
background: #1a1a1a;
}
.tm-watch-next-card img {
width: 140px;
height: 78px;
object-fit: cover;
margin-right: 12px;
}
.tm-watch-next-info {
flex: 1;
}
.tm-watch-next-title {
font-size: 13px;
color: #fff;
line-height: 1.3;
margin-bottom: 4px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tm-watch-next-channel {
font-size: 12px;
color: #999;
margin-bottom: 3px;
}
.tm-watch-next-meta {
font-size: 11px;
color: #666;
}
`);
const uiHTML = `
<div id="timeMachineUI" class="yt-time-machine-ui ${this.isVisible ? '' : 'hidden'}">
<div class="tm-title">
⏰ WayBackTube
<button class="tm-minimize-btn" id="tmMinimizeBtn">×</button>
</div>
<div class="tm-section">
<label>Travel to date:</label>
<input type="date" id="tmDateInput" class="tm-input" value="${this.timeMachine.getDateString()}">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px;">
<button id="tmApplyDate" class="tm-button">Apply Date</button>
<button id="tmRandomDate" class="tm-button">Random Date</button>
</div>
</div>
<div class="tm-section">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button id="tmToggle" class="tm-button">${this.timeMachine.settings.active ? 'Disable' : 'Enable'}</button>
<button id="tmClearCache" class="tm-button">Clear Cache</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px;">
<button id="tmRefreshVideosBtn" class="tm-button">Refresh Videos</button>
<button id="tmVintageToggle" class="tm-button tm-vintage-toggle">🕰️ 2011 Theme</button>
</div>
<div style="margin-top: 8px;">
<label style="display: flex; align-items: center; color: #fff; font-size: 12px;">
<input type="checkbox" id="tmDateRotation" style="margin-right: 6px;">
Auto-advance date daily (simulates real uploads)
</label>
</div>
<div style="margin-top: 8px;">
<label style="display: flex; align-items: center; color: #fff; font-size: 12px;">
<input type="checkbox" id="tmTestClock" style="margin-right: 6px;">
Real-time Clock (new videos every 4h, date advances daily)
</label>
</div>
</div>
<div class="tm-section">
<h3>Search Terms</h3>
<input type="text" id="tmSearchTermInput" class="tm-input" placeholder="Enter search term (e.g. memes, gaming)">
<button id="tmAddSearchTerm" class="tm-button" style="width: 100%; margin-top: 4px;">Add Search Term</button>
<div id="tmSearchTermList"></div>
<button id="tmClearSearchTerms" class="tm-button" style="width: 100%; margin-top: 8px; background: #dc3545;">Clear All Search Terms</button>
</div>
<div class="tm-section">
<h3>Real-Time Clock</h3>
<div style="text-align: center; padding: 12px; background: rgba(0,0,0,0.3); border-radius: 6px; margin-bottom: 12px;">
<div style="font-family: monospace; font-size: 18px; color: #00ff00; font-weight: bold;" id="tm-clock-display">
${GM_getValue('ytTestClockEnabled', false) ? 'Day 0, 00:00:00' : 'Clock not running'}
</div>
<div style="font-size: 11px; color: #888; margin-top: 4px;">
New videos every 4 hours • Date advances every 24 hours
</div>
</div>
</div>
<div class="tm-section">
<h3>Statistics</h3>
<div class="tm-stats">
<div class="tm-stat">
<div>API Calls</div>
<div id="tmApiCalls">0</div>
</div>
<div class="tm-stat">
<div>Videos Processed</div>
<div id="tmVideosProcessed">0</div>
</div>
<div class="tm-stat">
<div>Cache Hits</div>
<div id="tmCacheHits">0</div>
</div>
<div class="tm-stat">
<div>Videos Filtered</div>
<div id="tmVideosFiltered">0</div>
</div>
</div>
</div>
<div class="tm-section">
<h3>API Keys</h3>
<input type="text" id="tmApiKeyInput" class="tm-input" placeholder="Enter YouTube API key">
<div style="display: flex; gap: 4px; margin-top: 4px;">
<button id="tmAddApiKey" class="tm-button" style="flex: 1;">Add Key</button>
<button id="tmTestAllKeys" class="tm-button" style="flex: 1;">Test All</button>
</div>
<div id="tmApiKeyList"></div>
</div>
<div class="tm-section">
<h3>Subscriptions</h3>
<input type="text" id="tmSubscriptionInput" class="tm-input" placeholder="Enter channel name">
<button id="tmAddSubscription" class="tm-button" style="width: 100%; margin-top: 4px;">Add Channel</button>
<div id="tmSubscriptionList"></div>
</div>
<div style="font-size: 10px; color: #666; text-align: center; margin-top: 15px;">
Press Ctrl+Shift+T to toggle UI
</div>
</div>
<button id="timeMachineToggle" class="yt-time-machine-toggle ${this.isVisible ? '' : 'visible'}">⏰</button>
`;
// Safely insert UI with error handling
this.safeInsertUI(uiHTML);
}
safeInsertUI(uiHTML) {
const insertUI = () => {
try {
if (!document.body) {
// Body not ready yet, wait a bit
setTimeout(insertUI, 100);
return;
}
// Remove any existing UI first
const existingUI = document.querySelector('.yt-time-machine-ui');
const existingToggle = document.querySelector('.yt-time-machine-toggle');
if (existingUI) existingUI.remove();
if (existingToggle) existingToggle.remove();
// Create elements using safe DOM construction
this.createUIElements();
this.attachEventListeners();
this.initializeVintageTheme();
// Defer updateUI to ensure timeMachine is fully initialized
setTimeout(() => {
if (this.timeMachine && this.timeMachine.isInitialized) {
this.updateUI();
}
}, 100);
OptimizedUtils.log('UI successfully created');
} catch (error) {
OptimizedUtils.log('UI creation error:', error);
// Don't retry to prevent infinite loops
console.error('[WayBackTube] UI creation failed permanently:', error);
}
};
insertUI();
}
createUIElements() {
// Create main UI container
const uiContainer = document.createElement('div');
uiContainer.className = `yt-time-machine-ui ${this.isVisible ? '' : 'hidden'}`;
uiContainer.id = 'timeMachineUI';
// Create title section
const titleDiv = document.createElement('div');
titleDiv.className = 'tm-title';
titleDiv.textContent = '⏰ WayBackTube';
const minimizeBtn = document.createElement('button');
minimizeBtn.className = 'tm-minimize-btn';
minimizeBtn.id = 'tmMinimizeBtn';
minimizeBtn.textContent = '×';
titleDiv.appendChild(minimizeBtn);
uiContainer.appendChild(titleDiv);
// Create date section
const dateSection = document.createElement('div');
dateSection.className = 'tm-section';
const dateLabel = document.createElement('label');
dateLabel.textContent = 'Travel to date:';
dateSection.appendChild(dateLabel);
const dateInput = document.createElement('input');
dateInput.type = 'date';
dateInput.id = 'tmDateInput';
dateInput.className = 'tm-input';
dateSection.appendChild(dateInput);
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = 'display: flex; gap: 4px; margin-top: 4px;';
const applyBtn = document.createElement('button');
applyBtn.id = 'tmApplyDate';
applyBtn.className = 'tm-button';
applyBtn.style.flex = '1';
applyBtn.textContent = 'Apply';
const randomBtn = document.createElement('button');
randomBtn.id = 'tmRandomDate';
randomBtn.className = 'tm-button';
randomBtn.style.flex = '1';
randomBtn.textContent = 'Random';
buttonContainer.appendChild(applyBtn);
buttonContainer.appendChild(randomBtn);
dateSection.appendChild(buttonContainer);
uiContainer.appendChild(dateSection);
// Create control section
const controlSection = document.createElement('div');
controlSection.className = 'tm-section';
const controlGrid = document.createElement('div');
controlGrid.style.cssText = 'display: grid; grid-template-columns: 1fr 1fr; gap: 8px;';
const toggleBtn = document.createElement('button');
toggleBtn.id = 'tmToggle';
toggleBtn.className = 'tm-button primary';
toggleBtn.textContent = 'Activate';
const vintageBtn = document.createElement('button');
vintageBtn.id = 'tmVintageToggle';
vintageBtn.className = 'tm-button';
vintageBtn.textContent = '2011 Theme';
controlGrid.appendChild(toggleBtn);
controlGrid.appendChild(vintageBtn);
controlSection.appendChild(controlGrid);
uiContainer.appendChild(controlSection);
// Create statistics section
const statsSection = document.createElement('div');
statsSection.className = 'tm-section';
const statsTitle = document.createElement('h3');
statsTitle.textContent = 'Statistics';
statsSection.appendChild(statsTitle);
const statsGrid = document.createElement('div');
statsGrid.style.cssText = 'display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 12px;';
// Create stat items
const createStat = (label, id) => {
const statDiv = document.createElement('div');
statDiv.className = 'tm-stat';
const labelDiv = document.createElement('div');
labelDiv.textContent = label;
statDiv.appendChild(labelDiv);
const valueDiv = document.createElement('div');
valueDiv.id = id;
valueDiv.textContent = '0';
statDiv.appendChild(valueDiv);
return statDiv;
};
statsGrid.appendChild(createStat('API Calls', 'tmApiCalls'));
statsGrid.appendChild(createStat('Videos Processed', 'tmVideosProcessed'));
statsGrid.appendChild(createStat('Cache Hits', 'tmCacheHits'));
statsGrid.appendChild(createStat('Videos Filtered', 'tmVideosFiltered'));
statsSection.appendChild(statsGrid);
uiContainer.appendChild(statsSection);
// Create API Keys section
const apiSection = document.createElement('div');
apiSection.className = 'tm-section';
const apiTitle = document.createElement('h3');
apiTitle.textContent = 'API Keys';
apiSection.appendChild(apiTitle);
const apiKeyInput = document.createElement('input');
apiKeyInput.type = 'text';
apiKeyInput.id = 'tmApiKeyInput';
apiKeyInput.className = 'tm-input';
apiKeyInput.placeholder = 'Enter YouTube API key';
apiSection.appendChild(apiKeyInput);
const apiButtonContainer = document.createElement('div');
apiButtonContainer.style.cssText = 'display: flex; gap: 4px; margin-top: 4px;';
const addKeyBtn = document.createElement('button');
addKeyBtn.id = 'tmAddApiKey';
addKeyBtn.className = 'tm-button';
addKeyBtn.style.flex = '1';
addKeyBtn.textContent = 'Add Key';
const testKeysBtn = document.createElement('button');
testKeysBtn.id = 'tmTestAllKeys';
testKeysBtn.className = 'tm-button';
testKeysBtn.style.flex = '1';
testKeysBtn.textContent = 'Test All';
apiButtonContainer.appendChild(addKeyBtn);
apiButtonContainer.appendChild(testKeysBtn);
apiSection.appendChild(apiButtonContainer);
const apiKeyList = document.createElement('div');
apiKeyList.id = 'tmApiKeyList';
apiSection.appendChild(apiKeyList);
uiContainer.appendChild(apiSection);
// Create Subscriptions section
const subSection = document.createElement('div');
subSection.className = 'tm-section';
const subTitle = document.createElement('h3');
subTitle.textContent = 'Subscriptions';
subSection.appendChild(subTitle);
const subInput = document.createElement('input');
subInput.type = 'text';
subInput.id = 'tmSubscriptionInput';
subInput.className = 'tm-input';
subInput.placeholder = 'Enter channel name';
subSection.appendChild(subInput);
const addSubBtn = document.createElement('button');
addSubBtn.id = 'tmAddSubscription';
addSubBtn.className = 'tm-button';
addSubBtn.style.cssText = 'width: 100%; margin-top: 4px;';
addSubBtn.textContent = 'Add Channel';
subSection.appendChild(addSubBtn);
const subList = document.createElement('div');
subList.id = 'tmSubscriptionList';
subSection.appendChild(subList);
uiContainer.appendChild(subSection);
// Create Search Terms section
const searchSection = document.createElement('div');
searchSection.className = 'tm-section';
const searchTitle = document.createElement('h3');
searchTitle.textContent = 'Search Terms';
searchSection.appendChild(searchTitle);
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.id = 'tmSearchTermInput';
searchInput.className = 'tm-input';
searchInput.placeholder = 'Enter search term';
searchSection.appendChild(searchInput);
const addSearchBtn = document.createElement('button');
addSearchBtn.id = 'tmAddSearchTerm';
addSearchBtn.className = 'tm-button';
addSearchBtn.style.cssText = 'width: 100%; margin-top: 4px;';
addSearchBtn.textContent = 'Add Search Term';
searchSection.appendChild(addSearchBtn);
const searchList = document.createElement('div');
searchList.id = 'tmSearchTermList';
searchSection.appendChild(searchList);
uiContainer.appendChild(searchSection);
// Create footer text
const footerDiv = document.createElement('div');
footerDiv.style.cssText = 'font-size: 10px; color: #666; text-align: center; margin-top: 15px;';
footerDiv.textContent = 'Press Ctrl+Shift+T to toggle UI';
uiContainer.appendChild(footerDiv);
// Create toggle button
const toggleButton = document.createElement('button');
toggleButton.id = 'timeMachineToggle';
toggleButton.className = `yt-time-machine-toggle ${this.isVisible ? '' : 'visible'}`;
toggleButton.textContent = '⏰';
// Append to body
document.body.appendChild(uiContainer);
document.body.appendChild(toggleButton);
}
attachEventListeners() {
// Toggle button
document.getElementById('timeMachineToggle')?.addEventListener('click', () => {
this.toggleUI();
});
// Minimize button
document.getElementById('tmMinimizeBtn')?.addEventListener('click', () => {
this.toggleUI();
});
// Date controls
document.getElementById('tmApplyDate')?.addEventListener('click', () => {
const dateInput = document.getElementById('tmDateInput');
if (dateInput.value) {
this.timeMachine.setDate(dateInput.value);
this.updateUI();
}
});
document.getElementById('tmRandomDate')?.addEventListener('click', () => {
this.timeMachine.setRandomDate();
this.updateUI();
});
// Main controls
document.getElementById('tmToggle')?.addEventListener('click', () => {
this.timeMachine.toggle();
this.updateUI();
});
document.getElementById('tmClearCache')?.addEventListener('click', () => {
this.timeMachine.apiManager.clearCache();
this.timeMachine.videoCache.clear(); // Also clear persistent video cache
this.updateUI();
});
// Vintage theme toggle
document.getElementById('tmVintageToggle')?.addEventListener('click', () => {
this.handleVintageToggle();
});
// Refresh button
const refreshBtn = document.getElementById('tmRefreshVideosBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
refreshBtn.disabled = true;
refreshBtn.textContent = 'Refreshing...';
this.timeMachine.loadAllVideos(true).then(() => {
const container = document.querySelector('ytd-browse[page-subtype="home"] ytd-rich-grid-renderer');
if (container) {
this.timeMachine.replaceHomepage(container, true);
}
refreshBtn.textContent = 'Refreshed!';
}).catch(error => {
refreshBtn.textContent = 'Refresh Failed';
OptimizedUtils.log('Manual refresh failed:', error);
}).finally(() => {
setTimeout(() => {
refreshBtn.disabled = false;
refreshBtn.textContent = 'Refresh Videos';
}, 2000);
});
});
}
// API key management
document.getElementById('tmAddApiKey')?.addEventListener('click', () => {
const input = document.getElementById('tmApiKeyInput');
if (input.value.trim()) {
this.timeMachine.apiManager.addKey(input.value.trim());
input.value = '';
this.updateUI();
}
});
document.getElementById('tmTestAllKeys')?.addEventListener('click', async () => {
const testBtn = document.getElementById('tmTestAllKeys');
testBtn.disabled = true;
testBtn.textContent = 'Testing...';
try {
const result = await this.timeMachine.apiManager.testAllKeys();
this.showStatusMessage(result.message, result.success ? 'success' : 'error');
this.updateUI(); // Refresh to show updated key statuses
} catch (error) {
this.showStatusMessage('Error testing keys: ' + error.message, 'error');
}
testBtn.disabled = false;
testBtn.textContent = 'Test All';
});
// Proxy management
document.getElementById('tmProxyEnabled')?.addEventListener('change', (e) => {
this.timeMachine.apiManager.enableProxies(e.target.checked);
this.updateUI();
});
// Date rotation toggle
document.getElementById('tmDateRotation')?.addEventListener('change', (e) => {
this.timeMachine.enableDateRotation(e.target.checked);
this.updateUI();
});
document.getElementById('tmTestClock')?.addEventListener('change', (e) => {
this.timeMachine.toggleTestClock(e.target.checked);
this.updateUI();
});
// Subscription management
document.getElementById('tmAddSubscription')?.addEventListener('click', () => {
const input = document.getElementById('tmSubscriptionInput');
if (input.value.trim()) {
this.timeMachine.subscriptionManager.addSubscription(input.value.trim());
input.value = '';
this.updateUI();
}
});
// Search Terms management
document.getElementById('tmAddSearchTerm')?.addEventListener('click', () => {
const input = document.getElementById('tmSearchTermInput');
if (input.value.trim()) {
this.addSearchTerm(input.value.trim());
input.value = '';
this.updateSearchTermsList();
}
});
document.getElementById('tmSearchTermInput')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
const input = document.getElementById('tmSearchTermInput');
if (input.value.trim()) {
this.addSearchTerm(input.value.trim());
input.value = '';
this.updateSearchTermsList();
}
}
});
document.getElementById('tmClearSearchTerms')?.addEventListener('click', () => {
this.clearAllSearchTerms();
this.updateSearchTermsList();
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'T') {
e.preventDefault();
this.toggleUI();
}
});
}
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'T') {
e.preventDefault();
this.toggleUI();
}
});
}
toggleUI() {
this.isVisible = !this.isVisible;
GM_setValue('ytTimeMachineUIVisible', this.isVisible);
const ui = document.getElementById('timeMachineUI');
const toggle = document.getElementById('timeMachineToggle');
if (this.isVisible) {
ui?.classList.remove('hidden');
toggle?.classList.remove('visible');
} else {
ui?.classList.add('hidden');
toggle?.classList.add('visible');
}
}
updateUI() {
// Early exit if timeMachine is not ready
if (!this.timeMachine || !this.timeMachine.isInitialized) {
console.warn('[WayBackTube] UpdateUI called before timeMachine initialization');
return;
}
// Update date input
const dateInput = document.getElementById('tmDateInput');
if (dateInput && this.timeMachine.getDateString) {
dateInput.value = this.timeMachine.getDateString();
}
// Update toggle button
const toggleBtn = document.getElementById('tmToggle');
if (toggleBtn && this.timeMachine && this.timeMachine.settings) {
toggleBtn.textContent = this.timeMachine.settings.active ? 'Disable' : 'Enable';
}
// Only update statistics if elements exist
if (document.getElementById('tmApiCalls')) {
this.updateStatistics();
}
// Update API keys list
this.updateApiKeysList();
// Update subscriptions list
this.updateSubscriptionsList();
// Update search terms list
this.updateSearchTermsList();
// Update date rotation checkbox
const dateRotationCheckbox = document.getElementById('tmDateRotation');
if (dateRotationCheckbox) {
dateRotationCheckbox.checked = this.timeMachine.settings.dateRotationEnabled;
}
const testClockCheckbox = document.getElementById('tmTestClock');
if (testClockCheckbox) {
testClockCheckbox.checked = GM_getValue('wayback_persistent_clock_enabled', false);
}
}
showStatusMessage(message, type = 'info') {
// Create a temporary message overlay
const messageDiv = document.createElement('div');
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
border-radius: 6px;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
z-index: 999999;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
const colors = {
success: '#4CAF50',
error: '#f44336',
warning: '#ff9800',
info: '#2196F3'
};
messageDiv.style.backgroundColor = colors[type] || colors.info;
messageDiv.textContent = message;
document.body.appendChild(messageDiv);
// Auto-remove after 3 seconds
setTimeout(() => {
if (messageDiv.parentNode) {
messageDiv.remove();
}
}, 3000);
console.log(`[WayBackTube] ${type.toUpperCase()}: ${message}`);
}
updateStatistics() {
try {
// Check if statistics elements exist before trying to update
const apiCallsEl = document.getElementById('tmApiCalls');
const videosProcessedEl = document.getElementById('tmVideosProcessed');
const cacheHitsEl = document.getElementById('tmCacheHits');
const videosFilteredEl = document.getElementById('tmVideosFiltered');
// If no statistics elements exist, skip update
if (!apiCallsEl && !videosProcessedEl && !cacheHitsEl && !videosFilteredEl) {
return;
}
// Ensure timeMachine exists and has getStats method
if (!this.timeMachine || typeof this.timeMachine.getStats !== 'function') {
console.warn('[WayBackTube] TimeMachine not ready for statistics update');
return;
}
const stats = this.timeMachine.getStats() || {};
// Double-check each element exists before setting textContent
try {
if (apiCallsEl && apiCallsEl.parentNode) apiCallsEl.textContent = stats.apiCalls || '0';
} catch (e) { /* Ignore */ }
try {
if (videosProcessedEl && videosProcessedEl.parentNode) videosProcessedEl.textContent = stats.processed || '0';
} catch (e) { /* Ignore */ }
try {
if (cacheHitsEl && cacheHitsEl.parentNode) cacheHitsEl.textContent = stats.cacheHits || '0';
} catch (e) { /* Ignore */ }
try {
if (videosFilteredEl && videosFilteredEl.parentNode) videosFilteredEl.textContent = stats.filtered || '0';
} catch (e) { /* Ignore */ }
} catch (error) {
// Silently ignore all statistics update errors to prevent retry loops
console.warn('[WayBackTube] Statistics update failed:', error);
}
}
updateApiKeysList() {
const container = document.getElementById('tmApiKeyList');
if (!container) return;
const keys = this.timeMachine.apiManager.keys;
const currentKeyIndex = this.timeMachine.apiManager.currentKeyIndex;
const keyStats = this.timeMachine.apiManager.keyStats;
if (keys.length === 0) {
container.textContent = '';
const noKeysDiv = document.createElement('div');
noKeysDiv.style.cssText = 'color: #666; font-size: 11px; text-align: center; padding: 8px;';
noKeysDiv.textContent = 'No API keys added';
container.appendChild(noKeysDiv);
return;
}
container.textContent = '';
keys.forEach((key, index) => {
const isActive = index === currentKeyIndex;
const stats = keyStats[key] || {};
let status = 'Unknown';
let statusIcon = '❓';
let statusColor = '#666';
if (stats.quotaExceeded) {
status = 'Quota Exceeded';
statusIcon = '🚫';
statusColor = '#ff4444';
} else if (stats.failed) {
status = 'Failed';
statusIcon = '❌';
statusColor = '#ff6666';
} else if (isActive) {
status = 'Active';
statusIcon = '✅';
statusColor = '#4CAF50';
} else if (stats.successCount > 0) {
status = 'Standby';
statusIcon = '⏸️';
statusColor = '#ff9800';
} else {
status = 'Untested';
statusIcon = '⚪';
statusColor = '#999';
}
// Create key item using DOM methods
const keyItem = document.createElement('div');
keyItem.className = 'tm-api-key-item';
keyItem.dataset.key = key;
keyItem.style.cssText = 'margin-bottom: 8px; padding: 8px; background: #2a2a2a; border-radius: 4px;';
const flexDiv = document.createElement('div');
flexDiv.style.cssText = 'display: flex; justify-content: space-between; align-items: center;';
const infoDiv = document.createElement('div');
infoDiv.style.cssText = 'flex: 1; min-width: 0;';
const keyDiv = document.createElement('div');
keyDiv.style.cssText = 'font-size: 11px; color: #fff; margin-bottom: 4px;';
keyDiv.textContent = `${statusIcon} ${key.substring(0, 8)}...${key.substring(key.length - 4)}`;
const statsDiv = document.createElement('div');
statsDiv.style.cssText = 'display: flex; gap: 12px; font-size: 10px; color: #999;';
const statusSpan = document.createElement('span');
statusSpan.style.color = statusColor;
statusSpan.textContent = status;
const requestsSpan = document.createElement('span');
requestsSpan.textContent = `${stats.requestCount || 0} requests`;
const successSpan = document.createElement('span');
successSpan.textContent = `${stats.successCount || 0} successful`;
statsDiv.appendChild(statusSpan);
statsDiv.appendChild(requestsSpan);
statsDiv.appendChild(successSpan);
infoDiv.appendChild(keyDiv);
infoDiv.appendChild(statsDiv);
const removeBtn = document.createElement('button');
removeBtn.className = 'tm-remove-key';
removeBtn.dataset.key = key;
removeBtn.style.cssText = 'background: #ff4444; border: none; color: white; padding: 4px 8px; border-radius: 3px; font-size: 10px; cursor: pointer; margin-left: 8px;';
removeBtn.textContent = 'Remove';
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.timeMachine.apiManager.removeKey(key);
this.updateUI();
});
flexDiv.appendChild(infoDiv);
flexDiv.appendChild(removeBtn);
keyItem.appendChild(flexDiv);
container.appendChild(keyItem);
});
}
updateSubscriptionsList() {
const container = document.getElementById('tmSubscriptionList');
if (!container) return;
const subscriptions = this.timeMachine.subscriptionManager.getSubscriptions();
container.textContent = '';
if (subscriptions.length === 0) {
const noSubsDiv = document.createElement('div');
noSubsDiv.style.cssText = 'padding: 8px; text-align: center; color: #666;';
noSubsDiv.textContent = 'No subscriptions added';
container.appendChild(noSubsDiv);
return;
}
subscriptions.forEach((sub, index) => {
const subItem = document.createElement('div');
subItem.className = 'tm-subscription-item';
const subName = document.createElement('span');
subName.className = 'tm-subscription-name';
subName.textContent = sub.name;
const removeBtn = document.createElement('button');
removeBtn.className = 'tm-remove-btn';
removeBtn.textContent = 'Remove';
removeBtn.addEventListener('click', () => {
this.timeMachine.removeSubscription(index);
});
subItem.appendChild(subName);
subItem.appendChild(removeBtn);
container.appendChild(subItem);
});
}
// Search Terms Management Methods
loadSearchTerms() {
try {
const searchTerms = JSON.parse(GM_getValue('ytSearchTerms', '[]'));
return searchTerms.length > 0 ? searchTerms : this.getDefaultSearchTerms();
} catch (error) {
return this.getDefaultSearchTerms();
}
}
saveSearchTerms(searchTerms) {
GM_setValue('ytSearchTerms', JSON.stringify(searchTerms));
}
getDefaultSearchTerms() {
return ['memes', 'gaming', 'funny', 'music', 'tutorial'];
}
addSearchTerm(term) {
const searchTerms = this.loadSearchTerms();
if (!searchTerms.includes(term.toLowerCase())) {
searchTerms.push(term.toLowerCase());
this.saveSearchTerms(searchTerms);
}
}
removeSearchTerm(index) {
const searchTerms = this.loadSearchTerms();
searchTerms.splice(index, 1);
this.saveSearchTerms(searchTerms);
}
clearAllSearchTerms() {
this.saveSearchTerms([]);
}
getSearchTerms() {
return this.loadSearchTerms();
}
updateSearchTermsList() {
const container = document.getElementById('tmSearchTermList');
if (!container) return;
const searchTerms = this.loadSearchTerms();
container.textContent = '';
if (searchTerms.length === 0) {
const noTermsDiv = document.createElement('div');
noTermsDiv.style.cssText = 'padding: 8px; text-align: center; color: #666;';
noTermsDiv.textContent = 'No search terms added';
container.appendChild(noTermsDiv);
return;
}
searchTerms.forEach((term, index) => {
const termItem = document.createElement('div');
termItem.className = 'tm-subscription-item';
const termName = document.createElement('span');
termName.className = 'tm-subscription-name';
termName.textContent = term;
const removeBtn = document.createElement('button');
removeBtn.className = 'tm-remove-btn';
removeBtn.textContent = 'Remove';
removeBtn.addEventListener('click', () => {
this.removeSearchTerm(index);
this.updateSearchTermsList();
});
termItem.appendChild(termName);
termItem.appendChild(removeBtn);
container.appendChild(termItem);
});
}
handleVintageToggle() {
const currentState = GM_getValue('ytVintage2011Theme', false);
const newState = !currentState;
// Save the new state
GM_setValue('ytVintage2011Theme', newState);
// Force CSS re-injection if enabling theme
if (newState && !document.querySelector('style[data-wayback-vintage]')) {
OptimizedUtils.log('Re-injecting vintage CSS...');
this.injectVintageCSS();
}
// Update the DOM
if (newState) {
document.body.classList.add('wayback-2011-theme');
OptimizedUtils.log('Added wayback-2011-theme class to body');
} else {
document.body.classList.remove('wayback-2011-theme');
OptimizedUtils.log('Removed wayback-2011-theme class from body');
}
// Update ALL possible buttons (both UI systems)
const buttons = [
document.getElementById('tmVintageToggle'),
document.getElementById('wayback-vintage-toggle')
];
buttons.forEach(button => {
if (button) {
button.textContent = newState ? '🕰️ Modern Theme' : '🕰️ 2011 Theme';
button.classList.toggle('active', newState);
OptimizedUtils.log(`Button updated: ${button.textContent}`);
}
});
// Debug: Check if class is actually on body
OptimizedUtils.log(`Body classes: ${document.body.className}`);
OptimizedUtils.log(`2011 Vintage Theme ${newState ? 'enabled' : 'disabled'}`);
}
injectVintageCSS() {
// Make sure vintage CSS is injected
const vintage2011CSS = `
/* Simple 2011 Theme - NO BLUE TEXT (user request) */
body.wayback-2011-theme {
/* Remove blue text styling completely */
}
/* Remove ALL rounded corners - make everything square */
body.wayback-2011-theme *,
body.wayback-2011-theme *:before,
body.wayback-2011-theme *:after {
border-radius: 0 !important;
-webkit-border-radius: 0 !important;
-moz-border-radius: 0 !important;
}
/* FIXED: Square thumbnails with normal sizing */
body.wayback-2011-theme ytd-thumbnail img,
body.wayback-2011-theme .ytd-thumbnail img {
border-radius: 0 !important;
object-fit: cover !important; /* Use cover for normal thumbnail behavior */
}
/* Fix thumbnail containers */
body.wayback-2011-theme ytd-thumbnail,
body.wayback-2011-theme .ytd-thumbnail,
body.wayback-2011-theme #thumbnail {
border-radius: 0 !important;
}
/* Don't break all images - only target thumbnails specifically */
body.wayback-2011-theme ytd-video-renderer ytd-thumbnail img,
body.wayback-2011-theme ytd-rich-item-renderer ytd-thumbnail img,
body.wayback-2011-theme ytd-grid-video-renderer ytd-thumbnail img {
border-radius: 0 !important;
object-fit: cover !important;
}
/* Fix channel profile pictures - keep normal size */
body.wayback-2011-theme yt-img-shadow img,
body.wayback-2011-theme ytd-channel-avatar img,
body.wayback-2011-theme #avatar img,
body.wayback-2011-theme .ytd-channel-avatar img {
border-radius: 0 !important;
max-width: 36px !important;
max-height: 36px !important;
width: 36px !important;
height: 36px !important;
}
/* Square buttons */
body.wayback-2011-theme button,
body.wayback-2011-theme .yt-spec-button-shape-next,
body.wayback-2011-theme .yt-spec-button-shape-next__button {
border-radius: 0 !important;
}
/* Square search box */
body.wayback-2011-theme input,
body.wayback-2011-theme #search-form,
body.wayback-2011-theme ytd-searchbox,
body.wayback-2011-theme #search-form input {
border-radius: 0 !important;
}
/* Square video containers */
body.wayback-2011-theme ytd-video-renderer,
body.wayback-2011-theme ytd-grid-video-renderer,
body.wayback-2011-theme ytd-rich-item-renderer {
border-radius: 0 !important;
}
/* FIXED: Remove ALL hover elevation effects and animations */
body.wayback-2011-theme *,
body.wayback-2011-theme *:before,
body.wayback-2011-theme *:after,
body.wayback-2011-theme *:hover,
body.wayback-2011-theme *:hover:before,
body.wayback-2011-theme *:hover:after {
transition: none !important;
animation: none !important;
transform: none !important;
box-shadow: none !important;
-webkit-transition: none !important;
-webkit-animation: none !important;
-webkit-transform: none !important;
-webkit-box-shadow: none !important;
-moz-transition: none !important;
-moz-animation: none !important;
-moz-transform: none !important;
-moz-box-shadow: none !important;
}
/* Specifically disable hover effects on video containers */
body.wayback-2011-theme ytd-video-renderer:hover,
body.wayback-2011-theme ytd-rich-item-renderer:hover,
body.wayback-2011-theme ytd-grid-video-renderer:hover,
body.wayback-2011-theme .ytd-video-renderer:hover,
body.wayback-2011-theme .ytd-rich-item-renderer:hover,
body.wayback-2011-theme .ytd-grid-video-renderer:hover {
transform: none !important;
box-shadow: none !important;
transition: none !important;
animation: none !important;
-webkit-transform: none !important;
-webkit-box-shadow: none !important;
-webkit-transition: none !important;
-webkit-animation: none !important;
}
/* Disable thumbnail hover scaling/animations */
body.wayback-2011-theme ytd-thumbnail:hover img,
body.wayback-2011-theme .ytd-thumbnail:hover img,
body.wayback-2011-theme #thumbnail:hover img {
transform: none !important;
transition: none !important;
animation: none !important;
-webkit-transform: none !important;
-webkit-transition: none !important;
-webkit-animation: none !important;
}
/* Fix oversized watch next section */
body.wayback-2011-theme #secondary #secondary-inner,
body.wayback-2011-theme ytd-watch-next-secondary-results-renderer {
max-width: 400px !important;
}
body.wayback-2011-theme #secondary ytd-compact-video-renderer ytd-thumbnail,
body.wayback-2011-theme #secondary .ytd-compact-video-renderer .ytd-thumbnail {
width: 220px !important;
height: 124px !important;
min-width: 220px !important;
flex-shrink: 0 !important;
}
body.wayback-2011-theme #secondary ytd-compact-video-renderer ytd-thumbnail img,
body.wayback-2011-theme #secondary .ytd-compact-video-renderer .ytd-thumbnail img {
width: 220px !important;
height: 124px !important;
object-fit: cover !important;
border-radius: 0 !important;
}
/* Fix oversized emojis - be more specific to avoid breaking thumbnails */
body.wayback-2011-theme .emoji,
body.wayback-2011-theme img[src*="emoji"] {
max-width: 20px !important;
max-height: 20px !important;
width: auto !important;
height: auto !important;
}
/* Don't break main video thumbnails */
body.wayback-2011-theme ytd-video-renderer:not(#secondary *) ytd-thumbnail,
body.wayback-2011-theme ytd-rich-item-renderer:not(#secondary *) ytd-thumbnail,
body.wayback-2011-theme ytd-grid-video-renderer:not(#secondary *) ytd-thumbnail {
width: auto !important;
height: auto !important;
}
`;
// Create style element with marker
const styleElement = document.createElement('style');
styleElement.setAttribute('data-wayback-vintage', 'true');
styleElement.textContent = vintage2011CSS;
document.head.appendChild(styleElement);
// Also use GM_addStyle as backup
GM_addStyle(vintage2011CSS);
OptimizedUtils.log('Vintage 2011 CSS injected');
}
initializeVintageTheme() {
const isVintageActive = GM_getValue('ytVintage2011Theme', false);
const button = document.getElementById('tmVintageToggle');
// Always inject vintage CSS on initialization
this.injectVintageCSS();
// Apply saved theme state
if (isVintageActive) {
document.body.classList.add('wayback-2011-theme');
}
// Update button state
if (button) {
button.textContent = isVintageActive ? '🕰️ Modern Theme' : '🕰️ 2011 Theme';
button.classList.toggle('active', isVintageActive);
}
// Expose global vintage toggle function for compatibility
window.handleGlobalVintageToggle = () => this.handleVintageToggle();
OptimizedUtils.log(`2011 Vintage Theme initialized: ${isVintageActive ? 'active' : 'inactive'}`);
}
handleUrlChange() {
// Re-initialize elements that may have been replaced by navigation
setTimeout(() => {
this.timeMachine.searchManager?.injectSearchDateFilter();
}, 500);
}
}
// === INITIALIZATION ===
let wayBackTubeApp;
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeApp);
} else {
initializeApp();
}
function initializeApp() {
try {
wayBackTubeApp = new WayBackTubeOptimized();
// Global access for debugging and video caching
window.WayBackTube = wayBackTubeApp;
window.waybackTubeManager = wayBackTubeApp; // For video caching compatibility
// Removed relative date filtering to prevent double-processing and "in the future" issues
OptimizedUtils.log('WayBackTube Optimized loaded successfully');
} catch (error) {
console.error('Failed to initialize WayBackTube:', error);
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (wayBackTubeApp) {
wayBackTubeApp.cleanup();
}
});
// Handle navigation changes (for SPAs like YouTube)
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
if (wayBackTubeApp && wayBackTubeApp.isInitialized) {
// Re-initialize UI for new page if needed
setTimeout(() => {
wayBackTubeApp.uiManager.handleUrlChange();
}, 500);
}
}
}).observe(document, { subtree: true, childList: true });
OptimizedUtils.log('WayBackTube Optimized script loaded');
})();