import tkinter as tk from tkinter import ttk, messagebox, simpledialog import threading import time import os import json import datetime from pathlib import Path import subprocess import sys import requests # Required for server communication import queue import vlc # For video playback with hardware acceleration # Try importing PIL but provide fallback try: from PIL import Image, ImageTk PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False print("WARNING: PIL not available. Image display functionality will be limited.") # Import existing functions from python_functions import ( load_local_playlist, download_media_files, clean_unused_files, save_local_playlist, update_config_playlist_version, fetch_server_playlist, load_config ) from logging_config import Logger # Import virtual keyboard components from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton # Update the config file path to use the resources directory CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt') from player_app import SimpleMediaPlayerApp def setup_window(self): """Configure the main window""" self.root.title("Simple Signage Player") self.root.configure(bg='black') # Load window size from config try: config = load_config() width = int(config.get('screen_w', 1920)) height = int(config.get('screen_h', 1080)) # Load scaling mode preference self.scaling_mode = config.get('scaling_mode', 'fit') except: width, height = 800, 600 # Fallback size if config fails self.scaling_mode = 'fit' # Default scaling mode self.root.geometry(f"{width}x{height}") self.root.attributes('-fullscreen', True) # Bind events self.root.bind('', self.on_key_press) self.root.bind('', self.on_mouse_click) self.root.bind('', self.on_mouse_motion) self.root.focus_set() def setup_ui(self): """Create the user interface""" # Main content area - make sure it's black and fill the entire window self.content_frame = tk.Frame(self.root, bg='black') self.content_frame.pack(fill=tk.BOTH, expand=True) # Image display - expand to fill the entire window self.image_label = tk.Label(self.content_frame, bg='black') self.image_label.pack(fill=tk.BOTH, expand=True) # Status label - hidden by default self.status_label = tk.Label( self.content_frame, bg='black', fg='white', font=('Arial', 24), text="" ) # Don't place the status label by default to keep it hidden # Control panel self.create_control_panel() self.show_controls() self.schedule_hide_controls() def create_control_panel(self): """Create touch-optimized control panel with larger buttons""" # Create control frame with larger size for touch self.control_frame = tk.Frame( self.root, bg='#1a1a1a', # Dark background bd=2, relief=tk.RAISED, padx=15, pady=15 ) self.control_frame.place(relx=0.98, rely=0.98, anchor='se') # Touch-optimized button configuration button_config = { 'bg': '#333333', 'fg': 'white', 'activebackground': '#555555', 'activeforeground': 'white', 'relief': tk.FLAT, 'borderwidth': 0, 'width': 10, # Larger for touch 'height': 3, # Larger for touch 'font': ('Segoe UI', 10, 'bold'), # Larger font 'cursor': 'hand2' } # Previous button self.prev_btn = tk.Button( self.control_frame, text="⏮ Prev", command=self.previous_media, **button_config ) self.prev_btn.grid(row=0, column=0, padx=5) # Play/Pause button self.play_pause_btn = tk.Button( self.control_frame, text="⏸ Pause" if not self.is_paused else "▶ Play", command=self.toggle_play_pause, bg='#27ae60', # Green for play/pause activebackground='#35d974', **{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']} ) self.play_pause_btn.grid(row=0, column=1, padx=5) # Next button self.next_btn = tk.Button( self.control_frame, text="Next ⏭", command=self.next_media, **button_config ) self.next_btn.grid(row=0, column=2, padx=5) # Settings button self.settings_btn = tk.Button( self.control_frame, text="⚙️ Settings", command=self.open_settings, bg='#9b59b6', # Purple for settings activebackground='#bb8fce', **{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']} ) self.settings_btn.grid(row=0, column=3, padx=5) # Exit button with special styling self.exit_btn = tk.Button( self.control_frame, text="❌ EXIT", command=self.show_exit_dialog, bg='#e74c3c', # Red background fg='white', activebackground='#ec7063', activeforeground='white', relief=tk.FLAT, borderwidth=0, width=8, height=3, font=('Segoe UI', 10, 'bold'), cursor='hand2' ) self.exit_btn.grid(row=0, column=4, padx=5) # Add touch feedback to all buttons for button in [self.prev_btn, self.play_pause_btn, self.next_btn, self.settings_btn, self.exit_btn]: self.add_touch_feedback_to_control_button(button) def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'): """ Scale image to screen with different modes: - 'fit': Maintain aspect ratio, add black bars if needed (letterbox/pillarbox) - 'fill': Maintain aspect ratio, crop if needed to fill entire screen - 'stretch': Ignore aspect ratio, stretch to fill entire screen """ img_width, img_height = img.size if mode == 'stretch': # Stretch to fill entire screen, ignoring aspect ratio return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0) elif mode == 'fill': # Maintain aspect ratio, crop to fill entire screen screen_ratio = screen_width / screen_height img_ratio = img_width / img_height if img_ratio > screen_ratio: # Image is wider - scale by height and crop width new_height = screen_height new_width = int(screen_height * img_ratio) x_offset = (screen_width - new_width) // 2 y_offset = 0 else: # Image is taller - scale by width and crop height new_width = screen_width new_height = int(screen_width / img_ratio) x_offset = 0 y_offset = (screen_height - new_height) // 2 # Resize and crop img_resized = img.resize((new_width, new_height), Image.LANCZOS) # Create final image and paste (this will crop automatically) final_img = Image.new('RGB', (screen_width, screen_height), 'black') # Calculate crop area if image is larger than screen if new_width > screen_width: crop_x = (new_width - screen_width) // 2 img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height)) x_offset = 0 if new_height > screen_height: crop_y = (new_height - screen_height) // 2 img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height)) y_offset = 0 final_img.paste(img_resized, (x_offset, y_offset)) return final_img, (x_offset, y_offset) else: # mode == 'fit' (default) # Maintain aspect ratio, add black bars if needed screen_ratio = screen_width / screen_height img_ratio = img_width / img_height if img_ratio > screen_ratio: # Image is wider than screen - fit to width new_width = screen_width new_height = int(screen_width / img_ratio) else: # Image is taller than screen - fit to height new_height = screen_height new_width = int(screen_height * img_ratio) # Resize image img_resized = img.resize((new_width, new_height), Image.LANCZOS) # Create black background and center the image final_img = Image.new('RGB', (screen_width, screen_height), 'black') x_offset = (screen_width - new_width) // 2 y_offset = (screen_height - new_height) // 2 final_img.paste(img_resized, (x_offset, y_offset)) return final_img, (x_offset, y_offset) def add_touch_feedback_to_control_button(self, button): """Add touch feedback effects to control panel buttons""" original_bg = button.cget('bg') def on_press(e): button.configure(relief=tk.SUNKEN) def on_release(e): button.configure(relief=tk.FLAT) def on_enter(e): button.configure(relief=tk.RAISED) def on_leave(e): button.configure(relief=tk.FLAT) button.bind("", on_press) button.bind("", on_release) button.bind("", on_enter) button.bind("", on_leave) def initialize_playlist_from_server(self): """Initialize the playlist from the server on startup with fallback to local playlist""" # First try to load any existing local playlist as fallback fallback_playlist = None try: local_playlist_data = load_local_playlist() fallback_playlist = local_playlist_data.get('playlist', []) if fallback_playlist: Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items") except Exception as e: Logger.warning(f"No fallback playlist available: {e}") # Show connection status self.status_label.config(text="Connecting to server...\nPlease wait") self.status_label.place(relx=0.5, rely=0.5, anchor='center') self.root.update() # Load configuration config = load_config() server = config.get("server_ip", "") host = config.get("screen_name", "") quick = config.get("quickconnect_key", "") port = config.get("port", "") Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}") if not server or not host or not quick or not port: Logger.warning("Missing server configuration, using fallback playlist") self.status_label.place_forget() self.load_fallback_playlist(fallback_playlist) return # Attempt to fetch server playlist with timeout server_connection_successful = False try: # Add connection timeout and retry logic Logger.info("Attempting to connect to server...") self.status_label.config(text="Connecting to server...\nAttempting connection") self.root.update() server_playlist_data = fetch_server_playlist() server_playlist = server_playlist_data.get('playlist', []) server_version = server_playlist_data.get('version', 0) if server_playlist: Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}") server_connection_successful = True # Download media files and update local playlist self.status_label.config(text="Downloading media files...\nPlease wait") self.root.update() download_media_files(server_playlist, server_version) update_config_playlist_version(server_version) # Load the updated local playlist local_playlist_data = load_local_playlist() self.playlist = local_playlist_data.get('playlist', []) if self.playlist: Logger.info(f"Successfully loaded {len(self.playlist)} items from server") self.status_label.place_forget() self.play_current_media() return else: Logger.warning("Server playlist was empty, falling back to local playlist") else: Logger.warning("Server returned empty playlist, falling back to local playlist") except requests.exceptions.ConnectTimeout: Logger.error("Server connection timeout, using fallback playlist") except requests.exceptions.ConnectionError: Logger.error("Cannot connect to server, using fallback playlist") except requests.exceptions.Timeout: Logger.error("Server request timeout, using fallback playlist") except Exception as e: Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist") # If we reach here, server connection failed - use fallback if not server_connection_successful: self.status_label.config(text="Server unavailable\nLoading last playlist...") self.root.update() time.sleep(1) # Brief pause to show message self.status_label.place_forget() self.load_fallback_playlist(fallback_playlist) def load_fallback_playlist(self, fallback_playlist): """Load fallback playlist when server is unavailable""" if fallback_playlist and len(fallback_playlist) > 0: self.playlist = fallback_playlist Logger.info(f"Loaded fallback playlist with {len(self.playlist)} items") self.play_current_media() else: Logger.warning("No fallback playlist available, loading demo content") self.load_demo_or_local_playlist() def load_demo_or_local_playlist(self): """Load either the existing local playlist or demo content""" # First try to load the local playlist local_playlist_data = load_local_playlist() self.playlist = local_playlist_data.get('playlist', []) if self.playlist: Logger.info(f"Loaded existing local playlist with {len(self.playlist)} items") self.play_current_media() return # If no local playlist, try loading demo content Logger.info("No local playlist found, loading demo content") self.create_demo_content() if self.playlist: self.play_current_media() else: self.show_no_content_message() def create_demo_content(self): """Create demo content for testing""" demo_images = [] # First check static/resurse folder for any media static_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') if os.path.exists(static_dir): for file in os.listdir(static_dir): if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): full_path = os.path.join(static_dir, file) demo_images.append({ 'file_name': file, 'url': full_path, 'duration': 5 }) # If no files found in static/resurse, look in Resurse folder if not demo_images: demo_dir = './Resurse' if os.path.exists(demo_dir): for file in os.listdir(demo_dir): if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): demo_images.append({ 'file_name': file, 'url': os.path.join(demo_dir, file), 'duration': 5 }) if demo_images: self.playlist = demo_images Logger.info(f"Created demo playlist with {len(demo_images)} images") else: # Create a text-only demo if no images found self.playlist = [{ 'file_name': 'Demo Text', 'url': 'text://Welcome to Tkinter Media Player!\n\nPlease configure server settings', 'duration': 5 }] def show_no_content_message(self): """Show message when no content is available""" self.image_label.config(image='') self.status_label.config( text="No media content available.\nPress Settings to configure server connection." ) self.status_label.place(relx=0.5, rely=0.5, anchor='center') def show_error_message(self, message): """Show error message""" self.image_label.config(image='') self.status_label.config(text=f"Error: {message}") self.status_label.place(relx=0.5, rely=0.5, anchor='center') def play_current_media(self): """Play the current media item""" if not self.playlist or self.current_index >= len(self.playlist): self.show_no_content_message() return media = self.playlist[self.current_index] file_path = media.get('url', '') file_name = media.get('file_name', '') duration = media.get('duration', 10) # Handle relative paths by converting to absolute paths if file_path.startswith('static/resurse/'): # Convert relative path to absolute absolute_path = os.path.join(os.path.dirname(__file__), file_path) file_path = absolute_path Logger.info(f"Playing media: {file_name} from {file_path}") # Log media start self.log_event(file_name, "STARTED") # Cancel existing timers self.cancel_timers() # Handle different media types if file_path.startswith('text://'): self.show_text_content(file_path[7:], duration) elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')): self.play_video(file_path) elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')): self.show_image(file_path, duration) else: Logger.error(f"Unsupported or missing media: {file_path}") self.status_label.config(text=f"Missing or unsupported media:\n{file_name}") # Schedule next media after short delay self.auto_advance_timer = self.root.after(5000, self.next_media) def play_video(self, file_path): """Play video file using system VLC as a subprocess for robust hardware acceleration and stability.""" self.status_label.place_forget() def run_vlc_subprocess(): try: Logger.info(f"Starting system VLC subprocess for video: {file_path}") # Build VLC command vlc_cmd = [ 'cvlc', '--fullscreen', '--no-osd', '--no-video-title-show', '--play-and-exit', '--quiet', file_path ] proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) proc.wait() Logger.info(f"VLC subprocess finished: {file_path}") except Exception as e: Logger.error(f"VLC subprocess error: {e}") finally: self.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.root.after(1000, self.next_media))) threading.Thread(target=run_vlc_subprocess, daemon=True).start() def _update_video_frame(self, photo): """Update video frame from main thread""" try: self.image_label.config(image=photo) self.image_label.image = photo # Keep reference except Exception as e: Logger.error(f"Error updating video frame: {e}") def _show_video_error(self, error_msg): """Show video error from main thread""" try: self.status_label.config(text=f"Video Error:\n{error_msg}") self.status_label.place(relx=0.5, rely=0.5, anchor='center') self.auto_advance_timer = self.root.after(5000, self.next_media) except Exception as e: Logger.error(f"Error showing video error: {e}") self.auto_advance_timer = self.root.after(5000, self.next_media) def show_text_content(self, text, duration): """Display text content""" self.image_label.config(image='') self.status_label.config(text=text) # Schedule next media self.auto_advance_timer = self.root.after( int(duration * 1000), self.next_media ) def show_image(self, file_path, duration): """Display an image in full screen, properly fitted to screen size""" try: # Hide status label and clear any previous text self.status_label.place_forget() self.status_label.config(text="") if PIL_AVAILABLE: # Use PIL for better image handling img = Image.open(file_path) original_size = img.size # Get actual screen dimensions screen_width = self.root.winfo_width() screen_height = self.root.winfo_height() # Ensure we have valid screen dimensions if screen_width <= 1 or screen_height <= 1: screen_width = 1920 # Default fallback screen_height = 1080 # Scale image using the scaling helper final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode) # Convert to PhotoImage photo = ImageTk.PhotoImage(final_img) # Clear previous image and display new one self.image_label.config(image=photo) self.image_label.image = photo # Keep reference Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} " f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, " f"Mode: {self.scaling_mode}, Offset: {offset})") else: # Fall back to basic text display if PIL not available self.image_label.config(image='') self.status_label.config(text=f"IMAGE: {os.path.basename(file_path)}\n\n(Install PIL for image display)") self.status_label.place(relx=0.5, rely=0.5, anchor='center') Logger.warning("PIL not available - showing text placeholder for image") # Schedule next media self.auto_advance_timer = self.root.after( int(duration * 1000), self.next_media ) except Exception as e: Logger.error(f"Failed to show image {file_path}: {e}") self.image_label.config(image='') self.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}") self.status_label.place(relx=0.5, rely=0.5, anchor='center') self.auto_advance_timer = self.root.after(5000, self.next_media) def next_media(self): """Move to next media""" self.cancel_timers() if not self.playlist: return self.current_index = (self.current_index + 1) % len(self.playlist) # Check for playlist updates at end of cycle if self.current_index == 0: threading.Thread(target=self.check_playlist_updates, daemon=True).start() self.play_current_media() def previous_media(self): """Move to previous media""" self.cancel_timers() if not self.playlist: return self.current_index = (self.current_index - 1) % len(self.playlist) self.play_current_media() def toggle_play_pause(self): """Toggle play/pause state""" self.is_paused = not self.is_paused if self.is_paused: self.play_pause_btn.config(text="▶ Play") self.cancel_timers() else: self.play_pause_btn.config(text="⏸ Pause") # Resume current media self.play_current_media() Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}") def cancel_timers(self): """Cancel all active timers""" if self.auto_advance_timer: self.root.after_cancel(self.auto_advance_timer) self.auto_advance_timer = None def show_controls(self): """Show control panel""" if self.control_frame: self.control_frame.place(relx=0.98, rely=0.98, anchor='se') def hide_controls(self): """Hide control panel""" if self.control_frame: self.control_frame.place_forget() def schedule_hide_controls(self): """Schedule hiding controls after delay""" if self.hide_controls_timer: self.root.after_cancel(self.hide_controls_timer) self.hide_controls_timer = self.root.after(10000, self.hide_controls) def on_mouse_click(self, event): """Handle mouse clicks""" self.show_controls() self.schedule_hide_controls() def on_mouse_motion(self, event): """Handle mouse motion""" self.show_controls() self.schedule_hide_controls() def on_key_press(self, event): """Handle keyboard events""" key = event.keysym.lower() if key == 'f': self.toggle_fullscreen() elif key == 'space': self.toggle_play_pause() elif key == 'left': self.previous_media() elif key == 'right': self.next_media() elif key == 'escape': self.show_exit_dialog() elif key == '1': self.set_scaling_mode('fit') elif key == '2': self.set_scaling_mode('fill') elif key == '3': self.set_scaling_mode('stretch') elif event.state & 0x4: # Ctrl key pressed if key == 's': self.open_settings() self.show_controls() self.schedule_hide_controls() def set_scaling_mode(self, mode): """Change the scaling mode and refresh current media""" old_mode = self.scaling_mode self.scaling_mode = mode Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'") # Show temporary notification self.status_label.config(text=f"Scaling Mode: {mode.title()}\n" f"1=Fit 2=Fill 3=Stretch") self.status_label.place(relx=0.5, rely=0.05, anchor='center') # Hide notification after 2 seconds self.root.after(2000, lambda: self.status_label.place_forget()) # Refresh current media with new scaling if self.playlist and 0 <= self.current_index < len(self.playlist): self.cancel_timers() self.play_current_media() def toggle_fullscreen(self): """Toggle fullscreen mode""" self.is_fullscreen = not self.is_fullscreen self.root.attributes('-fullscreen', self.is_fullscreen) def open_settings(self): """Open settings window""" if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists(): self.settings_window.lift() return # Pause media playback when opening settings if not self.is_paused: self.toggle_play_pause() self.settings_window = SettingsWindow(self.root, self) # Add a callback to resume playback when the settings window is closed def on_settings_close(): if self.is_paused: self.toggle_play_pause() self.settings_window.protocol("WM_DELETE_WINDOW", on_settings_close) def show_exit_dialog(self): """Show modern password-protected exit dialog""" try: config = load_config() quickconnect_key = config.get('quickconnect_key', '') except: quickconnect_key = '' # Create modern exit dialog exit_dialog = tk.Toplevel(self.root) exit_dialog.title("Exit Application") exit_dialog.geometry("400x200") exit_dialog.configure(bg='#2d2d2d') exit_dialog.transient(self.root) exit_dialog.grab_set() exit_dialog.resizable(False, False) # Center the dialog using helper method self.center_dialog_on_screen(exit_dialog, 400, 200) # Header with icon header_frame = tk.Frame(exit_dialog, bg='#cc0000', height=60) header_frame.pack(fill=tk.X) header_frame.pack_propagate(False) icon_label = tk.Label(header_frame, text="⚠", font=('Arial', 20, 'bold'), fg='white', bg='#cc0000') icon_label.pack(side=tk.LEFT, padx=15, pady=15) title_label = tk.Label(header_frame, text="Exit Application", font=('Arial', 14, 'bold'), fg='white', bg='#cc0000') title_label.pack(side=tk.LEFT, pady=15) # Content frame content_frame = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20) content_frame.pack(fill=tk.BOTH, expand=True) # Password prompt prompt_label = tk.Label(content_frame, text="Enter password to exit:", font=('Arial', 11), fg='white', bg='#2d2d2d') prompt_label.pack(pady=(0, 10)) # Password entry password_var = tk.StringVar() password_entry = tk.Entry(content_frame, textvariable=password_var, font=('Arial', 11), show='*', width=25, bg='#404040', fg='white', insertbackground='white', relief=tk.FLAT, bd=5) password_entry.pack(pady=(0, 15)) password_entry.focus_set() # Button frame button_frame = tk.Frame(content_frame, bg='#2d2d2d') button_frame.pack(fill=tk.X) def check_password(): if password_var.get() == quickconnect_key: exit_dialog.destroy() self.exit_application() elif password_var.get(): # Only show error if password was entered # Show error in red error_label.config(text="✗ Incorrect password", fg='#ff4444') password_entry.delete(0, tk.END) password_entry.focus_set() def cancel_exit(): exit_dialog.destroy() # Error label (hidden initially) error_label = tk.Label(content_frame, text="", font=('Arial', 9), bg='#2d2d2d') error_label.pack() # Buttons cancel_btn = tk.Button(button_frame, text="Cancel", command=cancel_exit, bg='#555555', fg='white', font=('Arial', 10, 'bold'), relief=tk.FLAT, padx=20, pady=8, width=10) cancel_btn.pack(side=tk.RIGHT, padx=(10, 0)) exit_btn = tk.Button(button_frame, text="Exit", command=check_password, bg='#cc0000', fg='white', font=('Arial', 10, 'bold'), relief=tk.FLAT, padx=20, pady=8, width=10) exit_btn.pack(side=tk.RIGHT) # Bind Enter key to check password password_entry.bind('', lambda e: check_password()) exit_dialog.bind('', lambda e: cancel_exit()) def exit_application(self): """Exit the application""" Logger.info("Application exit requested") self.running = False self.root.quit() self.root.destroy() def center_dialog_on_screen(self, dialog, width, height): """Center a dialog window on screen regardless of screen size""" dialog.update_idletasks() # Ensure geometry is calculated screen_width = dialog.winfo_screenwidth() screen_height = dialog.winfo_screenheight() # Calculate center position center_x = int((screen_width - width) / 2) center_y = int((screen_height - height) / 2) # Ensure the dialog doesn't go off-screen on smaller displays center_x = max(0, min(center_x, screen_width - width)) center_y = max(0, min(center_y, screen_height - height)) dialog.geometry(f"{width}x{height}+{center_x}+{center_y}") # Bring to front and focus dialog.lift() dialog.focus_force() return center_x, center_y def check_playlist_updates(self): """Check for playlist updates from server with fallback protection""" try: config = load_config() local_version = config.get('playlist_version', 0) server_playlist_data = fetch_server_playlist() server_version = server_playlist_data.get('version', 0) if server_version > local_version: Logger.info(f"Updating playlist: {local_version} -> {server_version}") # Clean old files local_playlist_data = load_local_playlist() clean_unused_files(local_playlist_data.get('playlist', [])) # Download new content download_media_files( server_playlist_data.get('playlist', []), server_version ) # Update local playlist local_playlist_data = load_local_playlist() self.playlist = local_playlist_data.get('playlist', []) # Reset to beginning of playlist self.current_index = 0 Logger.info("Playlist updated successfully") # Continue with current media after update self.play_current_media() else: Logger.info("No playlist updates available") except requests.exceptions.ConnectTimeout: Logger.warning("Server connection timeout during update check - continuing with current playlist") except requests.exceptions.ConnectionError: Logger.warning("Cannot connect to server during update check - continuing with current playlist") except requests.exceptions.Timeout: Logger.warning("Server request timeout during update check - continuing with current playlist") except Exception as e: Logger.warning(f"Failed to check playlist updates: {e} - continuing with current playlist") def log_event(self, file_name, event): """Log media events""" try: timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') log_message = f"{timestamp} - {event}: {file_name}\n" # Update the log file path to the resources directory log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt') with open(log_file, 'a') as f: f.write(log_message) except Exception as e: Logger.error(f"Failed to log event: {e}") def start_periodic_checks(self): """Start periodic playlist checks""" def check_loop(): """Background thread for periodic checks""" while self.running: try: time.sleep(300) # Check every 5 minutes if self.running: self.check_playlist_updates() except Exception as e: Logger.error(f"Error in periodic check: {e}") # Start background thread threading.Thread(target=check_loop, daemon=True).start() def run(self): """Start the application""" Logger.info("Starting Simple Tkinter Media Player") try: self.root.mainloop() except KeyboardInterrupt: self.exit_application() except Exception as e: Logger.error(f"Application error: {e}") print(f"Error: {e}")