#!/usr/bin/env python3 """ Flask API Server for YouTube Web Interface Provides REST API endpoints for searching, downloading, and managing YouTube content Uses Flask-SocketIO for real-time progress updates """ import logging import os import sys from datetime import datetime, timezone from pathlib import Path from flask import Flask, send_from_directory from flask_cors import CORS from flask_socketio import SocketIO # Add parent directory to path to import YouTubeCLI sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # Add server directory to path for local imports sys.path.insert(0, str(Path(__file__).parent)) from youtube_cli.main import YouTubeCLI from models.queue_store import QueueStore from models.archive import ArchiveDB from download_engine import DownloadEngine from routes import search_bp, download_bp, queue_bp, archive_bp # Initialize YouTubeCLI yt_cli = YouTubeCLI() # Initialize Flask app static_dir = str(Path(__file__).parent.parent / 'web-app' / 'dist') app = Flask(__name__, static_folder=static_dir, static_url_path='/') app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'youtube-web-secret') CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=True) # Initialize SocketIO - use threading mode (compatible with gunicorn gthread worker) socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading') # Persistent config dir from env var config_dir = os.environ.get('CONFIG_DIR', str(Path.home() / '.config' / 'youtube_cli')) Path(config_dir).mkdir(parents=True, exist_ok=True) # Persistent file logging log_dir = os.environ.get('LOG_DIR', '/app/logs') Path(log_dir).mkdir(parents=True, exist_ok=True) file_handler = logging.FileHandler(str(Path(log_dir) / 'youtube-cli.log')) file_handler.setLevel(logging.INFO) file_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s')) root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) root_logger.addHandler(file_handler) # Initialize components with persistent paths queue_store = QueueStore(store_path=str(Path(config_dir) / 'queue.json')) archive_db = ArchiveDB(db_path=str(Path(config_dir) / 'archive.db')) download_engine = DownloadEngine(queue_store, archive_db, yt_cli, socketio) # ==================== Health & Config ==================== from utils import make_response, make_error_response @app.route('/api/health', methods=['GET']) def health_check(): """Health check endpoint.""" try: yt_dlp_version = yt_cli.get_yt_dlp_version() return make_response({ "status": "healthy", "timestamp": datetime.now(timezone.utc).isoformat(), "yt_dlp_version": yt_dlp_version }) except Exception as e: return make_error_response(f"Health check failed: {str(e)}", 500) @app.route('/api/config', methods=['GET']) def get_config(): """Get current configuration.""" try: config = yt_cli.config return make_response({ "download_dir": config.get("download_dir"), "default_locations": config.get("default_locations", []), "max_videos_per_page": config.get("max_videos_per_page", 15), "network_share_path": config.get("network_share_path"), "default_network_subfolder": config.get("default_network_subfolder") }) except Exception as e: return make_error_response(f"Failed to get config: {str(e)}", 500) @app.route('/api/categories', methods=['GET']) def get_categories(): """Get available download categories.""" try: config = yt_cli.config categories = yt_cli.get_categories(config) return make_response({ "categories": categories, "default_download_dir": config.get("download_dir") }) except Exception as e: return make_error_response(f"Failed to get categories: {str(e)}", 500) # ==================== Register Blueprints ==================== app.register_blueprint(search_bp) app.register_blueprint(download_bp) app.register_blueprint(queue_bp) app.register_blueprint(archive_bp) # ==================== WebSocket Events ==================== @socketio.on('connect') def handle_connect(): """Handle client WebSocket connection.""" from flask_socketio import emit emit('connected', {'message': 'Connected to server'}) @socketio.on('disconnect') def handle_disconnect(): """Handle client WebSocket disconnection.""" pass # ==================== Static File Serving ==================== @app.route('/') def serve_home(): """Serve React app home page.""" static_path = app.static_folder or static_dir return send_from_directory(static_path, 'index.html') # ==================== Error Handlers ==================== @app.errorhandler(404) def not_found(error): """Handle 404 errors - serve index.html for SPA routes.""" from flask import request # For SPA routes (not API), serve index.html if not request.path.startswith('/api'): static_path = app.static_folder or static_dir return send_from_directory(static_path, 'index.html') return make_error_response("Endpoint not found", 404) @app.errorhandler(500) def internal_error(error): """Handle 500 errors.""" return make_error_response("Internal server error", 500) # ==================== Run Server ==================== if __name__ == '__main__': port = int(os.environ.get('PORT', 4096)) debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true' # Use eventlet for WebSocket support (required by Flask-SocketIO) socketio.run(app, host='0.0.0.0', port=port, debug=debug, allow_unsafe_werkzeug=True)