#!/usr/bin/env python3 """ Flask API Server for YouTube Web Interface Provides REST API endpoints for searching, downloading, and managing YouTube content """ import json import os import sys import uuid from datetime import datetime from pathlib import Path from flask import Flask, jsonify, request from flask_cors import CORS # Add parent directory to path to import YouTubeCLI sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from youtube_cli.main import YouTubeCLI # Initialize Flask app app = Flask(__name__) CORS(app, resources={r"/api/*": {"origins": "*"}}) # Create YouTubeCLI instance yt_cli = YouTubeCLI() # ==================== Utility Functions ==================== def get_config(): """Get current configuration from YouTubeCLI instance.""" return yt_cli.config def make_response(data, status=200): """Create a JSON response with proper structure.""" return jsonify({ "success": True, "data": data }), status def make_error_response(message, status=400): """Create an error response.""" return jsonify({ "success": False, "error": message }), status # ==================== Health & Config ==================== @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.utcnow().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_endpoint(): """Get current configuration.""" try: config = get_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 = get_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) # ==================== Search ==================== @app.route('/api/search', methods=['GET']) def search(): """Search for videos.""" try: query = request.args.get('q', '').strip() page = int(request.args.get('page', 1)) if not query: return make_error_response("Search query is required", 400) if page < 1: return make_error_response("Page must be greater than 0", 400) config = get_config() videos = yt_cli.search_videos(query, config, page=page, return_results=True) return make_response({ "query": query, "page": page, "videos": videos }) except Exception as e: return make_error_response(f"Search failed: {str(e)}", 500) # ==================== Download ==================== # Queue to track downloads download_queue = {} @app.route('/api/download', methods=['POST']) def download_video(): """Download a video.""" try: data = request.get_json() url = data.get('url', '').strip() if data else '' category = data.get('category') if data else None network_folder = data.get('network_folder') if data else None if not url: return make_error_response("Video URL is required", 400) config = get_config() # Generate queue ID queue_id = str(uuid.uuid4()) # Create queue item queue_item = { "id": queue_id, "url": url, "category": category, "network_folder": network_folder, "status": "pending", "created_at": datetime.utcnow().isoformat(), "progress": 0, "message": "Queued for download" } # Add to queue download_queue[queue_id] = queue_item # Start download in background (simplified - would need threading in production) # For now, we do it synchronously def process_download(): try: download_queue[queue_id]["status"] = "downloading" download_queue[queue_id]["message"] = "Starting download..." # Download the video success = yt_cli.download_video( url=url, config=config, network_folder=network_folder, category=category ) if success: download_queue[queue_id]["status"] = "completed" download_queue[queue_id]["message"] = "Download completed successfully" download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat() else: download_queue[queue_id]["status"] = "failed" download_queue[queue_id]["message"] = "Download failed" download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat() except Exception as e: download_queue[queue_id]["status"] = "failed" download_queue[queue_id]["message"] = str(e) download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat() # Process download (synchronous for now) process_download() return make_response({ "queue_id": queue_id, "status": download_queue[queue_id]["status"], "message": download_queue[queue_id]["message"] }) except Exception as e: return make_error_response(f"Download failed: {str(e)}", 500) @app.route('/api/download/playlist', methods=['POST']) def download_playlist(): """Download a playlist.""" try: data = request.get_json() url = data.get('url', '').strip() if data else '' category = data.get('category') if data else None network_folder = data.get('network_folder') if data else None if not url: return make_error_response("Playlist URL is required", 400) config = get_config() # Generate queue ID queue_id = str(uuid.uuid4()) # Create queue item queue_item = { "id": queue_id, "url": url, "category": category, "network_folder": network_folder, "type": "playlist", "status": "pending", "created_at": datetime.utcnow().isoformat(), "progress": 0, "message": "Queued for download", "videos": [] } # Add to queue download_queue[queue_id] = queue_item # Start playlist download def process_playlist_download(): try: download_queue[queue_id]["status"] = "downloading" download_queue[queue_id]["message"] = "Starting playlist download..." # Download the playlist result = yt_cli.download_playlist( url=url, config=config, network_folder=network_folder, category=category ) if result: download_queue[queue_id]["status"] = "completed" download_queue[queue_id]["message"] = "Playlist download completed successfully" download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat() download_queue[queue_id]["videos"] = result else: download_queue[queue_id]["status"] = "failed" download_queue[queue_id]["message"] = "Playlist download failed" download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat() except Exception as e: download_queue[queue_id]["status"] = "failed" download_queue[queue_id]["message"] = str(e) download_queue[queue_id]["completed_at"] = datetime.utcnow().isoformat() # Process playlist download (synchronous for now) process_playlist_download() return make_response({ "queue_id": queue_id, "status": download_queue[queue_id]["status"], "message": download_queue[queue_id]["message"], "videos": download_queue[queue_id].get("videos", []) }) except Exception as e: return make_error_response(f"Playlist download failed: {str(e)}", 500) # ==================== Archive ==================== @app.route('/api/archive', methods=['GET']) def get_archive(): """Get download archive.""" try: archive = yt_cli.downloaded_videos return make_response({ "archive": archive, "total": len(archive) }) except Exception as e: return make_error_response(f"Failed to get archive: {str(e)}", 500) @app.route('/api/archive/', methods=['DELETE']) def remove_from_archive(video_id): """Remove a video from the archive.""" try: # Remove from archive if video_id in yt_cli.downloaded_videos: del yt_cli.downloaded_videos[video_id] yt_cli.save_archive() return make_response({"message": f"Video {video_id} removed from archive"}) else: return make_error_response(f"Video {video_id} not found in archive", 404) except Exception as e: return make_error_response(f"Failed to remove from archive: {str(e)}", 500) # ==================== Queue ==================== @app.route('/api/queue', methods=['GET']) def get_queue(): """Get all queue items.""" try: return make_response({ "queue": list(download_queue.values()), "total": len(download_queue) }) except Exception as e: return make_error_response(f"Failed to get queue: {str(e)}", 500) @app.route('/api/queue/', methods=['DELETE']) def remove_from_queue(queue_id): """Remove an item from the queue.""" try: if queue_id in download_queue: del download_queue[queue_id] return make_response({"message": f"Item {queue_id} removed from queue"}) else: return make_error_response(f"Queue item {queue_id} not found", 404) except Exception as e: return make_error_response(f"Failed to remove from queue: {str(e)}", 500) @app.route('/api/queue//retry', methods=['POST']) def retry_download(queue_id): """Retry a failed download.""" try: if queue_id not in download_queue: return make_error_response(f"Queue item {queue_id} not found", 404) item = download_queue[queue_id] # Reset queue item status item["status"] = "pending" item["message"] = "Retry queued" item["progress"] = 0 # Re-download based on type if item.get("type") == "playlist": return download_playlist_wrapper(queue_id) else: return download_video_wrapper(queue_id) except Exception as e: return make_error_response(f"Retry failed: {str(e)}", 500) @app.route('/api/queue//cancel', methods=['POST']) def cancel_download(queue_id): """Cancel a download.""" try: if queue_id not in download_queue: return make_error_response(f"Queue item {queue_id} not found", 404) item = download_queue[queue_id] if item["status"] in ["completed", "failed"]: return make_error_response(f"Cannot cancel {item['status']} download", 400) item["status"] = "cancelled" item["message"] = "Download cancelled by user" return make_response({ "queue_id": queue_id, "status": item["status"], "message": item["message"] }) except Exception as e: return make_error_response(f"Failed to cancel download: {str(e)}", 500) @app.route('/api/queue//status', methods=['GET']) def get_queue_status(queue_id): """Get the status of a queue item.""" try: if queue_id not in download_queue: return make_error_response(f"Queue item {queue_id} not found", 404) return make_response({ "queue_id": queue_id, "status": download_queue[queue_id]["status"] }) except Exception as e: return make_error_response(f"Failed to get queue status: {str(e)}", 500) @app.route('/api/queue/clear/completed', methods=['POST']) def clear_completed(): """Clear completed items from the queue.""" try: completed_ids = [ queue_id for queue_id, item in download_queue.items() if item["status"] == "completed" ] for queue_id in completed_ids: del download_queue[queue_id] return make_response({ "cleared": completed_ids, "count": len(completed_ids) }) except Exception as e: return make_error_response(f"Failed to clear completed items: {str(e)}", 500) @app.route('/api/queue/clear/failed', methods=['POST']) def clear_failed(): """Clear failed items from the queue.""" try: failed_ids = [ queue_id for queue_id, item in download_queue.items() if item["status"] == "failed" ] for queue_id in failed_ids: del download_queue[queue_id] return make_response({ "cleared": failed_ids, "count": len(failed_ids) }) except Exception as e: return make_error_response(f"Failed to clear failed items: {str(e)}", 500) # ==================== Helper Functions ==================== def download_video_wrapper(queue_id): """Wrapper to re-download a video from queue.""" try: item = download_queue[queue_id] config = get_config() item["status"] = "downloading" item["message"] = "Starting download..." success = yt_cli.download_video( url=item["url"], config=config, network_folder=item.get("network_folder"), category=item.get("category") ) if success: item["status"] = "completed" item["message"] = "Download completed successfully" item["completed_at"] = datetime.utcnow().isoformat() else: item["status"] = "failed" item["message"] = "Download failed" item["completed_at"] = datetime.utcnow().isoformat() return make_response({ "queue_id": queue_id, "status": item["status"], "message": item["message"] }) except Exception as e: download_queue[queue_id]["status"] = "failed" download_queue[queue_id]["message"] = str(e) return make_error_response(f"Download failed: {str(e)}", 500) def download_playlist_wrapper(queue_id): """Wrapper to re-download a playlist from queue.""" try: item = download_queue[queue_id] config = get_config() item["status"] = "downloading" item["message"] = "Starting playlist download..." result = yt_cli.download_playlist( url=item["url"], config=config, network_folder=item.get("network_folder"), category=item.get("category") ) if result: item["status"] = "completed" item["message"] = "Playlist download completed successfully" item["completed_at"] = datetime.utcnow().isoformat() item["videos"] = result else: item["status"] = "failed" item["message"] = "Playlist download failed" item["completed_at"] = datetime.utcnow().isoformat() return make_response({ "queue_id": queue_id, "status": item["status"], "message": item["message"], "videos": item.get("videos", []) }) except Exception as e: download_queue[queue_id]["status"] = "failed" download_queue[queue_id]["message"] = str(e) return make_error_response(f"Playlist download failed: {str(e)}", 500) # ==================== Error Handlers ==================== @app.errorhandler(404) def not_found(error): """Handle 404 errors.""" 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__': # Default to port 4096 port = int(os.environ.get('PORT', 4096)) debug = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true' app.run(host='0.0.0.0', port=port, debug=debug)