# debug_console.py - Enhanced with Folder Reset button import tkinter as tk from tkinter import ttk, scrolledtext, messagebox import threading import time import logging from datetime import datetime from collections import deque import sys class EnhancedDebugConsole: def __init__(self): self.root = tk.Tk() self.text_widget = None self.running = False self.logger = logging.getLogger(__name__) self.video_player = None self.message_queue = deque(maxlen=1000) self.lock = threading.Lock() self.nfc_buffer = "" self.nfc_timeout = 0.3 self.last_nfc_input = 0 # Global input capture self.global_listener = None self.global_input_method = None self.global_input_active = False # Track mute state self.is_muted = False self.stats = { 'videos_played': 0, 'key_presses': 0, 'errors': 0, 'start_time': datetime.now(), 'queue_depth': 0, 'current_video': 'None', 'fullscreen': True, 'folder_videos_played': 0 # New stat for folder videos } self.setup_gui() self.setup_global_input_capture() self.log("Enhanced debug console initialized with folder support") def setup_global_input_capture(self): """Setup global keyboard capture""" if self.try_pynput_global_capture(): return if sys.platform == "win32" and self.try_windows_global_capture(): return if sys.platform.startswith('linux') and self.try_linux_global_capture(): return self.log("Warning: Global input capture not available") def try_pynput_global_capture(self): """Try pynput global keyboard listener""" try: from pynput import keyboard def on_key_press(key): try: current_time = time.time() if key == keyboard.Key.enter: if self.nfc_buffer: self.log(f"Global NFC input: {self.nfc_buffer}") self.process_global_nfc_input(self.nfc_buffer.strip()) self.nfc_buffer = "" return if hasattr(key, 'char') and key.char and key.char.isdigit(): if current_time - self.last_nfc_input > self.nfc_timeout: self.nfc_buffer = "" self.nfc_buffer += key.char self.last_nfc_input = current_time except Exception as e: self.logger.debug(f"Global key press error: {e}") self.global_listener = keyboard.Listener(on_press=on_key_press) self.global_listener.start() self.global_input_method = "pynput" self.global_input_active = True self.log("Global input capture active (pynput)") return True except ImportError: return False except Exception as e: self.logger.debug(f"pynput setup failed: {e}") return False def try_windows_global_capture(self): """Windows-specific global keyboard hook""" try: import win32api import win32con import win32gui def low_level_keyboard_proc(nCode, wParam, lParam): try: if nCode >= 0 and wParam == win32con.WM_KEYDOWN: vk_code = lParam[0] current_time = time.time() if vk_code == 13: # Enter if self.nfc_buffer: self.log(f"Global NFC input: {self.nfc_buffer}") self.process_global_nfc_input(self.nfc_buffer.strip()) self.nfc_buffer = "" elif 48 <= vk_code <= 57: # 0-9 if current_time - self.last_nfc_input > self.nfc_timeout: self.nfc_buffer = "" digit = chr(vk_code) self.nfc_buffer += digit self.last_nfc_input = current_time except Exception as e: self.logger.debug(f"Windows hook error: {e}") return win32gui.CallNextHookEx(None, nCode, wParam, lParam) hook = win32gui.SetWindowsHookEx( win32con.WH_KEYBOARD_LL, low_level_keyboard_proc, win32api.GetModuleHandle(None), 0 ) if hook: self.global_listener = hook self.global_input_method = "windows" self.global_input_active = True self.log("Global input capture active (Windows)") return True except ImportError: return False except Exception as e: self.logger.debug(f"Windows hook setup failed: {e}") return False return False def try_linux_global_capture(self): """Linux-specific global keyboard listener""" # Implementation same as original return False def process_global_nfc_input(self, nfc_id): """Process NFC input captured globally""" if not nfc_id: return try: if hasattr(self, 'nfc_entry'): self.nfc_entry.delete(0, tk.END) self.nfc_entry.insert(0, nfc_id) except: pass self.process_nfc_input(nfc_id) def setup_gui(self): """Setup GUI with folder reset button""" self.root.title("Video Player Controller (Enhanced with Folder Support)") self.root.geometry("900x700") self.root.protocol("WM_DELETE_WINDOW", self.on_closing) try: self.root.state('zoomed') except: self.root.geometry("{0}x{1}+0+0".format( self.root.winfo_screenwidth()//2, self.root.winfo_screenheight() )) main_frame = ttk.Frame(self.root, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # Enhanced controls control_frame = ttk.LabelFrame(main_frame, text="Controls", padding="10") control_frame.pack(fill=tk.X) # Primary controls row controls_row1 = ttk.Frame(control_frame) controls_row1.pack(fill=tk.X, pady=(0, 5)) ttk.Button(controls_row1, text="Skip Video", command=self.skip_video).pack(side=tk.LEFT, padx=5) ttk.Button(controls_row1, text="Toggle Fullscreen", command=self.toggle_fullscreen).pack(side=tk.LEFT, padx=5) # Mute/Unmute button self.mute_button = ttk.Button(controls_row1, text="Mute", command=self.toggle_mute) self.mute_button.pack(side=tk.LEFT, padx=5) # NEW: Folder Reset Button reset_button = ttk.Button( controls_row1, text="🔄 Reset Folder Sequences", command=self.reset_folder_sequences ) reset_button.pack(side=tk.LEFT, padx=5) # Global input status self.global_input_label = ttk.Label(controls_row1, text="Global Input: Checking...", foreground="orange") self.global_input_label.pack(side=tk.LEFT, padx=20) # Exit button exit_button = ttk.Button(controls_row1, text="Exit Application", command=self.exit_application) exit_button.pack(side=tk.RIGHT, padx=5) # Secondary controls row controls_row2 = ttk.Frame(control_frame) controls_row2.pack(fill=tk.X, pady=(5, 0)) ttk.Button(controls_row2, text="Clear Log", command=self.clear_log).pack(side=tk.LEFT, padx=5) # Manual NFC input ttk.Label(controls_row2, text="NFC Test:").pack(side=tk.LEFT, padx=(20, 5)) self.nfc_entry = ttk.Entry(controls_row2, width=15) self.nfc_entry.pack(side=tk.LEFT, padx=5) self.nfc_entry.bind('', self.manual_nfc_input) ttk.Button(controls_row2, text="Send", command=self.manual_nfc_input).pack(side=tk.LEFT, padx=2) # Status indicator self.status_label = ttk.Label(controls_row2, text="Status: Starting...", foreground="orange") self.status_label.pack(side=tk.RIGHT, padx=5) # Logging display log_frame = ttk.LabelFrame(main_frame, text="System Log", padding="5") log_frame.pack(fill=tk.BOTH, expand=True) self.text_widget = scrolledtext.ScrolledText( log_frame, wrap=tk.WORD, font=('Consolas', 9), height=20 ) self.text_widget.pack(fill=tk.BOTH, expand=True) # Enhanced statistics stats_frame = ttk.LabelFrame(main_frame, text="Statistics", padding="5") stats_frame.pack(fill=tk.X) self.stats_labels = {} stats_grid = ttk.Frame(stats_frame) stats_grid.pack(fill=tk.X) stats_keys = [ ('videos_played', 'Videos Played'), ('folder_videos_played', 'Folder Videos'), ('key_presses', 'NFC Scans'), ('errors', 'Errors'), ('uptime', 'Uptime'), ('queue_depth', 'Queue Depth'), ('trailers_pool', 'Trailers Available'), ('current_video', 'Current Video') ] for i, (key, label) in enumerate(stats_keys): row = i // 2 col = (i % 2) * 2 ttk.Label(stats_grid, text=f"{label}:").grid(row=row, column=col, sticky=tk.W, padx=5) self.stats_labels[key] = ttk.Label(stats_grid, text="0", font=('Consolas', 9)) self.stats_labels[key].grid(row=row, column=col+1, sticky=tk.W, padx=5) # Bind local keys as fallback self.root.bind('', self.on_key_press) self.root.focus_set() def reset_folder_sequences(self): """Reset all folder video sequences to the beginning""" if messagebox.askyesno( "Reset Folder Sequences", "This will reset all folder video sequences back to the first video.\n\nAre you sure?" ): if self.video_player: try: self.log("Resetting all folder sequences...") self.status_label.config(text="Status: Resetting folders...", foreground="orange") if self.video_player.reset_all_folder_positions(): self.log("✓ All folder sequences reset successfully") messagebox.showinfo( "Reset Complete", "All folder video sequences have been reset to the beginning." ) self.status_label.config(text="Status: Folders reset", foreground="green") else: self.log_error("Failed to reset folder sequences") messagebox.showerror("Reset Failed", "Could not reset folder sequences.") self.status_label.config(text="Status: Reset failed", foreground="red") # Reset status after delay self.root.after(3000, lambda: self.status_label.config(text="Status: Ready", foreground="green")) except Exception as e: self.log_error(f"Folder reset error: {e}") messagebox.showerror("Error", f"Reset failed: {e}") self.status_label.config(text="Status: Error", foreground="red") else: self.log_error("Video player not connected") messagebox.showerror("Error", "Video player not connected") def update_global_input_status(self): """Update global input status indicator""" if self.global_input_active: self.global_input_label.config( text=f"Global Input: Active ({self.global_input_method})", foreground="green" ) else: self.global_input_label.config( text="Global Input: Inactive (Focus Required)", foreground="red" ) def on_closing(self): """Handle window close event""" if messagebox.askokcancel("Exit", "Do you want to exit the video player?"): self.exit_application() def exit_application(self): """Exit the application cleanly""" try: self.log("Exit requested - shutting down") self.status_label.config(text="Status: Shutting down...", foreground="red") # Stop global input capture if self.global_listener: try: if self.global_input_method == "pynput": self.global_listener.stop() elif self.global_input_method == "windows": import win32gui win32gui.UnhookWindowsHookEx(self.global_listener) self.log("Global input capture stopped") except Exception as e: self.log_error(f"Error stopping global input: {e}") # Stop video player if self.video_player: self.video_player.stop() self.log("Video player stopped") self.running = False self.root.quit() self.root.destroy() import sys sys.exit(0) except Exception as e: self.log_error(f"Error during exit: {e}") import sys sys.exit(1) def clear_log(self): """Clear the log display""" if self.text_widget: self.text_widget.delete(1.0, tk.END) self.log("Log cleared") def manual_nfc_input(self, event=None): """Handle manual NFC input""" nfc_id = self.nfc_entry.get().strip() if nfc_id: self.log(f"Manual NFC input: {nfc_id}") self.process_nfc_input(nfc_id) self.nfc_entry.delete(0, tk.END) def on_key_press(self, event): """Fallback NFC input handling""" if self.global_input_active: return current_time = time.time() try: if event.keysym == 'Return': if self.nfc_buffer: self.log(f"Local NFC input: {self.nfc_buffer}") self.process_nfc_input(self.nfc_buffer) self.nfc_buffer = "" return if event.char and event.char.isdigit(): if current_time - self.last_nfc_input > self.nfc_timeout: self.nfc_buffer = "" self.nfc_buffer += event.char self.last_nfc_input = current_time except Exception as e: self.log_error(f"Local key press error: {e}") def log(self, message, level="INFO"): """Enhanced logging""" timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] thread_name = threading.current_thread().name[:10] formatted = f"[{timestamp}][{thread_name}] {level}: {message}" with self.lock: self.message_queue.append(formatted) if level == "ERROR": self.stats['errors'] += 1 def log_error(self, message): """Log error message""" self.log(message, "ERROR") def log_video_played(self, video_name): """Log video playback - thread-safe version""" self.log(f"Video played: {video_name}") self.stats['videos_played'] += 1 self.stats['current_video'] = video_name # Update status label using thread-safe after() method if self.status_label: try: self.root.after(0, lambda: self._update_status_safe("Status: Playing video", "green")) except: pass # Ignore if GUI isn't ready def _update_status_safe(self, text, color): """Safely update status label from any thread""" try: if self.status_label: self.status_label.config(text=text, foreground=color) except: pass def log_key_press(self, key): """Log key press""" self.log(f"Key pressed: {key}") self.stats['key_presses'] += 1 def process_nfc_input(self, nfc_id=None): """Process NFC input""" if nfc_id is None: nfc_id = self.nfc_buffer.strip() if not nfc_id: return self.log(f"Processing NFC ID: {nfc_id}") self.log_key_press(nfc_id) if self.status_label: self.status_label.config(text=f"Status: Processing NFC {nfc_id}", foreground="orange") if self.video_player: try: self.video_player.play_specific_video(nfc_id) self.log(f"Sent NFC {nfc_id} to video player") self.root.after(2000, lambda: self.status_label.config(text="Status: Ready", foreground="green")) except Exception as e: self.log_error(f"Failed to send NFC: {e}") self.status_label.config(text="Status: Error", foreground="red") else: self.log_error("Video player not connected") self.status_label.config(text="Status: No video player", foreground="red") def set_video_player(self, video_player): """Set reference to video player""" self.video_player = video_player self.log("Video player connected to debug console") if self.status_label: self.status_label.config(text="Status: Connected", foreground="green") def skip_video(self): """Skip current video""" if self.video_player: try: self.log("Skipping video...") self.status_label.config(text="Status: Skipping...", foreground="orange") self.video_player.skip_current_video() self.video_player.force_next_video() self.log("Video skip requested") self.root.after(1000, lambda: self.status_label.config(text="Status: Ready", foreground="green")) except Exception as e: self.log_error(f"Skip video failed: {e}") self.status_label.config(text="Status: Error", foreground="red") else: self.log_error("Video player not connected") self.status_label.config(text="Status: No video player", foreground="red") def toggle_mute(self): """Toggle mute/unmute""" if self.video_player: try: if self.is_muted: self.video_player.set_volume(0.7) self.mute_button.config(text="Mute") self.is_muted = False self.log("Audio unmuted") self.status_label.config(text="Status: Audio unmuted", foreground="green") else: self.video_player.set_volume(0.0) self.mute_button.config(text="Unmute") self.is_muted = True self.log("Audio muted") self.status_label.config(text="Status: Audio muted", foreground="orange") self.root.after(2000, lambda: self.status_label.config(text="Status: Ready", foreground="green")) except Exception as e: self.log_error(f"Mute toggle failed: {e}") self.status_label.config(text="Status: Error", foreground="red") else: self.log_error("Video player not connected") def toggle_fullscreen(self): """Toggle fullscreen mode""" if self.video_player: try: self.status_label.config(text="Status: Toggling fullscreen...", foreground="orange") self.video_player.toggle_fullscreen() self.log("Fullscreen toggled") self.root.after(1000, lambda: self.status_label.config(text="Status: Ready", foreground="green")) except Exception as e: self.log_error(f"Fullscreen toggle failed: {e}") self.status_label.config(text="Status: Error", foreground="red") else: self.log_error("Video player not connected") def update_queue_depth(self, depth): """Update video queue depth""" self.stats['queue_depth'] = depth def update_display(self): """Update the console display""" messages = [] with self.lock: while self.message_queue: messages.append(self.message_queue.popleft()) for msg in messages: self.text_widget.insert(tk.END, msg + "\n") # Color coding if "ERROR" in msg: self.text_widget.tag_add("error", f"end-2l linestart", f"end-1l lineend") self.text_widget.tag_config("error", foreground="red", background="#ffe6e6") elif "WARNING" in msg: self.text_widget.tag_add("warning", f"end-2l linestart", f"end-1l lineend") self.text_widget.tag_config("warning", foreground="orange", background="#fff3cd") elif "Global NFC input" in msg or "NFC" in msg: self.text_widget.tag_add("nfc", f"end-2l linestart", f"end-1l lineend") self.text_widget.tag_config("nfc", foreground="blue", background="#e6f3ff") elif "Video played:" in msg or "SPECIFIC VIDEO" in msg or "Folder" in msg: self.text_widget.tag_add("video", f"end-2l linestart", f"end-1l lineend") self.text_widget.tag_config("video", foreground="green", background="#e6ffe6") elif "Reset" in msg or "reset" in msg: self.text_widget.tag_add("reset", f"end-2l linestart", f"end-1l lineend") self.text_widget.tag_config("reset", foreground="purple", background="#f0e6ff") self.text_widget.see(tk.END) # Limit text size lines = self.text_widget.get("1.0", tk.END).count('\n') if lines > 1000: self.text_widget.delete("1.0", "200.0") # Update statistics uptime = str(datetime.now() - self.stats['start_time']).split('.')[0] self.stats_labels['uptime'].config(text=uptime) for key in self.stats_labels: if key in self.stats and key != 'uptime': value = str(self.stats[key]) if key == 'current_video' and len(value) > 30: value = value[:27] + "..." self.stats_labels[key].config(text=value) # Update folder videos stat from video player if self.video_player and hasattr(self.video_player, 'stats'): folder_count = self.video_player.stats.get('folder_videos_played', 0) self.stats_labels['folder_videos_played'].config(text=str(folder_count)) # Update trailers pool stat if self.video_player: try: total_trailers = len(self.video_player.trailer_videos) if hasattr(self.video_player, 'trailer_videos') else 0 recent_count = len(self.video_player.recently_played_trailers[-25:]) if hasattr(self.video_player, 'recently_played_trailers') else 0 available = total_trailers - recent_count if available < 0: available = 0 self.stats_labels['trailers_pool'].config(text=f"{available}/{total_trailers}") except: pass # Update global input status self.update_global_input_status() def run(self): """Run the debug console""" self.running = True self.log("Enhanced debug console started with folder support") if self.status_label: self.status_label.config(text="Status: Starting...", foreground="orange") def update_loop(): if self.running: try: self.update_display() if (self.status_label and "Starting" in self.status_label.cget("text") and self.video_player): self.status_label.config(text="Status: Ready", foreground="green") self.root.after(100, update_loop) except Exception as e: self.log_error(f"Display update error: {e}") if self.running: self.root.after(1000, update_loop) update_loop() try: self.root.mainloop() except Exception as e: self.logger.error(f"Debug console error: {e}") finally: self.running = False def stop(self): """Stop the debug console""" self.running = False if self.global_listener: try: if self.global_input_method == "pynput": self.global_listener.stop() elif self.global_input_method == "windows": import win32gui win32gui.UnhookWindowsHookEx(self.global_listener) except Exception as e: self.logger.debug(f"Error stopping global input: {e}") if self.root: try: self.root.quit() self.root.destroy() except: pass