""" Updated get_playlists.py for Kiwy-Signage with DigiServer v2 authentication Uses secure auth flow: hostname → password/quickconnect → auth_code → API calls Now with HTTPS support and SSL certificate management """ import os import json import requests import logging from player_auth import PlayerAuth from ssl_utils import SSLManager # Set up logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Global auth instance _auth_instance = None def get_auth_instance(config_file='player_auth.json', use_https=True, verify_ssl=True): """Get or create global auth instance. Args: config_file: Authentication config file path use_https: Whether to use HTTPS verify_ssl: Whether to verify SSL certificates """ global _auth_instance if _auth_instance is None: _auth_instance = PlayerAuth(config_file, use_https=use_https, verify_ssl=verify_ssl) return _auth_instance def ensure_authenticated(config): """Ensure player is authenticated, authenticate if needed. Args: config: Legacy config dict with server_ip, screen_name, quickconnect_key, port Returns: PlayerAuth instance if authenticated, None otherwise """ auth = get_auth_instance( use_https=config.get('use_https', True), verify_ssl=config.get('verify_ssl', True) ) # If already authenticated and valid, return auth instance if auth.is_authenticated(): valid, _ = auth.verify_auth() if valid: logger.info("✅ Using existing authentication") return auth else: logger.warning("⚠️ Auth expired, re-authenticating...") # Need to authenticate server_ip = config.get("server_ip", "") hostname = config.get("screen_name", "") quickconnect_key = config.get("quickconnect_key", "") port = config.get("port", "") use_https = config.get("use_https", True) if not all([server_ip, hostname, quickconnect_key]): logger.error("❌ Missing configuration: server_ip, screen_name, or quickconnect_key") return None # Build server URL import re ip_pattern = r'^\d+\.\d+\.\d+\.\d+$' if re.match(ip_pattern, server_ip): if use_https: # Use HTTPS for IP addresses server_url = f'https://{server_ip}:{port}' if port else f'https://{server_ip}' else: server_url = f'http://{server_ip}:{port}' if port else f'http://{server_ip}' else: # For domain names, use HTTPS by default if use_https: server_url = f'https://{server_ip}' else: server_url = f'http://{server_ip}' # Authenticate using quickconnect code logger.info(f"🔐 Authenticating player: {hostname} at {server_url}") success, error = auth.authenticate( server_url=server_url, hostname=hostname, quickconnect_code=quickconnect_key ) if success: logger.info(f"✅ Authentication successful: {auth.get_player_name()}") return auth else: logger.error(f"❌ Authentication failed: {error}") return None 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 """ auth = ensure_authenticated(config) if not auth: logger.warning("Cannot send feedback - not authenticated") return False return auth.send_feedback( message=message, status=status, playlist_version=playlist_version, error_details=error_details ) def send_playlist_check_feedback(config, playlist_version=None): """Send feedback when playlist is checked for updates.""" 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.""" 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.""" 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.""" 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 authenticated API. Args: config: Legacy config dict Returns: dict: {'playlist': [...], 'version': int} """ auth = ensure_authenticated(config) if not auth: logger.error("❌ Cannot fetch playlist - authentication failed") return {'playlist': [], 'version': 0} # Get playlist using auth code playlist_data = auth.get_playlist() if playlist_data: return { 'playlist': playlist_data.get('playlist', []), 'version': playlist_data.get('playlist_version', 0) } else: logger.error("❌ Failed to fetch playlist") return {'playlist': [], 'version': 0} def save_playlist(playlist_data, playlist_dir): """Save playlist to a single file (no versioning).""" playlist_file = os.path.join(playlist_dir, 'server_playlist.json') # Ensure directory exists os.makedirs(playlist_dir, exist_ok=True) 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, ssl_manager=None, server_url=None): """Download media files from the server and save them to media_dir. Args: playlist: List of media items media_dir: Directory to save media files ssl_manager: Optional SSLManager for HTTPS downloads server_url: Server base URL for constructing full file URLs """ if not os.path.exists(media_dir): os.makedirs(media_dir) logger.info(f"📁 Created directory {media_dir} for media files") # Use SSL manager if provided, otherwise use requests directly session = ssl_manager.get_session() if ssl_manager else requests.Session() updated_playlist = [] for media in playlist: file_name = media.get('file_name', '') file_url = media.get('url', '') duration = media.get('duration', 10) item_type = media.get('type', '') # Web-link items have no file to download — pass the link through unchanged. if item_type == 'weblink': logger.info(f"🔗 Web link item (no download): {file_url}") updated_playlist.append({ 'file_name': file_name, 'type': 'weblink', 'url': file_url, # keep the original web address (not a local path) 'duration': duration, 'edit_on_player': False, }) continue local_path = os.path.join(media_dir, file_name) logger.info(f"📥 Preparing to download {file_name}...") if os.path.exists(local_path): logger.info(f"✓ File {file_name} already exists. Skipping download.") else: try: # Create parent directories if they don't exist (for nested paths like edited_media/5/) os.makedirs(os.path.dirname(local_path), exist_ok=True) # Construct full URL download_url = file_url # Handle localhost URLs - replace with actual server IP if 'localhost' in file_url or 'localhost' in (server_url or ''): if server_url: # Extract the path from localhost URL if 'localhost' in file_url: # URL like: https://localhost/static/uploads/file.jpg # Extract path: /static/uploads/file.jpg parts = file_url.split('localhost') if len(parts) > 1: path = parts[1] download_url = f"{server_url}{path}" logger.info(f"🔄 Replacing localhost with {server_url}") else: download_url = file_url else: download_url = file_url else: logger.warning(f"⚠️ localhost URL provided but no server_url available: {file_url}") download_url = file_url # Construct full URL if relative path is provided elif not file_url.startswith('http'): if server_url: download_url = f"{server_url}/{file_url}".replace('//', '/') # Fix the protocol part that might have been double-slashed download_url = download_url.replace('https:/', 'https://').replace('http:/', 'http://') else: logger.warning(f"⚠️ Relative URL provided but no server_url available: {file_url}") download_url = file_url logger.info(f"📥 Downloading from: {download_url}") response = session.get(download_url, timeout=30, verify=False) if response.status_code == 200: with open(local_path, 'wb') as file: file.write(response.content) logger.info(f"✅ Successfully downloaded {file_name} ({len(response.content)} bytes)") else: logger.error(f"❌ Failed to download {file_name}. Status: {response.status_code}") # Still add to playlist even if download failed - might be cached or available later except requests.exceptions.SSLError as e: logger.error(f"❌ SSL Error downloading {file_name}: {e}") # Don't skip - may still add to playlist except requests.exceptions.RequestException as e: logger.error(f"❌ Error downloading {file_name}: {e}") # Don't skip - may still add to playlist # Always add the media item to the playlist, even if download failed # (it might already exist or be available later) updated_media = { 'file_name': file_name, 'type': item_type, # Preserve media type (image/video/...) 'url': os.path.relpath(local_path, os.path.dirname(media_dir)), 'duration': duration, 'edit_on_player': media.get('edit_on_player', False) # Preserve edit_on_player flag } updated_playlist.append(updated_media) return updated_playlist def delete_unused_media(playlist_data, media_dir): """Delete media files not referenced in the current playlist.""" try: # Get list of media files referenced in current playlist referenced_files = set() for media in playlist_data.get('playlist', []): file_name = media.get('file_name', '') if file_name: referenced_files.add(file_name) logger.info(f"📋 Current playlist references {len(referenced_files)} files") if os.path.exists(media_dir): # Recursively get all media files deleted_count = 0 for root, dirs, files in os.walk(media_dir): for media_file in files: # Get relative path from media_dir full_path = os.path.join(root, media_file) rel_path = os.path.relpath(full_path, media_dir) # Skip if file is in current playlist if rel_path in referenced_files: continue # Delete unreferenced file try: os.remove(full_path) logger.info(f"🗑️ Deleted unused media: {rel_path}") deleted_count += 1 except Exception as e: logger.warning(f"⚠️ Could not delete {rel_path}: {e}") # Clean up empty directories for root, dirs, files in os.walk(media_dir, topdown=False): for dir_name in dirs: dir_path = os.path.join(root, dir_name) try: if not os.listdir(dir_path): # If directory is empty os.rmdir(dir_path) logger.debug(f"🗑️ Removed empty directory: {os.path.relpath(dir_path, media_dir)}") except Exception: pass 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 during media cleanup: {e}") def update_playlist_if_needed(config, playlist_dir, media_dir): """Check for and download updated playlist if available. Args: config: Configuration dict with server settings playlist_dir: Directory to save playlist media_dir: Directory to save media files """ try: # Initialize auth with SSL settings from config auth = ensure_authenticated(config) if not auth: logger.error("❌ Cannot update playlist - authentication failed") return None # Fetch latest playlist from server server_data = auth.get_playlist() if not server_data: logger.warning("⚠️ No valid playlist received from server") return None server_version = server_data.get('playlist_version', 0) if server_version == 0: logger.warning("⚠️ No valid playlist version received from server") return None # Check local version from single playlist file local_version = 0 playlist_file = os.path.join(playlist_dir, 'server_playlist.json') if os.path.exists(playlist_file): try: with open(playlist_file, 'r') as f: local_data = json.load(f) # Check for both 'version' and 'playlist_version' keys (for backward compatibility) local_version = local_data.get('version', local_data.get('playlist_version', 0)) except Exception as e: logger.warning(f"⚠️ Could not read local playlist: {e}") logger.info(f"📊 Playlist versions - Server: v{server_version}, Local: v{local_version}") # Update if needed if server_version > local_version: logger.info(f"🔄 Updating playlist from v{local_version} to v{server_version}") # Get SSL manager for downloads if using HTTPS ssl_manager = auth.ssl_manager if config.get('use_https', True) else None # Get server URL from auth server_url = auth.auth_data.get('server_url', '') # Download media files updated_playlist = download_media_files(server_data.get('playlist', []), media_dir, ssl_manager, server_url) server_data['playlist'] = updated_playlist # Save new playlist (single file, no versioning) playlist_file = save_playlist(server_data, playlist_dir) # Delete unused media files delete_unused_media(server_data, media_dir) logger.info(f"✅ Playlist updated successfully to v{server_version}") return playlist_file else: logger.info("✓ Playlist is up to date") return playlist_file except Exception as e: logger.error(f"❌ Error updating playlist: {e}") return None