Now with shitty YouTube tutorial!
I. Blah blah blah
Suppose one has had a YouTube account for many years, including some years wherein one was goin’ through some stuff, such as meth. A lot of meth, even. Or a divorce, maybe. Possibly both at once, as commonly occurs—it’s the age-old question: which came first, the divorce or the drug-abuse? We may never know.
Anyway, in this sort of situation, one might wish to delete one’s former comments en masse. But there’s a problem: YouTube doesn’t want one to do this!
I’m actually not sure why this is; the first time it was mentioned to me that there was no sort of filtering, bulk management, or other QoL features for YouTube comment history—nor for playlists, subscriptions, etc.—I said: “No, you’re just not looking in the right places.” I mean, obviously, right? Alphabet is a massive corporation with lots of people sitting around thinking about usability & so forth all day! No way they’d make this such a painful, time-consuming task!
In a shocking—and absolutely unprecedented—turn of events, it transpired that I was incorrect. YouTube does make this an irritating & time-consuming task…
II. Features, Instructions, Disclaimer
…but no longer! Here’s a script that will delete your shameful no-longer-necessary past YouTube comments, easily & painlessly!
Features:
Configurable date-range filter!
“Dry-run” w/ highlighting (to indicate comments to-be-deleted) capability!
Configurable max-deletion “safety-limit”! For safety!
Auto-scroll, so you can fire ‘n’ forget! (Note: “Fire ‘n’ forget” use not recommended.)
How It Do:
Navigate to “My Activity” —> “Other Activity” —> “YouTube Comments” on your Google account.
Hit F12 (or right-click —> select “Inspect”) & select “Console” tab (for Chrome & related browsers; should be something similar for others).
Paste the below-indicated code into the console.
Hit “Enter”, and follow the configuration instructions that pop-up thereafter.
DISCLAIMER
I’m not a 10x programmer, okay. Actually, I’m barely even a programmer at all. If this deletes a bunch of stuff you didn’t want it to, well, sorry; use at your own risk, etc. I’m just throwing it up here in case anyone else discovers a desire to clean out their old garbage elegant & cogent, but outdated, YouTube commentary.
Please note also that you shouldn’t be pasting random JavaScript into the console. I mean, in this case, it’s fine; it’s your ol’ amigo Kvel! He wouldn’t do nothin’ bad! But… just as a general policy, y’know.
For troubleshooting:
III. Code
// Kvel's YouTube Comment Mass-Deleter(TM) v2.0
// Script will delete past YouTube comments en-masse (with automatic scrolling, so it's not a pain-in-the-asse). Now with date-range filtering!
// WARNING: There is NO WAY TO RECOVER DELETED COMMENTS!
// Test carefully with Dry-Run & Safety-Limit settings before proceeding with large-scale deletion!
(function() {
'use strict';
// Configuration
// NOTE: Default speed = set somewhat conservative; could try lower delay-#s if fast connection & many comments to be deleted.
const config = {
dryRun: true, // Set to false to actually delete comments
delayBetweenActions: 2000, // Milliseconds between each deletion
limitDateStart: null, // Delete-after date
limitDateEnd: null, // Delete-before date
maxDeletions: 10, // Maximum # of comments to delete (safety limit)
debugMode: true, // Show detailed console logs
scrollDelay: 2500, // Delay after scrolling to await loading
maxScrollAttempts: 100 // Maximum # of scroll-attempts (to prevent infinite loops)
};
// State-tracking
const state = {
processedButtons: new WeakSet(), // Tracking already-seen buttons (comments)
deletableComments: [], // Array of comments matching criteria
deletedCount: 0,
skippedCount: 0,
scrollAttempts: 0
};
// Utility functions
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const log = (message, ...args) => {
if (config.debugMode) {
console.log(`[YT Comment Mass-Deleter v2.1] ${message}`, ...args);
}
};
// Check if end has been reached
function hasReachedEnd() {
const endMessages = [
"Looks like you've reached the end"
];
for (const message of endMessages) {
const elements = document.querySelectorAll('*');
for (const el of elements) {
if (el.textContent && el.textContent.includes(message)) {
log('Found end-of-comments msg');
return true;
}
}
}
return false;
}
// Scroll to bottom of page & wait for new comments to load
async function scrollAndLoadMore() {
const beforeHeight = document.documentElement.scrollHeight;
const beforeButtonCount = findDeleteButtons().length;
log(`Scrolling... (attempt ${state.scrollAttempts + 1})`);
// Scroll to bottom
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: 'smooth'
});
// Wait for smooth-scroll to complete
await sleep(config.scrollDelay / 5);
// Wait for new comments to load
await sleep(config.scrollDelay);
const afterHeight = document.documentElement.scrollHeight;
const afterButtonCount = findDeleteButtons().length;
const newContentLoaded = afterHeight > beforeHeight || afterButtonCount > beforeButtonCount;
if (newContentLoaded) {
log(`New content loaded! Height: ${beforeHeight} -> ${afterHeight} | Buttons: ${beforeButtonCount} -> ${afterButtonCount}`);
} else {
log('No new content loaded after scroll');
}
state.scrollAttempts++;
return newContentLoaded;
}
// Find all delete-buttons on the page
function findDeleteButtons() {
const selectors = [
'button[aria-label^="Delete activity item"]',
'button.VfPpkd-Bz112c-LgbsSe[aria-label*="Delete"]',
];
let buttons = [];
for (const selector of selectors) {
const found = document.querySelectorAll(selector);
if (found.length > 0) {
buttons = [...buttons, ...Array.from(found)];
}
}
// Remove duplicates & filter to only actual delete-buttons
const uniqueButtons = [...new Set(buttons)];
return uniqueButtons.filter(btn => {
const ariaLabel = btn.getAttribute('aria-label') || '';
return ariaLabel.toLowerCase().includes('delete activity item');
});
}
// Get only new (unprocessed) delete-buttons
function findNewDeleteButtons() {
const allButtons = findDeleteButtons();
return allButtons.filter(btn => !state.processedButtons.has(btn));
}
// Get comment-date via "data-date" attribute
function getCommentDate(deleteButton) {
let element = deleteButton;
let maxLevels = 20;
while (element && maxLevels-- > 0) {
if (element.hasAttribute && element.hasAttribute('data-date')) {
const dateStr = element.getAttribute('data-date');
// Parse YYYYMMDD format
if (dateStr && dateStr.length === 8) {
const year = parseInt(dateStr.substring(0, 4));
const month = parseInt(dateStr.substring(4, 6)) - 1;
const day = parseInt(dateStr.substring(6, 8));
const date = new Date(year, month, day);
if (!isNaN(date.getTime())) {
return date;
}
}
}
element = element.parentElement;
}
return null;
}
// Process a batch of new comments (buttons)
function processNewButtons() {
const newButtons = findNewDeleteButtons();
log(`Processing ${newButtons.length} new buttons...`);
let newDeletableCount = 0;
for (const button of newButtons) {
// Mark as processed
state.processedButtons.add(button);
// Skip if already enough deletable comments
if (state.deletableComments.length >= config.maxDeletions) {
continue;
}
// Check date-filter, if configured
if (config.limitDateStart || config.limitDateEnd) {
const commentDate = getCommentDate(button);
if (!commentDate) {
log('Skipping comment -- could not determine date');
state.skippedCount++;
continue;
}
// Check if comment is outside date-range
let outsideRange = false;
// If start-date is set, check if comment is prior to it
if (config.limitDateStart && commentDate < config.limitDateStart) {
log(`Skipping comment -- date ${commentDate.toDateString()} is before start-date ${config.limitDateStart.toDateString()}`);
outsideRange = true;
}
// If end-date is set, check if comment is posterior to it
if (config.limitDateEnd && commentDate >= config.limitDateEnd) {
log(`Skipping comment -- date ${commentDate.toDateString()} is on or after end-date ${config.limitDateEnd.toDateString()}`);
outsideRange = true;
}
if (outsideRange) {
state.skippedCount++;
// Clean up button-highlights from previous dry-run(s), if any
button.style.border = '';
button.style.backgroundColor = '';
continue;
}
log(`Comment date ${commentDate.toDateString()} is within range -- tbDeleted`);
}
// Comment is deletable!
state.deletableComments.push(button);
newDeletableCount++;
// Highlight (for dry-run mode)
if (config.dryRun) {
button.style.border = '3px solid red';
button.style.backgroundColor = 'rgba(255, 0, 0, 0.1)';
}
}
log(`Found ${newDeletableCount} new deletable comments (total: ${state.deletableComments.length})`);
return newDeletableCount;
}
// Delete a comment
async function deleteComment(button, index) {
log(`Deleting comment ${index + 1} of ${state.deletableComments.length}...`);
// Extract comment preview from aria-label
const ariaLabel = button.getAttribute('aria-label') || '';
const commentPreview = ariaLabel.replace('Delete activity item ', '').substring(0, 50) + '...';
log(`Comment preview: "${commentPreview}"`);
if (config.dryRun) {
log(`[DRY-RUN] Would delete comment ${index + 1}`);
state.deletedCount++;
return true;
}
try {
// Click the delete button
button.click();
log(`Clicked delete for comment ${index + 1}`);
// Wait for the deletion to process
await sleep(config.delayBetweenActions);
// Assume success if no error was thrown
log(`Successfully deleted comment ${index + 1}`);
state.deletedCount++;
return true;
} catch (error) {
log(`Error deleting comment ${index + 1}:`, error);
return false;
}
}
// Main execution function
async function deleteCommentsWithScroll() {
log('Starting comment deletion process...');
log('Configuration:', config);
// Initial processing of visible comments
processNewButtons();
// Keep scrolling & processing till comment-limit or page-end
while (state.deletableComments.length < config.maxDeletions &&
state.scrollAttempts < config.maxScrollAttempts) {
// Check if we've reached the end
if (hasReachedEnd()) {
log('Reached end of comment history');
break;
}
// Scroll to load more
const loaded = await scrollAndLoadMore();
if (!loaded) {
// Try one more time with a longer delay
log('Retrying scroll with longer delay...');
await sleep(config.scrollDelay * 2);
const retryLoaded = await scrollAndLoadMore();
if (!retryLoaded) {
log('No more content loading -- may have reached the end');
break;
}
}
// Process newly-loaded comments
processNewButtons();
}
// Log final status
log(`Scrolling complete. Found ${state.deletableComments.length} deletable comments`);
log(`Totals: processed: ${state.processedButtons.size} | skipped: ${state.skippedCount}`);
if (state.deletableComments.length === 0) {
log('No comments found matching deletion criteria!');
return;
}
// Delete collected comments
const toDelete = Math.min(state.deletableComments.length, config.maxDeletions);
log(`Proceeding to delete ${toDelete} comments...`);
for (let i = 0; i < toDelete; i++) {
const button = state.deletableComments[i];
// Check if button still exists (page might have changed)
if (!document.body.contains(button)) {
log(`Button ${i + 1} no longer exists, skipping...`);
continue;
}
await deleteComment(button, i);
// Wait between deletions
if (i < toDelete - 1) {
await sleep(config.delayBetweenActions);
}
}
log(`Process complete! Deleted: ${state.deletedCount} of ${toDelete} attempted`);
if (config.dryRun) {
log('This was a DRY RUN -- no comments were actually deleted.');
log('Set config.dryRun = false to perform actual deletions.');
}
}
// Interactive prompts for configuration
function configure() {
const proceed = confirm('YouTube Comment Mass-Deleter v2.0\n\nThis script will help with deleting all of your shameful---I mean, er, unnecessary---past YT comments.\n\nProceed with configuration?');
if (!proceed) return false;
// Dry-run prompt
const isDryRun = confirm('Do you want to run in DRY-RUN mode first?\n\nYES = Test only (recommended!)\nNO = Actually delete comments');
config.dryRun = isDryRun;
// Date-limit prompt
const useDateLimit = confirm('Do you want to filter comments-to-be-deleted by date?');
if (useDateLimit) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
const todayStr = `${year}-${month}-${day}`;
// Ask for date-range type
const rangeType = prompt(
'Choose date-filter type:\n\n' +
'1 = Delete comments BEFORE a specific date\n' +
'2 = Delete comments AFTER a specific date\n' +
'3 = Delete comments BETWEEN two dates\n\n' +
'Enter 1, 2, or 3:',
'1'
);
if (rangeType === '1') {
// Delete before date
const dateStr = prompt('Delete comments from BEFORE this date (YYYY-MM-DD):', todayStr);
if (dateStr) {
const parts = dateStr.split('-');
if (parts.length === 3) {
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
config.limitDateEnd = new Date(year, month, day);
}
}
} else if (rangeType === '2') {
// Delete after date
const dateStr = prompt('Delete comments from AFTER this date (YYYY-MM-DD):', '2019-01-01');
if (dateStr) {
const parts = dateStr.split('-');
if (parts.length === 3) {
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
config.limitDateStart = new Date(year, month, day);
}
}
} else if (rangeType === '3') {
// Delete between dates
const startStr = prompt('Delete comments from AFTER this date (YYYY-MM-DD):', '2019-01-01');
if (startStr) {
const parts = startStr.split('-');
if (parts.length === 3) {
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
config.limitDateStart = new Date(year, month, day);
}
}
const endStr = prompt('Delete comments from BEFORE this date (YYYY-MM-DD):', todayStr);
if (endStr) {
const parts = endStr.split('-');
if (parts.length === 3) {
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
config.limitDateEnd = new Date(year, month, day);
}
}
}
// Validate dates
if ((config.limitDateStart && isNaN(config.limitDateStart.getTime())) ||
(config.limitDateEnd && isNaN(config.limitDateEnd.getTime()))) {
alert('Invalid date-format. Proceeding without date filter.');
config.limitDateStart = null;
config.limitDateEnd = null;
} else if (config.limitDateStart && config.limitDateEnd &&
config.limitDateStart >= config.limitDateEnd) {
alert('Start-date must be before end-date. Proceeding without date filter.');
config.limitDateStart = null;
config.limitDateEnd = null;
} else {
// Show confirmation of date-range
let rangeMsg = 'Will delete comments ';
if (config.limitDateStart && config.limitDateEnd) {
rangeMsg += `between ${config.limitDateStart.toDateString()} & ${config.limitDateEnd.toDateString()}`;
} else if (config.limitDateStart) {
rangeMsg += `after ${config.limitDateStart.toDateString()}`;
} else if (config.limitDateEnd) {
rangeMsg += `before ${config.limitDateEnd.toDateString()}`;
}
alert(rangeMsg);
}
}
// Max-deletions prompt
const maxStr = prompt('Maximum number of comments to delete (safety limit):', '10');
config.maxDeletions = parseInt(maxStr) || 10;
// Auto-scroll confirmation
const confirmScroll = confirm('Enable auto-scrolling to load more comments?\n\n(Script will automatically scroll down to load older comments, until it finds enough to delete or reaches the end.)');
if (!confirmScroll) {
alert('Auto-scrolling disabled. Only currently-visible comments will be processed.');
return false;
}
return true;
}
// Start the process
if (configure()) {
deleteCommentsWithScroll();
}
})();
C’mon. You know you want to.