final touches

This commit is contained in:
2025-08-23 23:55:20 +03:00
parent 1579371395
commit da91677f5b
15 changed files with 3691 additions and 433 deletions

Binary file not shown.

View File

@@ -10,11 +10,27 @@ import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# Import the player module from player_app.py
from logging_config import Logger
from player_app import SimpleMediaPlayerApp
from splash_screen import SplashScreen
from playlist_manager import PlaylistManager
if __name__ == "__main__":
import tkinter as tk
root = tk.Tk()
player = SimpleMediaPlayerApp(root)
player.run()
# Play splash screen first, then start player
playlist_manager = PlaylistManager()
playlist_manager.fetch_playlist() # Start fetching playlist in parallel with splash
intro_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
Logger.info(f"[MAIN] About to show splash screen: {intro_path}")
def start_player():
Logger.info("[MAIN] Splash finished, waiting for playlist...")
playlist = playlist_manager.wait_for_playlist()
Logger.info(f"[MAIN] Playlist loaded: {playlist}")
player = SimpleMediaPlayerApp(root)
player.playlist = playlist or []
player.run()
splash = SplashScreen(root, intro_path, duration=10)
splash.show(on_finish=start_player)
Logger.info("[MAIN] splash.show() called, entering mainloop...")
root.mainloop()

View File

@@ -0,0 +1,146 @@
import threading
import os
from logging_config import Logger
from PIL import Image, ImageTk
class MediaPlaybackController:
def __init__(self, app, ui):
self.app = app # Reference to SimpleMediaPlayerApp
self.ui = ui # Reference to PlayerUI
self.auto_advance_timer = None
self.current_index = 0
self.playlist = []
self.scaling_mode = 'fit'
def set_playlist(self, playlist):
self.playlist = playlist
self.current_index = 0
def play_current_media(self):
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)
if file_path.startswith('static/resurse/'):
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}")
self.log_event(file_name, "STARTED")
self.cancel_timers()
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.ui.status_label.config(text=f"Missing or unsupported media:\n{file_name}")
self.auto_advance_timer = self.ui.root.after(5000, self.next_media)
def play_video(self, file_path):
self.ui.status_label.place_forget()
def run_vlc_subprocess():
import subprocess
try:
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
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.ui.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.ui.root.after(1000, self.next_media)))
threading.Thread(target=run_vlc_subprocess, daemon=True).start()
def show_image(self, file_path, duration):
try:
self.ui.status_label.place_forget()
self.ui.status_label.config(text="")
img = Image.open(file_path)
original_size = img.size
screen_width = self.ui.root.winfo_width()
screen_height = self.ui.root.winfo_height()
if screen_width <= 1 or screen_height <= 1:
screen_width = 1920
screen_height = 1080
final_img, offset = self.ui.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
photo = ImageTk.PhotoImage(final_img)
self.ui.image_label.config(image=photo)
self.ui.image_label.image = photo
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})")
self.auto_advance_timer = self.ui.root.after(
int(duration * 1000),
self.next_media
)
except Exception as e:
Logger.error(f"Failed to show image {file_path}: {e}")
self.ui.image_label.config(image='')
self.ui.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}")
self.ui.status_label.place(relx=0.5, rely=0.5, anchor='center')
self.auto_advance_timer = self.ui.root.after(5000, self.next_media)
def show_text_content(self, text, duration):
self.ui.image_label.config(image='')
self.ui.status_label.config(text=text)
self.auto_advance_timer = self.ui.root.after(
int(duration * 1000),
self.next_media
)
def next_media(self):
self.cancel_timers()
if not self.playlist:
return
self.current_index = (self.current_index + 1) % len(self.playlist)
self.play_current_media()
def previous_media(self):
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):
self.app.is_paused = not self.app.is_paused
if self.app.is_paused:
self.ui.play_pause_btn.config(text="▶ Play")
self.cancel_timers()
else:
self.ui.play_pause_btn.config(text="⏸ Pause")
self.play_current_media()
Logger.info(f"Media {'paused' if self.app.is_paused else 'resumed'}")
def cancel_timers(self):
if self.auto_advance_timer:
self.ui.root.after_cancel(self.auto_advance_timer)
self.auto_advance_timer = None
def show_no_content_message(self):
self.ui.status_label.config(text="No content available.")
def log_event(self, file_name, event):
import datetime
try:
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_message = f"{timestamp} - {event}: {file_name}\n"
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}")

View File

@@ -13,7 +13,12 @@ import subprocess
import sys
import requests # Required for server communication
import queue
import vlc # For video playback with hardware acceleration
from media_playback_controller import MediaPlaybackController
try:
import vlc # For video playback with hardware acceleration
except Exception as e:
vlc = None
print(f"WARNING: VLC not available: {e}. Video playback may be limited.")
try:
from PIL import Image, ImageTk
@@ -30,6 +35,8 @@ from python_functions import (
from logging_config import Logger
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
from settings_screen import SettingsWindow
from splash_screen import SplashScreen
from player_ui import PlayerUI
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
@@ -43,29 +50,86 @@ class SimpleMediaPlayerApp:
self.playlist = []
self.current_index = 0
self.scaling_mode = 'fit'
self.auto_advance_timer = None
self.hide_controls_timer = None
self.control_frame = None
self.settings_window = None
self.content_frame = None
self.image_label = None
self.status_label = None
self.play_pause_btn = None
self.prev_btn = None
self.next_btn = None
self.exit_btn = None
self.settings_btn = None
# UI extraction
self.ui = PlayerUI(root, control_callbacks={
'prev': self.previous_media,
'play_pause': self.toggle_play_pause,
'next': self.next_media,
'settings': self.open_settings,
'exit': self.show_exit_dialog
})
self.content_frame = self.ui.content_frame
self.image_label = self.ui.image_label
self.status_label = self.ui.status_label
self.play_pause_btn = self.ui.play_pause_btn
self.prev_btn = self.ui.prev_btn
self.next_btn = self.ui.next_btn
self.exit_btn = self.ui.exit_btn
self.settings_btn = self.ui.settings_btn
self.playback = MediaPlaybackController(self, self.ui)
self.setup_window()
self.setup_ui()
# Automatically check for new server playlist on startup
try:
from python_functions import check_for_new_server_playlist
check_for_new_server_playlist()
except Exception as e:
Logger.error(f"Failed to check for new server playlist: {e}")
self.initialize_playlist_from_server()
self.start_periodic_checks()
self.start_player_logic()
def start_player_logic(self):
# Start playlist/server thread
self._playlist_ready = threading.Event()
self._playlist_data = None
def fetch_playlist_logic():
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}")
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}")
playlist = None
# Try to fetch from server
if server and host and quick and port:
try:
Logger.info("Attempting to connect to server...")
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}")
download_media_files(server_playlist, server_version)
update_config_playlist_version(server_version)
local_playlist_data = load_local_playlist()
playlist = local_playlist_data.get('playlist', [])
if playlist:
Logger.info(f"Successfully loaded {len(playlist)} items from server")
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 Exception as e:
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
if not playlist:
playlist = fallback_playlist
if not playlist:
playlist = None
self._playlist_data = playlist
self._playlist_ready.set()
threading.Thread(target=fetch_playlist_logic, daemon=True).start()
self._playlist_ready.wait()
playlist = self._playlist_data
if playlist and len(playlist) > 0:
self.playlist = playlist
Logger.info(f"Loaded playlist with {len(self.playlist)} items")
self.play_current_media()
else:
Logger.warning("No playlist available, loading demo content")
self.load_demo_or_local_playlist()
self.start_periodic_checks()
def setup_window(self):
self.root.title("Simple Signage Player")
@@ -85,433 +149,37 @@ class SimpleMediaPlayerApp:
self.root.bind('<Motion>', self.on_mouse_motion)
self.root.focus_set()
def setup_ui(self):
self.content_frame = tk.Frame(self.root, bg='black')
self.content_frame.pack(fill=tk.BOTH, expand=True)
self.image_label = tk.Label(self.content_frame, bg='black')
self.image_label.pack(fill=tk.BOTH, expand=True)
self.status_label = tk.Label(
self.content_frame,
bg='black',
fg='white',
font=('Arial', 24),
text=""
)
self.create_control_panel()
self.show_controls()
self.schedule_hide_controls()
# --- FULL METHOD BODIES FROM tkinter_simple_player_old.py BELOW ---
def create_control_panel(self):
"""Create touch-optimized control panel with larger buttons"""
self.control_frame = tk.Frame(
self.root,
bg='#1a1a1a',
bd=2,
relief=tk.RAISED,
padx=15,
pady=15
)
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
button_config = {
'bg': '#333333',
'fg': 'white',
'activebackground': '#555555',
'activeforeground': 'white',
'relief': tk.FLAT,
'borderwidth': 0,
'width': 10,
'height': 3,
'font': ('Segoe UI', 10, 'bold'),
'cursor': 'hand2'
}
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)
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',
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)
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)
self.settings_btn = tk.Button(
self.control_frame,
text="⚙️ Settings",
command=self.open_settings,
bg='#9b59b6',
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)
self.exit_btn = tk.Button(
self.control_frame,
text="❌ EXIT",
command=self.show_exit_dialog,
bg='#e74c3c',
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)
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'):
img_width, img_height = img.size
if mode == 'stretch':
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
elif mode == 'fill':
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_height = screen_height
new_width = int(screen_height * img_ratio)
x_offset = (screen_width - new_width) // 2
y_offset = 0
else:
new_width = screen_width
new_height = int(screen_width / img_ratio)
x_offset = 0
y_offset = (screen_height - new_height) // 2
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
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:
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_width = screen_width
new_height = int(screen_width / img_ratio)
else:
new_height = screen_height
new_width = int(screen_height * img_ratio)
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
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):
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):
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}")
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()
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
server_connection_successful = False
try:
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
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)
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 not server_connection_successful:
self.status_label.config(text="Server unavailable\nLoading last playlist...")
self.root.update()
time.sleep(1)
self.status_label.place_forget()
self.load_fallback_playlist(fallback_playlist)
def load_fallback_playlist(self, fallback_playlist):
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):
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
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):
demo_images = []
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 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:
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):
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):
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):
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)
if file_path.startswith('static/resurse/'):
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}")
self.log_event(file_name, "STARTED")
self.cancel_timers()
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}")
self.auto_advance_timer = self.root.after(5000, self.next_media)
self.playback.play_current_media()
def play_video(self, file_path):
self.status_label.place_forget()
def run_vlc_subprocess():
try:
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
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):
try:
self.image_label.config(image=photo)
self.image_label.image = photo
except Exception as e:
Logger.error(f"Error updating video frame: {e}")
def _show_video_error(self, error_msg):
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):
self.image_label.config(image='')
self.status_label.config(text=text)
self.auto_advance_timer = self.root.after(
int(duration * 1000),
self.next_media
)
self.playback.play_video(file_path)
def show_image(self, file_path, duration):
try:
self.status_label.place_forget()
self.status_label.config(text="")
if PIL_AVAILABLE:
img = Image.open(file_path)
original_size = img.size
screen_width = self.root.winfo_width()
screen_height = self.root.winfo_height()
if screen_width <= 1 or screen_height <= 1:
screen_width = 1920
screen_height = 1080
final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
photo = ImageTk.PhotoImage(final_img)
self.image_label.config(image=photo)
self.image_label.image = photo
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:
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")
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)
self.playback.show_image(file_path, duration)
def show_text_content(self, text, duration):
self.playback.show_text_content(text, duration)
def next_media(self):
self.cancel_timers()
if not self.playlist:
return
self.current_index = (self.current_index + 1) % len(self.playlist)
if self.current_index == 0:
threading.Thread(target=self.check_playlist_updates, daemon=True).start()
self.play_current_media()
self.playback.next_media()
def previous_media(self):
self.cancel_timers()
if not self.playlist:
return
self.current_index = (self.current_index - 1) % len(self.playlist)
self.play_current_media()
self.playback.previous_media()
def toggle_play_pause(self):
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")
self.play_current_media()
Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}")
self.playback.toggle_play_pause()
def cancel_timers(self):
if self.auto_advance_timer:
self.root.after_cancel(self.auto_advance_timer)
self.auto_advance_timer = None
self.playback.cancel_timers()
def show_controls(self):
if self.control_frame:
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
if self.ui.control_frame:
self.ui.control_frame.place(relx=0.98, rely=0.98, anchor='se')
def hide_controls(self):
if self.control_frame:
self.control_frame.place_forget()
if self.ui.control_frame:
self.ui.control_frame.place_forget()
def schedule_hide_controls(self):
if self.hide_controls_timer:
@@ -709,6 +377,16 @@ class SimpleMediaPlayerApp:
Logger.error(f"Error in periodic check: {e}")
threading.Thread(target=check_loop, daemon=True).start()
def next_media(self):
self.cancel_timers()
if not self.playlist:
return
self.current_index = (self.current_index + 1) % len(self.playlist)
# At end of playlist, check for updates
if self.current_index == 0:
self.check_playlist_updates()
self.play_current_media()
def run(self):
Logger.info("Starting Simple Tkinter Media Player")
try:

View File

@@ -0,0 +1,833 @@
# player_app.py
# Main player application logic moved from tkinter_simple_player.py
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:
from PIL import Image, ImageTk
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
print("WARNING: PIL not available. Image display functionality will be limited.")
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
from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton
from settings_screen import SettingsWindow
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt')
class SimpleMediaPlayerApp:
def __init__(self, root):
self.root = root
self.running = True
self.is_paused = False
self.is_fullscreen = True
self.playlist = []
self.current_index = 0
self.scaling_mode = 'fit'
self.auto_advance_timer = None
self.hide_controls_timer = None
self.control_frame = None
self.settings_window = None
self.content_frame = None
self.image_label = None
self.status_label = None
self.play_pause_btn = None
self.prev_btn = None
self.next_btn = None
self.exit_btn = None
self.settings_btn = None
self.setup_window()
self.play_splash_then_start()
def play_splash_then_start(self):
# Start playlist/server thread
self._playlist_ready = threading.Event()
self._playlist_data = None
def fetch_playlist_logic():
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}")
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}")
playlist = None
# Try to fetch from server
if server and host and quick and port:
try:
Logger.info("Attempting to connect to server...")
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}")
download_media_files(server_playlist, server_version)
update_config_playlist_version(server_version)
local_playlist_data = load_local_playlist()
playlist = local_playlist_data.get('playlist', [])
if playlist:
Logger.info(f"Successfully loaded {len(playlist)} items from server")
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 Exception as e:
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
if not playlist:
playlist = fallback_playlist
if not playlist:
playlist = None
self._playlist_data = playlist
self._playlist_ready.set()
threading.Thread(target=fetch_playlist_logic, daemon=True).start()
# Only play intro on first playlist loop
self._first_playlist_loop = True
self.setup_ui()
self._playlist_ready.wait()
playlist = self._playlist_data
if playlist and len(playlist) > 0:
self.playlist = playlist
Logger.info(f"Loaded playlist with {len(self.playlist)} items")
self.play_current_media()
else:
Logger.warning("No playlist available, loading demo content")
self.load_demo_or_local_playlist()
self.start_periodic_checks()
# _after_splash is now handled inline in play_splash_then_start
def setup_window(self):
self.root.title("Simple Signage Player")
self.root.configure(bg='black')
try:
config = load_config()
width = int(config.get('screen_w', 1920))
height = int(config.get('screen_h', 1080))
self.scaling_mode = config.get('scaling_mode', 'fit')
except:
width, height = 800, 600
self.scaling_mode = 'fit'
self.root.geometry(f"{width}x{height}")
self.root.attributes('-fullscreen', True)
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):
self.content_frame = tk.Frame(self.root, bg='black')
self.content_frame.pack(fill=tk.BOTH, expand=True)
self.image_label = tk.Label(self.content_frame, bg='black')
self.image_label.pack(fill=tk.BOTH, expand=True)
self.status_label = tk.Label(
self.content_frame,
bg='black',
fg='white',
font=('Arial', 20),
text=""
)
self.create_control_panel()
self.show_controls()
self.schedule_hide_controls()
# --- FULL METHOD BODIES FROM tkinter_simple_player_old.py BELOW ---
def create_control_panel(self):
"""Create touch-optimized control panel with larger buttons"""
self.control_frame = tk.Frame(
self.root,
bg='#1a1a1a',
bd=2,
relief=tk.RAISED,
padx=15,
pady=15
)
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
button_config = {
'bg': '#333333',
'fg': 'white',
'activebackground': '#555555',
'activeforeground': 'white',
'relief': tk.FLAT,
'borderwidth': 0,
'width': 10,
'height': 3,
'font': ('Segoe UI', 10, 'bold'),
'cursor': 'hand2'
}
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)
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',
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)
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)
self.settings_btn = tk.Button(
self.control_frame,
text="⚙️ Settings",
command=self.open_settings,
bg='#9b59b6',
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)
self.exit_btn = tk.Button(
self.control_frame,
text="❌ EXIT",
command=self.show_exit_dialog,
bg='#e74c3c',
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)
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'):
img_width, img_height = img.size
if mode == 'stretch':
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
elif mode == 'fill':
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_height = screen_height
new_width = int(screen_height * img_ratio)
x_offset = (screen_width - new_width) // 2
y_offset = 0
else:
new_width = screen_width
new_height = int(screen_width / img_ratio)
x_offset = 0
y_offset = (screen_height - new_height) // 2
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
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:
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_width = screen_width
new_height = int(screen_width / img_ratio)
else:
new_height = screen_height
new_width = int(screen_height * img_ratio)
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
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):
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):
# No-op: logic now handled in play_splash_then_start
pass
def _play_intro_splash(self, on_finish=None, duration=5):
"""Embed intro1.mp4 as splash video in splash_frame using python-vlc. If video ends early, pause last frame until 'duration' seconds."""
intro_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
self._splash_ended = False
def finish():
self._stop_intro_splash()
if on_finish:
self.root.after(0, on_finish)
def on_end(event=None):
if not self._splash_ended:
self._splash_ended = True
# Pause on last frame
if hasattr(self, '_splash_player') and self._splash_player:
self._splash_player.set_pause(1)
if os.path.exists(intro_path) and hasattr(self, 'splash_frame'):
try:
self._splash_vlc_instance = vlc.Instance('--no-osd', '--no-video-title-show', '--quiet')
self._splash_media = self._splash_vlc_instance.media_new(intro_path)
self._splash_player = self._splash_vlc_instance.media_player_new()
self._splash_player.set_media(self._splash_media)
self.splash_frame.update_idletasks()
window_id = self.splash_frame.winfo_id()
self._splash_player.set_xwindow(window_id)
# Attach event for end of video
event_manager = self._splash_player.event_manager()
event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, on_end)
self._splash_player.play()
self.root.after(int(duration * 1000), finish)
except Exception as e:
Logger.warning(f"Could not play splash video with python-vlc: {e}")
finish()
else:
Logger.warning(f"Splash video not found or splash_frame missing: {intro_path}")
finish()
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._stop_intro_splash()
self.load_fallback_playlist(fallback_playlist)
return
server_connection_successful = False
try:
Logger.info("Attempting to connect to server...")
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(server_playlist, server_version)
update_config_playlist_version(server_version)
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._stop_intro_splash()
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 not server_connection_successful:
self._stop_intro_splash()
self.load_fallback_playlist(fallback_playlist)
def _stop_intro_splash(self):
"""Stop the intro splash video if running (python-vlc version)."""
if hasattr(self, '_splash_player') and self._splash_player:
try:
self._splash_player.stop()
except Exception:
pass
self._splash_player = None
if hasattr(self, '_splash_vlc_instance'):
self._splash_vlc_instance = None
def load_fallback_playlist(self, fallback_playlist):
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):
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
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):
demo_images = []
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 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:
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):
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):
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):
# On first playlist loop, play intro splash, then continue playlist
if getattr(self, '_first_playlist_loop', False):
self._first_playlist_loop = False
# Create splash_frame for intro video
self.splash_frame = tk.Frame(self.root, bg='black')
self.splash_frame.pack(fill=tk.BOTH, expand=True)
self.root.update_idletasks()
def after_intro():
if hasattr(self, 'splash_frame') and self.splash_frame:
self.splash_frame.destroy()
self.splash_frame = None
self._play_current_media_after_intro()
self._play_intro_splash(on_finish=after_intro, duration=5)
return
self._play_current_media_after_intro()
def _play_current_media_after_intro(self):
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)
if file_path.startswith('static/resurse/'):
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}")
self.log_event(file_name, "STARTED")
self.cancel_timers()
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}")
self.auto_advance_timer = self.root.after(5000, self.next_media)
def play_video(self, file_path):
self.status_label.place_forget()
def run_vlc_subprocess():
try:
Logger.info(f"Starting system VLC subprocess for video: {file_path}")
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):
try:
self.image_label.config(image=photo)
self.image_label.image = photo
except Exception as e:
Logger.error(f"Error updating video frame: {e}")
def _show_video_error(self, error_msg):
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):
self.image_label.config(image='')
self.status_label.config(text=text)
self.auto_advance_timer = self.root.after(
int(duration * 1000),
self.next_media
)
def show_image(self, file_path, duration):
try:
self.status_label.place_forget()
self.status_label.config(text="")
if PIL_AVAILABLE:
img = Image.open(file_path)
original_size = img.size
screen_width = self.root.winfo_width()
screen_height = self.root.winfo_height()
if screen_width <= 1 or screen_height <= 1:
screen_width = 1920
screen_height = 1080
final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode)
photo = ImageTk.PhotoImage(final_img)
self.image_label.config(image=photo)
self.image_label.image = photo
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:
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")
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):
self.cancel_timers()
if not self.playlist:
return
self.current_index = (self.current_index + 1) % len(self.playlist)
if self.current_index == 0:
threading.Thread(target=self.check_playlist_updates, daemon=True).start()
self.play_current_media()
def previous_media(self):
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):
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")
self.play_current_media()
Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}")
def cancel_timers(self):
if self.auto_advance_timer:
self.root.after_cancel(self.auto_advance_timer)
self.auto_advance_timer = None
def show_controls(self):
if self.control_frame:
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
def hide_controls(self):
if self.control_frame:
self.control_frame.place_forget()
def schedule_hide_controls(self):
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):
self.show_controls()
self.schedule_hide_controls()
def on_mouse_motion(self, event):
self.show_controls()
self.schedule_hide_controls()
def on_key_press(self, event):
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:
if key == 's':
self.open_settings()
self.show_controls()
self.schedule_hide_controls()
def set_scaling_mode(self, mode):
old_mode = self.scaling_mode
self.scaling_mode = mode
Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'")
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')
self.root.after(2000, lambda: self.status_label.place_forget())
if self.playlist and 0 <= self.current_index < len(self.playlist):
self.cancel_timers()
self.play_current_media()
def toggle_fullscreen(self):
self.is_fullscreen = not self.is_fullscreen
self.root.attributes('-fullscreen', self.is_fullscreen)
def open_settings(self):
if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists():
self.settings_window.lift()
return
if not self.is_paused:
self.toggle_play_pause()
self.settings_window = SettingsWindow(self.root, self)
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):
try:
config = load_config()
quickconnect_key = config.get('quickconnect_key', '')
except:
quickconnect_key = ''
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)
self.center_dialog_on_screen(exit_dialog, 400, 200)
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 = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20)
content_frame.pack(fill=tk.BOTH, expand=True)
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_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 = 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():
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 = tk.Label(content_frame, text="", font=('Arial', 9),
bg='#2d2d2d')
error_label.pack()
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)
password_entry.bind('<Return>', lambda e: check_password())
exit_dialog.bind('<Escape>', lambda e: cancel_exit())
def exit_application(self):
Logger.info("Application exit requested")
self.running = False
self.root.quit()
self.root.destroy()
def center_dialog_on_screen(self, dialog, width, height):
dialog.update_idletasks()
screen_width = dialog.winfo_screenwidth()
screen_height = dialog.winfo_screenheight()
center_x = int((screen_width - width) / 2)
center_y = int((screen_height - height) / 2)
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}")
dialog.lift()
dialog.focus_force()
return center_x, center_y
def check_playlist_updates(self):
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}")
local_playlist_data = load_local_playlist()
clean_unused_files(local_playlist_data.get('playlist', []))
download_media_files(
server_playlist_data.get('playlist', []),
server_version
)
local_playlist_data = load_local_playlist()
self.playlist = local_playlist_data.get('playlist', [])
self.current_index = 0
Logger.info("Playlist updated successfully")
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):
try:
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
log_message = f"{timestamp} - {event}: {file_name}\n"
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):
def check_loop():
while self.running:
try:
time.sleep(300)
if self.running:
self.check_playlist_updates()
except Exception as e:
Logger.error(f"Error in periodic check: {e}")
threading.Thread(target=check_loop, daemon=True).start()
def next_media(self):
self.cancel_timers()
if not self.playlist:
return
self.current_index = (self.current_index + 1) % len(self.playlist)
# At end of playlist, check for updates
if self.current_index == 0:
self.check_playlist_updates()
self.play_current_media()
def run(self):
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}")

View File

@@ -0,0 +1,162 @@
import tkinter as tk
from PIL import Image
class PlayerUI:
def __init__(self, root, control_callbacks=None):
self.root = root
self.control_frame = None
self.content_frame = None
self.image_label = None
self.status_label = None
self.play_pause_btn = None
self.prev_btn = None
self.next_btn = None
self.exit_btn = None
self.settings_btn = None
self.hide_controls_timer = None
self.setup_ui(control_callbacks)
def setup_ui(self, control_callbacks=None):
self.content_frame = tk.Frame(self.root, bg='black')
self.content_frame.pack(fill=tk.BOTH, expand=True)
self.image_label = tk.Label(self.content_frame, bg='black')
self.image_label.pack(fill=tk.BOTH, expand=True)
self.status_label = tk.Label(
self.content_frame,
bg='black',
fg='white',
font=('Arial', 20),
text=""
)
self.create_control_panel(control_callbacks)
self.show_controls()
self.schedule_hide_controls()
def create_control_panel(self, control_callbacks=None):
self.control_frame = tk.Frame(
self.root,
bg='#1a1a1a',
bd=2,
relief=tk.RAISED,
padx=15,
pady=15
)
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
button_config = {
'bg': '#333333',
'fg': 'white',
'activebackground': '#555555',
'activeforeground': 'white',
'relief': tk.FLAT,
'borderwidth': 0,
'width': 10,
'height': 3,
'font': ('Segoe UI', 10, 'bold'),
'cursor': 'hand2'
}
self.prev_btn = tk.Button(
self.control_frame,
text="⏮ Prev",
command=control_callbacks.get('prev') if control_callbacks else None,
**button_config
)
self.prev_btn.grid(row=0, column=0, padx=5)
self.play_pause_btn = tk.Button(
self.control_frame,
text="⏸ Pause",
command=control_callbacks.get('play_pause') if control_callbacks else None,
bg='#27ae60',
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)
self.next_btn = tk.Button(
self.control_frame,
text="Next ⏭",
command=control_callbacks.get('next') if control_callbacks else None,
**button_config
)
self.next_btn.grid(row=0, column=2, padx=5)
self.settings_btn = tk.Button(
self.control_frame,
text="⚙️ Settings",
command=control_callbacks.get('settings') if control_callbacks else None,
bg='#9b59b6',
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)
self.exit_btn = tk.Button(
self.control_frame,
text="❌ EXIT",
command=control_callbacks.get('exit') if control_callbacks else None,
bg='#e74c3c',
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)
def show_controls(self):
if self.control_frame:
self.control_frame.place(relx=0.98, rely=0.98, anchor='se')
def hide_controls(self):
if self.control_frame:
self.control_frame.place_forget()
def schedule_hide_controls(self):
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 scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'):
img_width, img_height = img.size
if mode == 'stretch':
return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0)
elif mode == 'fill':
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_height = screen_height
new_width = int(screen_height * img_ratio)
x_offset = (screen_width - new_width) // 2
y_offset = 0
else:
new_width = screen_width
new_height = int(screen_width / img_ratio)
x_offset = 0
y_offset = (screen_height - new_height) // 2
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
final_img = Image.new('RGB', (screen_width, screen_height), 'black')
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:
screen_ratio = screen_width / screen_height
img_ratio = img_width / img_height
if img_ratio > screen_ratio:
new_width = screen_width
new_height = int(screen_width / img_ratio)
else:
new_height = screen_height
new_width = int(screen_height * img_ratio)
img_resized = img.resize((new_width, new_height), Image.LANCZOS)
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)

