from flask import Flask, jsonify, request, send_from_directory import vlc import os import json import requests import logging import threading import time import datetime import hashlib # Configure logging LOG_FOLDER = './logs' os.makedirs(LOG_FOLDER, exist_ok=True) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(os.path.join(LOG_FOLDER, 'app.log')), logging.StreamHandler() ] ) Logger = logging.getLogger(__name__) app = Flask(__name__, static_folder='static') # VLC Media Player instance vlc_instance = vlc.Instance() player = vlc_instance.media_player_new() # File paths PLAYLIST_FILE = './static/resurse/playlist.json' APP_CONFIG_FILE = './static/app_config.json' # Moved to the static folder RESOURCES_FOLDER = './static/resurse' # Ensure the resources folder exists os.makedirs(RESOURCES_FOLDER, exist_ok=True) # Load playlist def load_playlist(): if os.path.exists(PLAYLIST_FILE): with open(PLAYLIST_FILE, 'r') as file: return json.load(file) return [] # Save playlist def save_playlist(playlist): with open(PLAYLIST_FILE, 'w') as file: json.dump(playlist, file) # Load app configuration def load_app_config(): if os.path.exists(APP_CONFIG_FILE): with open(APP_CONFIG_FILE, 'r') as file: return json.load(file) return { "player_orientation": "portrait", "player_name": "", "quickconnect_code": "", "server_address": "", "port": 1025 } # Save app configuration def save_app_config(config): with open(APP_CONFIG_FILE, 'w') as file: json.dump(config, file) # Download media files def download_media_files(playlist): Logger.info("Starting media file download...") for media in playlist: file_name = media.get('file_name', '') file_url = media.get('url', '') file_path = os.path.join(RESOURCES_FOLDER, file_name) if not file_name or not file_url: Logger.error(f"Invalid media entry: {media}") continue Logger.debug(f"Downloading file: {file_name} from {file_url}") try: response = requests.get(file_url, stream=True) if response.status_code == 200: with open(file_path, 'wb') as file: for chunk in response.iter_content(chunk_size=8192): file.write(chunk) Logger.info(f"Downloaded {file_name} to {file_path}") else: Logger.error(f"Failed to download {file_name}: {response.status_code}") except requests.exceptions.RequestException as e: Logger.error(f"Failed to download {file_name}: {e}") def calculate_file_hash(file_path): """Calculate the SHA256 hash of a file.""" if not os.path.exists(file_path): return None sha256 = hashlib.sha256() with open(file_path, 'rb') as file: while chunk := file.read(8192): sha256.update(chunk) return sha256.hexdigest() # Download playlist files from server def download_playlist_files_from_server(): Logger.info("Starting playlist file download using app configuration...") app_config = load_app_config() server_address = app_config.get('server_address', '') port = app_config.get('port', 1025) hostname = app_config.get('player_name', '') quickconnect_code = app_config.get('quickconnect_code', '') if not server_address or not hostname or not quickconnect_code: Logger.error("Missing required configuration values.") return server_ip = f'{server_address}:{port}' url = f'http://{server_ip}/api/playlists' params = { 'hostname': hostname, 'quickconnect_code': quickconnect_code } try: response = requests.get(url, params=params) Logger.debug(f"Status Code: {response.status_code}") Logger.debug(f"Response Content: {response.text}") if response.status_code == 200: try: server_playlist = response.json().get('playlist', []) if not server_playlist: Logger.error("Playlist is empty or missing in the server response.") return Logger.info("Server playlist retrieved successfully.") # Calculate the hash of the current local playlist local_playlist_hash = calculate_file_hash(PLAYLIST_FILE) # Calculate the hash of the server playlist server_playlist_hash = hashlib.sha256(json.dumps(server_playlist, sort_keys=True).encode()).hexdigest() # Compare hashes to determine if the playlist has changed if local_playlist_hash == server_playlist_hash: Logger.info("No changes detected in the server playlist. Skipping download.") return Logger.info("Changes detected in the server playlist. Downloading updated files...") # Compare and download missing or updated files for media in server_playlist: file_name = media.get('file_name', '') file_url = media.get('url', '') file_path = os.path.join(RESOURCES_FOLDER, file_name) if not file_name or not file_url: Logger.error(f"Invalid media entry: {media}") continue # Check if the file exists locally and matches the server version if not os.path.exists(file_path) or os.path.getsize(file_path) != media.get('size', 0): Logger.info(f"Downloading file: {file_name}") response = requests.get(file_url, stream=True) if response.status_code == 200: with open(file_path, 'wb') as file: for chunk in response.iter_content(chunk_size=8192): file.write(chunk) Logger.info(f"Downloaded {file_name} to {file_path}") else: Logger.error(f"Failed to download {file_name}: {response.status_code}") # Save the server playlist locally save_playlist(server_playlist) # Update the updated_playlist.json create_updated_playlist() except json.JSONDecodeError as e: Logger.error(f"Failed to parse server playlist JSON: {e}") else: Logger.error(f"Failed to retrieve server playlist: {response.text}") except requests.exceptions.RequestException as e: Logger.error(f"Failed to connect to server: {e}") @app.route('/') def serve_index(): """Serve the main index.html page.""" return send_from_directory(app.static_folder, 'index.html') @app.route('/api/playlist', methods=['GET']) def get_playlist(): """Get the current playlist.""" playlist = load_playlist() return jsonify({'playlist': playlist}) @app.route('/api/playlist', methods=['POST']) def update_playlist(): """Update the playlist.""" playlist = request.json.get('playlist', []) save_playlist(playlist) return jsonify({'status': 'success'}) @app.route('/api/play', methods=['POST']) def play_media(): """Play a media file.""" file_path = request.json.get('file_path') if not os.path.exists(file_path): return jsonify({'error': 'File not found'}), 404 media = vlc_instance.media_new(file_path) player.set_media(media) player.play() return jsonify({'status': 'playing', 'file': file_path}) @app.route('/api/stop', methods=['POST']) def stop_media(): """Stop media playback.""" player.stop() return jsonify({'status': 'stopped'}) @app.route('/api/config', methods=['GET']) def get_config(): """Get the app configuration.""" config = load_app_config() return jsonify(config) @app.route('/api/config', methods=['POST']) def update_config(): """Update the app configuration.""" config = request.json save_app_config(config) return jsonify({'status': 'success'}) @app.route('/api/download_playlist', methods=['POST']) def download_playlist(): """Download playlist files from the server.""" try: download_playlist_files_from_server() return jsonify({'status': 'success', 'message': 'Playlist files downloaded successfully.'}) except Exception as e: return jsonify({'status': 'error', 'message': str(e)}), 500 @app.route('/updated_playlist.json') def serve_updated_playlist(): """Serve the updated playlist file.""" return send_from_directory('.', 'updated_playlist.json') def create_updated_playlist(): """Create a new playlist file with local file paths.""" Logger.info("Creating updated playlist with local file paths...") # Load the existing server playlist if not os.path.exists(PLAYLIST_FILE): Logger.error(f"Server playlist file not found: {PLAYLIST_FILE}") return with open(PLAYLIST_FILE, 'r') as file: playlist = json.load(file) # Update the playlist with local file paths updated_playlist = [] for media in playlist: file_name = media.get('file_name', '') local_path = f"/static/resurse/{file_name}" # Use Flask's static folder path if os.path.exists(os.path.join(RESOURCES_FOLDER, file_name)): updated_media = { "type": "image" if file_name.lower().endswith(('.jpg', '.jpeg', '.png')) else "video", "url": local_path, "duration": media.get('duration', 0) # Keep the duration for images } updated_playlist.append(updated_media) else: Logger.warning(f"File not found in resurse folder: {file_name}") # Save the updated playlist to the root folder updated_playlist_file = './updated_playlist.json' with open(updated_playlist_file, 'w') as file: json.dump(updated_playlist, file, indent=4) Logger.info(f"Updated playlist saved to {updated_playlist_file}") # Check and download playlist on app startup def initialize_playlist(): Logger.info("Initializing playlist...") download_playlist_files_from_server() # Download playlist from the server create_updated_playlist() # Create the updated playlist with local file paths Logger.info("Playlist initialization complete.") # Function to check for playlist updates every 5 minutes def periodic_playlist_check(): while True: try: Logger.info("Checking for playlist updates...") download_playlist_files_from_server() # Download playlist from the server create_updated_playlist() # Create the updated playlist with local file paths delete_old_logs_and_unused_files() # Delete old logs and unused media files Logger.info("Playlist check complete.") except Exception as e: Logger.error(f"Error during playlist check: {e}") time.sleep(300) # Wait for 5 minutes (300 seconds) before checking again # Start the periodic playlist check in a background thread def start_playlist_check_thread(): thread = threading.Thread(target=periodic_playlist_check, daemon=True) thread.start() # Function to delete log files older than 2 days def delete_old_logs_and_unused_files(log_folder='./logs', resurse_folder='./static/resurse', days=2): """Delete old log files and unused media files.""" Logger.info("Checking for old log files to delete...") if not os.path.exists(log_folder): Logger.warning(f"Log folder does not exist: {log_folder}") else: now = time.time() cutoff = now - (days * 86400) # Convert days to seconds for file_name in os.listdir(log_folder): file_path = os.path.join(log_folder, file_name) if os.path.isfile(file_path): file_modified_time = os.path.getmtime(file_path) if file_modified_time < cutoff: try: os.remove(file_path) Logger.info(f"Deleted old log file: {file_path}") except Exception as e: Logger.error(f"Failed to delete log file {file_path}: {e}") Logger.info("Checking for unused media files to delete...") if not os.path.exists(resurse_folder): Logger.warning(f"Resurse folder does not exist: {resurse_folder}") return # Load the updated playlist to determine which files are in use updated_playlist_file = './updated_playlist.json' used_files = set() if os.path.exists(updated_playlist_file): with open(updated_playlist_file, 'r') as file: updated_playlist = json.load(file) for media in updated_playlist: file_name = os.path.basename(media.get('url', '')) used_files.add(file_name) # Always keep playlist.json used_files.add('playlist.json') # Delete files in the resurse folder that are not in the updated playlist for file_name in os.listdir(resurse_folder): file_path = os.path.join(resurse_folder, file_name) if os.path.isfile(file_path) and file_name not in used_files: try: os.remove(file_path) Logger.info(f"Deleted unused media file: {file_path}") except Exception as e: Logger.error(f"Failed to delete media file {file_path}: {e}") if __name__ == '__main__': initialize_playlist() # Check and download playlist on startup create_updated_playlist() # Create the updated playlist start_playlist_check_thread() # Start the background thread for periodic checks app.run(host='0.0.0.0', port=1025)