Compare commits

...

2 Commits

Author SHA1 Message Date
02d13b2eaa Enhanced player stability and added exit confirmation
- Fixed VLC display errors by implementing PIL fallback for images
- Added comprehensive timer management with individual error handling
- Implemented watchdog timers to prevent freezing during media transitions
- Enhanced exit functionality with quickconnect code confirmation dialog
- Improved settings window behavior with proper modal focus
- Added transition protection to prevent rapid media cycling
- Enhanced error handling throughout the application
- Fixed controls window cleanup and destruction process
2025-09-04 16:32:48 +03:00
d2a996feb9 Fix player freezing and rapid cycling issues
- Switch from VLC to PIL for image display to avoid VLC display errors
- Add transition protection to prevent rapid media cycling
- Remove conflicting next_media_loop calls
- Improve error handling and logging for better debugging
- Fix VLC configuration for better Raspberry Pi compatibility
2025-09-04 15:52:13 +03:00
6 changed files with 1129 additions and 72 deletions

View File

@@ -12,13 +12,33 @@ class AppSettingsWindow(tk.Tk):
self.geometry('440x600') # Increased height for better button visibility
self.resizable(False, False)
self.config(bg='#23272e')
# Ensure window appears on top and gets focus
self.attributes('-topmost', True)
self.lift()
self.focus_force()
self.grab_set() # Make window modal
# Center the window on screen
self.center_window()
self.fields = {}
self.load_config()
self.style = ttk.Style(self)
self.set_styles()
self.create_widgets()
# Ensure focus after widgets are created
self.after(100, self.focus_force)
def center_window(self):
"""Center the settings window on the screen"""
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f'{width}x{height}+{x}+{y}')
def set_styles(self):
self.style.theme_use('clam')

View File

@@ -54,9 +54,12 @@ def main():
def reload_playlist_if_updated():
new_playlist = load_latest_playlist()
if new_playlist != player.playlist:
print("[MAIN] Playlist updated, reloading...")
player.playlist = new_playlist
player.current_index = 0
player.show_current_media()
# Only restart if we're not already in a transition
if not hasattr(player, 'is_transitioning') or not player.is_transitioning:
player.show_current_media()
root.after(10000, reload_playlist_if_updated)
reload_playlist_if_updated()

View File

@@ -3147,3 +3147,27 @@
[INFO] [SignageApp] File big-buck-bunny-1080p-60fps-30sec.mp4 already exists. Skipping download.
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://10.232.7.231/media/one-piece-season-2-5120x2880-23673.jpg...
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[INFO] [SignageApp] Server response: {'hashed_quickconnect': '$2b$12$Prw4EUYn4j59CAdsZCsvsug6.xociqbOPaNr0oxOA2zwD9S2MSiK6', 'playlist': [{'duration': 30, 'file_name': 'HARTING_Safety_day_informare_2_page_003.jpg', 'url': 'http://10.232.7.231/media/HARTING_Safety_day_informare_2_page_003.jpg'}, {'duration': 30, 'file_name': 'big-buck-bunny-1080p-60fps-30sec.mp4', 'url': 'http://10.232.7.231/media/big-buck-bunny-1080p-60fps-30sec.mp4'}, {'duration': 30, 'file_name': 'one-piece-season-2-5120x2880-23673.jpg', 'url': 'http://10.232.7.231/media/one-piece-season-2-5120x2880-23673.jpg'}], 'playlist_version': 6}
[INFO] [SignageApp] Fetched updated playlist from server.
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 6
[INFO] [SignageApp] Preparing to download HARTING_Safety_day_informare_2_page_003.jpg from http://10.232.7.231/media/HARTING_Safety_day_informare_2_page_003.jpg...
[INFO] [SignageApp] File HARTING_Safety_day_informare_2_page_003.jpg already exists. Skipping download.
[INFO] [SignageApp] Preparing to download big-buck-bunny-1080p-60fps-30sec.mp4 from http://10.232.7.231/media/big-buck-bunny-1080p-60fps-30sec.mp4...
[INFO] [SignageApp] File big-buck-bunny-1080p-60fps-30sec.mp4 already exists. Skipping download.
[INFO] [SignageApp] Preparing to download one-piece-season-2-5120x2880-23673.jpg from http://10.232.7.231/media/one-piece-season-2-5120x2880-23673.jpg...
[INFO] [SignageApp] File one-piece-season-2-5120x2880-23673.jpg already exists. Skipping download.
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='10.232.7.231', port=80): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7f64699b90>, 'Connection to 10.232.7.231 timed out. (connect timeout=None)'))
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
[INFO] [SignageApp] Local playlist is already up to date.
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='10.232.7.231', port=80): Max retries exceeded with url: /api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by ConnectTimeoutError(<urllib3.connection.HTTPConnection object at 0x7f646b15d0>, 'Connection to 10.232.7.231 timed out. (connect timeout=None)'))
[INFO] [SignageApp] Local playlist version: 0, Server playlist version: 0
[INFO] [SignageApp] Local playlist is already up to date.
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}
[INFO] [SignageApp] Fetching playlist from URL: http://10.232.7.231:80/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'}

View File

