Spaces:
Paused
Paused
| from fastapi import FastAPI, HTTPException, Security, Depends | |
| from fastapi.security import APIKeyHeader | |
| from pydantic import HttpUrl | |
| import yt_dlp | |
| import uvicorn | |
| from typing import Dict, Optional | |
| from datetime import timedelta | |
| import os | |
| import logging | |
| # Set up logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI( | |
| title="Video Downloader API", | |
| description="API to extract direct download links (or streaming links as fallback), thumbnail, and video details for all yt-dlp supported sites", | |
| docs_url="/docs", | |
| redoc_url="/redoc" | |
| ) | |
| # API Key Authentication (only X-API-Key header) | |
| API_KEYS = ["123456", "marufking"] | |
| api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) | |
| async def get_api_key(api_key_header: Optional[str] = Security(api_key_header)): | |
| if api_key_header in API_KEYS: | |
| return api_key_header | |
| raise HTTPException(status_code=401, detail="Invalid or missing X-API-Key") | |
| # Path to cookies file (optional) | |
| COOKIES_FILE = "cookies.txt" | |
| async def get_video_info(video_url: HttpUrl, api_key: str = Depends(get_api_key)): | |
| """ | |
| Extract direct download links for video-only MP4 (720p and higher) or streaming links if no direct links are available, | |
| best thumbnail, and video details (title, size, duration) for any yt-dlp supported site. | |
| Works with or without cookies. | |
| """ | |
| # Convert HttpUrl to string to avoid type issues | |
| video_url_str = str(video_url) | |
| logger.info(f"Processing URL: {video_url_str}") | |
| ydl_opts = { | |
| 'quiet': True, | |
| 'no_warnings': True, | |
| 'format_sort': ['ext:mp4', 'vcodec', '+res:720'], # Prefer MP4, 720p or higher | |
| 'noplaylist': True, | |
| 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', | |
| 'force_generic_extractor': False, | |
| } | |
| # Add cookies file only if it exists and is non-empty | |
| if os.path.exists(COOKIES_FILE) and os.path.getsize(COOKIES_FILE) > 0: | |
| ydl_opts['cookiefile'] = COOKIES_FILE | |
| logger.info(f"Using cookies file: {COOKIES_FILE}") | |
| else: | |
| logger.info("No cookies file provided or file is empty; proceeding without cookies") | |
| try: | |
| with yt_dlp.YoutubeDL(ydl_opts) as ydl: | |
| # Extract info with retry for robustness | |
| try: | |
| info = ydl.extract_info(video_url_str, download=False) | |
| except yt_dlp.utils.DownloadError as de: | |
| logger.error(f"DownloadError for URL {video_url_str}: {str(de)}") | |
| raise HTTPException(status_code=500, detail=f"Failed to extract video info: {str(de)}. Try providing valid cookies if the video is private.") | |
| except Exception as e: | |
| logger.error(f"Unexpected error for URL {video_url_str}: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Error processing URL: {str(e)}. Ensure the URL is valid and accessible.") | |
| # Get video details | |
| title = info.get('title', 'Unknown Title') | |
| duration = info.get('duration', None) | |
| duration_str = str(timedelta(seconds=int(duration))) if duration else "Unknown" | |
| # Estimate file size | |
| file_size = None | |
| for fmt in info.get('formats', []): | |
| if fmt.get('filesize'): | |
| file_size = fmt['filesize'] | |
| break | |
| file_size_str = f"{file_size / (1024 * 1024):.2f} MB" if file_size else "Unknown" | |
| # Filter direct video-only MP4 formats (720p or higher, no .m3u8) | |
| direct_formats = [ | |
| f for f in info.get('formats', []) | |
| if f.get('vcodec') != 'none' | |
| and f.get('acodec') == 'none' | |
| and f.get('ext') == 'mp4' | |
| and not f.get('url', '').endswith('.m3u8') | |
| and f.get('height', 0) >= 720 | |
| ] | |
| # If no direct formats, fall back to streaming formats (720p or higher) | |
| if not direct_formats: | |
| logger.warning(f"No direct MP4 video-only formats (720p+) found for {video_url_str}, falling back to streaming formats") | |
| streaming_formats = [ | |
| f for f in info.get('formats', []) | |
| if f.get('vcodec') != 'none' | |
| and f.get('acodec') == 'none' | |
| and f.get('height', 0) >= 720 | |
| ] | |
| if not streaming_formats: | |
| logger.warning(f"No streaming formats (720p+) found for {video_url_str}") | |
| raise HTTPException( | |
| status_code=404, | |
| detail="No video-only formats (720p or higher) found, neither direct nor streaming." | |
| ) | |
| formats = streaming_formats | |
| link_type = "streaming" | |
| else: | |
| formats = direct_formats | |
| link_type = "direct" | |
| # Sort by resolution | |
| formats.sort(key=lambda x: x.get('height', 0), reverse=True) | |
| # Get download links | |
| download_links = [ | |
| { | |
| "quality": f"{f.get('height')}p", | |
| "url": f.get('url'), | |
| "format_id": f.get('format_id'), | |
| "type": link_type | |
| } | |
| for f in formats | |
| ] | |
| # Get best thumbnail | |
| thumbnails = info.get('thumbnails', []) | |
| best_thumbnail = max( | |
| thumbnails, | |
| key=lambda x: x.get('width', 0) * x.get('height', 0), | |
| default={} | |
| ).get('url', 'No thumbnail available') | |
| logger.info(f"Successfully extracted info for {video_url_str}: {len(download_links)} {link_type} formats found") | |
| return { | |
| "title": title, | |
| "duration": duration_str, | |
| "file_size": file_size_str, | |
| "download_links": download_links, | |
| "thumbnail": best_thumbnail, | |
| "link_type": link_type | |
| } | |
| except HTTPException as he: | |
| raise he | |
| except Exception as e: | |
| logger.error(f"Unexpected error for URL {video_url_str}: {str(e)}") | |
| raise HTTPException(status_code=500, detail=f"Error: {str(e)}") | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |