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.

Binary file not shown.

File diff suppressed because it is too large Load Diff

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.")