Files
moonlight-drive-in/debug_console.py
jessikitty 05a8bd237b Upload files to "/"
Add core Python modules
2025-12-09 16:49:13 +11:00

664 lines
26 KiB
Python

# 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('<Return>', 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('<Key>', 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