diff --git a/src/get_playlists.py b/src/get_playlists.py deleted file mode 100644 index 34e453c..0000000 --- a/src/get_playlists.py +++ /dev/null @@ -1,346 +0,0 @@ -import os -import json -import requests -import bcrypt -import re -import datetime -import logging - -# Set up logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - -def send_player_feedback(config, message, status="active", playlist_version=None, error_details=None): - """ - Send feedback to the server about player status. - - Args: - config (dict): Configuration containing server details - message (str): Main feedback message - status (str): Player status - "active", "playing", "error", "restarting" - playlist_version (int, optional): Current playlist version being played - error_details (str, optional): Error details if status is "error" - - Returns: - bool: True if feedback sent successfully, False otherwise - """ - try: - server = config.get("server_ip", "") - host = config.get("screen_name", "") - quick = config.get("quickconnect_key", "") - port = config.get("port", "") - - # Construct server URL - # Remove protocol if already present - server_clean = server.replace('http://', '').replace('https://', '') - ip_pattern = r'^\d+\.\d+\.\d+\.\d+$' - if re.match(ip_pattern, server_clean): - feedback_url = f'http://{server_clean}:{port}/api/player-feedback' - else: - # Use original server if it has protocol, otherwise add http:// - if server.startswith(('http://', 'https://')): - feedback_url = f'{server}/api/player-feedback' - else: - feedback_url = f'http://{server}/api/player-feedback' - - # Prepare feedback data - feedback_data = { - 'hostname': host, - 'quickconnect_code': quick, - 'message': message, - 'status': status, - 'timestamp': datetime.datetime.now().isoformat(), - 'playlist_version': playlist_version, - 'error_details': error_details - } - - logger.info(f"Sending feedback to {feedback_url}: {feedback_data}") - - # Send POST request - response = requests.post(feedback_url, json=feedback_data, timeout=10) - - if response.status_code == 200: - logger.info(f"Feedback sent successfully: {message}") - return True - else: - logger.warning(f"Feedback failed with status {response.status_code}: {response.text}") - return False - - except requests.exceptions.RequestException as e: - logger.error(f"Failed to send feedback: {e}") - return False - except Exception as e: - logger.error(f"Unexpected error sending feedback: {e}") - return False - -def send_playlist_check_feedback(config, playlist_version=None): - """ - Send feedback when playlist is checked for updates. - - Args: - config (dict): Configuration containing server details - playlist_version (int, optional): Current playlist version - - Returns: - bool: True if feedback sent successfully, False otherwise - """ - player_name = config.get("screen_name", "unknown") - version_info = f"v{playlist_version}" if playlist_version else "unknown" - message = f"player {player_name}, is active, Playing {version_info}" - - return send_player_feedback( - config=config, - message=message, - status="active", - playlist_version=playlist_version - ) - -def send_playlist_restart_feedback(config, playlist_version=None): - """ - Send feedback when playlist loop ends and restarts. - - Args: - config (dict): Configuration containing server details - playlist_version (int, optional): Current playlist version - - Returns: - bool: True if feedback sent successfully, False otherwise - """ - player_name = config.get("screen_name", "unknown") - version_info = f"v{playlist_version}" if playlist_version else "unknown" - message = f"player {player_name}, playlist loop completed, restarting {version_info}" - - return send_player_feedback( - config=config, - message=message, - status="restarting", - playlist_version=playlist_version - ) - -def send_player_error_feedback(config, error_message, playlist_version=None): - """ - Send feedback when an error occurs in the player. - - Args: - config (dict): Configuration containing server details - error_message (str): Description of the error - playlist_version (int, optional): Current playlist version - - Returns: - bool: True if feedback sent successfully, False otherwise - """ - player_name = config.get("screen_name", "unknown") - message = f"player {player_name}, error occurred" - - return send_player_feedback( - config=config, - message=message, - status="error", - playlist_version=playlist_version, - error_details=error_message - ) - -def send_playing_status_feedback(config, playlist_version=None, current_media=None): - """ - Send feedback about current playing status. - - Args: - config (dict): Configuration containing server details - playlist_version (int, optional): Current playlist version - current_media (str, optional): Currently playing media file - - Returns: - bool: True if feedback sent successfully, False otherwise - """ - player_name = config.get("screen_name", "unknown") - version_info = f"v{playlist_version}" if playlist_version else "unknown" - media_info = f" - {current_media}" if current_media else "" - message = f"player {player_name}, is active, Playing {version_info}{media_info}" - - return send_player_feedback( - config=config, - message=message, - status="playing", - playlist_version=playlist_version - ) - -def fetch_server_playlist(config): - """Fetch the updated playlist from the server using a config dict.""" - server = config.get("server_ip", "") - host = config.get("screen_name", "") - quick = config.get("quickconnect_key", "") - port = config.get("port", "") - try: - # Remove protocol if already present - server_clean = server.replace('http://', '').replace('https://', '') - ip_pattern = r'^\d+\.\d+\.\d+\.\d+$' - if re.match(ip_pattern, server_clean): - server_url = f'http://{server_clean}:{port}/api/playlists' - else: - # Use original server if it has protocol, otherwise add http:// - if server.startswith(('http://', 'https://')): - server_url = f'{server}/api/playlists' - else: - server_url = f'http://{server}/api/playlists' - params = { - 'hostname': host, - 'quickconnect_code': quick - } - logger.info(f"Fetching playlist from URL: {server_url} with params: {params}") - response = requests.get(server_url, params=params) - if response.status_code == 200: - response_data = response.json() - logger.info(f"Server response: {response_data}") - playlist = response_data.get('playlist', []) - version = response_data.get('playlist_version', None) - hashed_quickconnect = response_data.get('hashed_quickconnect', None) - if version is not None and hashed_quickconnect is not None: - if bcrypt.checkpw(quick.encode('utf-8'), hashed_quickconnect.encode('utf-8')): - logger.info("Fetched updated playlist from server.") - return {'playlist': playlist, 'version': version} - else: - logger.error("Quickconnect code validation failed.") - else: - logger.error("Failed to retrieve playlist or hashed quickconnect from the response.") - else: - logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}") - except requests.exceptions.RequestException as e: - logger.error(f"Failed to fetch playlist: {e}") - return {'playlist': [], 'version': 0} - -def save_playlist_with_version(playlist_data, playlist_dir): - version = playlist_data.get('version', 0) - playlist_file = os.path.join(playlist_dir, f'server_playlist_v{version}.json') - with open(playlist_file, 'w') as f: - json.dump(playlist_data, f, indent=2) - logger.info(f"Playlist saved to {playlist_file}") - return playlist_file - -def download_media_files(playlist, media_dir): - """Download media files from the server and save them to media_dir.""" - if not os.path.exists(media_dir): - os.makedirs(media_dir) - logger.info(f"Created directory {media_dir} for media files.") - - updated_playlist = [] - for media in playlist: - file_name = media.get('file_name', '') - file_url = media.get('url', '') - duration = media.get('duration', 10) - local_path = os.path.join(media_dir, file_name) - logger.info(f"Preparing to download {file_name} from {file_url}...") - if os.path.exists(local_path): - logger.info(f"File {file_name} already exists. Skipping download.") - else: - try: - response = requests.get(file_url, timeout=10) - if response.status_code == 200: - with open(local_path, 'wb') as file: - file.write(response.content) - logger.info(f"Successfully downloaded {file_name} to {local_path}") - else: - logger.error(f"Failed to download {file_name}. Status Code: {response.status_code}") - continue - except requests.exceptions.RequestException as e: - logger.error(f"Error downloading {file_name}: {e}") - continue - updated_media = { - 'file_name': file_name, - 'url': os.path.relpath(local_path, os.path.dirname(media_dir)), - 'duration': duration - } - updated_playlist.append(updated_media) - return updated_playlist - -def delete_old_playlists_and_media(current_version, playlist_dir, media_dir, keep_versions=1): - """ - Delete old playlist files and media files not referenced by the latest playlist version. - keep_versions: number of latest versions to keep (default 1) - """ - # Find all playlist files - playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')] - # Keep only the latest N versions - versions = sorted([int(f.split('_v')[-1].split('.json')[0]) for f in playlist_files], reverse=True) - keep = set(versions[:keep_versions]) - # Delete old playlist files - for f in playlist_files: - v = int(f.split('_v')[-1].split('.json')[0]) - if v not in keep: - os.remove(os.path.join(playlist_dir, f)) - # Collect all media files referenced by the kept playlists - referenced = set() - for v in keep: - path = os.path.join(playlist_dir, f'server_playlist_v{v}.json') - if os.path.exists(path): - with open(path, 'r') as f: - data = json.load(f) - for item in data.get('playlist', []): - referenced.add(item.get('file_name')) - # Delete media files not referenced - for f in os.listdir(media_dir): - if f not in referenced: - try: - os.remove(os.path.join(media_dir, f)) - except Exception as e: - logger.warning(f"Failed to delete media file {f}: {e}") - -def update_playlist_if_needed(local_playlist_path, config, media_dir, playlist_dir): - """ - Fetch the server playlist once, compare versions, and update if needed. - Returns True if updated, False if already up to date. - Also sends feedback to server about playlist check. - """ - server_data = fetch_server_playlist(config) - server_version = server_data.get('version', 0) - if not os.path.exists(local_playlist_path): - local_version = 0 - else: - with open(local_playlist_path, 'r') as f: - local_data = json.load(f) - local_version = local_data.get('version', 0) - - logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}") - - # Send feedback about playlist check - send_playlist_check_feedback(config, server_version if server_version > 0 else local_version) - - if local_version != server_version: - if server_data and server_data.get('playlist'): - updated_playlist = download_media_files(server_data['playlist'], media_dir) - server_data['playlist'] = updated_playlist - save_playlist_with_version(server_data, playlist_dir) - # Delete old playlists and unreferenced media - delete_old_playlists_and_media(server_version, playlist_dir, media_dir) - - # Send feedback about playlist update - player_name = config.get("screen_name", "unknown") - update_message = f"player {player_name}, playlist updated to v{server_version}" - send_player_feedback(config, update_message, "active", server_version) - - return True - else: - logger.warning("No playlist data fetched from server or playlist is empty.") - - # Send error feedback - send_player_error_feedback(config, "No playlist data fetched from server or playlist is empty", local_version) - - return False - else: - logger.info("Local playlist is already up to date.") - return False - -def is_playlist_up_to_date(local_playlist_path, config): - """ - Compare the version of the local playlist with the server playlist. - Returns True if up-to-date, False otherwise. - """ - if not os.path.exists(local_playlist_path): - logger.info(f"Local playlist file not found: {local_playlist_path}") - return False - with open(local_playlist_path, 'r') as f: - local_data = json.load(f) - local_version = local_data.get('version', 0) - server_data = fetch_server_playlist(config) - server_version = server_data.get('version', 0) - logger.info(f"Local playlist version: {local_version}, Server playlist version: {server_version}") - return local_version == server_version \ No newline at end of file diff --git a/src/get_playlists_v2.py b/src/get_playlists_v2.py index de61334..e9b134c 100644 --- a/src/get_playlists_v2.py +++ b/src/get_playlists_v2.py @@ -237,7 +237,8 @@ def download_media_files(playlist, media_dir): updated_media = { 'file_name': file_name, 'url': os.path.relpath(local_path, os.path.dirname(media_dir)), - 'duration': duration + 'duration': duration, + 'edit_on_player': media.get('edit_on_player', False) # Preserve edit_on_player flag } updated_playlist.append(updated_media) diff --git a/src/main.py b/src/main.py index b12ecd0..a1ccb5e 100644 --- a/src/main.py +++ b/src/main.py @@ -107,10 +107,11 @@ class DrawingLayer(Widget): class EditPopup(Popup): """Popup for editing/annotating images""" - def __init__(self, player_instance, image_path, **kwargs): + def __init__(self, player_instance, image_path, authenticated_user=None, **kwargs): super(EditPopup, self).__init__(**kwargs) self.player = player_instance self.image_path = image_path + self.authenticated_user = authenticated_user or "player_1" # Default to player_1 self.drawing_layer = None # Pause playback @@ -439,11 +440,20 @@ class EditPopup(Popup): self.right_sidebar.opacity = 1 # Create and save metadata - self._save_metadata(edited_dir, new_name, base_name, + 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) + import threading + upload_thread = threading.Thread( + target=self._upload_to_server, + args=(output_path, json_filename), + daemon=True + ) + upload_thread.start() + # Show confirmation self.title = f'Saved as {output_filename}' Clock.schedule_once(lambda dt: self.dismiss(), 1) @@ -476,7 +486,8 @@ class EditPopup(Popup): 'original_name': base_name, 'new_name': output_filename, 'original_path': self.image_path, - 'version': version + 'version': version, + 'user': self.authenticated_user } # Save metadata JSON @@ -486,6 +497,70 @@ class EditPopup(Popup): json.dump(metadata, f, indent=2) Logger.info(f"EditPopup: Saved metadata to {json_path}") + return json_path + + def _upload_to_server(self, image_path, metadata_path): + """Upload edited image and metadata to server (runs in background thread)""" + try: + import requests + import json + from get_playlists_v2 import get_auth_instance + + # Get authenticated instance + auth = get_auth_instance() + if not auth or not auth.is_authenticated(): + Logger.warning("EditPopup: Cannot upload - not authenticated (server will not receive edited media)") + return False + + server_url = auth.auth_data.get('server_url') + auth_code = auth.auth_data.get('auth_code') + + if not server_url or not auth_code: + Logger.warning("EditPopup: Missing server URL or auth code (upload skipped)") + return False + + # Load metadata from file + with open(metadata_path, 'r') as meta_file: + metadata = json.load(meta_file) + + # Prepare upload URL + upload_url = f"{server_url}/api/player-edit-media" + headers = {'Authorization': f'Bearer {auth_code}'} + + # Prepare file and data for upload + with open(image_path, 'rb') as img_file: + files = { + 'image_file': (metadata['new_name'], img_file, 'image/jpeg') + } + + # Send metadata as JSON string in form data + data = { + 'metadata': json.dumps(metadata) + } + + Logger.info(f"EditPopup: Uploading edited media to {upload_url}") + response = requests.post(upload_url, headers=headers, files=files, data=data, timeout=30) + + if response.status_code == 200: + response_data = response.json() + Logger.info(f"EditPopup: ✅ Successfully uploaded edited media to server: {response_data}") + return True + elif response.status_code == 404: + Logger.warning("EditPopup: ⚠️ Upload endpoint not available on server (404) - edited media saved locally only") + return False + else: + Logger.warning(f"EditPopup: ⚠️ Upload failed with status {response.status_code} - edited media saved locally only") + return False + + except requests.exceptions.Timeout: + Logger.warning("EditPopup: ⚠️ Upload timed out - edited media saved locally only") + return False + except requests.exceptions.ConnectionError: + Logger.warning("EditPopup: ⚠️ Cannot connect to server - edited media saved locally only") + return False + except Exception as e: + Logger.warning(f"EditPopup: ⚠️ Upload failed: {e} - edited media saved locally only") + return False def close_without_saving(self, instance): """Close without saving""" @@ -1111,7 +1186,8 @@ class SignagePlayer(Widget): if self.playlist: self.ids.status_label.text = f"Playlist loaded: {len(self.playlist)} items" - if not self.is_playing: + # Only start playback if intro has finished + if not self.is_playing and self.intro_played: Clock.schedule_once(self.start_playback, 1) else: self.ids.status_label.text = "No media in playlist" @@ -1253,13 +1329,13 @@ class SignagePlayer(Widget): # Video file Logger.info(f"SignagePlayer: Media type: VIDEO") self.play_video(media_path, duration) - elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif']: + 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) 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") + Logger.warning(f"SignagePlayer: Supported: .mp4/.avi/.mkv/.mov/.webm/.jpg/.jpeg/.png/.bmp/.gif/.webp") Logger.warning(f"SignagePlayer: Skipping to next media...") self.consecutive_errors += 1 self.next_media() @@ -1527,8 +1603,8 @@ class SignagePlayer(Widget): file_name = media_item.get('file_name', '') file_extension = os.path.splitext(file_name)[1].lower() - # Only allow editing images - if file_extension not in ['.jpg', '.jpeg', '.png', '.bmp']: + # Check 1: Only allow editing images + if file_extension not in ['.jpg', '.jpeg', '.png', '.bmp', '.webp']: Logger.warning(f"SignagePlayer: Cannot edit {file_extension} files, only images") # Show error message briefly self.ids.status_label.text = 'Can only edit image files' @@ -1536,6 +1612,28 @@ class SignagePlayer(Widget): Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) return + # Check 2: Verify edit_on_player permission from server + edit_allowed = media_item.get('edit_on_player', False) + if not edit_allowed: + Logger.warning(f"SignagePlayer: Edit not allowed for {file_name} (edit_on_player=false)") + # Show error message briefly + self.ids.status_label.text = 'Edit not permitted for this media' + self.ids.status_label.opacity = 1 + Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) + return + + # Check 3: Verify user authentication + # TODO: Implement card swipe authentication system + authenticated_user = "player_1" # Placeholder - will be replaced with card authentication + + if not authenticated_user: + Logger.warning(f"SignagePlayer: User not authenticated for editing") + # Show error message briefly + self.ids.status_label.text = 'User authentication required' + self.ids.status_label.opacity = 1 + Clock.schedule_once(lambda dt: setattr(self.ids.status_label, 'opacity', 0), 2) + return + # Get full path to current image image_path = os.path.join(self.media_dir, file_name) @@ -1543,10 +1641,10 @@ class SignagePlayer(Widget): Logger.error(f"SignagePlayer: Image not found: {image_path}") return - Logger.info(f"SignagePlayer: Opening edit interface for {file_name}") + Logger.info(f"SignagePlayer: Opening edit interface for {file_name} (user: {authenticated_user})") - # Open edit popup - popup = EditPopup(player_instance=self, image_path=image_path) + # Open edit popup with authenticated user + popup = EditPopup(player_instance=self, image_path=image_path, authenticated_user=authenticated_user) popup.open() def show_exit_popup(self, instance=None): diff --git a/src/player_auth.py b/src/player_auth.py index 488a712..cfd0e49 100644 --- a/src/player_auth.py +++ b/src/player_auth.py @@ -275,6 +275,8 @@ class PlayerAuth: feedback_url = f"{server_url}/api/player-feedback" headers = {'Authorization': f'Bearer {auth_code}'} payload = { + 'hostname': self.auth_data.get('hostname'), + 'quickconnect_code': self.auth_data.get('quickconnect_code'), 'message': message, 'status': status, 'playlist_version': playlist_version,