#!/usr/bin/env python3 """ REST API for YouTube CLI application """ import logging import subprocess from logging.handlers import RotatingFileHandler from pathlib import Path from flask import Flask, jsonify, request from youtube_cli.main import YouTubeCLI # Configure logging LOG_DIR = Path.home() / ".config" / "youtube_cli" / "logs" LOG_DIR.mkdir(parents=True, exist_ok=True) LOG_FILE = LOG_DIR / "app.log" # Use RotatingFileHandler for log rotation (10MB, 5 backups) file_handler = RotatingFileHandler( LOG_FILE, maxBytes=10 * 1024 * 1024, backupCount=5 ) file_handler.setLevel(logging.DEBUG) file_handler.setFormatter( logging.Formatter( "%(asctime)s | %(name)s | %(levelname)s | %(message)s", "%Y-%m-%d %H:%M:%S", ) ) # Create console handler console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter("%(message)s")) # Configure root logger logging.basicConfig( level=logging.DEBUG, handlers=[ file_handler, console_handler, ], ) logger = logging.getLogger(__name__) app = Flask(__name__) # Initialize YouTube CLI cli = YouTubeCLI() def search_youtube_api(query, page=1): """Search YouTube and return structured results for API""" try: # Use yt-dlp directly to get search results cmd = [ "yt-dlp", "--flat-playlist", # Get video info without downloading "--dump-single-json", # Output as JSON single item f"--playlist-start={15 * (page - 1) + 1}", f"--playlist-end={15 * page}", "--no-warnings", "--no-progress", "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", f"ytsearch{15 * page}:{query}", ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) if result.returncode != 0: return {"error": f"Error searching videos: {result.stderr}"} # Parse JSON output import json try: data = json.loads(result.stdout.strip()) except json.JSONDecodeError as e: return {"error": f"Error parsing search results: {e}"} # Process videos into our format videos = [] if isinstance(data, list): videos_data = data elif "entries" in data: videos_data = data["entries"] else: videos_data = [data] for entry in videos_data: if not entry: continue title = entry.get("title", "Unknown Title") author = entry.get("uploader", "Unknown Author") duration = entry.get("duration", 0) url = entry.get("url", "") or entry.get("webpage_url", "") view_count = entry.get("view_count", None) # Format duration length = format_duration(duration) # Check if this is a short video is_short = "/shorts/" in url or "/shorts" in url # Check if this is a playlist (look for playlist-specific attributes) is_playlist = "playlist" in url.lower() or "list=" in url # Validate URL before adding to videos list if not url or url.strip() == "": continue # Skip videos with invalid/missing URLs # Create video object with fields matching frontend expectations videos.append( { "id": entry.get("id", ""), "videoId": entry.get("id", ""), "title": title, "description": "", "thumbnail": entry.get("thumbnail", ""), "url": url, "category": "General", "duration": length, "views": str(view_count) if view_count else "0", "channel": author, "isShort": is_short, "published": "2024-01-01", } ) return { "results": videos, "total": len(videos), "page": page, "hasMore": len(videos) >= 15, } except Exception as e: return {"error": str(e)} def format_duration(seconds): """Convert seconds to MM:SS or HH:MM:SS format.""" if not seconds: return "0:00" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) secs = int(seconds % 60) if hours > 0: return f"{hours}:{minutes:02d}:{secs:02d}" else: return f"{minutes}:{secs:02d}" @app.route("/search", methods=["GET"]) def search_videos(): """Search YouTube videos""" query = request.args.get("q", "") page = int(request.args.get("page", 1)) if not query: return jsonify({"error": 'Query parameter "q" is required'}), 400 try: result = search_youtube_api(query, page) if "error" in result: return jsonify(result), 500 return jsonify(result) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/download", methods=["POST"]) def download_video(): """Download a video by URL""" data = request.get_json() url = data.get("url", "") if not url: return jsonify({"error": "URL is required"}), 400 try: # For now, return a placeholder response indicating download would start # In a real implementation, this would call the actual download functionality return jsonify( { "status": "download_started", "url": url, "message": "Download process initiated (not implemented in this demo)", } ) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/health", methods=["GET"]) def health_check(): """Health check endpoint""" return jsonify({"status": "healthy", "service": "youtube-cli-api"}) @app.route("/version", methods=["GET"]) def get_version(): """Get API version""" return jsonify({"version": "1.0.0"}) @app.route("/capabilities", methods=["GET"]) def get_capabilities(): """MCP capabilities endpoint""" return jsonify( { "name": "YouTube CLI API", "version": "1.0.0", "description": "YouTube CLI API for searching and downloading videos", "endpoints": [ { "path": "/search", "method": "GET", "description": "Search YouTube videos", }, { "path": "/download", "method": "POST", "description": "Download a video by URL", }, { "path": "/health", "method": "GET", "description": "Health check endpoint", }, { "path": "/version", "method": "GET", "description": "Get API version", }, { "path": "/capabilities", "method": "GET", "description": "MCP capabilities endpoint", }, { "path": "/openapi.json", "method": "GET", "description": "OpenAPI specification", }, ], "features": [ "Video search", "Video download", "Health monitoring", "Version information", "MCP compliance", ], } ) @app.route("/openapi.json", methods=["GET"]) def get_openapi(): """Serve the OpenAPI specification file""" try: # Read the openapi.json file from the filesystem # Try multiple locations to handle Docker vs local execution import json import os # Check if we're in Docker (working directory is /app) current_dir = os.getcwd() if current_dir == "/app": # In Docker, the file should be in /app file_path = "/app/openapi.json" else: # Local execution file_path = "openapi.json" with open(file_path, "r") as f: spec = json.load(f) return jsonify(spec) except Exception as e: return jsonify( {"error": f"OpenAPI specification not found: {str(e)}"} ), 404 if __name__ == "__main__": # Fix the port issue by using a different port app.run(host="0.0.0.0", port=4096, debug=True)