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 0000000..bb33674 Binary files /dev/null and b/tkinter_app/src/__pycache__/player_app.cpython-311.pyc differ diff --git a/tkinter_app/src/__pycache__/python_functions.cpython-311.pyc b/tkinter_app/src/__pycache__/python_functions.cpython-311.pyc index 4f0edde..02b5866 100644 Binary files a/tkinter_app/src/__pycache__/python_functions.cpython-311.pyc and b/tkinter_app/src/__pycache__/python_functions.cpython-311.pyc differ diff --git a/tkinter_app/src/__pycache__/settings_screen.cpython-311.pyc b/tkinter_app/src/__pycache__/settings_screen.cpython-311.pyc new file mode 100644 index 0000000..bdd2def Binary files /dev/null and b/tkinter_app/src/__pycache__/settings_screen.cpython-311.pyc differ 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}") + +