171 lines
5.6 KiB
Python

#!/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 download_engine import DownloadEngine
from models.archive import ArchiveDB
from models.queue_store import QueueStore
from routes import archive_bp, download_bp, queue_bp, search_bp
from youtube_cli.main import YouTubeCLI
# 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_error_response, make_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)