""" Updated get_playlists.py for Kiwy-Signage with DigiServer v2 authentication Uses secure auth flow: hostname → password/quickconnect → auth_code → API calls """ import os import json import requests import logging from player_auth import PlayerAuth # 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'): """Get or create global auth instance.""" global _auth_instance if _auth_instance is None: _auth_instance = PlayerAuth(config_file) 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() # 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", "") 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): server_url = f'http://{server_ip}:{port}' else: server_url = f'http://{server_ip}' # Authenticate using quickconnect code logger.info(f"🔐 Authenticating player: {hostname}") 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_with_version(playlist_data, playlist_dir): """Save playlist to file with version number.""" version = playlist_data.get('version', 0) playlist_file = os.path.join(playlist_dir, f'server_playlist_v{version}.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): """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}...") if os.path.exists(local_path): logger.info(f"✓ File {file_name} already exists. Skipping download.") else: try: response = requests.get(file_url, timeout=30) if response.status_code == 200: with open(local_path, 'wb') as file: file.write(response.content) logger.info(f"✅ Successfully downloaded {file_name}") else: logger.error(f"❌ Failed to download {file_name}. Status: {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, 'edit_on_player': media.get('edit_on_player', False) # Preserve edit_on_player flag } 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.""" try: # Find all playlist files playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')] # Extract versions and sort versions = [] for f in playlist_files: try: version = int(f.replace('server_playlist_v', '').replace('.json', '')) versions.append((version, f)) except ValueError: continue versions.sort(reverse=True) # Keep only the latest N versions files_to_delete = [f for v, f in versions[keep_versions:]] for f in files_to_delete: filepath = os.path.join(playlist_dir, f) os.remove(filepath) logger.info(f"🗑️ Deleted old playlist: {f}") # 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}") def update_playlist_if_needed(config, playlist_dir, media_dir): """Check for and download updated playlist if available.""" try: # Fetch latest playlist from server server_data = fetch_server_playlist(config) server_version = server_data.get('version', 0) if server_version == 0: logger.warning("⚠️ No valid playlist received from server") return None # Check local version local_version = 0 local_playlist_file = None if os.path.exists(playlist_dir): playlist_files = [f for f in os.listdir(playlist_dir) if f.startswith('server_playlist_v') and f.endswith('.json')] for f in playlist_files: try: version = int(f.replace('server_playlist_v', '').replace('.json', '')) if version > local_version: local_version = version local_playlist_file = os.path.join(playlist_dir, f) except ValueError: continue 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}") # Download media files updated_playlist = download_media_files(server_data['playlist'], media_dir) server_data['playlist'] = updated_playlist # Save new playlist playlist_file = save_playlist_with_version(server_data, playlist_dir) # Clean up old versions delete_old_playlists_and_media(server_version, playlist_dir, media_dir) logger.info(f"✅ Playlist updated successfully to v{server_version}") return playlist_file else: logger.info("✓ Playlist is up to date") return local_playlist_file except Exception as e: logger.error(f"❌ Error updating playlist: {e}") return None