Files
tkinter_player/tkinter_app/src/tkinter_simple_player_old.py

935 lines
37 KiB
Python

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('<Key>', self.on_key_press)
self.root.bind('<Button-1>', self.on_mouse_click)
self.root.bind('<Motion>', 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("<Button-1>", on_press)
button.bind("<ButtonRelease-1>", on_release)
button.bind("<Enter>", on_enter)
button.bind("<Leave>", 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('<Return>', lambda e: check_password())
exit_dialog.bind('<Escape>', 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}")