From 4c3ddbef73f91779b90c224614082812b81d1128 Mon Sep 17 00:00:00 2001 From: Kiwy Signage Player Date: Wed, 10 Dec 2025 00:09:20 +0200 Subject: [PATCH] updated to correctly play the playlist and reload images after edited --- .player_stop_requested | 1 + src/get_playlists_v2.py | 49 +++++++++++- src/main.py | 162 ++++++++++++++++++++++++++++++++++------ 3 files changed, 186 insertions(+), 26 deletions(-) create mode 100644 .player_stop_requested diff --git a/.player_stop_requested b/.player_stop_requested new file mode 100644 index 0000000..bb64142 --- /dev/null +++ b/.player_stop_requested @@ -0,0 +1 @@ +User requested exit via password \ No newline at end of file diff --git a/src/get_playlists_v2.py b/src/get_playlists_v2.py index e9b134c..ac9327b 100644 --- a/src/get_playlists_v2.py +++ b/src/get_playlists_v2.py @@ -271,8 +271,53 @@ def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, kee os.remove(filepath) logger.info(f"🗑️ Deleted old playlist: {f}") - # TODO: Clean up unused media files - logger.info(f"✅ Cleanup complete (kept {keep_versions} latest versions)") + # Clean up unused media files + logger.info("🔍 Checking for unused media files...") + + # Get list of media files referenced in current playlist + current_playlist_file = os.path.join(playlist_dir, f'server_playlist_v{current_version}.json') + referenced_files = set() + + if os.path.exists(current_playlist_file): + try: + with open(current_playlist_file, 'r') as f: + playlist_data = json.load(f) + for item in playlist_data.get('playlist', []): + file_name = item.get('file_name', '') + if file_name: + referenced_files.add(file_name) + + logger.info(f"📋 Current playlist references {len(referenced_files)} media files") + + # Get all files in media directory (excluding edited_media subfolder) + if os.path.exists(media_dir): + media_files = [f for f in os.listdir(media_dir) + if os.path.isfile(os.path.join(media_dir, f))] + + deleted_count = 0 + for media_file in media_files: + # Skip if file is in current playlist + if media_file in referenced_files: + continue + + # Delete unreferenced file + media_path = os.path.join(media_dir, media_file) + try: + os.remove(media_path) + logger.info(f"🗑️ Deleted unused media: {media_file}") + deleted_count += 1 + except Exception as e: + logger.warning(f"⚠️ Could not delete {media_file}: {e}") + + if deleted_count > 0: + logger.info(f"✅ Deleted {deleted_count} unused media files") + else: + logger.info("✅ No unused media files to delete") + + except Exception as e: + logger.error(f"❌ Error reading playlist for media cleanup: {e}") + + logger.info(f"✅ Cleanup complete (kept {keep_versions} latest playlist versions)") except Exception as e: logger.error(f"❌ Error during cleanup: {e}") diff --git a/src/main.py b/src/main.py index 36384ff..fc9ae31 100644 --- a/src/main.py +++ b/src/main.py @@ -382,13 +382,26 @@ class EditPopup(Popup): self.user_card_data = user_card_data # Store card data to send to server on save self.drawing_layer = None - # Pause playback + # Pause playback (without auto-resume timer) self.was_paused = self.player.is_paused if not self.was_paused: self.player.is_paused = True Clock.unschedule(self.player.next_media) + + # Cancel auto-resume timer if one exists (don't want auto-resume during editing) + if self.player.auto_resume_event: + Clock.unschedule(self.player.auto_resume_event) + self.player.auto_resume_event = None + Logger.info("EditPopup: Cancelled auto-resume timer") + + # Update button icon to play (to show it's paused) + self.player.ids.play_pause_btn.background_normal = self.player.resources_path + '/play.png' + self.player.ids.play_pause_btn.background_down = self.player.resources_path + '/play.png' + if self.player.current_widget and isinstance(self.player.current_widget, Video): self.player.current_widget.state = 'pause' + + Logger.info("EditPopup: ⏸ Paused playback (no auto-resume) for editing") # Show cursor try: @@ -703,6 +716,32 @@ class EditPopup(Popup): # Export only the visible content (image + drawings, no toolbars) self.content.export_to_png(output_path) + Logger.info(f"EditPopup: Saved edited image to {output_path}") + + # ALSO overwrite the original image with edited content + Logger.info(f"EditPopup: Overwriting original image at {self.image_path}") + import shutil + import time + + # Get original file info before overwrite + orig_size = os.path.getsize(self.image_path) + orig_mtime = os.path.getmtime(self.image_path) + + # Overwrite the file + shutil.copy2(output_path, self.image_path) + + # Force file system sync to ensure data is written to disk + os.sync() + + # Verify the overwrite + new_size = os.path.getsize(self.image_path) + new_mtime = os.path.getmtime(self.image_path) + + Logger.info(f"EditPopup: ✓ File overwritten:") + Logger.info(f" - Size: {orig_size} -> {new_size} bytes (changed: {new_size != orig_size})") + Logger.info(f" - Modified time: {orig_mtime} -> {new_mtime} (changed: {new_mtime > orig_mtime})") + Logger.info(f"EditPopup: ✓ File synced to disk") + # Restore toolbars self.top_toolbar.opacity = 1 self.right_sidebar.opacity = 1 @@ -711,9 +750,7 @@ class EditPopup(Popup): json_filename = self._save_metadata(edited_dir, new_name, base_name, new_version if version_match else 1, output_filename) - Logger.info(f"EditPopup: Saved edited image to {output_path}") - - # Upload to server asynchronously (non-blocking) + # Upload to server in background (continues after popup closes) import threading upload_thread = threading.Thread( target=self._upload_to_server, @@ -721,10 +758,42 @@ class EditPopup(Popup): daemon=True ) upload_thread.start() + Logger.info(f"EditPopup: Background upload thread started") - # Show confirmation - self.title = f'Saved as {output_filename}' - Clock.schedule_once(lambda dt: self.dismiss(), 1) + # NOW show saving popup AFTER everything is done + def show_saving_and_dismiss(dt): + from kivy.uix.popup import Popup + from kivy.uix.label import Label + + # Create label with background + save_label = Label( + text='✓ Saved! Reloading player...', + font_size='36sp', + color=(1, 1, 1, 1), + bold=True + ) + + saving_popup = Popup( + title='', + content=save_label, + size_hint=(0.8, 0.3), + auto_dismiss=False, + separator_height=0, + background_color=(0.2, 0.7, 0.2, 0.95) # Green background + ) + saving_popup.open() + Logger.info("EditPopup: Saving confirmation popup opened") + + # Dismiss both popups after 2 seconds + def dismiss_all(dt): + saving_popup.dismiss() + Logger.info(f"EditPopup: Dismissing to resume playback...") + self.dismiss() + + Clock.schedule_once(dismiss_all, 2.0) + + # Small delay to ensure UI is ready, then show popup + Clock.schedule_once(show_saving_and_dismiss, 0.1) except Exception as e: Logger.error(f"EditPopup: Error in export: {e}") @@ -734,6 +803,8 @@ class EditPopup(Popup): # Restore toolbars self.top_toolbar.opacity = 1 self.right_sidebar.opacity = 1 + # Still dismiss on error after brief delay + Clock.schedule_once(lambda dt: self.dismiss(), 1) Clock.schedule_once(do_export, 0.1) return @@ -851,16 +922,35 @@ class EditPopup(Popup): self.dismiss() def on_popup_dismiss(self, *args): - """Resume playback when popup closes""" - # Resume playback if it wasn't paused before + """Resume playback when popup closes - reload current image and continue""" + from kivy.clock import Clock + + # Force remove current widget immediately + if self.player.current_widget: + Logger.info("EditPopup: Removing current widget to force reload") + self.player.ids.content_area.remove_widget(self.player.current_widget) + self.player.current_widget = None + Logger.info("EditPopup: ✓ Widget removed, ready for fresh load") + + # Resume playback if it wasn't paused before editing if not self.was_paused: self.player.is_paused = False - self.player.play_current_media() + + # Update button icon to pause (to show it's playing) + self.player.ids.play_pause_btn.background_normal = self.player.resources_path + '/pause.png' + self.player.ids.play_pause_btn.background_down = self.player.resources_path + '/pause.png' + + # Add delay to ensure file write is complete and synced + def reload_media(dt): + Logger.info("EditPopup: ▶ Resuming playback and reloading edited image (force_reload=True)") + self.player.play_current_media(force_reload=True) + + Clock.schedule_once(reload_media, 0.5) + else: + Logger.info("EditPopup: Dismissed, keeping paused state") # Restart control hide timer self.player.schedule_hide_controls() - - Logger.info("EditPopup: Dismissed, playback resumed") # Custom keyboard container with close button class KeyboardContainer(BoxLayout): @@ -1656,8 +1746,12 @@ class SignagePlayer(Widget): self.current_index = 0 self.play_current_media() - def play_current_media(self): - """Play the current media item""" + def play_current_media(self, force_reload=False): + """Play the current media item + + Args: + force_reload: If True, clears image cache before loading (for edited images) + """ # Don't play if paused (unless we're explicitly resuming) if self.is_paused: Logger.debug(f"SignagePlayer: Skipping play_current_media - player is paused") @@ -1719,7 +1813,7 @@ class SignagePlayer(Widget): elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']: # Image file Logger.info(f"SignagePlayer: Media type: IMAGE") - self.play_image(media_path, duration) + self.play_image(media_path, duration, force_reload=force_reload) else: Logger.warning(f"SignagePlayer: ❌ Unsupported media type: {file_extension}") Logger.warning(f"SignagePlayer: Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif/.webp") @@ -1819,17 +1913,37 @@ class SignagePlayer(Widget): except Exception as e: Logger.warning(f"SignagePlayer: Could not log video info: {e}") - def play_image(self, image_path, duration): + def play_image(self, image_path, duration, force_reload=False): """Play an image file""" try: - Logger.info(f"SignagePlayer: Creating AsyncImage widget...") - self.current_widget = AsyncImage( - source=image_path, - allow_stretch=True, - keep_ratio=True, # Maintain aspect ratio - size_hint=(1, 1), - pos_hint={'center_x': 0.5, 'center_y': 0.5} - ) + # Log file info before loading + file_size = os.path.getsize(image_path) + file_mtime = os.path.getmtime(image_path) + Logger.info(f"SignagePlayer: Loading image: {os.path.basename(image_path)}") + Logger.info(f" - Size: {file_size:,} bytes, Modified: {file_mtime}") + + if force_reload: + Logger.info(f"SignagePlayer: Force reload - using Image widget (no async cache)") + # Use regular Image widget instead of AsyncImage to bypass all caching + from kivy.uix.image import Image + self.current_widget = Image( + source=image_path, + allow_stretch=True, + keep_ratio=True, + size_hint=(1, 1), + pos_hint={'center_x': 0.5, 'center_y': 0.5} + ) + # Force reload the texture + self.current_widget.reload() + else: + Logger.info(f"SignagePlayer: Creating AsyncImage widget...") + self.current_widget = AsyncImage( + source=image_path, + allow_stretch=True, + keep_ratio=True, # Maintain aspect ratio + size_hint=(1, 1), + pos_hint={'center_x': 0.5, 'center_y': 0.5} + ) Logger.info(f"SignagePlayer: Adding image widget to content area...") self.ids.content_area.add_widget(self.current_widget) # Schedule next media after duration (unschedule first to prevent overlaps)