import time import random import multiprocessing import subprocess import os import shutil import logging import sqlite3 import re from datetime import datetime import undetected_chromedriver as uc from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import tkinter as tk from tkinter import ttk, messagebox, scrolledtext from webdriver_manager.chrome import ChromeDriverManager from multiprocessing import freeze_support import threading import platform CHROME_DRIVER_PATH = ChromeDriverManager().install() PROFILES_BASE_PATH = os.path.abspath("./profiles") DB_PATH = os.path.abspath("youtube_bot.db") CHROME_CONFIG_PATH = "" KILL_COMMANDS = [] def extract_video_id(url): """Extract the YouTube video ID from a URL.""" if not url: return None pattern = r'(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})' shorts_pattern = r'youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})' shorts_match = re.search(shorts_pattern, url) if shorts_match: return shorts_match.group(1) match = re.search(pattern, url) if match: return match.group(1) return None def set_global_vars(): global CHROME_CONFIG_PATH, KILL_COMMANDS if platform.system() == "Windows": CHROME_CONFIG_PATH = os.path.join(os.path.expanduser("~"), "AppData", "Local", "Google", "Chrome", "User Data") KILL_COMMANDS = ["taskkill /F /IM chrome.exe", "taskkill /F /IM chromedriver.exe"] else: CHROME_CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".config", "google-chrome") KILL_COMMANDS = ["pkill chrome", "pkill chromedriver"] def kill_existing_chrome(): try: for cmd in KILL_COMMANDS: subprocess.call(cmd, shell=True) logging.info("Killed existing Chrome and ChromeDriver processes.") except Exception as e: logging.error(f"Failed to kill Chrome processes: {str(e)}") set_global_vars() logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler('bot.log'), logging.StreamHandler()] ) def simulate_typing(element, text): for char in text: element.send_keys(char) time.sleep(random.uniform(0.005, 0.01)) class DatabaseManager: def __init__(self, db_path=DB_PATH): self.db_path = db_path self.initialize_db() def get_connection(self): return sqlite3.connect(self.db_path) def initialize_db(self): """Create database tables if they don't exist""" with self.get_connection() as conn: cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS comment_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, video_id TEXT NOT NULL, profile TEXT NOT NULL, timestamp DATETIME NOT NULL ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS visited_videos ( video_id TEXT PRIMARY KEY, url TEXT NOT NULL, first_seen DATETIME NOT NULL ) ''') conn.commit() def add_comment_history(self, url, profile): """Add a comment to history""" video_id = extract_video_id(url) or "unknown" with self.get_connection() as conn: cursor = conn.cursor() cursor.execute( "INSERT INTO comment_history (url, video_id, profile, timestamp) VALUES (?, ?, ?, ?)", (url, video_id, profile, datetime.now().isoformat()) ) conn.commit() def get_comment_history(self, limit=100, search_term=""): """Get comment history with latest entries first, optionally filtered by search term""" with self.get_connection() as conn: cursor = conn.cursor() if search_term: cursor.execute( "SELECT timestamp, profile, url, video_id FROM comment_history WHERE url LIKE ? OR profile LIKE ? OR video_id LIKE ? ORDER BY timestamp DESC LIMIT ?", (f"%{search_term}%", f"%{search_term}%", f"%{search_term}%", limit) ) else: cursor.execute( "SELECT timestamp, profile, url, video_id FROM comment_history ORDER BY timestamp DESC LIMIT ?", (limit,) ) return cursor.fetchall() def check_video_exists(self, url): """Check if a video ID already exists in the visited_videos table""" video_id = extract_video_id(url) if not video_id: return False with self.get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT video_id FROM visited_videos WHERE video_id = ?", (video_id,)) return cursor.fetchone() is not None def add_video(self, url): """Add a video ID to the visited_videos table if it does not already exist""" video_id = extract_video_id(url) if not video_id: return False if not self.check_video_exists(url): with self.get_connection() as conn: cursor = conn.cursor() cursor.execute( "INSERT INTO visited_videos (video_id, url, first_seen) VALUES (?, ?, ?)", (video_id, url, datetime.now().isoformat()) ) conn.commit() return True return False def get_visited_videos_count(self): """Get count of visited videos""" with self.get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT COUNT(*) FROM visited_videos") return cursor.fetchone()[0] class ChromeProfileManager: def __init__(self): kill_existing_chrome() self.profile_map = {} self.setup_profiles() def setup_profiles(self): os.makedirs(PROFILES_BASE_PATH, exist_ok=True) profiles = [d for d in os.listdir(CHROME_CONFIG_PATH) if os.path.isdir(os.path.join(CHROME_CONFIG_PATH, d)) and (d == "Default" or d.startswith("Profile"))] for profile in profiles: if profile == "Default": self.profile_map[profile] = {'path': CHROME_CONFIG_PATH, 'name': profile} else: source_profile = os.path.join(CHROME_CONFIG_PATH, profile) dest_profile = os.path.join(PROFILES_BASE_PATH, profile) profile_dest_folder = os.path.join(dest_profile, profile) if not os.path.exists(profile_dest_folder): shutil.copytree(source_profile, profile_dest_folder) local_state_src = os.path.join(CHROME_CONFIG_PATH, "Local State") local_state_dest = os.path.join(dest_profile, "Local State") if os.path.exists(local_state_src): shutil.copy2(local_state_src, local_state_dest) self.profile_map[profile] = {'path': dest_profile, 'name': profile} return self.profile_map class YouTubeAutomation: def __init__(self, profile, video_urls, comments, delay, wait_before_comment=0, pause_video=True): self.profile = profile self.video_urls = video_urls self.comments = comments self.delay = delay self.wait_before_comment = wait_before_comment self.pause_video = pause_video self.driver = None self.db_manager = DatabaseManager() def start_driver(self, max_retries=2): for attempt in range(max_retries): try: options = uc.ChromeOptions() options.add_argument(f"--user-data-dir={self.profile['path']}") options.add_argument(f"--profile-directory={self.profile['name']}") prefs = { "profile.managed_default_content_settings.images": 2, "profile.managed_default_content_settings.media_stream": 2, "profile.managed_default_content_settings.autoplay": 2, "media.audio_category.playback.enabled": False, "audio.volume_control.enabled": False, "audio.mute.enabled": True } options.add_experimental_option("prefs", prefs) options.add_argument("--headless") options.add_argument("--autoplay-policy=no-user-gesture-required") options.add_argument("--disable-features=AutoplayIgnoreWebAudio,AutoplayIgnoreWebPreferences") options.add_argument("--disable-extensions") options.add_argument("--disable-notifications") options.add_argument("--blink-settings=imagesEnabled=false") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--mute-audio") return uc.Chrome(driver_executable_path=CHROME_DRIVER_PATH, options=options) except Exception as e: logging.error(f"Failed to start Chrome (attempt {attempt+1}/{max_retries}): {str(e)}") kill_existing_chrome() time.sleep(2) raise Exception("Failed to start Chrome after multiple attempts") @staticmethod def collect_videos_for_searches(profile, search_urls, max_videos, queue, db_manager, skip_count=0): collected_videos = [] video_sources = {} # Track which URLs each video came from driver = None try: options = uc.ChromeOptions() options.add_argument(f"--user-data-dir={profile['path']}") options.add_argument(f"--profile-directory={profile['name']}") options.add_argument("--headless") prefs = { "profile.managed_default_content_settings.images": 2, "profile.managed_default_content_settings.stylesheets": 2, "profile.managed_default_content_settings.plugins": 2, "profile.managed_default_content_settings.media_stream": 2, "media.audio_category.playback.enabled": False, "audio.volume_control.enabled": False, "audio.mute.enabled": True } options.add_experimental_option("prefs", prefs) options.add_argument("--blink-settings=imagesEnabled=false") options.add_argument("--disable-extensions") options.add_argument("--disable-notifications") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") driver = uc.Chrome(driver_executable_path=CHROME_DRIVER_PATH, options=options) for search_url in search_urls: counter = 0 videos_found = 0 total_videos_found = 0 try: driver.get(search_url) WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "ytd-video-renderer"))) if skip_count > 0: queue.put(f"Skipping first {skip_count} videos for {search_url}") videos_to_skip = skip_count last_height = driver.execute_script("return document.documentElement.scrollHeight") while total_videos_found < videos_to_skip: driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);") time.sleep(2) new_height = driver.execute_script("return document.documentElement.scrollHeight") if new_height == last_height: break last_height = new_height current_videos = driver.find_elements(By.CSS_SELECTOR, "a#video-title") total_videos_found = len(current_videos) # queue.put(f"Loaded {total_videos_found}/{videos_to_skip} videos for skipping") if total_videos_found >= videos_to_skip: break last_height = driver.execute_script("return document.documentElement.scrollHeight") collecting = True while collecting and counter < max_videos: driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);") time.sleep(2) new_height = driver.execute_script("return document.documentElement.scrollHeight") if new_height == last_height: queue.put(f"Reached end of results for {search_url}") break last_height = new_height all_videos = driver.find_elements(By.CSS_SELECTOR, "a#video-title") if len(all_videos) <= skip_count: continue videos_to_process = [el.get_attribute("href") for el in all_videos[skip_count:]] new_videos = [] for video in videos_to_process: if video and not db_manager.check_video_exists(video): video_id = extract_video_id(video) if video_id: new_videos.append(video) # Track which search URL found this video if video not in video_sources: video_sources[video] = set() video_sources[video].add(search_url) videos_found += 1 if videos_found >= max_videos: collecting = False break if len(new_videos) > 0: collected_videos.extend(new_videos) counter += len(new_videos) queue.put(f"Profile {profile['name']} found {len(new_videos)} new videos from {search_url}, total: {counter}") else: queue.put(f"No new unique videos found for {search_url}") if videos_found >= max_videos: break except Exception as e: queue.put(f"Error collecting videos from {search_url}: {str(e)}") continue finally: if driver: driver.quit() return collected_videos, video_sources def handle_shorts_comment(self): try: view_comments_button = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, "#comments-button button"))) if self.pause_video: self.driver.execute_script("document.querySelector('video').pause();") view_comments_button.click() comment_placeholder = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, "div#placeholder-area yt-formatted-string"))) comment_placeholder.click() comment_field = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "div#contenteditable-root"))) simulate_typing(comment_field, random.choice(self.comments)) post_button = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//ytd-button-renderer[@id='submit-button']"))) post_button.click() time.sleep(2) return True except Exception as e: logging.error(f"Shorts comment failed: {str(e)}") return False def post_comment(self, url): try: # Check if video exists in database video_id = extract_video_id(url) if video_id and self.db_manager.check_video_exists(url): logging.info(f"Skipping video {url} - already exists in database") return False self.driver.get(url) WebDriverWait(self.driver, 15).until(EC.presence_of_element_located((By.TAG_NAME, "body"))) time.sleep(self.wait_before_comment) if self.pause_video: self.driver.execute_script("document.querySelector('video').pause();") if "/shorts/" in url: return self.handle_shorts_comment() else: scroll_attempts = 2 for attempt in range(scroll_attempts): self.driver.execute_script("window.scrollBy(0, 500)") time.sleep(1) if self.pause_video: self.driver.execute_script("document.querySelector('video').pause();") try: comment_box = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, "//yt-formatted-string[@id='simplebox-placeholder']"))) break except: if attempt == scroll_attempts - 1: raise Exception("Comment box not found after scrolling") comment_box.click() comment_field = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "div#contenteditable-root"))) simulate_typing(comment_field, random.choice(self.comments)) post_button = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//ytd-button-renderer[@id='submit-button']"))) post_button.click() time.sleep(2) return True except Exception as e: logging.error(f"Comment failed: {str(e)}") return False def run(self, queue, stop_flag): try: queue.put(f"Profile {self.profile['name']} started commenting.") self.driver = self.start_driver() for video in self.video_urls: if stop_flag.value == 1: queue.put("Commenting stopped by user") break success = self.post_comment(video) if success: self.db_manager.add_comment_history(video, self.profile['name']) self.db_manager.add_video(video) video_id = extract_video_id(video) or "unknown" queue.put(f"✓ Commented on {video} (ID: {video_id})") else: self.db_manager.add_comment_history(video, self.profile['name']) self.db_manager.add_video(video) video_id = extract_video_id(video) or "unknown" queue.put(f"✗ Failed to comment on {video}") time.sleep(self.delay) finally: try: if self.driver: self.driver.quit() except: pass def collect_and_store_videos(profile, search_urls, max_videos, queue, db_manager, skip_count=0): collected_videos = [] video_sources = {} # Track which URLs each video came from driver = None try: options = uc.ChromeOptions() options.add_argument(f"--user-data-dir={profile['path']}") options.add_argument(f"--profile-directory={profile['name']}") options.add_argument("--headless") prefs = { "profile.managed_default_content_settings.images": 2, "profile.managed_default_content_settings.stylesheets": 2, "profile.managed_default_content_settings.plugins": 2, "profile.managed_default_content_settings.media_stream": 2, "media.audio_category.playback.enabled": False, "audio.volume_control.enabled": False, "audio.mute.enabled": True } options.add_experimental_option("prefs", prefs) options.add_argument("--blink-settings=imagesEnabled=false") options.add_argument("--disable-extensions") options.add_argument("--disable-notifications") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") driver = uc.Chrome(driver_executable_path=CHROME_DRIVER_PATH, options=options) for search_url in search_urls: counter = 0 videos_found = 0 total_videos_found = 0 try: driver.get(search_url) WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, "ytd-video-renderer"))) if skip_count > 0: queue.put(f"Skipping first {skip_count} videos for {search_url}") videos_to_skip = skip_count last_height = driver.execute_script("return document.documentElement.scrollHeight") while total_videos_found < videos_to_skip: driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);") time.sleep(2) new_height = driver.execute_script("return document.documentElement.scrollHeight") if new_height == last_height: break last_height = new_height current_videos = driver.find_elements(By.CSS_SELECTOR, "a#video-title") total_videos_found = len(current_videos) # queue.put(f"Loaded {total_videos_found}/{videos_to_skip} videos for skipping") if total_videos_found >= videos_to_skip: break last_height = driver.execute_script("return document.documentElement.scrollHeight") collecting = True while collecting and counter < max_videos: driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);") time.sleep(2) new_height = driver.execute_script("return document.documentElement.scrollHeight") if new_height == last_height: queue.put(f"Reached end of results for {search_url}") break last_height = new_height all_videos = driver.find_elements(By.CSS_SELECTOR, "a#video-title") if len(all_videos) <= skip_count: continue videos_to_process = [el.get_attribute("href") for el in all_videos[skip_count:]] new_videos = [] for video in videos_to_process: if video and not db_manager.check_video_exists(video): video_id = extract_video_id(video) if video_id: new_videos.append(video) # Track which search URL found this video if video not in video_sources: video_sources[video] = set() video_sources[video].add(search_url) videos_found += 1 if videos_found >= max_videos: collecting = False break if len(new_videos) > 0: collected_videos.extend(new_videos) counter += len(new_videos) queue.put(f"Profile {profile['name']} found {len(new_videos)} new videos from {search_url}, total: {counter}") else: queue.put(f"No new unique videos found for {search_url}") if videos_found >= max_videos: break except Exception as e: queue.put(f"Error collecting videos from {search_url}: {str(e)}") continue finally: if driver: driver.quit() # Log videos found in multiple search URLs for video in video_sources: if len(video_sources[video]) > 1: video_id = extract_video_id(video) queue.put(f"Video {video_id} found in multiple search URLs: {', '.join(video_sources[video])}") return collected_videos, video_sources class Application(tk.Tk): def __init__(self): super().__init__() self.title("YouTube Bot - V2") self.geometry("900x700") self.profiles = ChromeProfileManager().profile_map self.queue = multiprocessing.Queue() self.stop_flag = multiprocessing.Value('i', 0) self.db_manager = DatabaseManager() self.create_widgets() self.after(100, self.update_queue) self.running_processes = [] def create_widgets(self): main = ttk.Frame(self) main.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) entry_frame = ttk.Frame(main) entry_frame.pack(fill=tk.X, pady=(0, 10)) url_frame = ttk.LabelFrame(entry_frame, text="Search URLs") url_frame.pack(fill=tk.X, pady=5) self.search_entry = scrolledtext.ScrolledText(url_frame, height=4) self.search_entry.pack(fill=tk.X, padx=5, pady=5) comment_frame = ttk.LabelFrame(entry_frame, text="Comments") comment_frame.pack(fill=tk.X, pady=5) self.comment_entry = scrolledtext.ScrolledText(comment_frame, height=4) self.comment_entry.pack(fill=tk.X, padx=5, pady=5) settings_frame = ttk.LabelFrame(main, text="Settings") settings_frame.pack(fill=tk.X, pady=5) for i in range(8): settings_frame.columnconfigure(i, weight=1) ttk.Label(settings_frame, text="Videos per search:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.vid_count = ttk.Spinbox(settings_frame, from_=1, to=100, width=5) self.vid_count.set(20) self.vid_count.grid(row=0, column=1, padx=5, pady=5, sticky="w") ttk.Label(settings_frame, text="Delay (seconds):").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.delay_entry = ttk.Entry(settings_frame, width=5) self.delay_entry.insert(0, "1") self.delay_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w") ttk.Label(settings_frame, text="Wait before comment (sec):").grid(row=0, column=4, padx=5, pady=5, sticky="w") self.wait_before_comment_entry = ttk.Entry(settings_frame, width=5) self.wait_before_comment_entry.insert(0, "0") self.wait_before_comment_entry.grid(row=0, column=5, padx=5, pady=5, sticky="w") ttk.Label(settings_frame, text=f"Parallel profiles ({len(self.profiles)}):").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.parallel_profiles_entry = ttk.Entry(settings_frame, width=5) self.parallel_profiles_entry.insert(0, "1") self.parallel_profiles_entry.grid(row=1, column=1, padx=5, pady=5, sticky="w") ttk.Label(settings_frame, text="Videos per profile:").grid(row=1, column=2, padx=5, pady=5, sticky="w") self.videos_per_profile_entry = ttk.Entry(settings_frame, width=5) self.videos_per_profile_entry.insert(0, "3") self.videos_per_profile_entry.grid(row=1, column=3, padx=5, pady=5, sticky="w") ttk.Label(settings_frame, text="Batch size:").grid(row=1, column=4, padx=5, pady=5, sticky="w") self.batch_size_entry = ttk.Entry(settings_frame, width=5) self.batch_size_entry.insert(0, "20") self.batch_size_entry.grid(row=1, column=5, padx=5, pady=5, sticky="w") stats_frame = ttk.Frame(settings_frame) stats_frame.grid(row=2, column=0, columnspan=6, padx=5, pady=5, sticky="ew") checkbox_frame = ttk.Frame(stats_frame) checkbox_frame.pack(side=tk.LEFT, fill=tk.X, expand=True) self.pause_video_var = tk.BooleanVar(value=True) ttk.Checkbutton(checkbox_frame, text="Pause video", variable=self.pause_video_var).pack(side=tk.LEFT, padx=(0, 15)) self.infinite_loop_var = tk.BooleanVar(value=False) ttk.Checkbutton(checkbox_frame, text="Infinite loop mode", variable=self.infinite_loop_var).pack(side=tk.LEFT, padx=(0, 15)) visited_count = self.db_manager.get_visited_videos_count() self.stats_label = ttk.Label(stats_frame, text=f"Videos in database: {visited_count}") self.stats_label.pack(side=tk.RIGHT, padx=5) button_frame = ttk.Frame(settings_frame) button_frame.grid(row=3, column=0, columnspan=6, pady=10, sticky="ew") button_frame.columnconfigure(0, weight=1) button_frame.columnconfigure(1, weight=1) button_frame.columnconfigure(2, weight=1) button_frame.columnconfigure(3, weight=1) ttk.Button(button_frame, text="View History", command=self.show_history).grid(row=0, column=0, padx=5, sticky="e") ttk.Button(button_frame, text="Clear Database", command=self.confirm_clear_db).grid(row=0, column=1, padx=5, sticky="ew") self.start_button = ttk.Button(button_frame, text="Start", command=self.start) self.start_button.grid(row=0, column=2, padx=5, sticky="ew") self.stop_button = ttk.Button(button_frame, text="Stop", command=self.stop, state=tk.DISABLED) self.stop_button.grid(row=0, column=3, padx=5, sticky="w") log_frame = ttk.LabelFrame(main, text="Log") log_frame.pack(fill=tk.BOTH, expand=True, pady=5) self.log = scrolledtext.ScrolledText(log_frame, height=15) self.log.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) def confirm_clear_db(self): result = messagebox.askyesno("Confirm", "Are you sure you want to clear the database? This will erase all history and tracked videos.") if result: self.clear_database() def clear_database(self): """Delete and recreate the database""" try: if os.path.exists(DB_PATH): os.remove(DB_PATH) self.db_manager = DatabaseManager() self.update_stats() self.queue.put("Database cleared successfully") except Exception as e: self.queue.put(f"Error clearing database: {str(e)}") def update_stats(self): """Update the stats label with current database counts""" visited_count = self.db_manager.get_visited_videos_count() self.stats_label.config(text=f"Videos in database: {visited_count}") def start(self): comments = self.comment_entry.get("1.0", tk.END).strip().split("\n") if not comments: messagebox.showerror("Error", "Enter comments") return try: delay = int(self.delay_entry.get()) parallel_profiles = int(self.parallel_profiles_entry.get()) wait_before_comment = int(self.wait_before_comment_entry.get()) pause_video = self.pause_video_var.get() if parallel_profiles <= 0 or delay < 0 or wait_before_comment < 0: raise ValueError self.stop_flag.value = 0 self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) except ValueError: messagebox.showerror("Error", "Invalid delay, parallel profiles, or wait before commenting value") return threading.Thread(target=self.run_automation, daemon=True, args=(wait_before_comment, pause_video)).start() def stop(self): self.queue.put("Stopping all processes...") self.stop_flag.value = 1 for process in self.running_processes: if process.is_alive(): process.terminate() self.running_processes = [] self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self.queue.put("All processes stopped") kill_existing_chrome() def run_automation(self, wait_before_comment, pause_video): max_profiles = len(self.profiles) try: parallel_profiles = int(self.parallel_profiles_entry.get()) if parallel_profiles <= 0 or parallel_profiles > max_profiles: self.queue.put(f"Error: Parallel profiles must be between 1 and {max_profiles}") self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return except ValueError: self.queue.put("Error: Invalid parallel profiles value") self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return profiles = list(self.profiles.values()) search_urls = self.search_entry.get("1.0", tk.END).strip().split("\n") if not search_urls: self.queue.put("Error: No search URLs provided") self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) return infinite_loop = self.infinite_loop_var.get() if infinite_loop: self.queue.put("Running in infinite loop mode") try: batch_size = int(self.batch_size_entry.get()) if batch_size <= 0: batch_size = 20 self.queue.put("Invalid batch size, using default: 20") except ValueError: batch_size = 20 self.queue.put("Invalid batch size, using default: 20") videos_per_cycle = batch_size * len(search_urls) self.queue.put(f"Will attempt to collect {batch_size} videos per search URL ({videos_per_cycle} total per cycle)") skip_counts = {url: 0 for url in search_urls} while self.stop_flag.value == 0: self.queue.put(f"Starting new cycle with skip counts: {skip_counts}") # Collect videos using multiple profiles in parallel all_videos = [] all_video_sources = {} # Track sources across all collections for i in range(0, len(search_urls), parallel_profiles): if self.stop_flag.value == 1: break # Get the batch of URLs for this iteration url_batch = search_urls[i:i + parallel_profiles] self.queue.put(f"Processing URLs: {url_batch}") manager = multiprocessing.Manager() results = manager.list() collection_processes = [] # Start a process for each URL using different profiles for j, url in enumerate(url_batch): profile_idx = j % parallel_profiles self.queue.put(f"Using profile {profile_idx} to collect {batch_size} videos from {url} (skip: {skip_counts[url]})") process = multiprocessing.Process( target=collect_and_store_videos, args=(profiles[profile_idx], [url], batch_size, results, self.queue, self.db_manager, skip_counts[url]) ) collection_processes.append(process) self.running_processes.append(process) process.start() # Wait for all collection processes in this batch to complete for process in collection_processes: if self.stop_flag.value == 0: process.join() else: if process.is_alive(): process.terminate() # Add collected videos to main list for videos, sources in results: all_videos.extend(videos) for video, urls in sources.items(): if video not in all_video_sources: all_video_sources[video] = set() all_video_sources[video].update(urls) kill_existing_chrome() if self.stop_flag.value == 1: break if not all_videos: self.queue.put("No new videos found in this cycle, updating skip counts and continuing") for url in search_urls: skip_counts[url] += batch_size continue # Remove duplicates while preserving order seen = set() all_videos = [x for x in all_videos if not (x in seen or seen.add(x))] self.queue.put(f"Collected {len(all_videos)} unique videos, starting comment process") try: # Distribute videos evenly among available profiles videos_per_profile = len(all_videos) // parallel_profiles if videos_per_profile == 0: videos_per_profile = 1 chunks = [all_videos[i:i + videos_per_profile] for i in range(0, len(all_videos), videos_per_profile)] # Process chunks in parallel for i in range(0, len(chunks), parallel_profiles): if self.stop_flag.value == 1: break batch_chunks = chunks[i:i + parallel_profiles] comment_processes = [] for j, chunk in enumerate(batch_chunks): profile_idx = j % parallel_profiles automation = YouTubeAutomation(profiles[profile_idx], chunk, self.comment_entry.get("1.0", tk.END).strip().split("\n"), int(self.delay_entry.get()), wait_before_comment, pause_video) p = multiprocessing.Process(target=automation.run, args=(self.queue, self.stop_flag)) comment_processes.append(p) self.running_processes.append(p) p.start() for p in comment_processes: if self.stop_flag.value == 0: p.join() else: if p.is_alive(): p.terminate() kill_existing_chrome() except ValueError as e: self.queue.put(f"Error during commenting process: {str(e)}") break # Update skip counts for next cycle for url in search_urls: skip_counts[url] += batch_size self.queue.put(f"Completed cycle, next cycle will use skip counts: {skip_counts}") self.update_stats() # except ValueError as e: # self.queue.put(f"Error during process: {str(e)}") # self.start_button.config(state=tk.NORMAL) # self.stop_button.config(state=tk.DISABLED) # except Exception as e: # self.queue.put(f"Unexpected error: {str(e)}") # self.start_button.config(state=tk.NORMAL) # self.stop_button.config(state=tk.DISABLED) def update_queue(self): while not self.queue.empty(): message = self.queue.get_nowait() self.log.insert(tk.END, f"{message}\n") self.log.see(tk.END) self.after(100, self.update_queue) def show_history(self): history_window = tk.Toplevel(self) history_window.title("Operation History") history_window.geometry("900x500") main_frame = ttk.Frame(history_window) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) search_frame = ttk.Frame(main_frame) search_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label(search_frame, text="Search:").pack(side=tk.LEFT, padx=(0, 5)) search_entry = ttk.Entry(search_frame, width=40) search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) history_window.search_entry = search_entry ttk.Button(search_frame, text="Search", command=lambda: self.load_history_with_search(history_window)).pack(side=tk.LEFT) columns = ('timestamp', 'profile', 'url', 'video_id') tree = ttk.Treeview(main_frame, columns=columns, show='headings') tree.heading('timestamp', text='Date & Time') tree.heading('profile', text='Profile') tree.heading('url', text='URL') tree.heading('video_id', text='Video ID') tree.column('timestamp', width=180) tree.column('profile', width=100) tree.column('url', width=450) tree.column('video_id', width=150) vsb = ttk.Scrollbar(main_frame, orient="vertical", command=tree.yview) hsb = ttk.Scrollbar(main_frame, orient="horizontal", command=tree.xview) tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) vsb.pack(side=tk.RIGHT, fill=tk.Y) hsb.pack(side=tk.BOTTOM, fill=tk.X) self.load_history_to_tree(tree) history_window.tree = tree history_window.vsb = vsb history_window.hsb = hsb def load_history_with_search(self, history_window): """Load history with the search term from the history window's entry widget""" search_term = history_window.search_entry.get() self.load_history_to_tree(history_window.tree, search_term) def load_history_to_tree(self, tree, search_term=""): tree.delete(*tree.get_children()) try: history = self.db_manager.get_comment_history(limit=500, search_term=search_term) for entry in history: tree.insert('', tk.END, values=entry) except Exception as e: self.queue.put(f"Error loading history: {str(e)}") if __name__ == "__main__": freeze_support() Application().mainloop()