From 1706ae7afdc4a093ed5124657f81d2896b782e5a Mon Sep 17 00:00:00 2001 From: scheianu Date: Fri, 22 Aug 2025 23:02:31 +0300 Subject: [PATCH] Finalize: working player, automatic playlist version check, server URL fix, and robust playlist update logic --- tkinter_app/resources/local_playlist.json | 2 +- tkinter_app/resources/log.txt | 555 +++++ .../__pycache__/player_app.cpython-311.pyc | Bin 0 -> 51471 bytes .../python_functions.cpython-311.pyc | Bin 14937 -> 15947 bytes .../settings_screen.cpython-311.pyc | Bin 0 -> 64384 bytes tkinter_app/src/main.py | 10 +- tkinter_app/src/player_app.py | 722 ++++++ tkinter_app/src/python_functions.py | 17 +- tkinter_app/src/settings_screen.py | 1069 ++++++++- tkinter_app/src/tkinter_simple_player.py | 2048 ----------------- tkinter_app/src/tkinter_simple_player_old.py | 934 ++++++++ 11 files changed, 3302 insertions(+), 2055 deletions(-) create mode 100644 tkinter_app/src/__pycache__/player_app.cpython-311.pyc create mode 100644 tkinter_app/src/__pycache__/settings_screen.cpython-311.pyc delete mode 100644 tkinter_app/src/tkinter_simple_player.py create mode 100644 tkinter_app/src/tkinter_simple_player_old.py diff --git a/tkinter_app/resources/local_playlist.json b/tkinter_app/resources/local_playlist.json index c8813bc..aa83fb5 100644 --- a/tkinter_app/resources/local_playlist.json +++ b/tkinter_app/resources/local_playlist.json @@ -16,5 +16,5 @@ "duration": 5 } ], - "version": 5 + "version": 0 } \ No newline at end of file diff --git a/tkinter_app/resources/log.txt b/tkinter_app/resources/log.txt index 28d3d25..7922fb7 100644 --- a/tkinter_app/resources/log.txt +++ b/tkinter_app/resources/log.txt @@ -1704,3 +1704,558 @@ [INFO] [SignageApp] python_functions: Starting load_config function. [INFO] [SignageApp] python_functions: Configuration file loaded successfully. [INFO] [SignageApp] Application exit requested +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Starting load_local_playlist function. +[INFO] [SignageApp] python_functions: Local playlist loaded: {'playlist': [{'file_name': '1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'url': 'static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'duration': 20}, {'file_name': 'wp2782770-1846651530.jpg', 'url': 'static/resurse/wp2782770-1846651530.jpg', 'duration': 15}, {'file_name': 'SampleVideo_1280x720_1mb.mp4', 'url': 'static/resurse/SampleVideo_1280x720_1mb.mp4', 'duration': 5}], 'version': 5} +[INFO] [SignageApp] python_functions: Finished load_local_playlist function successfully. +[INFO] [SignageApp] Found fallback playlist with 3 items +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Initializing with settings: server=digi-signage.moto-adv.com, host=tv-terasa, port=8880 +[INFO] [SignageApp] Attempting to connect to server... +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[WARNING] [SignageApp] Server returned empty playlist, falling back to local playlist +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Loaded fallback playlist with 3 items +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:21:01 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Starting Simple Tkinter Media Player +[INFO] [SignageApp] Application exit requested +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Starting load_local_playlist function. +[INFO] [SignageApp] python_functions: Local playlist loaded: {'playlist': [{'file_name': '1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'url': 'static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'duration': 20}, {'file_name': 'wp2782770-1846651530.jpg', 'url': 'static/resurse/wp2782770-1846651530.jpg', 'duration': 15}, {'file_name': 'SampleVideo_1280x720_1mb.mp4', 'url': 'static/resurse/SampleVideo_1280x720_1mb.mp4', 'duration': 5}], 'version': 0} +[INFO] [SignageApp] python_functions: Finished load_local_playlist function successfully. +[INFO] [SignageApp] Found fallback playlist with 3 items +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Initializing with settings: server=digi-signage.moto-adv.com, host=tv-terasa, port=8880 +[INFO] [SignageApp] Attempting to connect to server... +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[WARNING] [SignageApp] Server returned empty playlist, falling back to local playlist +[INFO] [SignageApp] Loaded fallback playlist with 3 items +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:33:08 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Starting Simple Tkinter Media Player +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:33:29 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:33:45 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:33:52 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:34:13 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:34:29 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +2025-08-22 22:34:36 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:34:56 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:35:12 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:35:19 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:35:40 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:35:56 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:36:02 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:36:23 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:36:39 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:36:46 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:37:06 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:37:22 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +2025-08-22 22:37:29 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:37:50 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:38:06 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:38:13 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:38:33 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:38:49 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:38:56 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:39:17 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:39:33 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:39:40 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:40:00 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:40:16 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:40:23 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:40:43 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Media paused +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Application exit requested +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No new playlist on the server. Local version: 0, Server version: 0 +[INFO] [SignageApp] python_functions: Starting load_local_playlist function. +[INFO] [SignageApp] python_functions: Local playlist loaded: {'playlist': [{'file_name': '1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'url': 'static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'duration': 20}, {'file_name': 'wp2782770-1846651530.jpg', 'url': 'static/resurse/wp2782770-1846651530.jpg', 'duration': 15}, {'file_name': 'SampleVideo_1280x720_1mb.mp4', 'url': 'static/resurse/SampleVideo_1280x720_1mb.mp4', 'duration': 5}], 'version': 0} +[INFO] [SignageApp] python_functions: Finished load_local_playlist function successfully. +[INFO] [SignageApp] Found fallback playlist with 3 items +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Initializing with settings: server=digi-signage.moto-adv.com, host=tv-terasa, port=8880 +[INFO] [SignageApp] Attempting to connect to server... +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[WARNING] [SignageApp] Server returned empty playlist, falling back to local playlist +[INFO] [SignageApp] Loaded fallback playlist with 3 items +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:46:40 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Starting Simple Tkinter Media Player +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Application exit requested +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=http://digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='http', port=80): Max retries exceeded with url: /digi-signage.moto-adv.com:8880/api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NameResolutionError(": Failed to resolve 'http' ([Errno -2] Name or service not known)")) +[INFO] [SignageApp] No new playlist on the server. Local version: 0, Server version: 0 +[INFO] [SignageApp] python_functions: Starting load_local_playlist function. +[INFO] [SignageApp] python_functions: Local playlist loaded: {'playlist': [{'file_name': '1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'url': 'static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'duration': 20}, {'file_name': 'wp2782770-1846651530.jpg', 'url': 'static/resurse/wp2782770-1846651530.jpg', 'duration': 15}, {'file_name': 'SampleVideo_1280x720_1mb.mp4', 'url': 'static/resurse/SampleVideo_1280x720_1mb.mp4', 'duration': 5}], 'version': 0} +[INFO] [SignageApp] python_functions: Finished load_local_playlist function successfully. +[INFO] [SignageApp] Found fallback playlist with 3 items +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Initializing with settings: server=http://digi-signage.moto-adv.com, host=tv-terasa, port=8880 +[INFO] [SignageApp] Attempting to connect to server... +[INFO] [SignageApp] Fetching playlist from URL: http://http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist: HTTPConnectionPool(host='http', port=80): Max retries exceeded with url: /digi-signage.moto-adv.com:8880/api/playlists?hostname=tv-terasa&quickconnect_code=8887779 (Caused by NameResolutionError(": Failed to resolve 'http' ([Errno -2] Name or service not known)")) +[WARNING] [SignageApp] Server returned empty playlist, falling back to local playlist +[INFO] [SignageApp] Loaded fallback playlist with 3 items +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:47:16 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1080, Mode: fit, Offset: (96, 0)) +[INFO] [SignageApp] Starting Simple Tkinter Media Player +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Application exit requested +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Configuration loaded: server=digi-signage.moto-adv.com, host=tv-terasa, quick=8887779, port=8880 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No new playlist on the server. Local version: 0, Server version: 0 +[INFO] [SignageApp] python_functions: Starting load_local_playlist function. +[INFO] [SignageApp] python_functions: Local playlist loaded: {'playlist': [{'file_name': '1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'url': 'static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg', 'duration': 20}, {'file_name': 'wp2782770-1846651530.jpg', 'url': 'static/resurse/wp2782770-1846651530.jpg', 'duration': 15}, {'file_name': 'SampleVideo_1280x720_1mb.mp4', 'url': 'static/resurse/SampleVideo_1280x720_1mb.mp4', 'duration': 5}], 'version': 0} +[INFO] [SignageApp] python_functions: Finished load_local_playlist function successfully. +[INFO] [SignageApp] Found fallback playlist with 3 items +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Initializing with settings: server=digi-signage.moto-adv.com, host=tv-terasa, port=8880 +[INFO] [SignageApp] Attempting to connect to server... +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[WARNING] [SignageApp] Server returned empty playlist, falling back to local playlist +[INFO] [SignageApp] Loaded fallback playlist with 3 items +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:49:45 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Starting Simple Tkinter Media Player +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:50:05 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:50:22 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:50:29 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:50:49 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:51:06 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:51:13 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:51:34 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:51:50 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:51:57 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:52:18 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:52:34 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +2025-08-22 22:52:41 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:53:03 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:53:19 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:53:26 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:53:47 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:54:03 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:54:10 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:54:31 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:54:47 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:54:54 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:55:15 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:55:32 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:55:39 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:55:59 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:56:16 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +2025-08-22 22:56:23 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:56:43 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:57:00 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:57:07 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:57:27 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:57:44 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:57:51 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:58:11 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:58:28 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 22:58:35 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:58:55 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:59:12 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +2025-08-22 22:59:19 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 22:59:39 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 22:59:56 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +2025-08-22 23:00:03 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 23:00:24 - STARTED: wp2782770-1846651530.jpg +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 23:00:40 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +2025-08-22 23:00:48 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[ERROR] [SignageApp] Failed to fetch playlist. Status Code: 522 +[INFO] [SignageApp] No playlist updates available +[INFO] [SignageApp] Playing media: wp2782770-1846651530.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/wp2782770-1846651530.jpg +2025-08-22 23:01:09 - STARTED: wp2782770-1846651530.jpg +[INFO] [SignageApp] Successfully displayed image: wp2782770-1846651530.jpg (Original: (3840, 2400), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] Playing media: SampleVideo_1280x720_1mb.mp4 from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +2025-08-22 23:01:26 - STARTED: SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] Starting system VLC subprocess for video: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] VLC subprocess finished: /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/SampleVideo_1280x720_1mb.mp4 +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Playing media: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg from /home/pi/Desktop/tkinter_player/tkinter_app/src/static/resurse/1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Fetching playlist from URL: http://digi-signage.moto-adv.com:8880/api/playlists with params: {'hostname': 'tv-terasa', 'quickconnect_code': '8887779'} +2025-08-22 23:01:33 - STARTED: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg +[INFO] [SignageApp] Successfully displayed image: 1307306470-nature_wallpaper_hd_hd_nature_3-3828209637.jpg (Original: (1600, 1000), Screen: 1920x1018, Mode: fit, Offset: (146, 0)) +[INFO] [SignageApp] python_functions: Starting load_config function. +[INFO] [SignageApp] python_functions: Configuration file loaded successfully. +[INFO] [SignageApp] Application exit requested diff --git a/tkinter_app/src/__pycache__/player_app.cpython-311.pyc b/tkinter_app/src/__pycache__/player_app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb336747f15c9c523ff2ca03c4cbac1d2f65a488 GIT binary patch literal 51471 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 literal 0 HcmV?d00001 diff --git a/tkinter_app/src/__pycache__/python_functions.cpython-311.pyc b/tkinter_app/src/__pycache__/python_functions.cpython-311.pyc index 4f0eddea01d91f1e3d1171aff09e42929e880a2f..02b58662d07071083848f5c59db14cce10db4117 100644 GIT binary patch delta 1019 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 R@$fR^V}AVaWzO=y?q8}L-GBf9 literal 0 HcmV?d00001 diff --git a/tkinter_app/src/main.py b/tkinter_app/src/main.py index 8657d4a..2adf221 100644 --- a/tkinter_app/src/main.py +++ b/tkinter_app/src/main.py @@ -9,10 +9,12 @@ import sys # Add the current directory to the path so we can import our modules sys.path.append(os.path.dirname(os.path.abspath(__file__))) -# Import the player module -from tkinter_simple_player import SimpleMediaPlayerApp + +# Import the player module from player_app.py +from player_app import SimpleMediaPlayerApp if __name__ == "__main__": - # Create and run the player - player = SimpleMediaPlayerApp() + import tkinter as tk + root = tk.Tk() + player = SimpleMediaPlayerApp(root) player.run() \ No newline at end of file diff --git a/tkinter_app/src/player_app.py b/tkinter_app/src/player_app.py index e69de29..8c9f100 100644 --- a/tkinter_app/src/player_app.py +++ b/tkinter_app/src/player_app.py @@ -0,0 +1,722 @@ +# 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.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() + + + 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('', self.on_key_press) + self.root.bind('', self.on_mouse_click) + self.root.bind('', self.on_mouse_motion) + self.root.focus_set() + + def setup_ui(self): + 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("", on_press) + button.bind("", on_release) + button.bind("", on_enter) + button.bind("", 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) + + 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('', lambda e: check_password()) + exit_dialog.bind('', 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 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}") + + diff --git a/tkinter_app/src/python_functions.py b/tkinter_app/src/python_functions.py index 279b46e..6250079 100644 --- a/tkinter_app/src/python_functions.py +++ b/tkinter_app/src/python_functions.py @@ -193,4 +193,19 @@ def update_config_playlist_version(version): json.dump(config_data, file, indent=4) Logger.info(f"python_functions: Updated playlist version in app_config.txt to {version}.") except (IOError, json.JSONDecodeError) as e: - Logger.error(f"python_functions: Failed to update playlist version in app_config.txt. Error: {e}") \ No newline at end of file + Logger.error(f"python_functions: Failed to update playlist version in app_config.txt. Error: {e}") + +def check_for_new_server_playlist(): + """Check if the server has a new playlist version compared to app_config.txt.""" + config = load_config() + local_version = config.get('playlist_version', 0) + server_data = fetch_server_playlist() + server_version = server_data.get('version', 0) + if server_version > local_version: + print(f"A new playlist is available on the server: version {server_version} (local: {local_version})") + Logger.info(f"A new playlist is available on the server: version {server_version} (local: {local_version})") + return True + else: + print(f"No new playlist on the server. Local version: {local_version}, Server version: {server_version}") + Logger.info(f"No new playlist on the server. Local version: {local_version}, Server version: {server_version}") + return False \ No newline at end of file diff --git a/tkinter_app/src/settings_screen.py b/tkinter_app/src/settings_screen.py index 1a47351..158c7a4 100644 --- a/tkinter_app/src/settings_screen.py +++ b/tkinter_app/src/settings_screen.py @@ -16,4 +16,1071 @@ from python_functions import ( ) from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton -# ...Paste the full SettingsWindow class here (from tkinter_simple_player.py)... +class SettingsWindow: + def __init__(self, parent, app): + self.parent = parent + self.app = app + self.window = tk.Toplevel(parent) + + # Initialize virtual keyboard for touch displays + self.virtual_keyboard = VirtualKeyboard(self.window, dark_theme=True) + + self.setup_window() + self.create_widgets() + self.load_config() + + # Setup touch optimization + self.setup_touch_optimization() + + def setup_window(self): + """Setup settings window with enhanced dark theme""" + self.window.title("🎬 Signage Player Settings") + self.window.geometry("900x700") + + # Enhanced dark theme colors + self.colors = { + 'bg_primary': '#1e2124', # Very dark background + 'bg_secondary': '#2f3136', # Slightly lighter background + 'bg_tertiary': '#36393f', # Card backgrounds + 'accent': '#7289da', # Discord-like blue accent + 'accent_hover': '#677bc4', # Darker accent for hover + 'success': '#43b581', # Green for success + 'warning': '#faa61a', # Orange for warnings + 'danger': '#f04747', # Red for errors + 'text_primary': '#ffffff', # White text + 'text_secondary': '#b9bbbe', # Gray text + 'text_muted': '#72767d', # Muted text + 'border': '#202225' # Border color + } + + self.window.configure(bg=self.colors['bg_primary']) + self.window.transient(self.parent) + self.window.grab_set() + + # Set window properties + self.window.resizable(True, True) + self.window.minsize(700, 500) + + # Set window icon if available + try: + # Try to use a simple emoji icon + self.window.iconname("🎬") + except: + pass + + # Center the window on screen using helper method + self.center_window_on_screen(self.window, 900, 700) + + # Add subtle window border effect + self.window.configure(highlightbackground=self.colors['border'], highlightthickness=1) + + def create_widgets(self): + """Create settings widgets with enhanced dark theme styling""" + # Configure enhanced custom styles + style = ttk.Style() + style.theme_use('clam') + # Enhanced dark theme styles + style.configure('Dark.TNotebook', + background=self.colors['bg_secondary'], + borderwidth=0, + tabmargins=[2, 5, 2, 0]) + style.configure('Dark.TNotebook.Tab', + padding=[20, 12], + font=('Segoe UI', 11, 'bold'), + background=self.colors['bg_tertiary'], + foreground=self.colors['text_secondary'], + borderwidth=1, + focuscolor='none') + style.map('Dark.TNotebook.Tab', + background=[('selected', self.colors['accent']), + ('active', self.colors['accent_hover'])], + foreground=[('selected', self.colors['text_primary']), + ('active', self.colors['text_primary'])]) + style.configure('Dark.TFrame', background=self.colors['bg_secondary']) + style.configure('Dark.TLabel', + background=self.colors['bg_secondary'], + foreground=self.colors['text_primary'], + font=('Segoe UI', 10)) + style.configure('Dark.TEntry', + fieldbackground=self.colors['bg_tertiary'], + foreground=self.colors['text_primary'], + bordercolor=self.colors['border'], + lightcolor=self.colors['bg_tertiary'], + darkcolor=self.colors['bg_tertiary'], + font=('Segoe UI', 10), + insertcolor=self.colors['text_primary']) + + # Main container frame + main_frame = tk.Frame(self.window, bg=self.colors['bg_primary']) + main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) + + # Header section with gradient-like effect + header_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'], height=80) + header_frame.pack(fill=tk.X, padx=0, pady=0) + header_frame.pack_propagate(False) + + # Title with enhanced styling + title_container = tk.Frame(header_frame, bg=self.colors['bg_secondary']) + title_container.pack(expand=True, fill=tk.BOTH) + + title_label = tk.Label(title_container, + text="🎬 Digital Signage Control Center", + font=('Segoe UI', 24, 'bold'), + fg=self.colors['text_primary'], + bg=self.colors['bg_secondary']) + title_label.pack(expand=True) + + subtitle_label = tk.Label(title_container, + text="Configure your digital signage display settings", + font=('Segoe UI', 11), + fg=self.colors['text_secondary'], + bg=self.colors['bg_secondary']) + subtitle_label.pack() + + # Content area + content_frame = tk.Frame(main_frame, bg=self.colors['bg_primary']) + content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) + + # Create enhanced notebook with dark theme + notebook = ttk.Notebook(content_frame, style='Dark.TNotebook') + notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 20)) + + # Create tabs with enhanced styling + self.create_connection_tab_enhanced(notebook) + self.create_display_tab_enhanced(notebook) + self.create_advanced_tab_enhanced(notebook) + self.create_logs_tab_enhanced(notebook) + self.create_about_tab_enhanced(notebook) + + # Enhanced bottom button section + self.create_bottom_controls(content_frame) + + # Load initial data + self.load_logs() + + def create_connection_tab_enhanced(self, notebook): + """Create enhanced connection settings tab""" + tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) + notebook.add(tab_frame, text="🌐 Connection") + + # Create scrollable content + canvas = tk.Canvas(tab_frame, bg=self.colors['bg_secondary'], highlightthickness=0) + scrollbar = tk.Scrollbar(tab_frame, orient="vertical", command=canvas.yview, + bg=self.colors['bg_tertiary'], troughcolor=self.colors['bg_primary']) + scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_secondary']) + + scrollable_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + # Server connection card + server_card = self.create_settings_card(scrollable_frame, "🖥️ Server Connection", + "Configure your signage server connection details") + + # Server IP/Domain + self.create_input_field(server_card, "Server IP/Domain:", "server_ip_var", + placeholder="e.g., digi-server.example.com") + + # Port + self.create_input_field(server_card, "Port:", "port_var", width=15, + placeholder="8880") + + # Device settings card + device_card = self.create_settings_card(scrollable_frame, "📱 Device Settings", + "Identify this display device") + + # Screen/Device name + self.create_input_field(device_card, "Device Name:", "screen_name_var", + placeholder="e.g., lobby-display-01") + + # QuickConnect key + self.create_input_field(device_card, "QuickConnect Key:", "quickconnect_var", + password=True, placeholder="Enter your access key") + + # Connection testing card + test_card = self.create_settings_card(scrollable_frame, "🔗 Connection Test", + "Test your server connection") + + test_btn_frame = tk.Frame(test_card, bg=self.colors['bg_tertiary']) + test_btn_frame.pack(fill=tk.X, pady=10) + + test_btn = self.create_action_button(test_btn_frame, "🔗 Test Connection", + self.test_connection, self.colors['accent']) + test_btn.pack(side=tk.LEFT, padx=5) + + canvas.pack(side="left", fill="both", expand=True, padx=20, pady=20) + scrollbar.pack(side="right", fill="y") + + def create_display_tab_enhanced(self, notebook): + """Create enhanced display settings tab""" + tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) + notebook.add(tab_frame, text="🖼️ Display") + + main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) + main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) + + # Resolution settings card + resolution_card = self.create_settings_card(main_container, "🖥️ Screen Resolution", + "Configure display resolution settings") + + # Resolution input fields + resolution_inputs = tk.Frame(resolution_card, bg=self.colors['bg_tertiary']) + resolution_inputs.pack(fill=tk.X, pady=10) + + self.create_input_field(resolution_inputs, "Width (px):", "screen_w_var", + width=15, placeholder="1920") + self.create_input_field(resolution_inputs, "Height (px):", "screen_h_var", + width=15, placeholder="1080") + + # Preset buttons with enhanced styling + presets_frame = tk.Frame(resolution_card, bg=self.colors['bg_tertiary']) + presets_frame.pack(fill=tk.X, pady=15) + + tk.Label(presets_frame, text="Quick Presets:", + font=('Segoe UI', 11, 'bold'), + bg=self.colors['bg_tertiary'], + fg=self.colors['text_primary']).pack(anchor=tk.W, pady=(0, 10)) + + preset_buttons = tk.Frame(presets_frame, bg=self.colors['bg_tertiary']) + preset_buttons.pack(anchor=tk.W) + + presets = [("📱 HD", "1366", "768"), ("💻 Full HD", "1920", "1080"), + ("🖥️ 4K", "3840", "2160"), ("📺 Classic", "1024", "768")] + + for preset_name, width, height in presets: + btn = self.create_preset_button(preset_buttons, preset_name, width, height) + btn.pack(side=tk.LEFT, padx=3, pady=2) + + # Scaling mode settings card + scaling_card = self.create_settings_card(main_container, "📐 Image/Video Scaling", + "Configure how images and videos are displayed on screen") + + # Scaling mode options + self.scaling_mode_var = tk.StringVar(value=self.app.scaling_mode if hasattr(self.app, 'scaling_mode') else 'fit') + + scaling_label = tk.Label(scaling_card, text="Scaling Mode:", + font=('Segoe UI', 12, 'bold'), + bg=self.colors['bg_tertiary'], + fg=self.colors['text_primary']) + scaling_label.pack(anchor=tk.W, pady=(0, 10)) + + scaling_options = tk.Frame(scaling_card, bg=self.colors['bg_tertiary']) + scaling_options.pack(fill=tk.X, pady=5) + + # Radio buttons for scaling modes + modes = [ + ("fit", "🖼️ Fit", "Maintain aspect ratio, add black bars if needed"), + ("fill", "🔍 Fill", "Maintain aspect ratio, crop to fill entire screen"), + ("stretch", "↔️ Stretch", "Ignore aspect ratio, stretch to fill screen") + ] + + for mode_value, mode_label, mode_desc in modes: + mode_frame = tk.Frame(scaling_options, bg=self.colors['bg_tertiary']) + mode_frame.pack(fill=tk.X, pady=2) + + radio_btn = tk.Radiobutton(mode_frame, + text=mode_label, + variable=self.scaling_mode_var, + value=mode_value, + bg=self.colors['bg_tertiary'], + fg=self.colors['text_primary'], + font=('Segoe UI', 11, 'bold'), + selectcolor=self.colors['bg_primary'], + activebackground=self.colors['bg_tertiary'], + activeforeground=self.colors['text_primary'], + command=lambda: self.update_scaling_mode()) + radio_btn.pack(side=tk.LEFT, anchor=tk.W) + + desc_label = tk.Label(mode_frame, text=f" - {mode_desc}", + font=('Segoe UI', 9), + bg=self.colors['bg_tertiary'], + fg=self.colors['text_secondary']) + desc_label.pack(side=tk.LEFT, anchor=tk.W, padx=(10, 0)) + + # Keyboard shortcuts info + shortcuts_frame = tk.Frame(scaling_card, bg=self.colors['bg_tertiary']) + shortcuts_frame.pack(fill=tk.X, pady=(15, 0)) + + shortcuts_label = tk.Label(shortcuts_frame, + text="💡 Tip: Use keyboard shortcuts 1, 2, 3 to quickly change scaling mode during playback", + font=('Segoe UI', 9, 'italic'), + bg=self.colors['bg_tertiary'], + fg=self.colors['text_muted'], + wraplength=400) + shortcuts_label.pack(anchor=tk.W) + + def update_scaling_mode(self): + """Update the scaling mode in the main app""" + new_mode = self.scaling_mode_var.get() + if hasattr(self.app, 'set_scaling_mode'): + self.app.set_scaling_mode(new_mode) + else: + self.app.scaling_mode = new_mode + + def create_advanced_tab_enhanced(self, notebook): + """Create enhanced advanced settings tab""" + tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) + notebook.add(tab_frame, text="⚙️ Advanced") + + main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) + main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) + + # Sync settings card + sync_card = self.create_settings_card(main_container, "🔄 Synchronization", + "Configure automatic content updates") + + self.create_input_field(sync_card, "Auto-refresh (minutes):", "refresh_interval_var", + width=15, placeholder="15") + + # Performance settings card + perf_card = self.create_settings_card(main_container, "⚡ Performance", + "Optimize playback performance") + + self.hardware_accel_var = tk.BooleanVar(value=True) + self.create_checkbox(perf_card, "Enable hardware acceleration", self.hardware_accel_var) + self.settings_window = SettingsWindow(self.root, self, dark_theme=True) + self.cache_media_var = tk.BooleanVar(value=True) + self.create_checkbox(perf_card, "Cache media files locally", self.cache_media_var) + + self.auto_retry_var = tk.BooleanVar(value=True) + self.create_checkbox(perf_card, "Auto-retry failed downloads", self.auto_retry_var) + + def create_logs_tab_enhanced(self, notebook): + """Create enhanced logs tab""" + tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) + notebook.add(tab_frame, text="📋 Logs") + + main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) + main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) + + # Log header + log_header = tk.Frame(main_container, bg=self.colors['bg_secondary']) + log_header.pack(fill=tk.X, pady=(0, 15)) + + tk.Label(log_header, text="📋 Application Logs", + font=('Segoe UI', 16, 'bold'), + bg=self.colors['bg_secondary'], + fg=self.colors['text_primary']).pack(side=tk.LEFT) + + refresh_btn = self.create_action_button(log_header, "🔄 Refresh Logs", + self.load_logs, self.colors['accent']) + refresh_btn.pack(side=tk.RIGHT) + + # Log display area + log_container = tk.Frame(main_container, bg=self.colors['bg_tertiary'], + relief=tk.FLAT, bd=2) + log_container.pack(fill=tk.BOTH, expand=True) + + self.log_text = tk.Text(log_container, + font=('Consolas', 9), + bg=self.colors['bg_primary'], + fg=self.colors['text_primary'], + insertbackground=self.colors['accent'], + selectbackground=self.colors['accent'], + selectforeground=self.colors['text_primary'], + relief=tk.FLAT, + bd=10, + wrap=tk.WORD) + + log_scrollbar = tk.Scrollbar(log_container, command=self.log_text.yview, + bg=self.colors['bg_tertiary']) + self.log_text.configure(yscrollcommand=log_scrollbar.set) + + self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + def create_about_tab_enhanced(self, notebook): + """Create enhanced about tab""" + tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) + notebook.add(tab_frame, text="ℹ️ About") + + main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) + main_container.pack(fill=tk.BOTH, expand=True, padx=40, pady=40) + + # App branding section + branding_frame = tk.Frame(main_container, bg=self.colors['bg_secondary']) + branding_frame.pack(fill=tk.X, pady=(0, 40)) + + # Large app icon + icon_label = tk.Label(branding_frame, text="🎬", + font=('Segoe UI Emoji', 64), + bg=self.colors['bg_secondary'], + fg=self.colors['accent']) + icon_label.pack() + + # App title and version + title_label = tk.Label(branding_frame, text="Digital Signage Player", + font=('Segoe UI', 24, 'bold'), + bg=self.colors['bg_secondary'], + fg=self.colors['text_primary']) + title_label.pack(pady=(10, 5)) + + version_label = tk.Label(branding_frame, text="Version 2.1 - Enhanced Dark Edition", + font=('Segoe UI', 12), + bg=self.colors['bg_secondary'], + fg=self.colors['text_secondary']) + version_label.pack() + + # Feature highlights + features_text = ( + "🚀 Features:\n" + "• Modern dark theme interface\n" + "• High-resolution media display\n" + "• Remote playlist management\n" + "• Real-time content synchronization\n" + "• Hardware acceleration support\n" + "• Cross-platform compatibility\n" + "• Advanced logging and diagnostics\n\n" + "⌨️ Keyboard Shortcuts:\n" + "F11 - Toggle fullscreen mode\n" + "Space - Play/Pause media\n" + "← → - Navigate between media\n" + "1 - Set scaling mode to Fit (maintain aspect ratio with black bars)\n" + "2 - Set scaling mode to Fill (maintain aspect ratio, crop to fill screen)\n" + "3 - Set scaling mode to Stretch (ignore aspect ratio, stretch to fill)\n" + "Ctrl+S - Open settings panel\n" + "Escape - Exit application (password protected)\n\n" + "Built with ❤️ using Python & Tkinter" + ) + + features_label = tk.Label(main_container, text=features_text, + justify=tk.LEFT, + font=('Segoe UI', 11), + bg=self.colors['bg_secondary'], + fg=self.colors['text_primary'], + wraplength=600) + features_label.pack(anchor=tk.W) + + def create_bottom_controls(self, parent): + """Create enhanced bottom control buttons""" + controls_frame = tk.Frame(parent, bg=self.colors['bg_secondary'], + relief=tk.FLAT, bd=1) + controls_frame.pack(fill=tk.X, pady=(10, 0)) + + # Left side buttons + left_buttons = tk.Frame(controls_frame, bg=self.colors['bg_secondary']) + left_buttons.pack(side=tk.LEFT, padx=10, pady=15) + + save_btn = self.create_action_button(left_buttons, "💾 Save Configuration", + self.save_config, self.colors['success']) + save_btn.pack(side=tk.LEFT, padx=5) + + test_btn = self.create_action_button(left_buttons, "🔗 Test Connection", + self.test_connection, self.colors['accent']) + test_btn.pack(side=tk.LEFT, padx=5) + + refresh_btn = self.create_action_button(left_buttons, "🔄 Refresh Playlist", + self.force_playlist_refresh, self.colors['warning']) + refresh_btn.pack(side=tk.LEFT, padx=5) + + # Right side buttons + right_buttons = tk.Frame(controls_frame, bg=self.colors['bg_secondary']) + right_buttons.pack(side=tk.RIGHT, padx=10, pady=15) + + cancel_btn = self.create_action_button(right_buttons, "❌ Cancel", + self.window.destroy, self.colors['danger']) + cancel_btn.pack(side=tk.RIGHT, padx=5) + + # Status display + self.status_frame = tk.Frame(parent, bg=self.colors['bg_primary']) + self.status_frame.pack(fill=tk.X, pady=(10, 0)) + + self.connection_status = tk.Label(self.status_frame, + text="Ready to configure your digital signage settings", + bg=self.colors['bg_primary'], + fg=self.colors['text_secondary'], + font=('Segoe UI', 9)) + self.connection_status.pack(anchor=tk.W, padx=10, pady=5) + + def create_settings_card(self, parent, title, description): + """Create a settings card with enhanced styling""" + card_frame = tk.Frame(parent, bg=self.colors['bg_tertiary'], + relief=tk.FLAT, bd=2) + card_frame.pack(fill=tk.X, pady=(0, 20), padx=5) + + # Card header + header_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary']) + header_frame.pack(fill=tk.X, padx=20, pady=(15, 5)) + + title_label = tk.Label(header_frame, text=title, + font=('Segoe UI', 14, 'bold'), + bg=self.colors['bg_tertiary'], + fg=self.colors['text_primary']) + title_label.pack(anchor=tk.W) + + desc_label = tk.Label(header_frame, text=description, + font=('Segoe UI', 9), + bg=self.colors['bg_tertiary'], + fg=self.colors['text_secondary']) + desc_label.pack(anchor=tk.W, pady=(2, 0)) + + # Card content area + content_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary']) + content_frame.pack(fill=tk.X, padx=20, pady=(10, 15)) + + return content_frame + + def create_input_field(self, parent, label_text, var_name, width=35, placeholder="", password=False): + """Create a touch-optimized input field with virtual keyboard support""" + field_frame = tk.Frame(parent, bg=self.colors['bg_tertiary']) + field_frame.pack(fill=tk.X, pady=12) # Increased padding for touch + + label = tk.Label(field_frame, text=label_text, + font=('Segoe UI', 12, 'bold'), # Larger font for touch + bg=self.colors['bg_tertiary'], + fg=self.colors['text_primary'], + width=18, anchor='w') + label.pack(side=tk.LEFT) + + # Create StringVar if it doesn't exist + if not hasattr(self, var_name): + setattr(self, var_name, tk.StringVar()) + + var = getattr(self, var_name) + + # Use touch-optimized entry with virtual keyboard + entry = TouchOptimizedEntry(field_frame, + virtual_keyboard=self.virtual_keyboard, + textvariable=var, + width=width, + font=('Segoe UI', 12), # Larger font + bg=self.colors['bg_primary'], + fg=self.colors['text_primary'], + insertbackground=self.colors['accent'], + relief=tk.FLAT, + bd=10) # Larger border for easier touch + + if password: + entry.configure(show='*') + + entry.pack(side=tk.LEFT, padx=(15, 0), pady=5) + + # Add placeholder text effect + if placeholder: + self.add_placeholder(entry, placeholder) + + return entry + + def create_checkbox(self, parent, text, variable): + """Create a checkbox with dark theme styling""" + cb_frame = tk.Frame(parent, bg=self.colors['bg_tertiary']) + cb_frame.pack(fill=tk.X, pady=5) + + checkbox = tk.Checkbutton(cb_frame, text=text, variable=variable, + bg=self.colors['bg_tertiary'], + fg=self.colors['text_primary'], + font=('Segoe UI', 10), + selectcolor=self.colors['bg_primary'], + activebackground=self.colors['bg_tertiary'], + activeforeground=self.colors['text_primary']) + checkbox.pack(anchor=tk.W, padx=5) + + def create_action_button(self, parent, text, command, color): + """Create a touch-optimized action button with enhanced styling""" + button = TouchOptimizedButton(parent, + text=text, + command=command, + bg=color, + fg=self.colors['text_primary'], + font=('Segoe UI', 12, 'bold'), # Larger font + relief=tk.FLAT, + padx=25, # Larger padding for touch + pady=15, # Larger padding for touch + cursor='hand2', + bd=3) + + # Add enhanced hover effects for touch feedback + def on_enter(e): + button.configure(bg=self.lighten_color(color), relief=tk.RAISED) + + def on_leave(e): + button.configure(bg=color, relief=tk.FLAT) + + def on_press(e): + button.configure(relief=tk.SUNKEN) + + def on_release(e): + button.configure(relief=tk.FLAT) + + button.bind("", on_enter) + button.bind("", on_leave) + button.bind("", on_press) + button.bind("", on_release) + + return button + + def create_preset_button(self, parent, text, width, height): + """Create a touch-optimized preset resolution button""" + button = TouchOptimizedButton(parent, + text=text, + command=lambda: self.set_resolution_preset(width, height), + bg=self.colors['bg_primary'], + fg=self.colors['text_primary'], + font=('Segoe UI', 10, 'bold'), + relief=tk.FLAT, + padx=20, # Larger for touch + pady=10, # Larger for touch + cursor='hand2') + + def on_enter(e): + button.configure(bg=self.colors['accent'], relief=tk.RAISED) + + def on_leave(e): + button.configure(bg=self.colors['bg_primary'], relief=tk.FLAT) + + def on_press(e): + button.configure(relief=tk.SUNKEN) + + def on_release(e): + button.configure(relief=tk.FLAT) + + button.bind("", on_enter) + button.bind("", on_leave) + button.bind("", on_press) + button.bind("", on_release) + + return button + + def add_placeholder(self, entry, placeholder_text): + """Add placeholder text to entry widget""" + entry.insert(0, placeholder_text) + entry.configure(fg=self.colors['text_muted']) + + def on_focus_in(event): + if entry.get() == placeholder_text: + entry.delete(0, tk.END) + entry.configure(fg=self.colors['text_primary']) + + def on_focus_out(event): + if not entry.get(): + entry.insert(0, placeholder_text) + entry.configure(fg=self.colors['text_muted']) + + entry.bind('', on_focus_in) + entry.bind('', on_focus_out) + + def lighten_color(self, color): + """Lighten a hex color for hover effects""" + color = color.lstrip('#') + rgb = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) + lighter_rgb = tuple(min(255, int(c * 1.2)) for c in rgb) + return f"#{lighter_rgb[0]:02x}{lighter_rgb[1]:02x}{lighter_rgb[2]:02x}" + + def center_window_on_screen(self, window, width, height): + """Center a window on screen regardless of screen size""" + window.update_idletasks() # Ensure geometry is calculated + screen_width = window.winfo_screenwidth() + screen_height = window.winfo_screenheight() + + # Calculate center position + center_x = int((screen_width - width) / 2) + center_y = int((screen_height - height) / 2) + + # Ensure the window doesn't go off-screen on smaller displays + center_x = max(0, min(center_x, screen_width - width)) + center_y = max(0, min(center_y, screen_height - height)) + + window.geometry(f"{width}x{height}+{center_x}+{center_y}") + + # Bring to front and focus + window.lift() + window.focus_force() + + return center_x, center_y + + def setup_touch_optimization(self): + """Setup touch-friendly optimizations""" + # Hide virtual keyboard when clicking outside input fields + def hide_keyboard_on_click(event): + # Check if click is not on an entry widget + if not isinstance(event.widget, (tk.Entry, TouchOptimizedEntry)): + self.virtual_keyboard.hide_keyboard() + + self.window.bind("", hide_keyboard_on_click, "+") + + # Make window touch-friendly by increasing minimum size + self.window.minsize(800, 600) + + # Override window close to hide keyboard first + original_destroy = self.window.destroy + def enhanced_destroy(): + self.virtual_keyboard.hide_keyboard() + original_destroy() + + self.window.destroy = enhanced_destroy + self.window.protocol("WM_DELETE_WINDOW", enhanced_destroy) + + def set_resolution_preset(self, width, height): + """Set resolution to preset values with visual feedback""" + self.screen_w_var.set(width) + self.screen_h_var.set(height) + self.connection_status.configure( + text=f"✅ Resolution preset applied: {width}x{height}", + fg=self.colors['success'] + ) + + # Reset status color after 3 seconds + self.window.after(3000, lambda: self.connection_status.configure( + fg=self.colors['text_secondary'] + )) + + def load_config(self): + """Load current configuration with enhanced feedback""" + try: + Logger.info("Loading configuration in enhanced settings window") + config = load_config() + Logger.info(f"Config loaded: {config}") + + # Set values for connection settings + if hasattr(self, 'screen_name_var'): + self.screen_name_var.set(config.get('screen_name', '')) + if hasattr(self, 'server_ip_var'): + self.server_ip_var.set(config.get('server_ip', '')) + if hasattr(self, 'port_var'): + self.port_var.set(config.get('port', '8880')) + if hasattr(self, 'quickconnect_var'): + self.quickconnect_var.set(config.get('quickconnect_key', '')) + + # Set values for display settings + if hasattr(self, 'screen_w_var'): + self.screen_w_var.set(config.get('screen_w', '1920')) + if hasattr(self, 'screen_h_var'): + self.screen_h_var.set(config.get('screen_h', '1080')) + if hasattr(self, 'scaling_mode_var'): + self.scaling_mode_var.set(config.get('scaling_mode', 'fit')) + + # Set values for advanced settings + if hasattr(self, 'refresh_interval_var'): + self.refresh_interval_var.set(config.get('refresh_interval', '15')) + if hasattr(self, 'hardware_accel_var'): + self.hardware_accel_var.set(config.get('hardware_acceleration', True)) + if hasattr(self, 'cache_media_var'): + self.cache_media_var.set(config.get('cache_media', True)) + if hasattr(self, 'auto_retry_var'): + self.auto_retry_var.set(config.get('auto_retry', True)) + + # Update status with success message + if hasattr(self, 'connection_status'): + self.connection_status.configure( + text="✅ Configuration loaded successfully", + fg=self.colors['success'] + ) + # Reset status color after 3 seconds + self.window.after(3000, lambda: self.connection_status.configure( + fg=self.colors['text_secondary'] + )) + + Logger.info("Configuration values loaded successfully in enhanced settings") + + except Exception as e: + Logger.error(f"Failed to load config in enhanced settings: {e}") + + # Set default values if loading fails + if hasattr(self, 'screen_name_var'): + self.screen_name_var.set('') + if hasattr(self, 'server_ip_var'): + self.server_ip_var.set('') + if hasattr(self, 'port_var'): + self.port_var.set('8880') + if hasattr(self, 'quickconnect_var'): + self.quickconnect_var.set('') + if hasattr(self, 'screen_w_var'): + self.screen_w_var.set('1920') + if hasattr(self, 'screen_h_var'): + self.screen_h_var.set('1080') + + # Set advanced defaults + if hasattr(self, 'refresh_interval_var'): + self.refresh_interval_var.set('15') + if hasattr(self, 'hardware_accel_var'): + self.hardware_accel_var.set(True) + if hasattr(self, 'cache_media_var'): + self.cache_media_var.set(True) + if hasattr(self, 'auto_retry_var'): + self.auto_retry_var.set(True) + + # Show error message with enhanced styling + if hasattr(self, 'connection_status'): + self.connection_status.configure( + text=f"⚠️ Warning: Could not load existing config: {str(e)[:50]}...", + fg=self.colors['warning'] + ) + + def save_config(self): + """Save configuration with enhanced feedback""" + try: + # Load existing config or create new one + try: + config = load_config() + except: + config = {} + + # Update with new values from the enhanced interface + if hasattr(self, 'screen_name_var'): + config['screen_name'] = self.screen_name_var.get() + if hasattr(self, 'server_ip_var'): + config['server_ip'] = self.server_ip_var.get() + if hasattr(self, 'port_var'): + config['port'] = self.port_var.get() + if hasattr(self, 'quickconnect_var'): + config['quickconnect_key'] = self.quickconnect_var.get() + if hasattr(self, 'screen_w_var'): + config['screen_w'] = self.screen_w_var.get() + if hasattr(self, 'screen_h_var'): + config['screen_h'] = self.screen_h_var.get() + + # Save advanced settings if they exist + if hasattr(self, 'refresh_interval_var'): + config['refresh_interval'] = self.refresh_interval_var.get() + if hasattr(self, 'hardware_accel_var'): + config['hardware_acceleration'] = self.hardware_accel_var.get() + if hasattr(self, 'cache_media_var'): + config['cache_media'] = self.cache_media_var.get() + if hasattr(self, 'auto_retry_var'): + config['auto_retry'] = self.auto_retry_var.get() + + # Save display settings + if hasattr(self, 'scaling_mode_var'): + config['scaling_mode'] = self.scaling_mode_var.get() + # Also update the main app's scaling mode + if hasattr(self.app, 'scaling_mode'): + self.app.scaling_mode = self.scaling_mode_var.get() + + # Ensure directory exists + config_dir = os.path.dirname(CONFIG_FILE) + os.makedirs(config_dir, exist_ok=True) + + with open(CONFIG_FILE, 'w') as f: + json.dump(config, f, indent=4) + + Logger.info(f"Enhanced configuration saved to {CONFIG_FILE}") + + # Show enhanced success message + self.show_enhanced_success_message("Configuration saved successfully!") + + # Ask if user wants to refresh playlist now + if messagebox.askyesno("Refresh Playlist", + "Configuration saved! Would you like to refresh the playlist from server now?", + icon='question'): + self.force_playlist_refresh() + + self.window.destroy() + + except Exception as e: + Logger.error(f"Failed to save enhanced configuration: {e}") + self.show_enhanced_error_message(f"Failed to save configuration: {e}") + + def show_enhanced_success_message(self, message): + """Show an enhanced success message with dark theme""" + success_window = tk.Toplevel(self.window) + success_window.title("Success") + success_window.geometry("400x150") + success_window.configure(bg=self.colors['bg_primary']) + success_window.transient(self.window) + success_window.grab_set() + success_window.resizable(False, False) + + # Center the window using helper method + self.center_window_on_screen(success_window, 400, 150) + + # Main frame + main_frame = tk.Frame(success_window, bg=self.colors['bg_primary'], padx=30, pady=20) + main_frame.pack(fill=tk.BOTH, expand=True) + + # Success icon + icon_label = tk.Label(main_frame, text="✅", font=('Segoe UI Emoji', 32), + fg=self.colors['success'], bg=self.colors['bg_primary']) + icon_label.pack(pady=(0, 10)) + + # Success message + msg_label = tk.Label(main_frame, text=message, font=('Segoe UI', 12, 'bold'), + fg=self.colors['text_primary'], bg=self.colors['bg_primary'], + wraplength=340, justify=tk.CENTER) + msg_label.pack(pady=(0, 20)) + + # OK button + ok_btn = self.create_action_button(main_frame, "OK", success_window.destroy, + self.colors['success']) + ok_btn.pack() + + # Auto-close after 3 seconds + success_window.after(3000, success_window.destroy) + + def show_enhanced_error_message(self, message): + """Show an enhanced error message with dark theme""" + error_window = tk.Toplevel(self.window) + error_window.title("Error") + error_window.geometry("400x150") + error_window.configure(bg=self.colors['bg_primary']) + error_window.transient(self.window) + error_window.grab_set() + error_window.resizable(False, False) + + # Center the window using helper method + self.center_window_on_screen(error_window, 400, 150) + + # Main frame + main_frame = tk.Frame(error_window, bg=self.colors['bg_primary'], padx=30, pady=20) + main_frame.pack(fill=tk.BOTH, expand=True) + + # Error icon + icon_label = tk.Label(main_frame, text="❌", font=('Segoe UI Emoji', 32), + fg=self.colors['danger'], bg=self.colors['bg_primary']) + icon_label.pack(pady=(0, 10)) + + # Error message + msg_label = tk.Label(main_frame, text=message, font=('Segoe UI', 11), + fg=self.colors['text_primary'], bg=self.colors['bg_primary'], + wraplength=340, justify=tk.CENTER) + msg_label.pack(pady=(0, 20)) + + # OK button + ok_btn = self.create_action_button(main_frame, "OK", error_window.destroy, + self.colors['danger']) + ok_btn.pack() + + def show_success_message(self, message): + """Show a modern success message""" + success_window = tk.Toplevel(self.window) + success_window.title("Success") + success_window.geometry("300x120") + success_window.configure(bg='#2d5a2d') + success_window.transient(self.window) + success_window.grab_set() + + # Center the window using helper method + self.center_window_on_screen(success_window, 300, 120) + + # Success icon and message + icon_label = tk.Label(success_window, text="✓", font=('Arial', 24, 'bold'), + fg='white', bg='#2d5a2d') + icon_label.pack(pady=10) + + msg_label = tk.Label(success_window, text=message, font=('Arial', 10), + fg='white', bg='#2d5a2d', wraplength=250) + msg_label.pack(pady=5) + + ok_btn = tk.Button(success_window, text="OK", command=success_window.destroy, + bg='#4d7d4d', fg='white', font=('Arial', 10, 'bold'), + relief=tk.FLAT, padx=20, pady=5) + ok_btn.pack(pady=10) + + def force_playlist_refresh(self): + """Force refresh of playlist from server""" + try: + # Show connection message + self.connection_status.configure(text="Refreshing playlist from server...") + self.window.update() + + # Fetch server playlist + server_playlist_data = fetch_server_playlist() + server_playlist = server_playlist_data.get('playlist', []) + server_version = server_playlist_data.get('version', 0) + + if server_playlist: + # Clean old files + local_playlist_data = load_local_playlist() + clean_unused_files(local_playlist_data.get('playlist', [])) + + # Download new content + download_media_files(server_playlist, server_version) + update_config_playlist_version(server_version) + + # Let the app know to reload + self.app.playlist = load_local_playlist().get('playlist', []) + self.app.current_index = 0 # Reset to beginning + + self.connection_status.configure(text=f"Playlist refreshed! Downloaded {len(server_playlist)} media files.") + + # Force the app to play the current media + if self.app.playlist: + self.app.play_current_media() + else: + self.connection_status.configure(text="Server returned empty playlist. Check server connection settings.") + + except Exception as e: + self.connection_status.configure(text=f"Error refreshing playlist: {str(e)[:50]}...") + Logger.error(f"Failed to refresh playlist: {e}") + + def test_connection(self): + """Test server connection""" + try: + self.connection_status.configure(text="Testing connection...") + self.window.update() + + server_ip = self.server_ip_var.get() + port = self.port_var.get() + screen_name = self.screen_name_var.get() + quickconnect = self.quickconnect_var.get() + + if not all([server_ip, port, screen_name, quickconnect]): + self.connection_status.configure(text="Please fill all connection fields") + return + + url = f"http://{server_ip}:{port}/api/playlists" + params = { + 'hostname': screen_name, + 'quickconnect_code': quickconnect + } + + response = requests.get(url, params=params, timeout=10) + + if response.status_code == 200: + data = response.json() + version = data.get('playlist_version', 'Unknown') + num_items = len(data.get('playlist', [])) + self.connection_status.configure( + text=f"✓ Connected! Server playlist version: {version}, {num_items} media items available" + ) + else: + self.connection_status.configure( + text=f"✗ Connection failed (Status: {response.status_code})" + ) + + except Exception as e: + self.connection_status.configure(text=f"✗ Connection error: {str(e)[:50]}...") + + def load_logs(self): + """Load recent log entries""" + try: + # Update to use the resources directory + log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt') + + if os.path.exists(log_file): + with open(log_file, 'r') as f: + lines = f.readlines() + + # Show last 20 lines + recent_lines = lines[-20:] if len(lines) > 20 else lines + + self.log_text.delete(1.0, tk.END) + self.log_text.insert(tk.END, ''.join(recent_lines)) + self.log_text.see(tk.END) + else: + self.log_text.delete(1.0, tk.END) + self.log_text.insert(tk.END, "No log file found") + + except Exception as e: + self.log_text.delete(1.0, tk.END) + self.log_text.insert(tk.END, f"Error loading logs: {e}") + + def load_playlist_view(self): + """Load playlist items into treeview""" + try: + # Clear existing items + for item in self.playlist_view.get_children(): + self.playlist_view.delete(item) + except Exception as e: + Logger.error(f"Failed to load playlist view: {e}") + messagebox.showerror("Error", f"Failed to load playlist: {e}") + + def show_video_placeholder(self, filename, duration): + """Show placeholder for video files""" + self.image_label.config(image='') + self.status_label.config(text="") # Clear any status text for cleaner display + + # Schedule next media + self.auto_advance_timer = self.root.after( + int(duration * 1000), + self.next_media + ) diff --git a/tkinter_app/src/tkinter_simple_player.py b/tkinter_app/src/tkinter_simple_player.py deleted file mode 100644 index e0760d0..0000000 --- a/tkinter_app/src/tkinter_simple_player.py +++ /dev/null @@ -1,2048 +0,0 @@ -#!/usr/bin/env python3 -""" -Tkinter Simple Media Player - A lightweight version with minimal dependencies -Features: -- Image display -- Basic playlist management -- Settings configuration -- Auto-hiding controls -- Touch display optimization with virtual keyboard -""" - -import tkinter as tk -from tkinter import ttk, messagebox, simpledialog -import threading -import time -import os -import json -import datetime -from pathlib import Path -import subprocess -import sys -import requests # Required for server communication -import queue -import vlc # For video playback with hardware acceleration - -# Try importing PIL but provide fallback -try: - from PIL import Image, ImageTk - PIL_AVAILABLE = True -except ImportError: - PIL_AVAILABLE = False - print("WARNING: PIL not available. Image display functionality will be limited.") - -# Import existing functions -from python_functions import ( - load_local_playlist, download_media_files, clean_unused_files, - save_local_playlist, update_config_playlist_version, fetch_server_playlist, - load_config -) -from logging_config import Logger - -# Import virtual keyboard components -from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton - -# Update the config file path to use the resources directory -CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt') - -class SimpleMediaPlayerApp: - def __init__(self): - self.root = tk.Tk() - self.setup_window() - - # Media player state - self.playlist = [] - self.current_index = 0 - self.is_paused = False - self.is_fullscreen = True - self.auto_advance_timer = None - self.hide_controls_timer = None - self.video_thread = None - self.update_queue = queue.Queue() - - # UI Elements - self.image_label = None - self.status_label = None - self.control_frame = None - self.settings_window = None - - # Running state - self.running = True - - # Display scaling mode ('fit', 'fill', 'stretch') - self.scaling_mode = 'fit' # Default to fit (maintain aspect ratio with black bars) - - # VLC will be used for video/audio playback (no pygame needed) - - self.setup_ui() - - # Initialize from server - self.initialize_playlist_from_server() - - # Start periodic playlist checks - self.start_periodic_checks() - - def setup_window(self): - """Configure the main window""" - self.root.title("Simple Signage Player") - self.root.configure(bg='black') - - # Load window size from config - try: - config = load_config() - width = int(config.get('screen_w', 1920)) - height = int(config.get('screen_h', 1080)) - # Load scaling mode preference - self.scaling_mode = config.get('scaling_mode', 'fit') - except: - width, height = 800, 600 # Fallback size if config fails - self.scaling_mode = 'fit' # Default scaling mode - - self.root.geometry(f"{width}x{height}") - self.root.attributes('-fullscreen', True) - - # Bind events - self.root.bind('', self.on_key_press) - self.root.bind('', self.on_mouse_click) - self.root.bind('', self.on_mouse_motion) - self.root.focus_set() - - def setup_ui(self): - """Create the user interface""" - # Main content area - make sure it's black and fill the entire window - self.content_frame = tk.Frame(self.root, bg='black') - self.content_frame.pack(fill=tk.BOTH, expand=True) - - # Image display - expand to fill the entire window - self.image_label = tk.Label(self.content_frame, bg='black') - self.image_label.pack(fill=tk.BOTH, expand=True) - - # Status label - hidden by default - self.status_label = tk.Label( - self.content_frame, - bg='black', - fg='white', - font=('Arial', 24), - text="" - ) - # Don't place the status label by default to keep it hidden - - # Control panel - self.create_control_panel() - self.show_controls() - self.schedule_hide_controls() - - def create_control_panel(self): - """Create touch-optimized control panel with larger buttons""" - # Create control frame with larger size for touch - self.control_frame = tk.Frame( - self.root, - bg='#1a1a1a', # Dark background - bd=2, - relief=tk.RAISED, - padx=15, - pady=15 - ) - self.control_frame.place(relx=0.98, rely=0.98, anchor='se') - - # Touch-optimized button configuration - button_config = { - 'bg': '#333333', - 'fg': 'white', - 'activebackground': '#555555', - 'activeforeground': 'white', - 'relief': tk.FLAT, - 'borderwidth': 0, - 'width': 10, # Larger for touch - 'height': 3, # Larger for touch - 'font': ('Segoe UI', 10, 'bold'), # Larger font - 'cursor': 'hand2' - } - - # Previous button - self.prev_btn = tk.Button( - self.control_frame, - text="⏮ Prev", - command=self.previous_media, - **button_config - ) - self.prev_btn.grid(row=0, column=0, padx=5) - - # Play/Pause button - self.play_pause_btn = tk.Button( - self.control_frame, - text="⏸ Pause" if not self.is_paused else "▶ Play", - command=self.toggle_play_pause, - bg='#27ae60', # Green for play/pause - activebackground='#35d974', - **{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']} - ) - self.play_pause_btn.grid(row=0, column=1, padx=5) - - # Next button - self.next_btn = tk.Button( - self.control_frame, - text="Next ⏭", - command=self.next_media, - **button_config - ) - self.next_btn.grid(row=0, column=2, padx=5) - - # Settings button - self.settings_btn = tk.Button( - self.control_frame, - text="⚙️ Settings", - command=self.open_settings, - bg='#9b59b6', # Purple for settings - activebackground='#bb8fce', - **{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']} - ) - self.settings_btn.grid(row=0, column=3, padx=5) - - # Exit button with special styling - self.exit_btn = tk.Button( - self.control_frame, - text="❌ EXIT", - command=self.show_exit_dialog, - bg='#e74c3c', # Red background - fg='white', - activebackground='#ec7063', - activeforeground='white', - relief=tk.FLAT, - borderwidth=0, - width=8, - height=3, - font=('Segoe UI', 10, 'bold'), - cursor='hand2' - ) - self.exit_btn.grid(row=0, column=4, padx=5) - - # Add touch feedback to all buttons - for button in [self.prev_btn, self.play_pause_btn, self.next_btn, - self.settings_btn, self.exit_btn]: - self.add_touch_feedback_to_control_button(button) - - def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'): - """ - Scale image to screen with different modes: - - 'fit': Maintain aspect ratio, add black bars if needed (letterbox/pillarbox) - - 'fill': Maintain aspect ratio, crop if needed to fill entire screen - - 'stretch': Ignore aspect ratio, stretch to fill entire screen - """ - img_width, img_height = img.size - - if mode == 'stretch': - # Stretch to fill entire screen, ignoring aspect ratio - return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0) - - elif mode == 'fill': - # Maintain aspect ratio, crop to fill entire screen - screen_ratio = screen_width / screen_height - img_ratio = img_width / img_height - - if img_ratio > screen_ratio: - # Image is wider - scale by height and crop width - new_height = screen_height - new_width = int(screen_height * img_ratio) - x_offset = (screen_width - new_width) // 2 - y_offset = 0 - else: - # Image is taller - scale by width and crop height - new_width = screen_width - new_height = int(screen_width / img_ratio) - x_offset = 0 - y_offset = (screen_height - new_height) // 2 - - # Resize and crop - img_resized = img.resize((new_width, new_height), Image.LANCZOS) - - # Create final image and paste (this will crop automatically) - final_img = Image.new('RGB', (screen_width, screen_height), 'black') - - # Calculate crop area if image is larger than screen - if new_width > screen_width: - crop_x = (new_width - screen_width) // 2 - img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height)) - x_offset = 0 - if new_height > screen_height: - crop_y = (new_height - screen_height) // 2 - img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height)) - y_offset = 0 - - final_img.paste(img_resized, (x_offset, y_offset)) - return final_img, (x_offset, y_offset) - - else: # mode == 'fit' (default) - # Maintain aspect ratio, add black bars if needed - screen_ratio = screen_width / screen_height - img_ratio = img_width / img_height - - if img_ratio > screen_ratio: - # Image is wider than screen - fit to width - new_width = screen_width - new_height = int(screen_width / img_ratio) - else: - # Image is taller than screen - fit to height - new_height = screen_height - new_width = int(screen_height * img_ratio) - - # Resize image - img_resized = img.resize((new_width, new_height), Image.LANCZOS) - - # Create black background and center the image - final_img = Image.new('RGB', (screen_width, screen_height), 'black') - x_offset = (screen_width - new_width) // 2 - y_offset = (screen_height - new_height) // 2 - final_img.paste(img_resized, (x_offset, y_offset)) - - return final_img, (x_offset, y_offset) - - def add_touch_feedback_to_control_button(self, button): - """Add touch feedback effects to control panel buttons""" - original_bg = button.cget('bg') - - def on_press(e): - button.configure(relief=tk.SUNKEN) - - def on_release(e): - button.configure(relief=tk.FLAT) - - def on_enter(e): - button.configure(relief=tk.RAISED) - - def on_leave(e): - button.configure(relief=tk.FLAT) - - button.bind("", on_press) - button.bind("", on_release) - button.bind("", on_enter) - button.bind("", on_leave) - - def initialize_playlist_from_server(self): - """Initialize the playlist from the server on startup with fallback to local playlist""" - # First try to load any existing local playlist as fallback - fallback_playlist = None - try: - local_playlist_data = load_local_playlist() - fallback_playlist = local_playlist_data.get('playlist', []) - if fallback_playlist: - Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items") - except Exception as e: - Logger.warning(f"No fallback playlist available: {e}") - - # Show connection status - self.status_label.config(text="Connecting to server...\nPlease wait") - self.status_label.place(relx=0.5, rely=0.5, anchor='center') - self.root.update() - - # Load configuration - config = load_config() - server = config.get("server_ip", "") - host = config.get("screen_name", "") - quick = config.get("quickconnect_key", "") - port = config.get("port", "") - - Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}") - - if not server or not host or not quick or not port: - Logger.warning("Missing server configuration, using fallback playlist") - self.status_label.place_forget() - self.load_fallback_playlist(fallback_playlist) - return - - # Attempt to fetch server playlist with timeout - server_connection_successful = False - try: - # Add connection timeout and retry logic - Logger.info("Attempting to connect to server...") - self.status_label.config(text="Connecting to server...\nAttempting connection") - self.root.update() - - server_playlist_data = fetch_server_playlist() - server_playlist = server_playlist_data.get('playlist', []) - server_version = server_playlist_data.get('version', 0) - - if server_playlist: - Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}") - server_connection_successful = True - - # Download media files and update local playlist - self.status_label.config(text="Downloading media files...\nPlease wait") - self.root.update() - - download_media_files(server_playlist, server_version) - update_config_playlist_version(server_version) - - # Load the updated local playlist - local_playlist_data = load_local_playlist() - self.playlist = local_playlist_data.get('playlist', []) - - if self.playlist: - Logger.info(f"Successfully loaded {len(self.playlist)} items from server") - self.status_label.place_forget() - self.play_current_media() - return - else: - Logger.warning("Server playlist was empty, falling back to local playlist") - else: - Logger.warning("Server returned empty playlist, falling back to local playlist") - - except requests.exceptions.ConnectTimeout: - Logger.error("Server connection timeout, using fallback playlist") - except requests.exceptions.ConnectionError: - Logger.error("Cannot connect to server, using fallback playlist") - except requests.exceptions.Timeout: - Logger.error("Server request timeout, using fallback playlist") - except Exception as e: - Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist") - - # If we reach here, server connection failed - use fallback - if not server_connection_successful: - self.status_label.config(text="Server unavailable\nLoading last playlist...") - self.root.update() - time.sleep(1) # Brief pause to show message - - self.status_label.place_forget() - self.load_fallback_playlist(fallback_playlist) - - def load_fallback_playlist(self, fallback_playlist): - """Load fallback playlist when server is unavailable""" - if fallback_playlist and len(fallback_playlist) > 0: - self.playlist = fallback_playlist - Logger.info(f"Loaded fallback playlist with {len(self.playlist)} items") - self.play_current_media() - else: - Logger.warning("No fallback playlist available, loading demo content") - self.load_demo_or_local_playlist() - - def load_demo_or_local_playlist(self): - """Load either the existing local playlist or demo content""" - # First try to load the local playlist - local_playlist_data = load_local_playlist() - self.playlist = local_playlist_data.get('playlist', []) - - if self.playlist: - Logger.info(f"Loaded existing local playlist with {len(self.playlist)} items") - self.play_current_media() - return - - # If no local playlist, try loading demo content - Logger.info("No local playlist found, loading demo content") - self.create_demo_content() - - if self.playlist: - self.play_current_media() - else: - self.show_no_content_message() - - def create_demo_content(self): - """Create demo content for testing""" - demo_images = [] - - # First check static/resurse folder for any media - static_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') - if os.path.exists(static_dir): - for file in os.listdir(static_dir): - if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): - full_path = os.path.join(static_dir, file) - demo_images.append({ - 'file_name': file, - 'url': full_path, - 'duration': 5 - }) - - # If no files found in static/resurse, look in Resurse folder - if not demo_images: - demo_dir = './Resurse' - if os.path.exists(demo_dir): - for file in os.listdir(demo_dir): - if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): - demo_images.append({ - 'file_name': file, - 'url': os.path.join(demo_dir, file), - 'duration': 5 - }) - - if demo_images: - self.playlist = demo_images - Logger.info(f"Created demo playlist with {len(demo_images)} images") - else: - # Create a text-only demo if no images found - self.playlist = [{ - 'file_name': 'Demo Text', - 'url': 'text://Welcome to Tkinter Media Player!\n\nPlease configure server settings', - 'duration': 5 - }] - - def show_no_content_message(self): - """Show message when no content is available""" - self.image_label.config(image='') - self.status_label.config( - text="No media content available.\nPress Settings to configure server connection." - ) - self.status_label.place(relx=0.5, rely=0.5, anchor='center') - - def show_error_message(self, message): - """Show error message""" - self.image_label.config(image='') - self.status_label.config(text=f"Error: {message}") - self.status_label.place(relx=0.5, rely=0.5, anchor='center') - - def play_current_media(self): - """Play the current media item""" - if not self.playlist or self.current_index >= len(self.playlist): - self.show_no_content_message() - return - - media = self.playlist[self.current_index] - file_path = media.get('url', '') - file_name = media.get('file_name', '') - duration = media.get('duration', 10) - - # Handle relative paths by converting to absolute paths - if file_path.startswith('static/resurse/'): - # Convert relative path to absolute - absolute_path = os.path.join(os.path.dirname(__file__), file_path) - file_path = absolute_path - - Logger.info(f"Playing media: {file_name} from {file_path}") - - # Log media start - self.log_event(file_name, "STARTED") - - # Cancel existing timers - self.cancel_timers() - - # Handle different media types - if file_path.startswith('text://'): - self.show_text_content(file_path[7:], duration) - elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')): - self.play_video(file_path) - elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')): - self.show_image(file_path, duration) - else: - Logger.error(f"Unsupported or missing media: {file_path}") - self.status_label.config(text=f"Missing or unsupported media:\n{file_name}") - # Schedule next media after short delay - self.auto_advance_timer = self.root.after(5000, self.next_media) - - def play_video(self, file_path): - """Play video file using system VLC as a subprocess for robust hardware acceleration and stability.""" - self.status_label.place_forget() - def run_vlc_subprocess(): - try: - Logger.info(f"Starting system VLC subprocess for video: {file_path}") - # Build VLC command - vlc_cmd = [ - 'cvlc', - '--fullscreen', - '--no-osd', - '--no-video-title-show', - '--play-and-exit', - '--quiet', - file_path - ] - proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - proc.wait() - Logger.info(f"VLC subprocess finished: {file_path}") - except Exception as e: - Logger.error(f"VLC subprocess error: {e}") - finally: - self.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.root.after(1000, self.next_media))) - threading.Thread(target=run_vlc_subprocess, daemon=True).start() - - def _update_video_frame(self, photo): - """Update video frame from main thread""" - try: - self.image_label.config(image=photo) - self.image_label.image = photo # Keep reference - except Exception as e: - Logger.error(f"Error updating video frame: {e}") - - def _show_video_error(self, error_msg): - """Show video error from main thread""" - try: - self.status_label.config(text=f"Video Error:\n{error_msg}") - self.status_label.place(relx=0.5, rely=0.5, anchor='center') - self.auto_advance_timer = self.root.after(5000, self.next_media) - except Exception as e: - Logger.error(f"Error showing video error: {e}") - self.auto_advance_timer = self.root.after(5000, self.next_media) - - def show_text_content(self, text, duration): - """Display text content""" - self.image_label.config(image='') - self.status_label.config(text=text) - - # Schedule next media - self.auto_advance_timer = self.root.after( - int(duration * 1000), - self.next_media - ) - - def show_image(self, file_path, duration): - """Display an image in full screen, properly fitted to screen size""" - try: - # Hide status label and clear any previous text - self.status_label.place_forget() - self.status_label.config(text="") - - if PIL_AVAILABLE: - # Use PIL for better image handling - img = Image.open(file_path) - original_size = img.size - - # Get actual screen dimensions - screen_width = self.root.winfo_width() - screen_height = self.root.winfo_height() - - # Ensure we have valid screen dimensions - if screen_width <= 1 or screen_height <= 1: - screen_width = 1920 # Default fallback - screen_height = 1080 - - # Scale image using the scaling helper - final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode) - - # Convert to PhotoImage - photo = ImageTk.PhotoImage(final_img) - - # Clear previous image and display new one - self.image_label.config(image=photo) - self.image_label.image = photo # Keep reference - - Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} " - f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, " - f"Mode: {self.scaling_mode}, Offset: {offset})") - else: - # Fall back to basic text display if PIL not available - self.image_label.config(image='') - self.status_label.config(text=f"IMAGE: {os.path.basename(file_path)}\n\n(Install PIL for image display)") - self.status_label.place(relx=0.5, rely=0.5, anchor='center') - Logger.warning("PIL not available - showing text placeholder for image") - - # Schedule next media - self.auto_advance_timer = self.root.after( - int(duration * 1000), - self.next_media - ) - - except Exception as e: - Logger.error(f"Failed to show image {file_path}: {e}") - self.image_label.config(image='') - self.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}") - self.status_label.place(relx=0.5, rely=0.5, anchor='center') - self.auto_advance_timer = self.root.after(5000, self.next_media) - - def next_media(self): - """Move to next media""" - self.cancel_timers() - - if not self.playlist: - return - - self.current_index = (self.current_index + 1) % len(self.playlist) - - # Check for playlist updates at end of cycle - if self.current_index == 0: - threading.Thread(target=self.check_playlist_updates, daemon=True).start() - - self.play_current_media() - - def previous_media(self): - """Move to previous media""" - self.cancel_timers() - - if not self.playlist: - return - - self.current_index = (self.current_index - 1) % len(self.playlist) - self.play_current_media() - - def toggle_play_pause(self): - """Toggle play/pause state""" - self.is_paused = not self.is_paused - - if self.is_paused: - self.play_pause_btn.config(text="▶ Play") - self.cancel_timers() - else: - self.play_pause_btn.config(text="⏸ Pause") - # Resume current media - self.play_current_media() - - Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}") - - def cancel_timers(self): - """Cancel all active timers""" - if self.auto_advance_timer: - self.root.after_cancel(self.auto_advance_timer) - self.auto_advance_timer = None - - def show_controls(self): - """Show control panel""" - if self.control_frame: - self.control_frame.place(relx=0.98, rely=0.98, anchor='se') - - def hide_controls(self): - """Hide control panel""" - if self.control_frame: - self.control_frame.place_forget() - - def schedule_hide_controls(self): - """Schedule hiding controls after delay""" - if self.hide_controls_timer: - self.root.after_cancel(self.hide_controls_timer) - self.hide_controls_timer = self.root.after(10000, self.hide_controls) - - def on_mouse_click(self, event): - """Handle mouse clicks""" - self.show_controls() - self.schedule_hide_controls() - - def on_mouse_motion(self, event): - """Handle mouse motion""" - self.show_controls() - self.schedule_hide_controls() - - def on_key_press(self, event): - """Handle keyboard events""" - key = event.keysym.lower() - - if key == 'f': - self.toggle_fullscreen() - elif key == 'space': - self.toggle_play_pause() - elif key == 'left': - self.previous_media() - elif key == 'right': - self.next_media() - elif key == 'escape': - self.show_exit_dialog() - elif key == '1': - self.set_scaling_mode('fit') - elif key == '2': - self.set_scaling_mode('fill') - elif key == '3': - self.set_scaling_mode('stretch') - elif event.state & 0x4: # Ctrl key pressed - if key == 's': - self.open_settings() - - self.show_controls() - self.schedule_hide_controls() - - def set_scaling_mode(self, mode): - """Change the scaling mode and refresh current media""" - old_mode = self.scaling_mode - self.scaling_mode = mode - Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'") - - # Show temporary notification - self.status_label.config(text=f"Scaling Mode: {mode.title()}\n" - f"1=Fit 2=Fill 3=Stretch") - self.status_label.place(relx=0.5, rely=0.05, anchor='center') - - # Hide notification after 2 seconds - self.root.after(2000, lambda: self.status_label.place_forget()) - - # Refresh current media with new scaling - if self.playlist and 0 <= self.current_index < len(self.playlist): - self.cancel_timers() - self.play_current_media() - - def toggle_fullscreen(self): - """Toggle fullscreen mode""" - self.is_fullscreen = not self.is_fullscreen - self.root.attributes('-fullscreen', self.is_fullscreen) - - def open_settings(self): - """Open settings window""" - if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists(): - self.settings_window.lift() - return - - # Pause media playback when opening settings - if not self.is_paused: - self.toggle_play_pause() - - self.settings_window = SettingsWindow(self.root, self) - - # Add a callback to resume playback when the settings window is closed - def on_settings_close(): - if self.is_paused: - self.toggle_play_pause() - - self.settings_window.protocol("WM_DELETE_WINDOW", on_settings_close) - - def show_exit_dialog(self): - """Show modern password-protected exit dialog""" - try: - config = load_config() - quickconnect_key = config.get('quickconnect_key', '') - except: - quickconnect_key = '' - - # Create modern exit dialog - exit_dialog = tk.Toplevel(self.root) - exit_dialog.title("Exit Application") - exit_dialog.geometry("400x200") - exit_dialog.configure(bg='#2d2d2d') - exit_dialog.transient(self.root) - exit_dialog.grab_set() - exit_dialog.resizable(False, False) - - # Center the dialog using helper method - self.center_dialog_on_screen(exit_dialog, 400, 200) - - # Header with icon - header_frame = tk.Frame(exit_dialog, bg='#cc0000', height=60) - header_frame.pack(fill=tk.X) - header_frame.pack_propagate(False) - - icon_label = tk.Label(header_frame, text="⚠", font=('Arial', 20, 'bold'), - fg='white', bg='#cc0000') - icon_label.pack(side=tk.LEFT, padx=15, pady=15) - - title_label = tk.Label(header_frame, text="Exit Application", - font=('Arial', 14, 'bold'), fg='white', bg='#cc0000') - title_label.pack(side=tk.LEFT, pady=15) - - # Content frame - content_frame = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20) - content_frame.pack(fill=tk.BOTH, expand=True) - - # Password prompt - prompt_label = tk.Label(content_frame, text="Enter password to exit:", - font=('Arial', 11), fg='white', bg='#2d2d2d') - prompt_label.pack(pady=(0, 10)) - - # Password entry - password_var = tk.StringVar() - password_entry = tk.Entry(content_frame, textvariable=password_var, - font=('Arial', 11), show='*', width=25, - bg='#404040', fg='white', insertbackground='white', - relief=tk.FLAT, bd=5) - password_entry.pack(pady=(0, 15)) - password_entry.focus_set() - - # Button frame - button_frame = tk.Frame(content_frame, bg='#2d2d2d') - button_frame.pack(fill=tk.X) - - def check_password(): - if password_var.get() == quickconnect_key: - exit_dialog.destroy() - self.exit_application() - elif password_var.get(): # Only show error if password was entered - # Show error in red - error_label.config(text="✗ Incorrect password", fg='#ff4444') - password_entry.delete(0, tk.END) - password_entry.focus_set() - - def cancel_exit(): - exit_dialog.destroy() - - # Error label (hidden initially) - error_label = tk.Label(content_frame, text="", font=('Arial', 9), - bg='#2d2d2d') - error_label.pack() - - # Buttons - cancel_btn = tk.Button(button_frame, text="Cancel", command=cancel_exit, - bg='#555555', fg='white', font=('Arial', 10, 'bold'), - relief=tk.FLAT, padx=20, pady=8, width=10) - cancel_btn.pack(side=tk.RIGHT, padx=(10, 0)) - - exit_btn = tk.Button(button_frame, text="Exit", command=check_password, - bg='#cc0000', fg='white', font=('Arial', 10, 'bold'), - relief=tk.FLAT, padx=20, pady=8, width=10) - exit_btn.pack(side=tk.RIGHT) - - # Bind Enter key to check password - password_entry.bind('', lambda e: check_password()) - exit_dialog.bind('', lambda e: cancel_exit()) - - def exit_application(self): - """Exit the application""" - Logger.info("Application exit requested") - self.running = False - self.root.quit() - self.root.destroy() - - def center_dialog_on_screen(self, dialog, width, height): - """Center a dialog window on screen regardless of screen size""" - dialog.update_idletasks() # Ensure geometry is calculated - screen_width = dialog.winfo_screenwidth() - screen_height = dialog.winfo_screenheight() - - # Calculate center position - center_x = int((screen_width - width) / 2) - center_y = int((screen_height - height) / 2) - - # Ensure the dialog doesn't go off-screen on smaller displays - center_x = max(0, min(center_x, screen_width - width)) - center_y = max(0, min(center_y, screen_height - height)) - - dialog.geometry(f"{width}x{height}+{center_x}+{center_y}") - - # Bring to front and focus - dialog.lift() - dialog.focus_force() - - return center_x, center_y - - def check_playlist_updates(self): - """Check for playlist updates from server with fallback protection""" - try: - config = load_config() - local_version = config.get('playlist_version', 0) - - server_playlist_data = fetch_server_playlist() - server_version = server_playlist_data.get('version', 0) - - if server_version > local_version: - Logger.info(f"Updating playlist: {local_version} -> {server_version}") - - # Clean old files - local_playlist_data = load_local_playlist() - clean_unused_files(local_playlist_data.get('playlist', [])) - - # Download new content - download_media_files( - server_playlist_data.get('playlist', []), - server_version - ) - - # Update local playlist - local_playlist_data = load_local_playlist() - self.playlist = local_playlist_data.get('playlist', []) - - # Reset to beginning of playlist - self.current_index = 0 - - Logger.info("Playlist updated successfully") - - # Continue with current media after update - self.play_current_media() - else: - Logger.info("No playlist updates available") - - except requests.exceptions.ConnectTimeout: - Logger.warning("Server connection timeout during update check - continuing with current playlist") - except requests.exceptions.ConnectionError: - Logger.warning("Cannot connect to server during update check - continuing with current playlist") - except requests.exceptions.Timeout: - Logger.warning("Server request timeout during update check - continuing with current playlist") - except Exception as e: - Logger.warning(f"Failed to check playlist updates: {e} - continuing with current playlist") - - def log_event(self, file_name, event): - """Log media events""" - try: - timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - log_message = f"{timestamp} - {event}: {file_name}\n" - - # Update the log file path to the resources directory - log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt') - with open(log_file, 'a') as f: - f.write(log_message) - - except Exception as e: - Logger.error(f"Failed to log event: {e}") - - def start_periodic_checks(self): - """Start periodic playlist checks""" - def check_loop(): - """Background thread for periodic checks""" - while self.running: - try: - time.sleep(300) # Check every 5 minutes - if self.running: - self.check_playlist_updates() - except Exception as e: - Logger.error(f"Error in periodic check: {e}") - - # Start background thread - threading.Thread(target=check_loop, daemon=True).start() - - def run(self): - """Start the application""" - Logger.info("Starting Simple Tkinter Media Player") - try: - self.root.mainloop() - except KeyboardInterrupt: - self.exit_application() - except Exception as e: - Logger.error(f"Application error: {e}") - print(f"Error: {e}") - - -class SettingsWindow: - def __init__(self, parent, app): - self.parent = parent - self.app = app - self.window = tk.Toplevel(parent) - - # Initialize virtual keyboard for touch displays - self.virtual_keyboard = VirtualKeyboard(self.window, dark_theme=True) - - self.setup_window() - self.create_widgets() - self.load_config() - - # Setup touch optimization - self.setup_touch_optimization() - - def setup_window(self): - """Setup settings window with enhanced dark theme""" - self.window.title("🎬 Signage Player Settings") - self.window.geometry("900x700") - - # Enhanced dark theme colors - self.colors = { - 'bg_primary': '#1e2124', # Very dark background - 'bg_secondary': '#2f3136', # Slightly lighter background - 'bg_tertiary': '#36393f', # Card backgrounds - 'accent': '#7289da', # Discord-like blue accent - 'accent_hover': '#677bc4', # Darker accent for hover - 'success': '#43b581', # Green for success - 'warning': '#faa61a', # Orange for warnings - 'danger': '#f04747', # Red for errors - 'text_primary': '#ffffff', # White text - 'text_secondary': '#b9bbbe', # Gray text - 'text_muted': '#72767d', # Muted text - 'border': '#202225' # Border color - } - - self.window.configure(bg=self.colors['bg_primary']) - self.window.transient(self.parent) - self.window.grab_set() - - # Set window properties - self.window.resizable(True, True) - self.window.minsize(700, 500) - - # Set window icon if available - try: - # Try to use a simple emoji icon - self.window.iconname("🎬") - except: - pass - - # Center the window on screen using helper method - self.center_window_on_screen(self.window, 900, 700) - - # Add subtle window border effect - self.window.configure(highlightbackground=self.colors['border'], highlightthickness=1) - - def create_widgets(self): - """Create settings widgets with enhanced dark theme styling""" - # Configure enhanced custom styles - style = ttk.Style() - style.theme_use('clam') - # Enhanced dark theme styles - style.configure('Dark.TNotebook', - background=self.colors['bg_secondary'], - borderwidth=0, - tabmargins=[2, 5, 2, 0]) - style.configure('Dark.TNotebook.Tab', - padding=[20, 12], - font=('Segoe UI', 11, 'bold'), - background=self.colors['bg_tertiary'], - foreground=self.colors['text_secondary'], - borderwidth=1, - focuscolor='none') - style.map('Dark.TNotebook.Tab', - background=[('selected', self.colors['accent']), - ('active', self.colors['accent_hover'])], - foreground=[('selected', self.colors['text_primary']), - ('active', self.colors['text_primary'])]) - style.configure('Dark.TFrame', background=self.colors['bg_secondary']) - style.configure('Dark.TLabel', - background=self.colors['bg_secondary'], - foreground=self.colors['text_primary'], - font=('Segoe UI', 10)) - style.configure('Dark.TEntry', - fieldbackground=self.colors['bg_tertiary'], - foreground=self.colors['text_primary'], - bordercolor=self.colors['border'], - lightcolor=self.colors['bg_tertiary'], - darkcolor=self.colors['bg_tertiary'], - font=('Segoe UI', 10), - insertcolor=self.colors['text_primary']) - - # Main container frame - main_frame = tk.Frame(self.window, bg=self.colors['bg_primary']) - main_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=0) - - # Header section with gradient-like effect - header_frame = tk.Frame(main_frame, bg=self.colors['bg_secondary'], height=80) - header_frame.pack(fill=tk.X, padx=0, pady=0) - header_frame.pack_propagate(False) - - # Title with enhanced styling - title_container = tk.Frame(header_frame, bg=self.colors['bg_secondary']) - title_container.pack(expand=True, fill=tk.BOTH) - - title_label = tk.Label(title_container, - text="🎬 Digital Signage Control Center", - font=('Segoe UI', 24, 'bold'), - fg=self.colors['text_primary'], - bg=self.colors['bg_secondary']) - title_label.pack(expand=True) - - subtitle_label = tk.Label(title_container, - text="Configure your digital signage display settings", - font=('Segoe UI', 11), - fg=self.colors['text_secondary'], - bg=self.colors['bg_secondary']) - subtitle_label.pack() - - # Content area - content_frame = tk.Frame(main_frame, bg=self.colors['bg_primary']) - content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) - - # Create enhanced notebook with dark theme - notebook = ttk.Notebook(content_frame, style='Dark.TNotebook') - notebook.pack(fill=tk.BOTH, expand=True, pady=(0, 20)) - - # Create tabs with enhanced styling - self.create_connection_tab_enhanced(notebook) - self.create_display_tab_enhanced(notebook) - self.create_advanced_tab_enhanced(notebook) - self.create_logs_tab_enhanced(notebook) - self.create_about_tab_enhanced(notebook) - - # Enhanced bottom button section - self.create_bottom_controls(content_frame) - - # Load initial data - self.load_logs() - - def create_connection_tab_enhanced(self, notebook): - """Create enhanced connection settings tab""" - tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) - notebook.add(tab_frame, text="🌐 Connection") - - # Create scrollable content - canvas = tk.Canvas(tab_frame, bg=self.colors['bg_secondary'], highlightthickness=0) - scrollbar = tk.Scrollbar(tab_frame, orient="vertical", command=canvas.yview, - bg=self.colors['bg_tertiary'], troughcolor=self.colors['bg_primary']) - scrollable_frame = tk.Frame(canvas, bg=self.colors['bg_secondary']) - - scrollable_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) - canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") - canvas.configure(yscrollcommand=scrollbar.set) - - # Server connection card - server_card = self.create_settings_card(scrollable_frame, "🖥️ Server Connection", - "Configure your signage server connection details") - - # Server IP/Domain - self.create_input_field(server_card, "Server IP/Domain:", "server_ip_var", - placeholder="e.g., digi-server.example.com") - - # Port - self.create_input_field(server_card, "Port:", "port_var", width=15, - placeholder="8880") - - # Device settings card - device_card = self.create_settings_card(scrollable_frame, "📱 Device Settings", - "Identify this display device") - - # Screen/Device name - self.create_input_field(device_card, "Device Name:", "screen_name_var", - placeholder="e.g., lobby-display-01") - - # QuickConnect key - self.create_input_field(device_card, "QuickConnect Key:", "quickconnect_var", - password=True, placeholder="Enter your access key") - - # Connection testing card - test_card = self.create_settings_card(scrollable_frame, "🔗 Connection Test", - "Test your server connection") - - test_btn_frame = tk.Frame(test_card, bg=self.colors['bg_tertiary']) - test_btn_frame.pack(fill=tk.X, pady=10) - - test_btn = self.create_action_button(test_btn_frame, "🔗 Test Connection", - self.test_connection, self.colors['accent']) - test_btn.pack(side=tk.LEFT, padx=5) - - canvas.pack(side="left", fill="both", expand=True, padx=20, pady=20) - scrollbar.pack(side="right", fill="y") - - def create_display_tab_enhanced(self, notebook): - """Create enhanced display settings tab""" - tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) - notebook.add(tab_frame, text="🖼️ Display") - - main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) - main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) - - # Resolution settings card - resolution_card = self.create_settings_card(main_container, "🖥️ Screen Resolution", - "Configure display resolution settings") - - # Resolution input fields - resolution_inputs = tk.Frame(resolution_card, bg=self.colors['bg_tertiary']) - resolution_inputs.pack(fill=tk.X, pady=10) - - self.create_input_field(resolution_inputs, "Width (px):", "screen_w_var", - width=15, placeholder="1920") - self.create_input_field(resolution_inputs, "Height (px):", "screen_h_var", - width=15, placeholder="1080") - - # Preset buttons with enhanced styling - presets_frame = tk.Frame(resolution_card, bg=self.colors['bg_tertiary']) - presets_frame.pack(fill=tk.X, pady=15) - - tk.Label(presets_frame, text="Quick Presets:", - font=('Segoe UI', 11, 'bold'), - bg=self.colors['bg_tertiary'], - fg=self.colors['text_primary']).pack(anchor=tk.W, pady=(0, 10)) - - preset_buttons = tk.Frame(presets_frame, bg=self.colors['bg_tertiary']) - preset_buttons.pack(anchor=tk.W) - - presets = [("📱 HD", "1366", "768"), ("💻 Full HD", "1920", "1080"), - ("🖥️ 4K", "3840", "2160"), ("📺 Classic", "1024", "768")] - - for preset_name, width, height in presets: - btn = self.create_preset_button(preset_buttons, preset_name, width, height) - btn.pack(side=tk.LEFT, padx=3, pady=2) - - # Scaling mode settings card - scaling_card = self.create_settings_card(main_container, "📐 Image/Video Scaling", - "Configure how images and videos are displayed on screen") - - # Scaling mode options - self.scaling_mode_var = tk.StringVar(value=self.app.scaling_mode if hasattr(self.app, 'scaling_mode') else 'fit') - - scaling_label = tk.Label(scaling_card, text="Scaling Mode:", - font=('Segoe UI', 12, 'bold'), - bg=self.colors['bg_tertiary'], - fg=self.colors['text_primary']) - scaling_label.pack(anchor=tk.W, pady=(0, 10)) - - scaling_options = tk.Frame(scaling_card, bg=self.colors['bg_tertiary']) - scaling_options.pack(fill=tk.X, pady=5) - - # Radio buttons for scaling modes - modes = [ - ("fit", "🖼️ Fit", "Maintain aspect ratio, add black bars if needed"), - ("fill", "🔍 Fill", "Maintain aspect ratio, crop to fill entire screen"), - ("stretch", "↔️ Stretch", "Ignore aspect ratio, stretch to fill screen") - ] - - for mode_value, mode_label, mode_desc in modes: - mode_frame = tk.Frame(scaling_options, bg=self.colors['bg_tertiary']) - mode_frame.pack(fill=tk.X, pady=2) - - radio_btn = tk.Radiobutton(mode_frame, - text=mode_label, - variable=self.scaling_mode_var, - value=mode_value, - bg=self.colors['bg_tertiary'], - fg=self.colors['text_primary'], - font=('Segoe UI', 11, 'bold'), - selectcolor=self.colors['bg_primary'], - activebackground=self.colors['bg_tertiary'], - activeforeground=self.colors['text_primary'], - command=lambda: self.update_scaling_mode()) - radio_btn.pack(side=tk.LEFT, anchor=tk.W) - - desc_label = tk.Label(mode_frame, text=f" - {mode_desc}", - font=('Segoe UI', 9), - bg=self.colors['bg_tertiary'], - fg=self.colors['text_secondary']) - desc_label.pack(side=tk.LEFT, anchor=tk.W, padx=(10, 0)) - - # Keyboard shortcuts info - shortcuts_frame = tk.Frame(scaling_card, bg=self.colors['bg_tertiary']) - shortcuts_frame.pack(fill=tk.X, pady=(15, 0)) - - shortcuts_label = tk.Label(shortcuts_frame, - text="💡 Tip: Use keyboard shortcuts 1, 2, 3 to quickly change scaling mode during playback", - font=('Segoe UI', 9, 'italic'), - bg=self.colors['bg_tertiary'], - fg=self.colors['text_muted'], - wraplength=400) - shortcuts_label.pack(anchor=tk.W) - - def update_scaling_mode(self): - """Update the scaling mode in the main app""" - new_mode = self.scaling_mode_var.get() - if hasattr(self.app, 'set_scaling_mode'): - self.app.set_scaling_mode(new_mode) - else: - self.app.scaling_mode = new_mode - - def create_advanced_tab_enhanced(self, notebook): - """Create enhanced advanced settings tab""" - tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) - notebook.add(tab_frame, text="⚙️ Advanced") - - main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) - main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) - - # Sync settings card - sync_card = self.create_settings_card(main_container, "🔄 Synchronization", - "Configure automatic content updates") - - self.create_input_field(sync_card, "Auto-refresh (minutes):", "refresh_interval_var", - width=15, placeholder="15") - - # Performance settings card - perf_card = self.create_settings_card(main_container, "⚡ Performance", - "Optimize playback performance") - - self.hardware_accel_var = tk.BooleanVar(value=True) - self.create_checkbox(perf_card, "Enable hardware acceleration", self.hardware_accel_var) - self.settings_window = SettingsWindow(self.root, self, dark_theme=True) - self.cache_media_var = tk.BooleanVar(value=True) - self.create_checkbox(perf_card, "Cache media files locally", self.cache_media_var) - - self.auto_retry_var = tk.BooleanVar(value=True) - self.create_checkbox(perf_card, "Auto-retry failed downloads", self.auto_retry_var) - - def create_logs_tab_enhanced(self, notebook): - """Create enhanced logs tab""" - tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) - notebook.add(tab_frame, text="📋 Logs") - - main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) - main_container.pack(fill=tk.BOTH, expand=True, padx=20, pady=20) - - # Log header - log_header = tk.Frame(main_container, bg=self.colors['bg_secondary']) - log_header.pack(fill=tk.X, pady=(0, 15)) - - tk.Label(log_header, text="📋 Application Logs", - font=('Segoe UI', 16, 'bold'), - bg=self.colors['bg_secondary'], - fg=self.colors['text_primary']).pack(side=tk.LEFT) - - refresh_btn = self.create_action_button(log_header, "🔄 Refresh Logs", - self.load_logs, self.colors['accent']) - refresh_btn.pack(side=tk.RIGHT) - - # Log display area - log_container = tk.Frame(main_container, bg=self.colors['bg_tertiary'], - relief=tk.FLAT, bd=2) - log_container.pack(fill=tk.BOTH, expand=True) - - self.log_text = tk.Text(log_container, - font=('Consolas', 9), - bg=self.colors['bg_primary'], - fg=self.colors['text_primary'], - insertbackground=self.colors['accent'], - selectbackground=self.colors['accent'], - selectforeground=self.colors['text_primary'], - relief=tk.FLAT, - bd=10, - wrap=tk.WORD) - - log_scrollbar = tk.Scrollbar(log_container, command=self.log_text.yview, - bg=self.colors['bg_tertiary']) - self.log_text.configure(yscrollcommand=log_scrollbar.set) - - self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - def create_about_tab_enhanced(self, notebook): - """Create enhanced about tab""" - tab_frame = tk.Frame(notebook, bg=self.colors['bg_secondary']) - notebook.add(tab_frame, text="ℹ️ About") - - main_container = tk.Frame(tab_frame, bg=self.colors['bg_secondary']) - main_container.pack(fill=tk.BOTH, expand=True, padx=40, pady=40) - - # App branding section - branding_frame = tk.Frame(main_container, bg=self.colors['bg_secondary']) - branding_frame.pack(fill=tk.X, pady=(0, 40)) - - # Large app icon - icon_label = tk.Label(branding_frame, text="🎬", - font=('Segoe UI Emoji', 64), - bg=self.colors['bg_secondary'], - fg=self.colors['accent']) - icon_label.pack() - - # App title and version - title_label = tk.Label(branding_frame, text="Digital Signage Player", - font=('Segoe UI', 24, 'bold'), - bg=self.colors['bg_secondary'], - fg=self.colors['text_primary']) - title_label.pack(pady=(10, 5)) - - version_label = tk.Label(branding_frame, text="Version 2.1 - Enhanced Dark Edition", - font=('Segoe UI', 12), - bg=self.colors['bg_secondary'], - fg=self.colors['text_secondary']) - version_label.pack() - - # Feature highlights - features_text = ( - "🚀 Features:\n" - "• Modern dark theme interface\n" - "• High-resolution media display\n" - "• Remote playlist management\n" - "• Real-time content synchronization\n" - "• Hardware acceleration support\n" - "• Cross-platform compatibility\n" - "• Advanced logging and diagnostics\n\n" - "⌨️ Keyboard Shortcuts:\n" - "F11 - Toggle fullscreen mode\n" - "Space - Play/Pause media\n" - "← → - Navigate between media\n" - "1 - Set scaling mode to Fit (maintain aspect ratio with black bars)\n" - "2 - Set scaling mode to Fill (maintain aspect ratio, crop to fill screen)\n" - "3 - Set scaling mode to Stretch (ignore aspect ratio, stretch to fill)\n" - "Ctrl+S - Open settings panel\n" - "Escape - Exit application (password protected)\n\n" - "Built with ❤️ using Python & Tkinter" - ) - - features_label = tk.Label(main_container, text=features_text, - justify=tk.LEFT, - font=('Segoe UI', 11), - bg=self.colors['bg_secondary'], - fg=self.colors['text_primary'], - wraplength=600) - features_label.pack(anchor=tk.W) - - def create_bottom_controls(self, parent): - """Create enhanced bottom control buttons""" - controls_frame = tk.Frame(parent, bg=self.colors['bg_secondary'], - relief=tk.FLAT, bd=1) - controls_frame.pack(fill=tk.X, pady=(10, 0)) - - # Left side buttons - left_buttons = tk.Frame(controls_frame, bg=self.colors['bg_secondary']) - left_buttons.pack(side=tk.LEFT, padx=10, pady=15) - - save_btn = self.create_action_button(left_buttons, "💾 Save Configuration", - self.save_config, self.colors['success']) - save_btn.pack(side=tk.LEFT, padx=5) - - test_btn = self.create_action_button(left_buttons, "🔗 Test Connection", - self.test_connection, self.colors['accent']) - test_btn.pack(side=tk.LEFT, padx=5) - - refresh_btn = self.create_action_button(left_buttons, "🔄 Refresh Playlist", - self.force_playlist_refresh, self.colors['warning']) - refresh_btn.pack(side=tk.LEFT, padx=5) - - # Right side buttons - right_buttons = tk.Frame(controls_frame, bg=self.colors['bg_secondary']) - right_buttons.pack(side=tk.RIGHT, padx=10, pady=15) - - cancel_btn = self.create_action_button(right_buttons, "❌ Cancel", - self.window.destroy, self.colors['danger']) - cancel_btn.pack(side=tk.RIGHT, padx=5) - - # Status display - self.status_frame = tk.Frame(parent, bg=self.colors['bg_primary']) - self.status_frame.pack(fill=tk.X, pady=(10, 0)) - - self.connection_status = tk.Label(self.status_frame, - text="Ready to configure your digital signage settings", - bg=self.colors['bg_primary'], - fg=self.colors['text_secondary'], - font=('Segoe UI', 9)) - self.connection_status.pack(anchor=tk.W, padx=10, pady=5) - - def create_settings_card(self, parent, title, description): - """Create a settings card with enhanced styling""" - card_frame = tk.Frame(parent, bg=self.colors['bg_tertiary'], - relief=tk.FLAT, bd=2) - card_frame.pack(fill=tk.X, pady=(0, 20), padx=5) - - # Card header - header_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary']) - header_frame.pack(fill=tk.X, padx=20, pady=(15, 5)) - - title_label = tk.Label(header_frame, text=title, - font=('Segoe UI', 14, 'bold'), - bg=self.colors['bg_tertiary'], - fg=self.colors['text_primary']) - title_label.pack(anchor=tk.W) - - desc_label = tk.Label(header_frame, text=description, - font=('Segoe UI', 9), - bg=self.colors['bg_tertiary'], - fg=self.colors['text_secondary']) - desc_label.pack(anchor=tk.W, pady=(2, 0)) - - # Card content area - content_frame = tk.Frame(card_frame, bg=self.colors['bg_tertiary']) - content_frame.pack(fill=tk.X, padx=20, pady=(10, 15)) - - return content_frame - - def create_input_field(self, parent, label_text, var_name, width=35, placeholder="", password=False): - """Create a touch-optimized input field with virtual keyboard support""" - field_frame = tk.Frame(parent, bg=self.colors['bg_tertiary']) - field_frame.pack(fill=tk.X, pady=12) # Increased padding for touch - - label = tk.Label(field_frame, text=label_text, - font=('Segoe UI', 12, 'bold'), # Larger font for touch - bg=self.colors['bg_tertiary'], - fg=self.colors['text_primary'], - width=18, anchor='w') - label.pack(side=tk.LEFT) - - # Create StringVar if it doesn't exist - if not hasattr(self, var_name): - setattr(self, var_name, tk.StringVar()) - - var = getattr(self, var_name) - - # Use touch-optimized entry with virtual keyboard - entry = TouchOptimizedEntry(field_frame, - virtual_keyboard=self.virtual_keyboard, - textvariable=var, - width=width, - font=('Segoe UI', 12), # Larger font - bg=self.colors['bg_primary'], - fg=self.colors['text_primary'], - insertbackground=self.colors['accent'], - relief=tk.FLAT, - bd=10) # Larger border for easier touch - - if password: - entry.configure(show='*') - - entry.pack(side=tk.LEFT, padx=(15, 0), pady=5) - - # Add placeholder text effect - if placeholder: - self.add_placeholder(entry, placeholder) - - return entry - - def create_checkbox(self, parent, text, variable): - """Create a checkbox with dark theme styling""" - cb_frame = tk.Frame(parent, bg=self.colors['bg_tertiary']) - cb_frame.pack(fill=tk.X, pady=5) - - checkbox = tk.Checkbutton(cb_frame, text=text, variable=variable, - bg=self.colors['bg_tertiary'], - fg=self.colors['text_primary'], - font=('Segoe UI', 10), - selectcolor=self.colors['bg_primary'], - activebackground=self.colors['bg_tertiary'], - activeforeground=self.colors['text_primary']) - checkbox.pack(anchor=tk.W, padx=5) - - def create_action_button(self, parent, text, command, color): - """Create a touch-optimized action button with enhanced styling""" - button = TouchOptimizedButton(parent, - text=text, - command=command, - bg=color, - fg=self.colors['text_primary'], - font=('Segoe UI', 12, 'bold'), # Larger font - relief=tk.FLAT, - padx=25, # Larger padding for touch - pady=15, # Larger padding for touch - cursor='hand2', - bd=3) - - # Add enhanced hover effects for touch feedback - def on_enter(e): - button.configure(bg=self.lighten_color(color), relief=tk.RAISED) - - def on_leave(e): - button.configure(bg=color, relief=tk.FLAT) - - def on_press(e): - button.configure(relief=tk.SUNKEN) - - def on_release(e): - button.configure(relief=tk.FLAT) - - button.bind("", on_enter) - button.bind("", on_leave) - button.bind("", on_press) - button.bind("", on_release) - - return button - - def create_preset_button(self, parent, text, width, height): - """Create a touch-optimized preset resolution button""" - button = TouchOptimizedButton(parent, - text=text, - command=lambda: self.set_resolution_preset(width, height), - bg=self.colors['bg_primary'], - fg=self.colors['text_primary'], - font=('Segoe UI', 10, 'bold'), - relief=tk.FLAT, - padx=20, # Larger for touch - pady=10, # Larger for touch - cursor='hand2') - - def on_enter(e): - button.configure(bg=self.colors['accent'], relief=tk.RAISED) - - def on_leave(e): - button.configure(bg=self.colors['bg_primary'], relief=tk.FLAT) - - def on_press(e): - button.configure(relief=tk.SUNKEN) - - def on_release(e): - button.configure(relief=tk.FLAT) - - button.bind("", on_enter) - button.bind("", on_leave) - button.bind("", on_press) - button.bind("", on_release) - - return button - - def add_placeholder(self, entry, placeholder_text): - """Add placeholder text to entry widget""" - entry.insert(0, placeholder_text) - entry.configure(fg=self.colors['text_muted']) - - def on_focus_in(event): - if entry.get() == placeholder_text: - entry.delete(0, tk.END) - entry.configure(fg=self.colors['text_primary']) - - def on_focus_out(event): - if not entry.get(): - entry.insert(0, placeholder_text) - entry.configure(fg=self.colors['text_muted']) - - entry.bind('', on_focus_in) - entry.bind('', on_focus_out) - - def lighten_color(self, color): - """Lighten a hex color for hover effects""" - color = color.lstrip('#') - rgb = tuple(int(color[i:i+2], 16) for i in (0, 2, 4)) - lighter_rgb = tuple(min(255, int(c * 1.2)) for c in rgb) - return f"#{lighter_rgb[0]:02x}{lighter_rgb[1]:02x}{lighter_rgb[2]:02x}" - - def center_window_on_screen(self, window, width, height): - """Center a window on screen regardless of screen size""" - window.update_idletasks() # Ensure geometry is calculated - screen_width = window.winfo_screenwidth() - screen_height = window.winfo_screenheight() - - # Calculate center position - center_x = int((screen_width - width) / 2) - center_y = int((screen_height - height) / 2) - - # Ensure the window doesn't go off-screen on smaller displays - center_x = max(0, min(center_x, screen_width - width)) - center_y = max(0, min(center_y, screen_height - height)) - - window.geometry(f"{width}x{height}+{center_x}+{center_y}") - - # Bring to front and focus - window.lift() - window.focus_force() - - return center_x, center_y - - def setup_touch_optimization(self): - """Setup touch-friendly optimizations""" - # Hide virtual keyboard when clicking outside input fields - def hide_keyboard_on_click(event): - # Check if click is not on an entry widget - if not isinstance(event.widget, (tk.Entry, TouchOptimizedEntry)): - self.virtual_keyboard.hide_keyboard() - - self.window.bind("", hide_keyboard_on_click, "+") - - # Make window touch-friendly by increasing minimum size - self.window.minsize(800, 600) - - # Override window close to hide keyboard first - original_destroy = self.window.destroy - def enhanced_destroy(): - self.virtual_keyboard.hide_keyboard() - original_destroy() - - self.window.destroy = enhanced_destroy - self.window.protocol("WM_DELETE_WINDOW", enhanced_destroy) - - def set_resolution_preset(self, width, height): - """Set resolution to preset values with visual feedback""" - self.screen_w_var.set(width) - self.screen_h_var.set(height) - self.connection_status.configure( - text=f"✅ Resolution preset applied: {width}x{height}", - fg=self.colors['success'] - ) - - # Reset status color after 3 seconds - self.window.after(3000, lambda: self.connection_status.configure( - fg=self.colors['text_secondary'] - )) - - def load_config(self): - """Load current configuration with enhanced feedback""" - try: - Logger.info("Loading configuration in enhanced settings window") - config = load_config() - Logger.info(f"Config loaded: {config}") - - # Set values for connection settings - if hasattr(self, 'screen_name_var'): - self.screen_name_var.set(config.get('screen_name', '')) - if hasattr(self, 'server_ip_var'): - self.server_ip_var.set(config.get('server_ip', '')) - if hasattr(self, 'port_var'): - self.port_var.set(config.get('port', '8880')) - if hasattr(self, 'quickconnect_var'): - self.quickconnect_var.set(config.get('quickconnect_key', '')) - - # Set values for display settings - if hasattr(self, 'screen_w_var'): - self.screen_w_var.set(config.get('screen_w', '1920')) - if hasattr(self, 'screen_h_var'): - self.screen_h_var.set(config.get('screen_h', '1080')) - if hasattr(self, 'scaling_mode_var'): - self.scaling_mode_var.set(config.get('scaling_mode', 'fit')) - - # Set values for advanced settings - if hasattr(self, 'refresh_interval_var'): - self.refresh_interval_var.set(config.get('refresh_interval', '15')) - if hasattr(self, 'hardware_accel_var'): - self.hardware_accel_var.set(config.get('hardware_acceleration', True)) - if hasattr(self, 'cache_media_var'): - self.cache_media_var.set(config.get('cache_media', True)) - if hasattr(self, 'auto_retry_var'): - self.auto_retry_var.set(config.get('auto_retry', True)) - - # Update status with success message - if hasattr(self, 'connection_status'): - self.connection_status.configure( - text="✅ Configuration loaded successfully", - fg=self.colors['success'] - ) - # Reset status color after 3 seconds - self.window.after(3000, lambda: self.connection_status.configure( - fg=self.colors['text_secondary'] - )) - - Logger.info("Configuration values loaded successfully in enhanced settings") - - except Exception as e: - Logger.error(f"Failed to load config in enhanced settings: {e}") - - # Set default values if loading fails - if hasattr(self, 'screen_name_var'): - self.screen_name_var.set('') - if hasattr(self, 'server_ip_var'): - self.server_ip_var.set('') - if hasattr(self, 'port_var'): - self.port_var.set('8880') - if hasattr(self, 'quickconnect_var'): - self.quickconnect_var.set('') - if hasattr(self, 'screen_w_var'): - self.screen_w_var.set('1920') - if hasattr(self, 'screen_h_var'): - self.screen_h_var.set('1080') - - # Set advanced defaults - if hasattr(self, 'refresh_interval_var'): - self.refresh_interval_var.set('15') - if hasattr(self, 'hardware_accel_var'): - self.hardware_accel_var.set(True) - if hasattr(self, 'cache_media_var'): - self.cache_media_var.set(True) - if hasattr(self, 'auto_retry_var'): - self.auto_retry_var.set(True) - - # Show error message with enhanced styling - if hasattr(self, 'connection_status'): - self.connection_status.configure( - text=f"⚠️ Warning: Could not load existing config: {str(e)[:50]}...", - fg=self.colors['warning'] - ) - - def save_config(self): - """Save configuration with enhanced feedback""" - try: - # Load existing config or create new one - try: - config = load_config() - except: - config = {} - - # Update with new values from the enhanced interface - if hasattr(self, 'screen_name_var'): - config['screen_name'] = self.screen_name_var.get() - if hasattr(self, 'server_ip_var'): - config['server_ip'] = self.server_ip_var.get() - if hasattr(self, 'port_var'): - config['port'] = self.port_var.get() - if hasattr(self, 'quickconnect_var'): - config['quickconnect_key'] = self.quickconnect_var.get() - if hasattr(self, 'screen_w_var'): - config['screen_w'] = self.screen_w_var.get() - if hasattr(self, 'screen_h_var'): - config['screen_h'] = self.screen_h_var.get() - - # Save advanced settings if they exist - if hasattr(self, 'refresh_interval_var'): - config['refresh_interval'] = self.refresh_interval_var.get() - if hasattr(self, 'hardware_accel_var'): - config['hardware_acceleration'] = self.hardware_accel_var.get() - if hasattr(self, 'cache_media_var'): - config['cache_media'] = self.cache_media_var.get() - if hasattr(self, 'auto_retry_var'): - config['auto_retry'] = self.auto_retry_var.get() - - # Save display settings - if hasattr(self, 'scaling_mode_var'): - config['scaling_mode'] = self.scaling_mode_var.get() - # Also update the main app's scaling mode - if hasattr(self.app, 'scaling_mode'): - self.app.scaling_mode = self.scaling_mode_var.get() - - # Ensure directory exists - config_dir = os.path.dirname(CONFIG_FILE) - os.makedirs(config_dir, exist_ok=True) - - with open(CONFIG_FILE, 'w') as f: - json.dump(config, f, indent=4) - - Logger.info(f"Enhanced configuration saved to {CONFIG_FILE}") - - # Show enhanced success message - self.show_enhanced_success_message("Configuration saved successfully!") - - # Ask if user wants to refresh playlist now - if messagebox.askyesno("Refresh Playlist", - "Configuration saved! Would you like to refresh the playlist from server now?", - icon='question'): - self.force_playlist_refresh() - - self.window.destroy() - - except Exception as e: - Logger.error(f"Failed to save enhanced configuration: {e}") - self.show_enhanced_error_message(f"Failed to save configuration: {e}") - - def show_enhanced_success_message(self, message): - """Show an enhanced success message with dark theme""" - success_window = tk.Toplevel(self.window) - success_window.title("Success") - success_window.geometry("400x150") - success_window.configure(bg=self.colors['bg_primary']) - success_window.transient(self.window) - success_window.grab_set() - success_window.resizable(False, False) - - # Center the window using helper method - self.center_window_on_screen(success_window, 400, 150) - - # Main frame - main_frame = tk.Frame(success_window, bg=self.colors['bg_primary'], padx=30, pady=20) - main_frame.pack(fill=tk.BOTH, expand=True) - - # Success icon - icon_label = tk.Label(main_frame, text="✅", font=('Segoe UI Emoji', 32), - fg=self.colors['success'], bg=self.colors['bg_primary']) - icon_label.pack(pady=(0, 10)) - - # Success message - msg_label = tk.Label(main_frame, text=message, font=('Segoe UI', 12, 'bold'), - fg=self.colors['text_primary'], bg=self.colors['bg_primary'], - wraplength=340, justify=tk.CENTER) - msg_label.pack(pady=(0, 20)) - - # OK button - ok_btn = self.create_action_button(main_frame, "OK", success_window.destroy, - self.colors['success']) - ok_btn.pack() - - # Auto-close after 3 seconds - success_window.after(3000, success_window.destroy) - - def show_enhanced_error_message(self, message): - """Show an enhanced error message with dark theme""" - error_window = tk.Toplevel(self.window) - error_window.title("Error") - error_window.geometry("400x150") - error_window.configure(bg=self.colors['bg_primary']) - error_window.transient(self.window) - error_window.grab_set() - error_window.resizable(False, False) - - # Center the window using helper method - self.center_window_on_screen(error_window, 400, 150) - - # Main frame - main_frame = tk.Frame(error_window, bg=self.colors['bg_primary'], padx=30, pady=20) - main_frame.pack(fill=tk.BOTH, expand=True) - - # Error icon - icon_label = tk.Label(main_frame, text="❌", font=('Segoe UI Emoji', 32), - fg=self.colors['danger'], bg=self.colors['bg_primary']) - icon_label.pack(pady=(0, 10)) - - # Error message - msg_label = tk.Label(main_frame, text=message, font=('Segoe UI', 11), - fg=self.colors['text_primary'], bg=self.colors['bg_primary'], - wraplength=340, justify=tk.CENTER) - msg_label.pack(pady=(0, 20)) - - # OK button - ok_btn = self.create_action_button(main_frame, "OK", error_window.destroy, - self.colors['danger']) - ok_btn.pack() - - def show_success_message(self, message): - """Show a modern success message""" - success_window = tk.Toplevel(self.window) - success_window.title("Success") - success_window.geometry("300x120") - success_window.configure(bg='#2d5a2d') - success_window.transient(self.window) - success_window.grab_set() - - # Center the window using helper method - self.center_window_on_screen(success_window, 300, 120) - - # Success icon and message - icon_label = tk.Label(success_window, text="✓", font=('Arial', 24, 'bold'), - fg='white', bg='#2d5a2d') - icon_label.pack(pady=10) - - msg_label = tk.Label(success_window, text=message, font=('Arial', 10), - fg='white', bg='#2d5a2d', wraplength=250) - msg_label.pack(pady=5) - - ok_btn = tk.Button(success_window, text="OK", command=success_window.destroy, - bg='#4d7d4d', fg='white', font=('Arial', 10, 'bold'), - relief=tk.FLAT, padx=20, pady=5) - ok_btn.pack(pady=10) - - def force_playlist_refresh(self): - """Force refresh of playlist from server""" - try: - # Show connection message - self.connection_status.configure(text="Refreshing playlist from server...") - self.window.update() - - # Fetch server playlist - server_playlist_data = fetch_server_playlist() - server_playlist = server_playlist_data.get('playlist', []) - server_version = server_playlist_data.get('version', 0) - - if server_playlist: - # Clean old files - local_playlist_data = load_local_playlist() - clean_unused_files(local_playlist_data.get('playlist', [])) - - # Download new content - download_media_files(server_playlist, server_version) - update_config_playlist_version(server_version) - - # Let the app know to reload - self.app.playlist = load_local_playlist().get('playlist', []) - self.app.current_index = 0 # Reset to beginning - - self.connection_status.configure(text=f"Playlist refreshed! Downloaded {len(server_playlist)} media files.") - - # Force the app to play the current media - if self.app.playlist: - self.app.play_current_media() - else: - self.connection_status.configure(text="Server returned empty playlist. Check server connection settings.") - - except Exception as e: - self.connection_status.configure(text=f"Error refreshing playlist: {str(e)[:50]}...") - Logger.error(f"Failed to refresh playlist: {e}") - - def test_connection(self): - """Test server connection""" - try: - self.connection_status.configure(text="Testing connection...") - self.window.update() - - server_ip = self.server_ip_var.get() - port = self.port_var.get() - screen_name = self.screen_name_var.get() - quickconnect = self.quickconnect_var.get() - - if not all([server_ip, port, screen_name, quickconnect]): - self.connection_status.configure(text="Please fill all connection fields") - return - - url = f"http://{server_ip}:{port}/api/playlists" - params = { - 'hostname': screen_name, - 'quickconnect_code': quickconnect - } - - response = requests.get(url, params=params, timeout=10) - - if response.status_code == 200: - data = response.json() - version = data.get('playlist_version', 'Unknown') - num_items = len(data.get('playlist', [])) - self.connection_status.configure( - text=f"✓ Connected! Server playlist version: {version}, {num_items} media items available" - ) - else: - self.connection_status.configure( - text=f"✗ Connection failed (Status: {response.status_code})" - ) - - except Exception as e: - self.connection_status.configure(text=f"✗ Connection error: {str(e)[:50]}...") - - def load_logs(self): - """Load recent log entries""" - try: - # Update to use the resources directory - log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt') - - if os.path.exists(log_file): - with open(log_file, 'r') as f: - lines = f.readlines() - - # Show last 20 lines - recent_lines = lines[-20:] if len(lines) > 20 else lines - - self.log_text.delete(1.0, tk.END) - self.log_text.insert(tk.END, ''.join(recent_lines)) - self.log_text.see(tk.END) - else: - self.log_text.delete(1.0, tk.END) - self.log_text.insert(tk.END, "No log file found") - - except Exception as e: - self.log_text.delete(1.0, tk.END) - self.log_text.insert(tk.END, f"Error loading logs: {e}") - - def load_playlist_view(self): - """Load playlist items into treeview""" - try: - # Clear existing items - for item in self.playlist_view.get_children(): - self.playlist_view.delete(item) - except Exception as e: - Logger.error(f"Failed to load playlist view: {e}") - messagebox.showerror("Error", f"Failed to load playlist: {e}") - - def show_video_placeholder(self, filename, duration): - """Show placeholder for video files""" - self.image_label.config(image='') - self.status_label.config(text="") # Clear any status text for cleaner display - - # Schedule next media - self.auto_advance_timer = self.root.after( - int(duration * 1000), - self.next_media - ) diff --git a/tkinter_app/src/tkinter_simple_player_old.py b/tkinter_app/src/tkinter_simple_player_old.py new file mode 100644 index 0000000..2c0e364 --- /dev/null +++ b/tkinter_app/src/tkinter_simple_player_old.py @@ -0,0 +1,934 @@ +import tkinter as tk +from tkinter import ttk, messagebox, simpledialog +import threading +import time +import os +import json +import datetime +from pathlib import Path +import subprocess +import sys +import requests # Required for server communication +import queue +import vlc # For video playback with hardware acceleration + +# Try importing PIL but provide fallback +try: + from PIL import Image, ImageTk + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False + print("WARNING: PIL not available. Image display functionality will be limited.") + +# Import existing functions +from python_functions import ( + load_local_playlist, download_media_files, clean_unused_files, + save_local_playlist, update_config_playlist_version, fetch_server_playlist, + load_config +) +from logging_config import Logger + +# Import virtual keyboard components +from virtual_keyboard import VirtualKeyboard, TouchOptimizedEntry, TouchOptimizedButton + +# Update the config file path to use the resources directory +CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'app_config.txt') + +from player_app import SimpleMediaPlayerApp + + def setup_window(self): + """Configure the main window""" + self.root.title("Simple Signage Player") + self.root.configure(bg='black') + + # Load window size from config + try: + config = load_config() + width = int(config.get('screen_w', 1920)) + height = int(config.get('screen_h', 1080)) + # Load scaling mode preference + self.scaling_mode = config.get('scaling_mode', 'fit') + except: + width, height = 800, 600 # Fallback size if config fails + self.scaling_mode = 'fit' # Default scaling mode + + self.root.geometry(f"{width}x{height}") + self.root.attributes('-fullscreen', True) + + # Bind events + self.root.bind('', self.on_key_press) + self.root.bind('', self.on_mouse_click) + self.root.bind('', self.on_mouse_motion) + self.root.focus_set() + + def setup_ui(self): + """Create the user interface""" + # Main content area - make sure it's black and fill the entire window + self.content_frame = tk.Frame(self.root, bg='black') + self.content_frame.pack(fill=tk.BOTH, expand=True) + + # Image display - expand to fill the entire window + self.image_label = tk.Label(self.content_frame, bg='black') + self.image_label.pack(fill=tk.BOTH, expand=True) + + # Status label - hidden by default + self.status_label = tk.Label( + self.content_frame, + bg='black', + fg='white', + font=('Arial', 24), + text="" + ) + # Don't place the status label by default to keep it hidden + + # Control panel + self.create_control_panel() + self.show_controls() + self.schedule_hide_controls() + + def create_control_panel(self): + """Create touch-optimized control panel with larger buttons""" + # Create control frame with larger size for touch + self.control_frame = tk.Frame( + self.root, + bg='#1a1a1a', # Dark background + bd=2, + relief=tk.RAISED, + padx=15, + pady=15 + ) + self.control_frame.place(relx=0.98, rely=0.98, anchor='se') + + # Touch-optimized button configuration + button_config = { + 'bg': '#333333', + 'fg': 'white', + 'activebackground': '#555555', + 'activeforeground': 'white', + 'relief': tk.FLAT, + 'borderwidth': 0, + 'width': 10, # Larger for touch + 'height': 3, # Larger for touch + 'font': ('Segoe UI', 10, 'bold'), # Larger font + 'cursor': 'hand2' + } + + # Previous button + self.prev_btn = tk.Button( + self.control_frame, + text="⏮ Prev", + command=self.previous_media, + **button_config + ) + self.prev_btn.grid(row=0, column=0, padx=5) + + # Play/Pause button + self.play_pause_btn = tk.Button( + self.control_frame, + text="⏸ Pause" if not self.is_paused else "▶ Play", + command=self.toggle_play_pause, + bg='#27ae60', # Green for play/pause + activebackground='#35d974', + **{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']} + ) + self.play_pause_btn.grid(row=0, column=1, padx=5) + + # Next button + self.next_btn = tk.Button( + self.control_frame, + text="Next ⏭", + command=self.next_media, + **button_config + ) + self.next_btn.grid(row=0, column=2, padx=5) + + # Settings button + self.settings_btn = tk.Button( + self.control_frame, + text="⚙️ Settings", + command=self.open_settings, + bg='#9b59b6', # Purple for settings + activebackground='#bb8fce', + **{k: v for k, v in button_config.items() if k not in ['bg', 'activebackground']} + ) + self.settings_btn.grid(row=0, column=3, padx=5) + + # Exit button with special styling + self.exit_btn = tk.Button( + self.control_frame, + text="❌ EXIT", + command=self.show_exit_dialog, + bg='#e74c3c', # Red background + fg='white', + activebackground='#ec7063', + activeforeground='white', + relief=tk.FLAT, + borderwidth=0, + width=8, + height=3, + font=('Segoe UI', 10, 'bold'), + cursor='hand2' + ) + self.exit_btn.grid(row=0, column=4, padx=5) + + # Add touch feedback to all buttons + for button in [self.prev_btn, self.play_pause_btn, self.next_btn, + self.settings_btn, self.exit_btn]: + self.add_touch_feedback_to_control_button(button) + + def scale_image_to_screen(self, img, screen_width, screen_height, mode='fit'): + """ + Scale image to screen with different modes: + - 'fit': Maintain aspect ratio, add black bars if needed (letterbox/pillarbox) + - 'fill': Maintain aspect ratio, crop if needed to fill entire screen + - 'stretch': Ignore aspect ratio, stretch to fill entire screen + """ + img_width, img_height = img.size + + if mode == 'stretch': + # Stretch to fill entire screen, ignoring aspect ratio + return img.resize((screen_width, screen_height), Image.LANCZOS), (0, 0) + + elif mode == 'fill': + # Maintain aspect ratio, crop to fill entire screen + screen_ratio = screen_width / screen_height + img_ratio = img_width / img_height + + if img_ratio > screen_ratio: + # Image is wider - scale by height and crop width + new_height = screen_height + new_width = int(screen_height * img_ratio) + x_offset = (screen_width - new_width) // 2 + y_offset = 0 + else: + # Image is taller - scale by width and crop height + new_width = screen_width + new_height = int(screen_width / img_ratio) + x_offset = 0 + y_offset = (screen_height - new_height) // 2 + + # Resize and crop + img_resized = img.resize((new_width, new_height), Image.LANCZOS) + + # Create final image and paste (this will crop automatically) + final_img = Image.new('RGB', (screen_width, screen_height), 'black') + + # Calculate crop area if image is larger than screen + if new_width > screen_width: + crop_x = (new_width - screen_width) // 2 + img_resized = img_resized.crop((crop_x, 0, crop_x + screen_width, new_height)) + x_offset = 0 + if new_height > screen_height: + crop_y = (new_height - screen_height) // 2 + img_resized = img_resized.crop((0, crop_y, new_width, crop_y + screen_height)) + y_offset = 0 + + final_img.paste(img_resized, (x_offset, y_offset)) + return final_img, (x_offset, y_offset) + + else: # mode == 'fit' (default) + # Maintain aspect ratio, add black bars if needed + screen_ratio = screen_width / screen_height + img_ratio = img_width / img_height + + if img_ratio > screen_ratio: + # Image is wider than screen - fit to width + new_width = screen_width + new_height = int(screen_width / img_ratio) + else: + # Image is taller than screen - fit to height + new_height = screen_height + new_width = int(screen_height * img_ratio) + + # Resize image + img_resized = img.resize((new_width, new_height), Image.LANCZOS) + + # Create black background and center the image + final_img = Image.new('RGB', (screen_width, screen_height), 'black') + x_offset = (screen_width - new_width) // 2 + y_offset = (screen_height - new_height) // 2 + final_img.paste(img_resized, (x_offset, y_offset)) + + return final_img, (x_offset, y_offset) + + def add_touch_feedback_to_control_button(self, button): + """Add touch feedback effects to control panel buttons""" + original_bg = button.cget('bg') + + def on_press(e): + button.configure(relief=tk.SUNKEN) + + def on_release(e): + button.configure(relief=tk.FLAT) + + def on_enter(e): + button.configure(relief=tk.RAISED) + + def on_leave(e): + button.configure(relief=tk.FLAT) + + button.bind("", on_press) + button.bind("", on_release) + button.bind("", on_enter) + button.bind("", on_leave) + + def initialize_playlist_from_server(self): + """Initialize the playlist from the server on startup with fallback to local playlist""" + # First try to load any existing local playlist as fallback + fallback_playlist = None + try: + local_playlist_data = load_local_playlist() + fallback_playlist = local_playlist_data.get('playlist', []) + if fallback_playlist: + Logger.info(f"Found fallback playlist with {len(fallback_playlist)} items") + except Exception as e: + Logger.warning(f"No fallback playlist available: {e}") + + # Show connection status + self.status_label.config(text="Connecting to server...\nPlease wait") + self.status_label.place(relx=0.5, rely=0.5, anchor='center') + self.root.update() + + # Load configuration + config = load_config() + server = config.get("server_ip", "") + host = config.get("screen_name", "") + quick = config.get("quickconnect_key", "") + port = config.get("port", "") + + Logger.info(f"Initializing with settings: server={server}, host={host}, port={port}") + + if not server or not host or not quick or not port: + Logger.warning("Missing server configuration, using fallback playlist") + self.status_label.place_forget() + self.load_fallback_playlist(fallback_playlist) + return + + # Attempt to fetch server playlist with timeout + server_connection_successful = False + try: + # Add connection timeout and retry logic + Logger.info("Attempting to connect to server...") + self.status_label.config(text="Connecting to server...\nAttempting connection") + self.root.update() + + server_playlist_data = fetch_server_playlist() + server_playlist = server_playlist_data.get('playlist', []) + server_version = server_playlist_data.get('version', 0) + + if server_playlist: + Logger.info(f"Server playlist found with {len(server_playlist)} items, version {server_version}") + server_connection_successful = True + + # Download media files and update local playlist + self.status_label.config(text="Downloading media files...\nPlease wait") + self.root.update() + + download_media_files(server_playlist, server_version) + update_config_playlist_version(server_version) + + # Load the updated local playlist + local_playlist_data = load_local_playlist() + self.playlist = local_playlist_data.get('playlist', []) + + if self.playlist: + Logger.info(f"Successfully loaded {len(self.playlist)} items from server") + self.status_label.place_forget() + self.play_current_media() + return + else: + Logger.warning("Server playlist was empty, falling back to local playlist") + else: + Logger.warning("Server returned empty playlist, falling back to local playlist") + + except requests.exceptions.ConnectTimeout: + Logger.error("Server connection timeout, using fallback playlist") + except requests.exceptions.ConnectionError: + Logger.error("Cannot connect to server, using fallback playlist") + except requests.exceptions.Timeout: + Logger.error("Server request timeout, using fallback playlist") + except Exception as e: + Logger.error(f"Failed to fetch playlist from server: {e}, using fallback playlist") + + # If we reach here, server connection failed - use fallback + if not server_connection_successful: + self.status_label.config(text="Server unavailable\nLoading last playlist...") + self.root.update() + time.sleep(1) # Brief pause to show message + + self.status_label.place_forget() + self.load_fallback_playlist(fallback_playlist) + + def load_fallback_playlist(self, fallback_playlist): + """Load fallback playlist when server is unavailable""" + if fallback_playlist and len(fallback_playlist) > 0: + self.playlist = fallback_playlist + Logger.info(f"Loaded fallback playlist with {len(self.playlist)} items") + self.play_current_media() + else: + Logger.warning("No fallback playlist available, loading demo content") + self.load_demo_or_local_playlist() + + def load_demo_or_local_playlist(self): + """Load either the existing local playlist or demo content""" + # First try to load the local playlist + local_playlist_data = load_local_playlist() + self.playlist = local_playlist_data.get('playlist', []) + + if self.playlist: + Logger.info(f"Loaded existing local playlist with {len(self.playlist)} items") + self.play_current_media() + return + + # If no local playlist, try loading demo content + Logger.info("No local playlist found, loading demo content") + self.create_demo_content() + + if self.playlist: + self.play_current_media() + else: + self.show_no_content_message() + + def create_demo_content(self): + """Create demo content for testing""" + demo_images = [] + + # First check static/resurse folder for any media + static_dir = os.path.join(os.path.dirname(__file__), 'static', 'resurse') + if os.path.exists(static_dir): + for file in os.listdir(static_dir): + if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): + full_path = os.path.join(static_dir, file) + demo_images.append({ + 'file_name': file, + 'url': full_path, + 'duration': 5 + }) + + # If no files found in static/resurse, look in Resurse folder + if not demo_images: + demo_dir = './Resurse' + if os.path.exists(demo_dir): + for file in os.listdir(demo_dir): + if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')): + demo_images.append({ + 'file_name': file, + 'url': os.path.join(demo_dir, file), + 'duration': 5 + }) + + if demo_images: + self.playlist = demo_images + Logger.info(f"Created demo playlist with {len(demo_images)} images") + else: + # Create a text-only demo if no images found + self.playlist = [{ + 'file_name': 'Demo Text', + 'url': 'text://Welcome to Tkinter Media Player!\n\nPlease configure server settings', + 'duration': 5 + }] + + def show_no_content_message(self): + """Show message when no content is available""" + self.image_label.config(image='') + self.status_label.config( + text="No media content available.\nPress Settings to configure server connection." + ) + self.status_label.place(relx=0.5, rely=0.5, anchor='center') + + def show_error_message(self, message): + """Show error message""" + self.image_label.config(image='') + self.status_label.config(text=f"Error: {message}") + self.status_label.place(relx=0.5, rely=0.5, anchor='center') + + def play_current_media(self): + """Play the current media item""" + if not self.playlist or self.current_index >= len(self.playlist): + self.show_no_content_message() + return + + media = self.playlist[self.current_index] + file_path = media.get('url', '') + file_name = media.get('file_name', '') + duration = media.get('duration', 10) + + # Handle relative paths by converting to absolute paths + if file_path.startswith('static/resurse/'): + # Convert relative path to absolute + absolute_path = os.path.join(os.path.dirname(__file__), file_path) + file_path = absolute_path + + Logger.info(f"Playing media: {file_name} from {file_path}") + + # Log media start + self.log_event(file_name, "STARTED") + + # Cancel existing timers + self.cancel_timers() + + # Handle different media types + if file_path.startswith('text://'): + self.show_text_content(file_path[7:], duration) + elif file_path.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')): + self.play_video(file_path) + elif os.path.exists(file_path) and file_path.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp')): + self.show_image(file_path, duration) + else: + Logger.error(f"Unsupported or missing media: {file_path}") + self.status_label.config(text=f"Missing or unsupported media:\n{file_name}") + # Schedule next media after short delay + self.auto_advance_timer = self.root.after(5000, self.next_media) + + def play_video(self, file_path): + """Play video file using system VLC as a subprocess for robust hardware acceleration and stability.""" + self.status_label.place_forget() + def run_vlc_subprocess(): + try: + Logger.info(f"Starting system VLC subprocess for video: {file_path}") + # Build VLC command + vlc_cmd = [ + 'cvlc', + '--fullscreen', + '--no-osd', + '--no-video-title-show', + '--play-and-exit', + '--quiet', + file_path + ] + proc = subprocess.Popen(vlc_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + proc.wait() + Logger.info(f"VLC subprocess finished: {file_path}") + except Exception as e: + Logger.error(f"VLC subprocess error: {e}") + finally: + self.root.after_idle(lambda: setattr(self, 'auto_advance_timer', self.root.after(1000, self.next_media))) + threading.Thread(target=run_vlc_subprocess, daemon=True).start() + + def _update_video_frame(self, photo): + """Update video frame from main thread""" + try: + self.image_label.config(image=photo) + self.image_label.image = photo # Keep reference + except Exception as e: + Logger.error(f"Error updating video frame: {e}") + + def _show_video_error(self, error_msg): + """Show video error from main thread""" + try: + self.status_label.config(text=f"Video Error:\n{error_msg}") + self.status_label.place(relx=0.5, rely=0.5, anchor='center') + self.auto_advance_timer = self.root.after(5000, self.next_media) + except Exception as e: + Logger.error(f"Error showing video error: {e}") + self.auto_advance_timer = self.root.after(5000, self.next_media) + + def show_text_content(self, text, duration): + """Display text content""" + self.image_label.config(image='') + self.status_label.config(text=text) + + # Schedule next media + self.auto_advance_timer = self.root.after( + int(duration * 1000), + self.next_media + ) + + def show_image(self, file_path, duration): + """Display an image in full screen, properly fitted to screen size""" + try: + # Hide status label and clear any previous text + self.status_label.place_forget() + self.status_label.config(text="") + + if PIL_AVAILABLE: + # Use PIL for better image handling + img = Image.open(file_path) + original_size = img.size + + # Get actual screen dimensions + screen_width = self.root.winfo_width() + screen_height = self.root.winfo_height() + + # Ensure we have valid screen dimensions + if screen_width <= 1 or screen_height <= 1: + screen_width = 1920 # Default fallback + screen_height = 1080 + + # Scale image using the scaling helper + final_img, offset = self.scale_image_to_screen(img, screen_width, screen_height, self.scaling_mode) + + # Convert to PhotoImage + photo = ImageTk.PhotoImage(final_img) + + # Clear previous image and display new one + self.image_label.config(image=photo) + self.image_label.image = photo # Keep reference + + Logger.info(f"Successfully displayed image: {os.path.basename(file_path)} " + f"(Original: {original_size}, Screen: {screen_width}x{screen_height}, " + f"Mode: {self.scaling_mode}, Offset: {offset})") + else: + # Fall back to basic text display if PIL not available + self.image_label.config(image='') + self.status_label.config(text=f"IMAGE: {os.path.basename(file_path)}\n\n(Install PIL for image display)") + self.status_label.place(relx=0.5, rely=0.5, anchor='center') + Logger.warning("PIL not available - showing text placeholder for image") + + # Schedule next media + self.auto_advance_timer = self.root.after( + int(duration * 1000), + self.next_media + ) + + except Exception as e: + Logger.error(f"Failed to show image {file_path}: {e}") + self.image_label.config(image='') + self.status_label.config(text=f"Image Error:\n{os.path.basename(file_path)}\n{str(e)}") + self.status_label.place(relx=0.5, rely=0.5, anchor='center') + self.auto_advance_timer = self.root.after(5000, self.next_media) + + def next_media(self): + """Move to next media""" + self.cancel_timers() + + if not self.playlist: + return + + self.current_index = (self.current_index + 1) % len(self.playlist) + + # Check for playlist updates at end of cycle + if self.current_index == 0: + threading.Thread(target=self.check_playlist_updates, daemon=True).start() + + self.play_current_media() + + def previous_media(self): + """Move to previous media""" + self.cancel_timers() + + if not self.playlist: + return + + self.current_index = (self.current_index - 1) % len(self.playlist) + self.play_current_media() + + def toggle_play_pause(self): + """Toggle play/pause state""" + self.is_paused = not self.is_paused + + if self.is_paused: + self.play_pause_btn.config(text="▶ Play") + self.cancel_timers() + else: + self.play_pause_btn.config(text="⏸ Pause") + # Resume current media + self.play_current_media() + + Logger.info(f"Media {'paused' if self.is_paused else 'resumed'}") + + def cancel_timers(self): + """Cancel all active timers""" + if self.auto_advance_timer: + self.root.after_cancel(self.auto_advance_timer) + self.auto_advance_timer = None + + def show_controls(self): + """Show control panel""" + if self.control_frame: + self.control_frame.place(relx=0.98, rely=0.98, anchor='se') + + def hide_controls(self): + """Hide control panel""" + if self.control_frame: + self.control_frame.place_forget() + + def schedule_hide_controls(self): + """Schedule hiding controls after delay""" + if self.hide_controls_timer: + self.root.after_cancel(self.hide_controls_timer) + self.hide_controls_timer = self.root.after(10000, self.hide_controls) + + def on_mouse_click(self, event): + """Handle mouse clicks""" + self.show_controls() + self.schedule_hide_controls() + + def on_mouse_motion(self, event): + """Handle mouse motion""" + self.show_controls() + self.schedule_hide_controls() + + def on_key_press(self, event): + """Handle keyboard events""" + key = event.keysym.lower() + + if key == 'f': + self.toggle_fullscreen() + elif key == 'space': + self.toggle_play_pause() + elif key == 'left': + self.previous_media() + elif key == 'right': + self.next_media() + elif key == 'escape': + self.show_exit_dialog() + elif key == '1': + self.set_scaling_mode('fit') + elif key == '2': + self.set_scaling_mode('fill') + elif key == '3': + self.set_scaling_mode('stretch') + elif event.state & 0x4: # Ctrl key pressed + if key == 's': + self.open_settings() + + self.show_controls() + self.schedule_hide_controls() + + def set_scaling_mode(self, mode): + """Change the scaling mode and refresh current media""" + old_mode = self.scaling_mode + self.scaling_mode = mode + Logger.info(f"Scaling mode changed from '{old_mode}' to '{mode}'") + + # Show temporary notification + self.status_label.config(text=f"Scaling Mode: {mode.title()}\n" + f"1=Fit 2=Fill 3=Stretch") + self.status_label.place(relx=0.5, rely=0.05, anchor='center') + + # Hide notification after 2 seconds + self.root.after(2000, lambda: self.status_label.place_forget()) + + # Refresh current media with new scaling + if self.playlist and 0 <= self.current_index < len(self.playlist): + self.cancel_timers() + self.play_current_media() + + def toggle_fullscreen(self): + """Toggle fullscreen mode""" + self.is_fullscreen = not self.is_fullscreen + self.root.attributes('-fullscreen', self.is_fullscreen) + + def open_settings(self): + """Open settings window""" + if hasattr(self, 'settings_window') and self.settings_window and self.settings_window.winfo_exists(): + self.settings_window.lift() + return + + # Pause media playback when opening settings + if not self.is_paused: + self.toggle_play_pause() + + self.settings_window = SettingsWindow(self.root, self) + + # Add a callback to resume playback when the settings window is closed + def on_settings_close(): + if self.is_paused: + self.toggle_play_pause() + + self.settings_window.protocol("WM_DELETE_WINDOW", on_settings_close) + + def show_exit_dialog(self): + """Show modern password-protected exit dialog""" + try: + config = load_config() + quickconnect_key = config.get('quickconnect_key', '') + except: + quickconnect_key = '' + + # Create modern exit dialog + exit_dialog = tk.Toplevel(self.root) + exit_dialog.title("Exit Application") + exit_dialog.geometry("400x200") + exit_dialog.configure(bg='#2d2d2d') + exit_dialog.transient(self.root) + exit_dialog.grab_set() + exit_dialog.resizable(False, False) + + # Center the dialog using helper method + self.center_dialog_on_screen(exit_dialog, 400, 200) + + # Header with icon + header_frame = tk.Frame(exit_dialog, bg='#cc0000', height=60) + header_frame.pack(fill=tk.X) + header_frame.pack_propagate(False) + + icon_label = tk.Label(header_frame, text="⚠", font=('Arial', 20, 'bold'), + fg='white', bg='#cc0000') + icon_label.pack(side=tk.LEFT, padx=15, pady=15) + + title_label = tk.Label(header_frame, text="Exit Application", + font=('Arial', 14, 'bold'), fg='white', bg='#cc0000') + title_label.pack(side=tk.LEFT, pady=15) + + # Content frame + content_frame = tk.Frame(exit_dialog, bg='#2d2d2d', padx=20, pady=20) + content_frame.pack(fill=tk.BOTH, expand=True) + + # Password prompt + prompt_label = tk.Label(content_frame, text="Enter password to exit:", + font=('Arial', 11), fg='white', bg='#2d2d2d') + prompt_label.pack(pady=(0, 10)) + + # Password entry + password_var = tk.StringVar() + password_entry = tk.Entry(content_frame, textvariable=password_var, + font=('Arial', 11), show='*', width=25, + bg='#404040', fg='white', insertbackground='white', + relief=tk.FLAT, bd=5) + password_entry.pack(pady=(0, 15)) + password_entry.focus_set() + + # Button frame + button_frame = tk.Frame(content_frame, bg='#2d2d2d') + button_frame.pack(fill=tk.X) + + def check_password(): + if password_var.get() == quickconnect_key: + exit_dialog.destroy() + self.exit_application() + elif password_var.get(): # Only show error if password was entered + # Show error in red + error_label.config(text="✗ Incorrect password", fg='#ff4444') + password_entry.delete(0, tk.END) + password_entry.focus_set() + + def cancel_exit(): + exit_dialog.destroy() + + # Error label (hidden initially) + error_label = tk.Label(content_frame, text="", font=('Arial', 9), + bg='#2d2d2d') + error_label.pack() + + # Buttons + cancel_btn = tk.Button(button_frame, text="Cancel", command=cancel_exit, + bg='#555555', fg='white', font=('Arial', 10, 'bold'), + relief=tk.FLAT, padx=20, pady=8, width=10) + cancel_btn.pack(side=tk.RIGHT, padx=(10, 0)) + + exit_btn = tk.Button(button_frame, text="Exit", command=check_password, + bg='#cc0000', fg='white', font=('Arial', 10, 'bold'), + relief=tk.FLAT, padx=20, pady=8, width=10) + exit_btn.pack(side=tk.RIGHT) + + # Bind Enter key to check password + password_entry.bind('', lambda e: check_password()) + exit_dialog.bind('', lambda e: cancel_exit()) + + def exit_application(self): + """Exit the application""" + Logger.info("Application exit requested") + self.running = False + self.root.quit() + self.root.destroy() + + def center_dialog_on_screen(self, dialog, width, height): + """Center a dialog window on screen regardless of screen size""" + dialog.update_idletasks() # Ensure geometry is calculated + screen_width = dialog.winfo_screenwidth() + screen_height = dialog.winfo_screenheight() + + # Calculate center position + center_x = int((screen_width - width) / 2) + center_y = int((screen_height - height) / 2) + + # Ensure the dialog doesn't go off-screen on smaller displays + center_x = max(0, min(center_x, screen_width - width)) + center_y = max(0, min(center_y, screen_height - height)) + + dialog.geometry(f"{width}x{height}+{center_x}+{center_y}") + + # Bring to front and focus + dialog.lift() + dialog.focus_force() + + return center_x, center_y + + def check_playlist_updates(self): + """Check for playlist updates from server with fallback protection""" + try: + config = load_config() + local_version = config.get('playlist_version', 0) + + server_playlist_data = fetch_server_playlist() + server_version = server_playlist_data.get('version', 0) + + if server_version > local_version: + Logger.info(f"Updating playlist: {local_version} -> {server_version}") + + # Clean old files + local_playlist_data = load_local_playlist() + clean_unused_files(local_playlist_data.get('playlist', [])) + + # Download new content + download_media_files( + server_playlist_data.get('playlist', []), + server_version + ) + + # Update local playlist + local_playlist_data = load_local_playlist() + self.playlist = local_playlist_data.get('playlist', []) + + # Reset to beginning of playlist + self.current_index = 0 + + Logger.info("Playlist updated successfully") + + # Continue with current media after update + self.play_current_media() + else: + Logger.info("No playlist updates available") + + except requests.exceptions.ConnectTimeout: + Logger.warning("Server connection timeout during update check - continuing with current playlist") + except requests.exceptions.ConnectionError: + Logger.warning("Cannot connect to server during update check - continuing with current playlist") + except requests.exceptions.Timeout: + Logger.warning("Server request timeout during update check - continuing with current playlist") + except Exception as e: + Logger.warning(f"Failed to check playlist updates: {e} - continuing with current playlist") + + def log_event(self, file_name, event): + """Log media events""" + try: + timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + log_message = f"{timestamp} - {event}: {file_name}\n" + + # Update the log file path to the resources directory + log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources', 'log.txt') + with open(log_file, 'a') as f: + f.write(log_message) + + except Exception as e: + Logger.error(f"Failed to log event: {e}") + + def start_periodic_checks(self): + """Start periodic playlist checks""" + def check_loop(): + """Background thread for periodic checks""" + while self.running: + try: + time.sleep(300) # Check every 5 minutes + if self.running: + self.check_playlist_updates() + except Exception as e: + Logger.error(f"Error in periodic check: {e}") + + # Start background thread + threading.Thread(target=check_loop, daemon=True).start() + + def run(self): + """Start the application""" + Logger.info("Starting Simple Tkinter Media Player") + try: + self.root.mainloop() + except KeyboardInterrupt: + self.exit_application() + except Exception as e: + Logger.error(f"Application error: {e}") + print(f"Error: {e}") + +