import pytchat import sys import os import time import re import json from datetime import datetime from rich.console import Console from rich.style import Style from rich.text import Text console = Console() # Define the emoji pattern and coloring function emoji_pattern = re.compile( r'(' r' \U0001F600-\U0001F64F' # emoticons r' \U0001F300-\U0001F5FF' # symbols & pictographs r' \U0001F680-\U0001F6FF' # transport & map symbols r' \U0001F1E0-\U0001F1FF' # flags (iOS) r' \u2702-\u27B0' # Dingbats r' \u24C2-\u2B55' # Enclosed characters r' \U0001F900-\U0001F9FF' # Supplemental Symbols & Pictographs r' \u200D' # ZWJ r' \uFE0F' # VS-16 r')+', flags=re.UNICODE) def colour_emoji(txt): return emoji_pattern.sub(r'[magenta]\1[/magenta]', txt) # --- User Color Management --- USER_COLORS_FILE = "user_colors.json" # A palette of distinct colors for usernames COLOR_PALETTE = [ "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#00FFFF", "#FF00FF", "#FFA500", "#800080", "#008000", "#FFC0CB", "#808000", "#008080", "#C0C0C0", "#800000", "#000080", "#FFD700", "#ADFF2F", "#FF69B4" ] user_color_map = {} def load_user_colors(): global user_color_map if os.path.exists(USER_COLORS_FILE): try: with open(USER_COLORS_FILE, 'r') as f: user_color_map = json.load(f) except json.JSONDecodeError: console.print(f"[red]Error loading {USER_COLORS_FILE}. File might be corrupted. Starting with empty colors.[/red]") user_color_map = {} def save_user_colors(): with open(USER_COLORS_FILE, 'w') as f: json.dump(user_color_map, f, indent=4) def get_user_color(author_id): global user_color_map if author_id not in user_color_map: # Determine which colors are already in use by existing users used_colors = set(user_color_map.values()) # Find available colors from the palette that are not yet used available_colors = [color for color in COLOR_PALETTE if color not in used_colors] if available_colors: # If there are unused colors in the palette next_color_index = len(user_color_map) % len(available_colors) user_color_map[author_id] = available_colors[next_color_index] else: # If all colors in the palette are already used, cycle through the palette next_color_index = len(user_color_map) % len(COLOR_PALETTE) user_color_map[author_id] = COLOR_PALETTE[next_color_index] console.print("[yellow]Warning: All colors in palette are reserved. Cycling through existing colors.[/yellow]") save_user_colors() # Save immediately after assigning a new color return user_color_map[author_id] # --- Main Script Logic --- def main(): # Set terminal title sys.stdout.write("\033]0;YouTube Live Chat\007") sys.stdout.flush() # Clear the terminal screen os.system('clear') video_id = input("Enter the YouTube Live Stream Video ID: ") # Setup chat logging log_dir = "chat_logs" os.makedirs(log_dir, exist_ok=True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") log_filename = os.path.join(log_dir, f"chat_{video_id}_{timestamp}.log") log_file = open(log_filename, "w", encoding="utf-8") console.print(f"[green]Chat will be logged to {log_filename}[/green]") # Load existing user colors from user_colors.json load_user_colors() try: livechat = pytchat.create(video_id=video_id) console.print(f"[green]Listening to live chat for video ID: {video_id}[/green]") console.print(f"[green]Video ID accepted: {video_id}[/green]") # Wait for 5 seconds, then clear the screen time.sleep(5) os.system('clear') message_count = 0 while livechat.is_alive(): for c in livechat.get().sync_items(): author_display_name = c.author.name # pytchat provides author.channelId, which is perfect for unique identification author_channel_id = c.author.channelId message_text = c.message # Process text-based emotes (e.g., :face-purple-sweating:) message_text = re.sub(r'(:[a-zA-Z0-9_-]+:)', r'[blue]\1[/blue]', message_text) # Apply emoji coloring using the new function message_text = colour_emoji(message_text) # Get persistent color for the user user_color = get_user_color(author_channel_id) username_style = Style(color=user_color, bold=True) # Create a Text object for the username and apply style directly username_text = Text(f"{author_display_name}: ", style=username_style) # Create a Text object for the message, interpreting rich markup # This allows rich markup (e.g., [bold]) within the message text itself to be rendered. message_text_rich = Text.from_markup(message_text) # Assemble the full message full_message_text = Text.assemble(username_text, message_text_rich) # Alternate background styles background_style = Style(bgcolor="#2B2B2B") if message_count % 2 == 0 else Style(bgcolor="#3A3A3A") # Apply the background style to the entire Text object # This ensures the background color extends across the full width of the terminal # for all lines of the message, even if it wraps. full_message_text.stylize(background_style) # Print the message, letting rich handle wrapping and filling the width # The 'width' parameter ensures the background fills the terminal width. # 'overflow="crop"' prevents text from wrapping beyond the terminal width. console.print(full_message_text, width=console.width, overflow="crop") log_file.write(f"{datetime.now().strftime("%H:%M:%S")} {full_message_text.plain}\n") message_count += 1 except Exception as e: console.print(f"[red]An error occurred: {e}[/red]") finally: global _stop_input_thread _stop_input_thread = True # Signal the input thread to stop # input_thread.join() # Optionally wait for the input thread to finish, but daemon=True makes it optional log_file.close() save_log = input(f"\nDo you want to save the chat log to {log_filename}? (y/n): ").lower() if save_log != 'y' and save_log != 'yes': os.remove(log_filename) console.print(f"[red]Chat log not saved. {log_filename} deleted.[/red]") else: console.print(f"[green]Chat log saved to {log_filename}[/green]") if __name__ == '__main__': main()