Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mguterl/bae46f913ec269c712951d52706d36ea to your computer and use it in GitHub Desktop.

Select an option

Save mguterl/bae46f913ec269c712951d52706d36ea to your computer and use it in GitHub Desktop.
Remove video shorts from YT Watch History
// YouTube Watch History Cleaner
// Paste into console at: https://myactivity.google.com/product/youtube
// orig: https://gist.github.com/miketromba/334282421c4784d7d9a191ca25095c09
// ============================================================================
// CONFIGURATION - Edit this section to customize what gets deleted
// ============================================================================
const CONFIG = {
// DRY RUN MODE: When true, logs what would be deleted without actually deleting
// Set to false when you're ready to actually delete
dryRun: false,
// How to combine filters:
// 'any' = delete if ANY filter matches (OR logic)
// 'all' = delete only if ALL enabled filters match (AND logic)
filterMode: 'any',
// Timing settings (milliseconds)
cycleIntervalMs: 1800,
confirmCheckIntervalMs: 2000,
// -------------------------------------------------------------------------
// FILTERS - Enable/disable and configure each filter
// -------------------------------------------------------------------------
filters: {
// Delete videos shorter than a certain duration (i.e., Shorts)
shortVideos: {
enabled: true,
maxDurationMinutes: 1.5, // Videos under 1:30 are considered "shorts"
},
// Delete videos watched during certain hours (e.g., sleep time)
timeOfDay: {
enabled: true,
// 24-hour format. This example targets 10 PM - 6 AM
startHour: 22, // Start of deletion window (inclusive)
endHour: 6, // End of deletion window (exclusive)
// Note: Handles overnight ranges correctly (23:00 -> 06:00)
},
// Delete videos from specific channels
channels: {
enabled: false,
names: [
// 'SomeChannelName',
// 'AnotherChannel',
],
},
// Delete videos whose title contains certain keywords
keywords: {
enabled: true,
terms: [
'biden',
'democrat',
'doj',
'epstein',
'gop',
'gordon',
'nightmares',
'ramsay',
'republican',
'trump',
],
caseSensitive: false,
},
// Delete ads (items without a duration)
ads: {
enabled: false,
},
},
};
// ============================================================================
// IMPLEMENTATION - You probably don't need to edit below this line
// ============================================================================
const state = {
alreadyProcessed: [],
deletedCount: 0,
skippedCount: 0,
running: true,
};
// --- Utility functions ---
const log = (msg, ...args) => console.log(`[yt-cleaner] ${msg}`, ...args);
const warn = (msg, ...args) => console.warn(`[yt-cleaner] ${msg}`, ...args);
const getVideoIdentifiers = (el) => {
const anchors = [...el.getElementsByTagName('a')];
const [videoName, channelName] = anchors.map(a => a.textContent.trim());
return { videoName, channelName };
};
const getUniqueKey = (el) => {
const { videoName, channelName } = getVideoIdentifiers(el);
return `${videoName}|${channelName}`;
};
const wasAlreadyProcessed = (el) => state.alreadyProcessed.includes(getUniqueKey(el));
// --- Duration parsing ---
const getDurationString = (el) => {
const durationEl = el.querySelector('[aria-label="Video duration"]');
return durationEl?.textContent?.trim() || null;
};
const getDurationMs = (el) => {
const dString = getDurationString(el);
if (!dString) return null; // No duration found (might be an ad)
const parts = dString.split(':');
if (parts.length > 2) {
// Over an hour: HH:MM:SS
const [hrs, mins, secs] = parts.map(n => parseInt(n, 10));
return ((hrs * 60 + mins) * 60 + secs) * 1000;
}
// Under an hour: MM:SS
const [mins, secs] = parts.map(n => parseInt(n, 10));
return (mins * 60 + secs) * 1000;
};
// --- Timestamp parsing ---
const parseActivityTime = (el) => {
// Try to find timestamp text in the element
const fullText = el.textContent || '';
// Match patterns like "Today 11:42 PM", "Yesterday 2:15 AM", "Dec 25 3:30 PM"
const timeMatch = fullText.match(/(\d{1,2}):(\d{2})\s*(AM|PM)/i);
if (!timeMatch) return null;
let [, hourStr, minStr, ampm] = timeMatch;
let hour = parseInt(hourStr, 10);
const minute = parseInt(minStr, 10);
// Convert to 24-hour format
if (ampm.toUpperCase() === 'PM' && hour !== 12) hour += 12;
if (ampm.toUpperCase() === 'AM' && hour === 12) hour = 0;
return { hour, minute };
};
// --- Filter implementations ---
const filters = {
shortVideos: (el) => {
const cfg = CONFIG.filters.shortVideos;
const durationMs = getDurationMs(el);
if (durationMs === null) return false;
const maxMs = cfg.maxDurationMinutes * 60 * 1000;
return durationMs < maxMs;
},
timeOfDay: (el) => {
const cfg = CONFIG.filters.timeOfDay;
const time = parseActivityTime(el);
if (!time) return false;
const { hour } = time;
const { startHour, endHour } = cfg;
if (startHour > endHour) {
// Overnight: matches if hour >= start OR hour < end
return hour >= startHour || hour < endHour;
} else {
// Same-day range: matches if hour >= start AND hour < end
return hour >= startHour && hour < endHour;
}
},
channels: (el) => {
const cfg = CONFIG.filters.channels;
const { channelName } = getVideoIdentifiers(el);
return cfg.names.some(name =>
channelName.toLowerCase().includes(name.toLowerCase())
);
},
keywords: (el) => {
const cfg = CONFIG.filters.keywords;
const { videoName } = getVideoIdentifiers(el);
const title = cfg.caseSensitive ? videoName : videoName.toLowerCase();
return cfg.terms.some(term => {
const needle = cfg.caseSensitive ? term : term.toLowerCase();
return title.includes(needle);
});
},
ads: (el) => {
return getDurationString(el) === null;
},
};
// --- Main filter logic ---
const shouldDelete = (el) => {
const enabledFilters = Object.entries(filters)
.filter(([name]) => CONFIG.filters[name]?.enabled);
if (enabledFilters.length === 0) {
warn('No filters enabled! Enable at least one filter in CONFIG.');
return false;
}
const results = enabledFilters.map(([name, fn]) => ({
name,
matches: fn(el),
}));
if (CONFIG.filterMode === 'all') {
return results.every(r => r.matches);
} else {
return results.some(r => r.matches);
}
};
const getMatchingFilters = (el) => {
return Object.entries(filters)
.filter(([name]) => CONFIG.filters[name]?.enabled)
.filter(([, fn]) => fn(el))
.map(([name]) => name);
};
// --- Deletion logic ---
const getNextItemToDelete = () => {
const items = document.querySelectorAll('div[role="listitem"]');
for (const item of items) {
if (wasAlreadyProcessed(item)) continue;
if (shouldDelete(item)) return item;
}
return null;
};
const deleteNext = async () => {
if (!state.running) return;
const item = getNextItemToDelete();
if (!item) {
log(`No more matching items found. Deleted: ${state.deletedCount}, Skipped: ${state.skippedCount}`);
log('Scroll down to load more history, or the script will check again in 10s...');
setTimeout(deleteNext, 10000);
return;
}
const { videoName, channelName } = getVideoIdentifiers(item);
const durationStr = getDurationString(item) || 'no duration';
const timeInfo = parseActivityTime(item);
const timeStr = timeInfo ? `${timeInfo.hour}:${String(timeInfo.minute).padStart(2, '0')}` : 'unknown time';
const matchedFilters = getMatchingFilters(item);
state.alreadyProcessed.push(getUniqueKey(item));
if (CONFIG.dryRun) {
log(`[DRY RUN] Would delete: "${videoName}" by ${channelName} (${durationStr}, watched at ${timeStr}) - matched: ${matchedFilters.join(', ')}`);
state.skippedCount++;
setTimeout(deleteNext, 500); // Faster in dry-run mode
return;
}
// Actually delete
log(`Deleting: "${videoName}" by ${channelName} (${durationStr}, ${timeStr}) - matched: ${matchedFilters.join(', ')}`);
const deleteButton = item.getElementsByTagName('button')[0];
if (!deleteButton) {
warn('Could not find delete button, skipping...');
setTimeout(deleteNext, CONFIG.cycleIntervalMs);
return;
}
deleteButton.click();
state.deletedCount++;
// Handle confirmation dialog (appears on first delete)
setTimeout(() => {
const menu = item.querySelector('[aria-label="Activity options menu"]');
if (menu) {
const confirmBtn = menu.querySelector('[aria-label="Delete activity item"]');
confirmBtn?.click();
}
setTimeout(deleteNext, CONFIG.cycleIntervalMs);
}, CONFIG.confirmCheckIntervalMs);
};
// --- Control functions ---
window.ytCleanerStop = () => {
state.running = false;
log('Stopped. Call ytCleanerStart() to resume.');
};
window.ytCleanerStart = () => {
state.running = true;
log('Started.');
deleteNext();
};
window.ytCleanerStatus = () => {
log(`Status: ${state.running ? 'running' : 'stopped'}, Deleted: ${state.deletedCount}, Processed: ${state.alreadyProcessed.length}`);
log(`Dry run: ${CONFIG.dryRun}`);
log('Enabled filters:', Object.entries(CONFIG.filters).filter(([, v]) => v.enabled).map(([k]) => k).join(', '));
};
// --- Startup ---
log('YouTube Watch History Cleaner loaded');
log(`Mode: ${CONFIG.dryRun ? 'DRY RUN (no deletions)' : 'LIVE (will delete!)'}`);
log(`Filter mode: ${CONFIG.filterMode} (${CONFIG.filterMode === 'any' ? 'delete if ANY filter matches' : 'delete only if ALL filters match'})`);
log('Enabled filters:', Object.entries(CONFIG.filters).filter(([, v]) => v.enabled).map(([k]) => k).join(', ') || 'none!');
log('---');
log('Controls: ytCleanerStop(), ytCleanerStart(), ytCleanerStatus()');
log('---');
deleteNext();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment