import os import json import requests import bcrypt import re import datetime from logging_config import Logger # Global variable to track server connectivity status SERVER_CONNECTION_STATUS = { 'is_online': True, 'last_successful_connection': None, 'last_playlist_update': None, 'error_message': None } def get_server_status(): """Get current server connection status""" return SERVER_CONNECTION_STATUS.copy() def get_last_playlist_update_time(): """Get the timestamp of the last playlist update from filesystem""" try: playlist_dir = os.path.join(os.path.dirname(__file__), 'static_data', 'playlist') 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')] if playlist_files: # Get the most recent playlist file latest_file = max([os.path.join(playlist_dir, f) for f in playlist_files], key=os.path.getmtime) mod_time = os.path.getmtime(latest_file) return datetime.datetime.fromtimestamp(mod_time) return None except Exception as e: Logger.error(f"Error getting last playlist update time: {e}") return None def set_server_offline(error_message=None): """Mark server as offline with optional error message""" global SERVER_CONNECTION_STATUS SERVER_CONNECTION_STATUS['is_online'] = False SERVER_CONNECTION_STATUS['error_message'] = error_message Logger.warning(f"Server marked as offline: {error_message}") def set_server_online(): """Mark server as online and update connection time""" global SERVER_CONNECTION_STATUS SERVER_CONNECTION_STATUS['is_online'] = True SERVER_CONNECTION_STATUS['last_successful_connection'] = datetime.datetime.now() SERVER_CONNECTION_STATUS['error_message'] = None Logger.info("Server connection restored") 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 ip_pattern = r'^\d+\.\d+\.\d+\.\d+$' if re.match(ip_pattern, server): feedback_url = f'http://{server}:{port}/api/player-feedback' else: feedback_url = f'http://{server}/api/player-feedback' # Prepare feedback data feedback_data = { 'player_name': 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}") # Mark server as online on successful feedback set_server_online() return True else: Logger.warning(f"Feedback failed with status {response.status_code}: {response.text}") set_server_offline(f"Feedback failed with status {response.status_code}") return False except requests.exceptions.RequestException as e: Logger.error(f"Failed to send feedback: {e}") set_server_offline(f"Network error during feedback: {e}") return False except Exception as e: Logger.error(f"Unexpected error sending feedback: {e}") set_server_offline(f"Unexpected error during feedback: {e}") return False def send_playlist_check_feedback(config, playlist_version=None): """ Send feedback when server is interrogated for playlist 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"playlist v{playlist_version}" if playlist_version else "unknown" message = f"player {player_name}, server interrogation, checking for updates : {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"playlist v{playlist_version}" if playlist_version else "unknown" message = f"player {player_name}, playlist working in loop, cycle completed : {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 playlist starting (first media). Args: config (dict): Configuration containing server details playlist_version (int, optional): Current playlist version current_media (str, optional): First media file in playlist Returns: bool: True if feedback sent successfully, False otherwise """ player_name = config.get("screen_name", "unknown") version_info = f"playlist v{playlist_version}" if playlist_version else "unknown" message = f"player {player_name}, playlist started : {version_info}" return send_player_feedback( config=config, message=message, status="playing", playlist_version=playlist_version ) 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. """ import json 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 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: ip_pattern = r'^\d+\.\d+\.\d+\.\d+$' if re.match(ip_pattern, server): server_url = f'http://{server}:{port}/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, timeout=15) 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.") # Mark server as online on successful connection set_server_online() return {'playlist': playlist, 'version': version} else: Logger.error("Quickconnect code validation failed.") set_server_offline("Authentication failed - invalid quickconnect code") else: Logger.error("Failed to retrieve playlist or hashed quickconnect from the response.") set_server_offline("Invalid server response - missing playlist data") else: Logger.error(f"Failed to fetch playlist. Status Code: {response.status_code}") set_server_offline(f"Server returned error code: {response.status_code}") except requests.exceptions.ConnectTimeout as e: Logger.error(f"Connection timeout while fetching playlist: {e}") set_server_offline("Connection timeout - server unreachable") except requests.exceptions.ConnectionError as e: Logger.error(f"Connection error while fetching playlist: {e}") set_server_offline("Connection failed - server unreachable") except requests.exceptions.RequestException as e: Logger.error(f"Request error while fetching playlist: {e}") set_server_offline(f"Network error: {str(e)}") except Exception as e: Logger.error(f"Unexpected error while fetching playlist: {e}") set_server_offline(f"Unexpected error: {str(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) print(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. """ import json global SERVER_CONNECTION_STATUS 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}") # Only send feedback if server is online if SERVER_CONNECTION_STATUS['is_online']: send_playlist_check_feedback(config, server_version if server_version > 0 else local_version) if local_version != server_version and server_version > 0: 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) # Update last playlist update time SERVER_CONNECTION_STATUS['last_playlist_update'] = datetime.datetime.now() return True else: Logger.warning("No playlist data fetched from server or playlist is empty.") return False else: Logger.info("Local playlist is already up to date or server is offline.") return False