// ==UserScript==
// @license MIT
// @name YouTube Auto Heart Comments
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Automatically hearts comments on your YouTube videos
// @author __plasma (Patched by Claude)
// @match https://studio.youtube.com/*
// @match https://www.youtube.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// Add styles
GM_addStyle(`
#youtube-auto-heart-settings {
position: fixed;
bottom: 10px;
left: 10px;
z-index: 9999;
background-color: #FF0000;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 12px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
#youtube-auto-heart-counter {
position: fixed;
bottom: 10px;
left: 150px;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
font-size: 12px;
}
.youtube-auto-heart-notification {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 15px;
border-radius: 4px;
z-index: 9999;
max-width: 300px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
animation: fadeInOut 2s forwards;
}
@keyframes fadeInOut {
0% { opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { opacity: 0; }
}
`);
// Configuration
const CONFIG = {
checkInterval: 1000, // Check every second
showNotifications: true,
maxCommentsPerBatch: 50,
heartDelay: 100, // Delay between heart clicks
scrollDelay: 3000, // Delay between auto-scroll actions (milliseconds)
scrollStep: 500, // Pixels to scroll down each time
debug: true // Enable debugging
};
// Variables to track state
let processedComments = new Set();
let isProcessing = false;
let isEnabled = GM_getValue('autoHeartEnabled', true);
let heartInterval;
let urlCheckInterval;
let totalHearted = GM_getValue('totalHearted', 0);
let counterElement = null;
let currentUrl = location.href;
let lastCheckTime = 0;
let isStudioComments = false;
// Debugging function
function debug(message) {
if (CONFIG.debug) {
console.log(`[YouTube Auto Heart Debug] ${message}`);
}
}
// Function to show notifications
function showNotification(message) {
if (!CONFIG.showNotifications) return;
const existingNotification = document.querySelector('.youtube-auto-heart-notification');
if (existingNotification) {
existingNotification.remove();
}
const notification = document.createElement('div');
notification.className = 'youtube-auto-heart-notification';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
if (notification && notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 2000);
}
// Update the counter display
function updateCounter() {
if (!counterElement) {
counterElement = document.createElement('div');
counterElement.id = 'youtube-auto-heart-counter';
document.body.appendChild(counterElement);
}
counterElement.textContent = `Hearts: ${totalHearted}`;
GM_setValue('totalHearted', totalHearted);
}
// Check if a comment is already hearted
function isCommentHearted(heartButton) {
if (!heartButton) return true; // If button doesn't exist, assume we can't heart it
if (isStudioComments) {
// Studio-specific checks
const heartIcon = heartButton.querySelector('tp-yt-iron-icon');
if (heartIcon && heartIcon.getAttribute('icon') === 'favorite') {
debug('Comment already hearted (Studio - filled heart icon)');
return true;
}
if (heartButton.getAttribute('data-hearted') === 'true' ||
heartButton.classList.contains('hearted')) {
debug('Comment already hearted (Studio - data-hearted/hearted class)');
return true;
}
} else {
// Regular YouTube checks
if (heartButton.getAttribute('aria-pressed') === 'true' ||
heartButton.querySelector('.yt-spec-icon-badge-shape__icon--filled')) {
debug('Comment already hearted (Regular - aria-pressed/filled icon)');
return true;
}
}
// Common checks for both interfaces
if (heartButton.getAttribute('aria-label') &&
(heartButton.getAttribute('aria-label').includes('Remove heart') ||
heartButton.getAttribute('aria-label').includes('Hearted'))) {
debug('Comment already hearted (Common - aria-label)');
return true;
}
debug('Comment determined NOT to be hearted');
return false;
}
// Process comments in batches
function processCommentBatch(commentBatch) {
if (commentBatch.length === 0) {
isProcessing = false;
return;
}
let processedCount = 0;
for (let i = 0; i < commentBatch.length; i++) {
const current = commentBatch[i];
setTimeout(() => {
try {
if (current.button && document.body.contains(current.button) && !isCommentHearted(current.button)) {
debug(`Clicking heart button for comment: ${current.id}`);
current.button.click();
processedCount++;
totalHearted++;
if (totalHearted % 5 === 0) {
updateCounter();
}
processedComments.add(current.id);
} else {
processedComments.add(current.id); // Mark as processed even if already hearted
}
} catch (e) {
debug(`Error when clicking heart: ${e.message} for comment: ${current.id}`);
processedComments.add(current.id);
}
if (i === commentBatch.length - 1) {
if (processedCount > 0) {
showNotification(`Hearted ${processedCount} comments`);
console.log(`[YouTube Auto Heart] Hearted ${processedCount} comments in this batch.`);
updateCounter();
} else {
debug("No comments were hearted in this batch.");
}
isProcessing = false;
}
}, i * CONFIG.heartDelay);
}
}
// Find heart buttons based on context
function findHeartButtons(contextElement = document) {
let selectors = [];
if (isStudioComments) {
selectors = [
'ytcp-comment-creator-heart#creator-heart ytcp-icon-button',
'ytcp-comment-action-buttons#action-buttons ytcp-icon-button[aria-label*="Heart"]',
'tp-yt-iron-icon[icon="favorite_border"]'
];
} else {
selectors = [
'#like-button button[aria-label*="Heart"]',
'button[aria-label="Heart"]',
'#actions button[aria-label*="heart"]'
];
}
return contextElement.querySelectorAll(selectors.join(', '));
}
// Find comment elements based on context
function findCommentElements() {
let selectors = [];
if (isStudioComments) {
selectors = [
'ytcp-comment-thread',
'ytcp-comment'
];
} else {
selectors = [
'ytd-comment-thread-renderer',
'ytd-comment-renderer'
];
}
return document.querySelectorAll(selectors.join(', '));
}
// Main function to find and heart comments
function heartComments() {
if (isProcessing || !isEnabled) return;
isStudioComments = window.location.href.startsWith('https://studio.youtube.com/');
debug(`Processing comments. Is Studio: ${isStudioComments}. URL: ${window.location.href}`);
if (Date.now() - lastCheckTime < 1500) {
debug("Delaying check due to recent URL change.");
return;
}
isProcessing = true;
try {
const commentElements = findCommentElements();
if (!commentElements || commentElements.length === 0) {
debug('No comment elements found on page.');
isProcessing = false;
return;
}
debug(`Found ${commentElements.length} comment elements.`);
let commentsToProcess = [];
for (let i = 0; i < commentElements.length; i++) {
if (commentsToProcess.length >= CONFIG.maxCommentsPerBatch) {
debug(`Reached batch limit (${CONFIG.maxCommentsPerBatch})`);
break;
}
const commentElement = commentElements[i];
let commentId = commentElement.getAttribute('comment-id') ||
commentElement.getAttribute('data-comment-id') ||
commentElement.id;
if (!commentId) {
let commentTextElement = commentElement.querySelector(isStudioComments ? '.comment-text-content, .comment-content, #content-text' : '#content-text');
let commentText = commentTextElement ? commentTextElement.textContent.trim().slice(0, 30) : `no-text-${i}`;
commentId = `comment-${commentText}-${Date.now()}-${Math.random()}`;
}
if (processedComments.has(commentId)) {
continue;
}
const heartButtonsInComment = findHeartButtons(commentElement);
if (heartButtonsInComment.length === 0) {
processedComments.add(commentId);
continue;
}
const heartButton = heartButtonsInComment[0];
if (heartButton && !heartButton.disabled && !isCommentHearted(heartButton)) {
debug(`Found unhearted comment: ${commentId}`);
commentsToProcess.push({
element: commentElement,
button: heartButton,
id: commentId
});
} else {
processedComments.add(commentId);
}
}
if (commentsToProcess.length > 0) {
debug(`Collected ${commentsToProcess.length} comments to process in this batch.`);
processCommentBatch(commentsToProcess);
} else {
debug('No new unhearted comments found in this check.');
isProcessing = false;
}
} catch (error) {
console.error('[YouTube Auto Heart] Error in heartComments:', error);
debug(`Error in heartComments: ${error.message}`);
isProcessing = false;
}
}
// Auto-scroll functionality
let lastScrollPosition = 0;
let scrollInterval;
function startAutoScroll() {
if (scrollInterval) return; // Prevent multiple intervals
debug('Starting auto-scroll interval...');
scrollInterval = setInterval(() => {
const currentScrollPosition = window.scrollY || document.documentElement.scrollTop;
if (currentScrollPosition <= lastScrollPosition) {
debug('Scrolling down...');
window.scrollBy(0, CONFIG.scrollStep); // Scroll down by scrollStep pixels
}
lastScrollPosition = currentScrollPosition;
}, CONFIG.scrollDelay);
}
function stopAutoScroll() {
if (scrollInterval) {
debug('Stopping auto-scroll interval...');
clearInterval(scrollInterval);
scrollInterval = null;
}
}
// Check for URL changes
function checkUrlChange() {
const newUrl = location.href;
if (newUrl !== currentUrl) {
debug(`URL changed from ${currentUrl} to ${newUrl}`);
const oldUrlBase = currentUrl.split('?')[0].split('#')[0];
const newUrlBase = newUrl.split('?')[0].split('#')[0];
currentUrl = newUrl;
lastCheckTime = Date.now();
if (oldUrlBase !== newUrlBase) {
processedComments.clear();
console.log('[YouTube Auto Heart] URL base changed, cleared processed comments history.');
debug('URL base changed, cleared processed comments set.');
setTimeout(heartComments, 500);
} else {
debug("URL changed but base is the same, not clearing history.");
}
}
}
// Add settings button
function addSettingsButton() {
if (document.getElementById('youtube-auto-heart-settings')) return;
const settingsButton = document.createElement('button');
settingsButton.id = 'youtube-auto-heart-settings';
settingsButton.textContent = isEnabled ? '❤️ Auto Heart (ON)' : '🤍 Auto Heart (OFF)';
settingsButton.style.backgroundColor = isEnabled ? '#FF0000' : '#666666';
settingsButton.addEventListener('click', () => {
isEnabled = !isEnabled;
GM_setValue('autoHeartEnabled', isEnabled);
settingsButton.textContent = isEnabled ? '❤️ Auto Heart (ON)' : '🤍 Auto Heart (OFF)';
settingsButton.style.backgroundColor = isEnabled ? '#FF0000' : '#666666';
if (isEnabled) {
startAutoHeartProcess();
startAutoScroll(); // Start auto-scroll when enabling the script
showNotification('YouTube Auto Heart is now enabled');
debug("Auto Heart Enabled via button");
heartComments();
} else {
stopAutoHeartProcess();
stopAutoScroll(); // Stop auto-scroll when disabling the script
showNotification('YouTube Auto Heart is now disabled');
debug("Auto Heart Disabled via button");
}
});
document.body.appendChild(settingsButton);
updateCounter();
}
// Start auto heart process
function startAutoHeartProcess() {
if (!heartInterval) {
debug(`Starting heart interval (${CONFIG.checkInterval}ms)`);
heartInterval = setInterval(heartComments, CONFIG.checkInterval);
}
}
// Stop auto heart process
function stopAutoHeartProcess() {
if (heartInterval) {
debug("Stopping heart interval");
clearInterval(heartInterval);
heartInterval = null;
}
isProcessing = false;
}
// Initialize script
function initScript() {
console.log('[YouTube Auto Heart] Initializing script v1.8...');
debug('Script init');
currentUrl = location.href;
isStudioComments = window.location.href.startsWith('https://studio.youtube.com/');
debug(`Initial URL: ${currentUrl}, isStudio: ${isStudioComments}`);
addSettingsButton();
if (urlCheckInterval) clearInterval(urlCheckInterval);
urlCheckInterval = setInterval(checkUrlChange, 500);
if (isEnabled) {
startAutoHeartProcess();
startAutoScroll(); // Start auto-scroll on initialization if enabled
setTimeout(heartComments, 1500);
} else {
debug("Script initialized but is disabled by user setting.");
}
debug("Initialization complete.");
}
// Wait for page to load before initializing
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(initScript, 2000);
} else {
window.addEventListener('DOMContentLoaded', () => setTimeout(initScript, 2000));
}
})();