#!/usr/bin/env python3 """ REST API for YouTube CLI application """ import logging import os 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 _config_dir = os.environ.get("CONFIG_DIR", str(Path.home() / ".config" / "youtube_cli")) LOG_DIR = Path(_config_dir) / "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)