youtube-cli/app.py

242 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
REST API for YouTube CLI application
"""
from flask import Flask, request, jsonify
from youtube_cli.main import YouTubeCLI
import json
import os
import subprocess
import sys
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)
playlist_title = entry.get("playlist_title", "")
# 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
videos.append(
{
"title": title,
"author": author,
"length": length,
"url": url,
"is_short": is_short,
"is_playlist": is_playlist,
"id": entry.get("id", ""),
"thumbnail": entry.get("thumbnail", ""),
"view_count": view_count,
}
)
return {
'query': query,
'page': page,
'videos': videos,
'total': len(videos)
}
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 os
import json
# 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)