@@ -16,6 +16,15 @@ class SimpleTkPlayer:
self.current_index = 0
self.paused = False
self.pause_timer = None
self.is_transitioning = False # Flag to prevent rapid cycling
self.is_exiting = False # Flag to prevent operations during exit
# Initialize all timer variables to None
self.hide_controls_timer = None
self.video_watchdog = None
self.image_watchdog = None
self.image_timer = None
self.label = tk.Label(root, bg='black')
self.label.pack(fill=tk.BOTH, expand=True)
self.create_controls()
@@ -106,16 +115,25 @@ class SimpleTkPlayer:
self.schedule_hide_controls()
def hide_controls(self):
self.controls_win.withdraw()
self.hide_mouse()
try:
if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists():
self.controls_win.withdraw()
self.hide_mouse()
except Exception as e:
print(f"[CONTROLS] Error hiding controls: {e}")
def schedule_hide_controls(self):
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
self.root.after_cancel(self.hide_controls_timer)
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
try:
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
self.root.after_cancel(self.hide_controls_timer)
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
except Exception as e:
print(f"[CONTROLS] Error scheduling hide controls: {e}")
self.hide_controls_timer = None
def on_activity(self, event=None):
self.show_controls()
if not self.is_exiting:
self.show_controls()
def prev_media(self):
self.current_index = (self.current_index - 1) % len(self.playlist)
@@ -162,7 +180,6 @@ class SimpleTkPlayer:
def after_intro(self):
self.show_current_media()
self.root.after(100, self.next_media_loop)
def show_video(self, file_path, on_end=None, duration=None):
try:
@@ -176,10 +193,20 @@ class SimpleTkPlayer:
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
# Configure VLC for Raspberry Pi with better compatibility options
vlc_args = [
'--no-xlib',
'--vout=gl',
'--aout=alsa',
'--intf=dummy',
'--quiet',
'--no-video-title-show',
'--no-stats',
'--disable-screensaver'
]
self.vlc_instance = vlc.Instance(vlc_args)
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if video doesn't end
@@ -193,9 +220,11 @@ class SimpleTkPlayer:
max_duration = duration if duration is not None else 60 # fallback max 60s
self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog)
def finish_video():
if hasattr(self, 'video_watchdog'):
if hasattr(self, 'video_watchdog') and self.video_watchdog:
self.root.after_cancel(self.video_watchdog)
self.vlc_player.stop()
self.video_watchdog = None
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
@@ -241,12 +270,70 @@ class SimpleTkPlayer:
if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
self.show_video(file_path, on_end=self.next_media, duration=duration)
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media)
# Use PIL for images instead of VLC to avoid display issues
self.show_image_via_pil(file_path, duration if duration is not None else 10, on_end=self.next_media)
else:
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
self.root.after(2000, self.next_media)
def show_image_via_pil(self, file_path, duration, on_end=None):
"""Fallback image display using PIL and Tkinter"""
try:
print(f"[PLAYER] Displaying image via PIL: {file_path}")
from PIL import Image, ImageTk
# Stop any VLC player that might be running
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
# Hide video canvas if exists and show label
if hasattr(self, 'video_canvas'):
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
# Load and resize image
img = Image.open(file_path)
screen_width = self.root.winfo_screenwidth()
screen_height = self.root.winfo_screenheight()
# Calculate scaling to fit screen while maintaining aspect ratio
img_ratio = img.width / img.height
screen_ratio = screen_width / screen_height
if img_ratio > screen_ratio:
# Image is wider than screen ratio
new_width = screen_width
new_height = int(screen_width / img_ratio)
else:
# Image is taller than screen ratio
new_height = screen_height
new_width = int(screen_height * img_ratio)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
self.photo = ImageTk.PhotoImage(img)
self.label.config(image=self.photo, text="", bg='black')
print(f"[PLAYER] Image displayed successfully, will show for {duration} seconds")
# Schedule next media after duration
def finish_image():
print(f"[PLAYER] Image duration completed for {os.path.basename(file_path)}")
self.label.config(image="", text="")
if hasattr(self, 'photo'):
del self.photo # Free memory
if hasattr(self, 'image_timer'):
self.image_timer = None # Clear timer reference
if on_end:
on_end()
self.image_timer = self.root.after(int(duration * 1000), finish_image)
except Exception as e:
print(f"[PLAYER] PIL image display failed: {e}. Skipping.")
self.label.config(text=f"Image Error: {os.path.basename(file_path)}", fg='red', font=('Arial', 24))
self.root.after(2000, lambda: on_end() if on_end else None)
def reload_playlist_and_continue(self):
print("[PLAYER] Attempting to reload playlist...")
new_playlist = load_latest_playlist()
@@ -259,9 +346,36 @@ class SimpleTkPlayer:
print("[PLAYER] Still no playlist. Will retry.")
self.root.after(10000, self.reload_playlist_and_continue)
def validate_image_file(self, file_path):
"""Validate if image file is not corrupted"""
try:
import imghdr
# Check if file has valid image header
img_type = imghdr.what(file_path)
if img_type is None:
return False
# Additional check for file size (empty files)
file_size = os.path.getsize(file_path)
if file_size < 100: # Too small to be a valid image
return False
return True
except Exception as e:
print(f"[PLAYER] Error validating image {file_path}: {e}")
return False
def show_image_via_vlc(self, file_path, duration, on_end=None):
try:
print(f"[PLAYER] Attempting to show image: {file_path}")
# Validate file before attempting to display
if not self.validate_image_file(file_path):
print(f"[PLAYER] Invalid or corrupted image file: {file_path}. Skipping.")
if on_end:
on_end()
return
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
@@ -271,10 +385,20 @@ class SimpleTkPlayer:
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
# Configure VLC for Raspberry Pi with better compatibility options
vlc_args = [
'--no-xlib',
'--vout=gl',
'--aout=dummy',
'--intf=dummy',
'--quiet',
'--no-video-title-show',
'--no-stats',
'--disable-screensaver'
]
self.vlc_instance = vlc.Instance(vlc_args)
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if image doesn't advance
@@ -287,11 +411,21 @@ class SimpleTkPlayer:
on_end()
self.image_watchdog = self.root.after(int(duration * 1200), watchdog)
def finish_image():
if hasattr(self, 'image_watchdog'):
self.root.after_cancel(self.image_watchdog)
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
try:
if hasattr(self, 'image_watchdog') and self.image_watchdog:
self.root.after_cancel(self.image_watchdog)
self.image_watchdog = None
except Exception as e:
print(f"[VLC] Error canceling image_watchdog: {e}")
try:
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
except Exception as e:
print(f"[VLC] Error in finish_image cleanup: {e}")
if on_end:
on_end()
self.root.after(int(duration * 1000), finish_image)
@@ -301,21 +435,320 @@ class SimpleTkPlayer:
on_end()
def next_media(self):
if self.is_transitioning:
print("[PLAYER] Already transitioning, ignoring next_media call")
return
self.is_transitioning = True
self.current_index = (self.current_index + 1) % len(self.playlist)
print(f"[PLAYER] Moving to next media: index {self.current_index}")
# Clear any existing timers safely
timer_names = ['video_watchdog', 'image_watchdog', 'image_timer']
for timer_name in timer_names:
try:
if hasattr(self, timer_name):
timer_id = getattr(self, timer_name)
if timer_id is not None:
self.root.after_cancel(timer_id)
setattr(self, timer_name, None)
except Exception as e:
print(f"[PLAYER] Error canceling {timer_name} during transition: {e}")
self.show_current_media()
# Reset transition flag after a brief delay
self.root.after(500, lambda: setattr(self, 'is_transitioning', False))
def next_media_loop(self):
if not self.playlist or self.paused:
self.root.after(1000, self.next_media_loop)
return
self.show_current_media()
# This function is no longer needed as media transitions are handled by the callback system
pass
def exit_app(self):
"""Show exit confirmation dialog instead of immediate exit"""
print("[EXIT] Exit button pressed, showing confirmation dialog...")
self.show_exit_confirmation()
def show_exit_confirmation(self):
"""Show exit confirmation popup with quickconnect code verification"""
print("[EXIT] Opening exit confirmation window...")
# Pause playback if not already paused
if not self.paused:
self.paused = True
self.pause_btn.config(text='▶ Resume')
# Explicitly pause VLC video if playing
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
self.vlc_player.pause()
print("[EXIT] VLC player paused")
except Exception as e:
print(f"[EXIT] Error pausing VLC: {e}")
# Hide controls immediately and prevent them from reappearing
print("[EXIT] Hiding controls for confirmation dialog...")
try:
# Cancel any pending control show/hide timers
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
self.root.after_cancel(self.hide_controls_timer)
self.hide_controls_timer = None
# Hide controls window
if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists():
self.controls_win.withdraw()
print("[EXIT] Controls hidden successfully")
# Temporarily unbind mouse events to prevent controls from showing
self.root.unbind('<Motion>')
self.root.unbind('<Button-1>')
print("[EXIT] Mouse events unbound")
except Exception as e:
print(f"[EXIT] Error hiding controls: {e}")
# Create exit confirmation window
self.create_exit_confirmation_window()
def create_exit_confirmation_window(self):
"""Create the exit confirmation popup window"""
try:
# Create confirmation window
self.exit_window = tk.Toplevel(self.root)
self.exit_window.title('Exit Confirmation')
self.exit_window.geometry('400x250')
self.exit_window.resizable(False, False)
self.exit_window.config(bg='#2c3e50')
# Make window modal and on top
self.exit_window.attributes('-topmost', True)
self.exit_window.lift()
self.exit_window.focus_force()
self.exit_window.grab_set()
# Center the window
self.center_exit_window()
# Create content
self.create_exit_window_content()
# Bind window close event
self.exit_window.protocol('WM_DELETE_WINDOW', self.cancel_exit)
print("[EXIT] Exit confirmation window created")
except Exception as e:
print(f"[EXIT] Error creating confirmation window: {e}")
# If window creation fails, restore normal functionality
self.cancel_exit()
def center_exit_window(self):
"""Center the exit confirmation window on screen"""
self.exit_window.update_idletasks()
width = self.exit_window.winfo_width()
height = self.exit_window.winfo_height()
x = (self.exit_window.winfo_screenwidth() // 2) - (width // 2)
y = (self.exit_window.winfo_screenheight() // 2) - (height // 2)
self.exit_window.geometry(f'{width}x{height}+{x}+{y}')
def create_exit_window_content(self):
"""Create the content for the exit confirmation window"""
# Title
title_label = tk.Label(
self.exit_window,
text='⚠️ Exit Application',
font=('Arial', 16, 'bold'),
fg='#e74c3c',
bg='#2c3e50'
)
title_label.pack(pady=(20, 10))
# Instructions
instruction_label = tk.Label(
self.exit_window,
text='Enter the quickconnect code to exit:',
font=('Arial', 12),
fg='#ecf0f1',
bg='#2c3e50'
)
instruction_label.pack(pady=(0, 15))
# Code entry
self.code_var = tk.StringVar()
self.code_entry = tk.Entry(
self.exit_window,
textvariable=self.code_var,
font=('Arial', 14),
width=15,
justify='center',
show='*' # Hide the input like a password
)
self.code_entry.pack(pady=(0, 20))
self.code_entry.focus_set()
# Bind Enter key to confirm
self.code_entry.bind('<Return>', lambda e: self.verify_exit_code())
# Buttons frame
buttons_frame = tk.Frame(self.exit_window, bg='#2c3e50')
buttons_frame.pack(pady=(0, 20))
# Cancel button
cancel_btn = tk.Button(
buttons_frame,
text='Cancel',
font=('Arial', 12, 'bold'),
bg='#95a5a6',
fg='white',
padx=20,
pady=8,
command=self.cancel_exit
)
cancel_btn.pack(side=tk.LEFT, padx=(0, 10))
# Confirm button
confirm_btn = tk.Button(
buttons_frame,
text='Exit App',
font=('Arial', 12, 'bold'),
bg='#e74c3c',
fg='white',
padx=20,
pady=8,
command=self.verify_exit_code
)
confirm_btn.pack(side=tk.LEFT, padx=(10, 0))
def verify_exit_code(self):
"""Verify the entered code and proceed with exit if correct"""
try:
entered_code = self.code_var.get().strip()
# Load the quickconnect code from config
with open(CONFIG_PATH, 'r') as f:
config = json.load(f)
quickconnect_code = config.get('quickconnect_key', '')
print(f"[EXIT] Verifying exit code...")
if entered_code == quickconnect_code:
print("[EXIT] Correct code entered, proceeding with exit...")
# Close confirmation window
self.exit_window.destroy()
# Proceed with actual exit
self.perform_actual_exit()
else:
print("[EXIT] Incorrect code entered")
# Show error and clear entry
self.show_exit_error()
except Exception as e:
print(f"[EXIT] Error verifying code: {e}")
self.show_exit_error()
def show_exit_error(self):
"""Show error message for incorrect code"""
# Clear the entry
self.code_var.set('')
# Temporarily change entry background to indicate error
original_bg = self.code_entry.cget('bg')
self.code_entry.config(bg='#e74c3c')
# Reset background after 1 second
self.exit_window.after(1000, lambda: self.code_entry.config(bg=original_bg))
# Focus back to entry
self.code_entry.focus_set()
def cancel_exit(self):
"""Cancel exit and restore normal functionality"""
print("[EXIT] Exit cancelled, restoring normal functionality...")
try:
# Close confirmation window if it exists
if hasattr(self, 'exit_window') and self.exit_window:
self.exit_window.destroy()
del self.exit_window
except Exception as e:
print(f"[EXIT] Error closing confirmation window: {e}")
# Restore normal functionality (similar to settings restoration)
self.restore_after_exit_cancel()
def restore_after_exit_cancel(self):
"""Restore normal player functionality after exit cancellation"""
if self.is_exiting:
return # Don't restore if we're actually exiting
try:
print("[EXIT] Restoring player functionality after exit cancellation...")
# Resume playback
if self.paused:
self.resume_play()
print("[EXIT] Playback resumed")
# Re-bind mouse and button events
self.root.bind('<Motion>', self.on_activity)
self.root.bind('<Button-1>', self.on_activity)
print("[EXIT] Mouse events re-bound")
# Restore controls if needed
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
self.create_controls()
print("[EXIT] Controls recreated")
# Force focus back to main window
self.root.focus_force()
self.root.lift()
print("[EXIT] Focus restored to main window")
# Resume VLC if it was paused
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
if self.vlc_player.get_state() == vlc.State.Paused:
self.vlc_player.play()
print("[EXIT] VLC playback resumed")
except Exception as e:
print(f"[EXIT] Error resuming VLC: {e}")
print("[EXIT] Player functionality fully restored after exit cancellation")
except Exception as e:
print(f"[EXIT] Error restoring functionality: {e}")
def perform_actual_exit(self):
"""Perform the actual application exit after code verification"""
print("[EXIT] Starting application exit...")
self.is_exiting = True # Set exit flag to prevent further operations
# Signal all threads and flags to stop
if hasattr(self, 'stop_event') and self.stop_event:
self.stop_event.set()
if hasattr(self, 'app_running') and self.app_running:
self.app_running[0] = False
# Cancel all pending timers first
timer_names = ['hide_controls_timer', 'video_watchdog', 'image_watchdog', 'image_timer', 'pause_timer']
for timer_name in timer_names:
try:
if hasattr(self, timer_name):
timer_id = getattr(self, timer_name)
if timer_id is not None:
self.root.after_cancel(timer_id)
print(f"[EXIT] Canceled {timer_name}")
setattr(self, timer_name, None)
except Exception as e:
print(f"[EXIT] Error canceling {timer_name}: {e}")
# Continue with other timers even if one fails
# Stop VLC player
try:
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
self.vlc_player = None
except Exception as e:
print(f"[EXIT] Error stopping VLC: {e}")
# Unbind all events to prevent callbacks after destroy
try:
self.root.unbind('<Motion>')
@@ -323,80 +756,234 @@ class SimpleTkPlayer:
self.root.unbind('<Configure>')
except Exception:
pass
# Attempt to destroy all Toplevel windows before root
# Force hide and destroy controls immediately
try:
# Withdraw controls_win if it exists
print("[EXIT] Destroying controls...")
# First, try to hide and destroy the main controls window
if hasattr(self, 'controls_win') and self.controls_win:
if self.controls_win.winfo_exists():
self.controls_win.withdraw()
# Destroy controls_win if it exists (this will also destroy controls_frame)
if hasattr(self, 'controls_win') and self.controls_win:
if self.controls_win.winfo_exists():
self.controls_win.destroy()
self.controls_win = None
# Fallback: destroy controls_frame if it somehow still exists
try:
if self.controls_win.winfo_exists():
self.controls_win.withdraw()
self.controls_win.destroy()
self.controls_win = None
print("[EXIT] Main controls window destroyed")
except Exception as e:
print(f"[EXIT] Error destroying main controls_win: {e}")
# Destroy controls_frame separately if it exists
if hasattr(self, 'controls_frame') and self.controls_frame:
if self.controls_frame.winfo_exists():
self.controls_frame.destroy()
self.controls_frame = None
# Fallback: destroy any remaining Toplevels in the app
for widget in self.root.winfo_children():
if isinstance(widget, tk.Toplevel):
try:
if self.controls_frame.winfo_exists():
self.controls_frame.destroy()
self.controls_frame = None
print("[EXIT] Controls frame destroyed")
except Exception as e:
print(f"[EXIT] Error destroying controls_frame: {e}")
# Destroy all button references
button_names = ['prev_btn', 'pause_btn', 'next_btn', 'settings_btn', 'exit_btn']
for btn_name in button_names:
try:
if hasattr(self, btn_name):
btn = getattr(self, btn_name)
if btn and btn.winfo_exists():
btn.destroy()
setattr(self, btn_name, None)
except Exception as e:
print(f"[EXIT] Error destroying {btn_name}: {e}")
# Destroy any remaining Toplevel windows (more aggressive search)
try:
# Get all toplevel windows including potential orphans
all_toplevels = []
for widget in list(self.root.winfo_children()):
if isinstance(widget, tk.Toplevel):
all_toplevels.append(widget)
# Also check if there are any floating Toplevel references
if hasattr(tk, '_default_root') and tk._default_root:
for child in tk._default_root.winfo_children():
if isinstance(child, tk.Toplevel) and child not in all_toplevels:
all_toplevels.append(child)
print(f"[EXIT] Found {len(all_toplevels)} toplevel windows to destroy")
for widget in all_toplevels:
try:
widget.destroy()
except Exception:
pass
if widget.winfo_exists():
widget.withdraw()
widget.quit() if hasattr(widget, 'quit') else None
widget.destroy()
print(f"[EXIT] Destroyed toplevel widget: {type(widget).__name__}")
except Exception as e:
print(f"[EXIT] Error destroying toplevel {widget}: {e}")
except Exception as e:
print(f"[EXIT] Error in comprehensive toplevel cleanup: {e}")
except Exception as e:
print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}")
# Destroy any other Toplevels if needed (add here if you have more)
print(f"[EXIT] Error in controls cleanup: {e}")
# Final step: destroy root window
try:
if self.root.winfo_exists():
self.root.destroy()
print("[EXIT] Destroying main window...")
# Force update to process any pending widget operations
self.root.update_idletasks()
# One final check for any remaining Toplevel windows
try:
remaining_toplevels = [w for w in self.root.winfo_children() if isinstance(w, tk.Toplevel)]
if remaining_toplevels:
print(f"[EXIT] Found {len(remaining_toplevels)} remaining toplevel windows, destroying...")
for widget in remaining_toplevels:
try:
widget.withdraw()
widget.destroy()
print(f"[EXIT] Destroyed remaining toplevel: {widget}")
except Exception as e:
print(f"[EXIT] Error destroying remaining toplevel: {e}")
except Exception as e:
print(f"[EXIT] Error checking for remaining toplevels: {e}")
# Force another update
self.root.update_idletasks()
# Exit the main loop and destroy
self.root.quit() # Exit mainloop first
self.root.update() # Process the quit
self.root.destroy()
print("[EXIT] Application exit complete")
except Exception as e:
print(f"[EXIT] Error destroying root: {e}")
# Force exit using system methods
try:
import os
os._exit(0)
except Exception:
import sys
sys.exit(0)
def open_settings(self):
if self.paused is not True:
print("[SETTINGS] Opening settings window...")
# Pause playback if not already paused
if not self.paused:
self.paused = True
self.pause_btn.config(text='▶ Resume')
# Explicitly pause VLC video if playing
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
self.vlc_player.pause()
except Exception:
pass
# Destroy controls overlay so settings window is always interactive
if hasattr(self, 'controls_win') and self.controls_win:
self.controls_win.destroy()
self.controls_win = None
print("[SETTINGS] VLC player paused")
except Exception as e:
print(f"[SETTINGS] Error pausing VLC: {e}")
# Hide controls immediately and prevent them from reappearing
print("[SETTINGS] Hiding controls...")
try:
# Cancel any pending control show/hide timers
if hasattr(self, 'hide_controls_timer'):
self.root.after_cancel(self.hide_controls_timer)
# Hide controls window
if hasattr(self, 'controls_win') and self.controls_win and self.controls_win.winfo_exists():
self.controls_win.withdraw()
print("[SETTINGS] Controls hidden successfully")
# Temporarily unbind mouse events to prevent controls from showing
self.root.unbind('<Motion>')
self.root.unbind('<Button-1>')
print("[SETTINGS] Mouse events unbound")
except Exception as e:
print(f"[SETTINGS] Error hiding controls: {e}")
# Launch settings window
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
# Open settings in a new process so it doesn't block the main player
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
# Give the window manager a moment to focus the new window
self.root.after(300, lambda: self.root.focus_force())
# Wait for the settings window to close, then resume
self.root.after(1000, lambda: self.check_settings_closed(proc))
try:
# Open settings in a new process
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
print(f"[SETTINGS] Settings process started with PID: {proc.pid}")
# Start monitoring the settings window
self.root.after(500, lambda: self.check_settings_closed(proc))
except Exception as e:
print(f"[SETTINGS] Error opening settings: {e}")
# Restore functionality if settings failed to open
self.restore_after_settings()
def check_settings_closed(self, proc):
if self.is_exiting:
print("[SETTINGS] Exit in progress, stopping settings monitoring")
return
if proc.poll() is not None:
# Resume playback and unpause VLC if needed
self.resume_play()
# Restore and recreate controls overlay
self.root.deiconify()
self.create_controls()
# Re-bind mouse and button events to new controls
print("[SETTINGS] Settings window closed, restoring player functionality...")
self.restore_after_settings()
else:
# Continue checking every second
self.root.after(1000, lambda: self.check_settings_closed(proc))
def restore_after_settings(self):
"""Restore normal player functionality after settings window closes"""
if self.is_exiting:
print("[SETTINGS] Exit in progress, skipping settings restoration")
return
try:
print("[SETTINGS] Restoring player functionality...")
# Resume playback
if self.paused:
self.resume_play()
print("[SETTINGS] Playback resumed")
# Re-bind mouse and button events
self.root.bind('<Motion>', self.on_activity)
self.root.bind('<Button-1>', self.on_activity)
self.show_controls()
print("[SETTINGS] Mouse events re-bound")
# Ensure controls are properly destroyed before recreating
if hasattr(self, 'controls_win') and self.controls_win:
try:
if self.controls_win.winfo_exists():
self.controls_win.destroy()
self.controls_win = None
except Exception as e:
print(f"[SETTINGS] Error destroying old controls: {e}")
# Recreate controls
self.create_controls()
print("[SETTINGS] Controls recreated")
# Ensure exit button is properly configured after recreation
if hasattr(self, 'exit_btn') and self.exit_btn:
self.exit_btn.config(command=self.exit_app)
print("[SETTINGS] Exit button command restored")
# Force focus back to main window
self.root.focus_force()
self.root.lift()
print("[SETTINGS] Focus restored to main window")
# Resume VLC if it was paused
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
# Only resume if it was paused by us
if self.vlc_player.get_state() == vlc.State.Paused:
self.vlc_player.play()
except Exception:
pass
else:
self.root.after(1000, lambda: self.check_settings_closed(proc))
print("[SETTINGS] VLC playback resumed")
except Exception as e:
print(f"[SETTINGS] Error resuming VLC: {e}")
print("[SETTINGS] Player functionality fully restored")
except Exception as e:
print(f"[SETTINGS] Error restoring functionality: {e}")
def main_start(self):
self.play_intro_video()

View File

@@ -0,0 +1,423 @@
import os
import json
import tkinter as tk
import vlc
import subprocess
import sys
CONFIG_PATH = os.path.join(os.path.dirname(__file__), 'main_data', 'app_config.txt')
PLAYLIST_DIR = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist')
MEDIA_DATA_PATH = os.path.join(os.path.dirname(__file__), 'static_data', 'media')
class SimpleTkPlayer:
def __init__(self, root, playlist):
self.root = root
self.playlist = playlist
self.current_index = 0
self.paused = False
self.pause_timer = None
self.label = tk.Label(root, bg='black')
self.label.pack(fill=tk.BOTH, expand=True)
self.create_controls()
self.hide_controls()
self.root.bind('<Motion>', self.on_activity)
self.root.bind('<Button-1>', self.on_activity)
self.root.after(100, self.ensure_fullscreen)
self.root.after(200, self.hide_mouse)
self.root.after(300, self.move_mouse_to_corner)
self.root.protocol('WM_DELETE_WINDOW', self.exit_app)
def ensure_fullscreen(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
def create_controls(self):
# Create a transparent, borderless top-level window for controls
self.controls_win = tk.Toplevel(self.root)
self.controls_win.overrideredirect(True)
self.controls_win.attributes('-topmost', True)
self.controls_win.attributes('-alpha', 0.92)
self.controls_win.configure(bg='')
# Place the window at the bottom right
def place_controls():
self.controls_win.update_idletasks()
w = self.controls_win.winfo_reqwidth()
h = self.controls_win.winfo_reqheight()
sw = self.root.winfo_screenwidth()
sh = self.root.winfo_screenheight()
x = sw - w - 30
y = sh - h - 30
self.controls_win.geometry(f'+{x}+{y}')
self.controls_frame = tk.Frame(self.controls_win, bg='#222', bd=2, relief='ridge')
self.controls_frame.pack()
btn_style = {
'bg': '#333',
'fg': 'white',
'activebackground': '#555',
'activeforeground': '#00e6e6',
'font': ('Arial', 16, 'bold'),
'bd': 0,
'highlightthickness': 0,
'relief': 'flat',
'cursor': 'hand2',
'padx': 10,
'pady': 6
}
self.prev_btn = tk.Button(self.controls_frame, text='⏮ Prev', command=self.prev_media, **btn_style)
self.prev_btn.grid(row=0, column=0, padx=4)
self.pause_btn = tk.Button(self.controls_frame, text='⏸ Pause', command=self.toggle_pause, **btn_style)
self.pause_btn.grid(row=0, column=1, padx=4)
self.next_btn = tk.Button(self.controls_frame, text='Next ⏭', command=self.next_media, **btn_style)
self.next_btn.grid(row=0, column=2, padx=4)
self.settings_btn = tk.Button(self.controls_frame, text='⚙ Settings', command=self.open_settings, **btn_style)
self.settings_btn.grid(row=0, column=3, padx=4)
self.exit_btn = tk.Button(self.controls_frame, text='⏻ Exit', command=self.exit_app, **btn_style)
self.exit_btn.grid(row=0, column=4, padx=4)
self.exit_btn.config(fg='#ff4d4d')
self.controls_win.withdraw()
self.controls_win.after(200, place_controls)
self.root.bind('<Configure>', lambda e: self.controls_win.after(200, place_controls))
def hide_mouse(self):
self.root.config(cursor='none')
if hasattr(self, 'controls_win'):
self.controls_win.config(cursor='none')
def show_mouse(self):
self.root.config(cursor='arrow')
if hasattr(self, 'controls_win'):
self.controls_win.config(cursor='arrow')
def move_mouse_to_corner(self):
try:
import pyautogui
sw = self.root.winfo_screenwidth()
sh = self.root.winfo_screenheight()
pyautogui.moveTo(sw-2, sh-2)
except Exception:
pass
def show_controls(self):
if not hasattr(self, 'controls_win') or self.controls_win is None or not self.controls_win.winfo_exists():
self.create_controls()
self.controls_win.deiconify()
self.controls_win.lift()
self.show_mouse()
self.schedule_hide_controls()
def hide_controls(self):
self.controls_win.withdraw()
self.hide_mouse()
def schedule_hide_controls(self):
if hasattr(self, 'hide_controls_timer') and self.hide_controls_timer:
self.root.after_cancel(self.hide_controls_timer)
self.hide_controls_timer = self.root.after(5000, self.hide_controls)
def on_activity(self, event=None):
self.show_controls()
def prev_media(self):
self.current_index = (self.current_index - 1) % len(self.playlist)
self.show_current_media()
def next_media(self):
# If at the last media, update playlist before looping
if self.current_index == len(self.playlist) - 1:
self.update_playlist_from_server()
self.current_index = 0
self.show_current_media()
else:
self.current_index = (self.current_index + 1) % len(self.playlist)
self.show_current_media()
def update_playlist_from_server(self):
# Dummy implementation: replace with your actual update logic
# For example, call a function to fetch and reload the playlist
print("[INFO] Updating playlist from server...")
# You can import and call your real update function here
# Example: self.playlist = get_latest_playlist()
def toggle_pause(self):
if not self.paused:
self.paused = True
self.pause_btn.config(text='▶ Resume')
self.pause_timer = self.root.after(30000, self.resume_play)
else:
self.resume_play()
def resume_play(self):
self.paused = False
self.pause_btn.config(text='⏸ Pause')
if self.pause_timer:
self.root.after_cancel(self.pause_timer)
self.pause_timer = None
def play_intro_video(self):
intro_path = os.path.join(os.path.dirname(__file__), 'main_data', 'intro1.mp4')
if os.path.exists(intro_path):
self.show_video(intro_path, on_end=self.after_intro)
else:
self.after_intro()
def after_intro(self):
self.show_current_media()
self.root.after(100, self.next_media_loop)
def show_video(self, file_path, on_end=None, duration=None):
try:
print(f"[PLAYER] Attempting to play video: {file_path}")
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if video doesn't end
def watchdog():
print(f"[WATCHDOG] Video watchdog triggered for {file_path}")
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
max_duration = duration if duration is not None else 60 # fallback max 60s
self.video_watchdog = self.root.after(int(max_duration * 1200), watchdog)
def finish_video():
if hasattr(self, 'video_watchdog'):
self.root.after_cancel(self.video_watchdog)
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
if duration is not None:
self.root.after(int(duration * 1000), finish_video)
else:
def check_end():
try:
if self.vlc_player.get_state() == vlc.State.Ended:
finish_video()
elif self.vlc_player.get_state() == vlc.State.Error:
print(f"[VLC] Error state detected for {file_path}")
finish_video()
else:
self.root.after(200, check_end)
except Exception as e:
print(f"[VLC] Exception in check_end: {e}")
finish_video()
check_end()
except Exception as e:
print(f"[VLC] Error playing video {file_path}: {e}")
if on_end:
on_end()
def show_current_media(self):
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
if not self.playlist:
print("[PLAYER] Playlist is empty. No media to show.")
self.label.config(text="No media available", fg='white', font=('Arial', 32))
# Try to reload playlist after 10 seconds
self.root.after(10000, self.reload_playlist_and_continue)
return
media = self.playlist[self.current_index]
file_path = os.path.join(MEDIA_DATA_PATH, media['file_name'])
ext = file_path.lower()
duration = media.get('duration', None)
if not os.path.isfile(file_path):
print(f"[PLAYER] File missing: {file_path}. Skipping to next.")
self.next_media()
return
if ext.endswith(('.mp4', '.avi', '.mov', '.mkv')):
self.show_video(file_path, on_end=self.next_media, duration=duration)
elif ext.endswith(('.jpg', '.jpeg', '.png', '.bmp', '.gif')):
self.show_image_via_vlc(file_path, duration if duration is not None else 10, on_end=self.next_media)
else:
print(f"[PLAYER] Unsupported file type: {media['file_name']}")
self.label.config(text=f"Unsupported: {media['file_name']}", fg='yellow')
self.root.after(2000, self.next_media)
def reload_playlist_and_continue(self):
print("[PLAYER] Attempting to reload playlist...")
new_playlist = load_latest_playlist()
if new_playlist:
self.playlist = new_playlist
self.current_index = 0
print("[PLAYER] Playlist reloaded. Continuing playback.")
self.show_current_media()
else:
print("[PLAYER] Still no playlist. Will retry.")
self.root.after(10000, self.reload_playlist_and_continue)
def show_image_via_vlc(self, file_path, duration, on_end=None):
try:
print(f"[PLAYER] Attempting to show image: {file_path}")
if hasattr(self, 'vlc_player') and self.vlc_player:
self.vlc_player.stop()
if not hasattr(self, 'video_canvas'):
self.video_canvas = tk.Canvas(self.root, bg='black', highlightthickness=0)
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.label.pack_forget()
self.video_canvas.pack(fill=tk.BOTH, expand=True)
self.root.attributes('-fullscreen', True)
self.root.update_idletasks()
self.vlc_instance = vlc.Instance('--vout=x11')
self.vlc_player = self.vlc_instance.media_player_new()
self.vlc_player.set_mrl(file_path)
self.vlc_player.set_fullscreen(True)
self.vlc_player.set_xwindow(self.video_canvas.winfo_id())
self.vlc_player.play()
# Watchdog timer: fallback if image doesn't advance
def watchdog():
print(f"[WATCHDOG] Image watchdog triggered for {file_path}")
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
self.image_watchdog = self.root.after(int(duration * 1200), watchdog)
def finish_image():
if hasattr(self, 'image_watchdog'):
self.root.after_cancel(self.image_watchdog)
self.vlc_player.stop()
self.video_canvas.pack_forget()
self.label.pack(fill=tk.BOTH, expand=True)
if on_end:
on_end()
self.root.after(int(duration * 1000), finish_image)
except Exception as e:
print(f"[VLC] Error showing image {file_path}: {e}")
if on_end:
on_end()
def next_media(self):
self.current_index = (self.current_index + 1) % len(self.playlist)
self.show_current_media()
def next_media_loop(self):
if not self.playlist or self.paused:
self.root.after(1000, self.next_media_loop)
return
self.show_current_media()
def exit_app(self):
# Signal all threads and flags to stop
if hasattr(self, 'stop_event') and self.stop_event:
self.stop_event.set()
if hasattr(self, 'app_running') and self.app_running:
self.app_running[0] = False
# Unbind all events to prevent callbacks after destroy
try:
self.root.unbind('<Motion>')
self.root.unbind('<Button-1>')
self.root.unbind('<Configure>')
except Exception:
pass
# Attempt to destroy all Toplevel windows before root
try:
# Withdraw controls_win if it exists
if hasattr(self, 'controls_win') and self.controls_win:
if self.controls_win.winfo_exists():
self.controls_win.withdraw()
# Destroy controls_win if it exists (this will also destroy controls_frame)
if hasattr(self, 'controls_win') and self.controls_win:
if self.controls_win.winfo_exists():
self.controls_win.destroy()
self.controls_win = None
# Fallback: destroy controls_frame if it somehow still exists
if hasattr(self, 'controls_frame') and self.controls_frame:
if self.controls_frame.winfo_exists():
self.controls_frame.destroy()
self.controls_frame = None
# Fallback: destroy any remaining Toplevels in the app
for widget in self.root.winfo_children():
if isinstance(widget, tk.Toplevel):
try:
widget.destroy()
except Exception:
pass
except Exception as e:
print(f"[EXIT] Error destroying controls_win/frame/toplevels: {e}")
# Destroy any other Toplevels if needed (add here if you have more)
try:
if self.root.winfo_exists():
self.root.destroy()
except Exception as e:
print(f"[EXIT] Error destroying root: {e}")
def open_settings(self):
if self.paused is not True:
self.paused = True
self.pause_btn.config(text='▶ Resume')
# Explicitly pause VLC video if playing
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
self.vlc_player.pause()
except Exception:
pass
# Destroy controls overlay so settings window is always interactive
if hasattr(self, 'controls_win') and self.controls_win:
self.controls_win.destroy()
self.controls_win = None
settings_path = os.path.join(os.path.dirname(__file__), 'appsettings.py')
# Open settings in a new process so it doesn't block the main player
proc = subprocess.Popen([sys.executable, settings_path], close_fds=True)
# Give the window manager a moment to focus the new window
self.root.after(300, lambda: self.root.focus_force())
# Wait for the settings window to close, then resume
self.root.after(1000, lambda: self.check_settings_closed(proc))
def check_settings_closed(self, proc):
if proc.poll() is not None:
# Resume playback and unpause VLC if needed
self.resume_play()
# Restore and recreate controls overlay
self.root.deiconify()
self.create_controls()
# Re-bind mouse and button events to new controls
self.root.bind('<Motion>', self.on_activity)
self.root.bind('<Button-1>', self.on_activity)
self.show_controls()
if hasattr(self, 'vlc_player') and self.vlc_player:
try:
# Only resume if it was paused by us
if self.vlc_player.get_state() == vlc.State.Paused:
self.vlc_player.play()
except Exception:
pass
else:
self.root.after(1000, lambda: self.check_settings_closed(proc))
def main_start(self):
self.play_intro_video()
def load_latest_playlist():
files = [f for f in os.listdir(PLAYLIST_DIR) if f.startswith('server_playlist_v') and f.endswith('.json')]
if not files:
return []
# Sort by version number descending
files.sort(key=lambda x: int(x.split('_v')[-1].split('.json')[0]), reverse=True)
latest_file = files[0]
with open(os.path.join(PLAYLIST_DIR, latest_file), 'r') as f:
data = json.load(f)
playlist = data.get('playlist', [])
# Validate playlist: skip missing or unsupported files
valid_exts = ('.mp4', '.avi', '.mov', '.mkv', '.jpg', '.jpeg', '.png', '.bmp', '.gif')
valid_playlist = []
for item in playlist:
file_path = os.path.join(MEDIA_DATA_PATH, item.get('file_name', ''))
if os.path.isfile(file_path) and file_path.lower().endswith(valid_exts):
valid_playlist.append(item)
else:
print(f"[PLAYLIST] Skipping missing or unsupported file: {item.get('file_name')}")
return valid_playlist