View File

@@ -0,0 +1,59 @@
import threading
from logging_config import Logger
from python_functions import (
load_local_playlist, download_media_files, update_config_playlist_version, fetch_server_playlist, load_config
)
class PlaylistManager:
def __init__(self):
self._playlist_ready = threading.Event()
self._playlist_data = None
def fetch_playlist(self):
def fetch_logic():
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}")
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}")
playlist = None
if server and host and quick and port:
try:
Logger.info("Attempting to connect to server...")
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}")
download_media_files(server_playlist, server_version)
update_config_playlist_version(server_version)
local_playlist_data = load_local_playlist()
playlist = local_playlist_data.get('playlist', [])
if playlist:
Logger.info(f"Successfully loaded {len(playlist)} items from server")
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 Exception as e:
Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist")
if not playlist:
playlist = fallback_playlist
if not playlist:
playlist = None
self._playlist_data = playlist
self._playlist_ready.set()
threading.Thread(target=fetch_logic, daemon=True).start()
def wait_for_playlist(self):
self._playlist_ready.wait()
return self._playlist_data

View File

@@ -0,0 +1,48 @@
import os
import subprocess
import time
import tkinter as tk
from logging_config import Logger
class SplashScreen:
def __init__(self, root, video_path, duration=5):
self.root = root
self.video_path = video_path
self.duration = duration
def show(self, on_finish=None):
Logger.info(f"[SplashScreen] Running splash as standalone VLC subprocess: {self.video_path}")
if os.path.exists(self.video_path):
try:
vlc_cmd = [
'cvlc',
'--fullscreen',
'--no-osd',
'--no-video-title-show',
'--play-and-exit',
'--quiet',
self.video_path
]
Logger.info(f"[SplashScreen] Launching: {' '.join(vlc_cmd)}")
proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
proc.wait() # Remove timeout for immediate transition
Logger.info("[SplashScreen] VLC splash finished.")
except Exception as e:
Logger.warning(f"[SplashScreen] VLC splash failed: {e}")
else:
Logger.warning(f"[SplashScreen] Splash video not found: {self.video_path}")
if on_finish:
self.root.after(0, on_finish)
if __name__ == "__main__":
root = tk.Tk()
# Use the same logic as main.py to resolve the video path
base_dir = os.path.dirname(os.path.dirname(__file__))
video_path = os.path.join(base_dir, 'resources', 'intro1.mp4')
Logger.info(f"[SplashScreen] Standalone test: video_path={video_path}")
def on_finish():
Logger.info("[SplashScreen] Standalone test: Splash finished, exiting app.")
root.destroy()
splash = SplashScreen(root, video_path, duration=10)
splash.show(on_finish=on_finish)
root.mainloop()

View File

@@ -0,0 +1,30 @@
import vlc
import time
import os
from logging_config import Logger
if __name__ == "__main__":
video_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'intro1.mp4')
Logger.info(f"[VLC TEST] Attempting to play video: {video_path}")
if not os.path.exists(video_path):
Logger.error(f"[VLC TEST] File does not exist: {video_path}")
exit(1)
instance = vlc.Instance('--no-osd', '--no-video-title-show', '--quiet', '--fullscreen')
player = instance.media_player_new()
media = instance.media_new(video_path)
player.set_media(media)
player.play()
Logger.info("[VLC TEST] player.play() called. Waiting for video to finish...")
# Wait for video to finish (max 30 seconds)
start = time.time()
while True:
state = player.get_state()
if state in (vlc.State.Ended, vlc.State.Error):
Logger.info(f"[VLC TEST] Playback ended with state: {state}")
break
if time.time() - start > 30:
Logger.warning("[VLC TEST] Timeout waiting for video to finish.")
break
time.sleep(0.2)
player.stop()
Logger.info("[VLC TEST] Done.")