import os import tempfile import uuid import json import subprocess import urllib.request import urllib.parse import re import socket from fastapi import FastAPI, HTTPException, Query, BackgroundTasks from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware app = FastAPI(title="END Downloader API", version="1.0") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) JAR_PATH = "extractor-cli.jar" # --- Global DoH DNS Cache Override --- dns_cache = {} original_getaddrinfo = socket.getaddrinfo def custom_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): if host in dns_cache: return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (dns_cache[host], port))] return original_getaddrinfo(host, port, family, type, proto, flags) socket.getaddrinfo = custom_getaddrinfo def resolve_hostname_doh(hostname: str) -> str: if hostname in ["google.com", "huggingface.co", "github.com", "noembed.com", "dns.google"]: return None try: url = f"https://dns.google/resolve?name={hostname}&type=A" req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=5) as response: data = json.loads(response.read().decode("utf-8")) answers = data.get("Answer", []) for ans in answers: if ans.get("type") == 1: # A record ip = ans.get("data") dns_cache[hostname] = ip print(f"DoH Resolved: {hostname} -> {ip}") return ip except Exception as e: print(f"DoH failed for {hostname}: {e}") return None def cleanup_file(filepath: str): try: if os.path.exists(filepath): os.remove(filepath) print(f"Successfully deleted temp file: {filepath}") except Exception as e: print(f"Error removing file {filepath}: {e}") @app.get("/") def read_root(): return {"message": "END Downloader API is running", "status": "ok", "extractor": "NewPipeExtractor v0.26.2"} def extract_video_id(url: str) -> str: pattern = r'(?:https?://)?(?:www\.)?(?:youtube\.com/(?:watch\?v=|shorts/|embed/|v/)|youtu\.be/)([a-zA-Z0-9_-]{11})' match = re.search(pattern, url) if match: return match.group(1) return "" def fetch_from_noembed(url: str, video_id: str) -> dict: """Guaranteed fallback for basic video details""" try: api_url = f"https://noembed.com/embed?url=https://www.youtube.com/watch?v={video_id}" req = urllib.request.Request(api_url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode("utf-8")) title = data.get("title", "YouTube Video") uploader = data.get("author_name", "Unknown Uploader") thumbnail = data.get("thumbnail_url", f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg") # Generate a basic format as fallback formats = [ { "format_id": "fallback_mp4", "resolution": "720p (Fallback)", "ext": "mp4", "filesize": None, "vcodec": "avc1", "acodec": "aac", "hdr": "SDR", "type": "muxed", "url": f"https://youtube.com/watch?v={video_id}", # fallback URL "fps": 30.0 } ] return { "id": video_id, "title": title, "thumbnail": thumbnail, "duration": None, "uploader": uploader, "description": f"Video Title: {title}. Stream formats could not be extracted directly because of YouTube rate limits on cloud servers.", "formats": formats } except Exception as e: print(f"Noembed fallback failed: {e}") return None def fetch_from_invidious_backup(video_id: str) -> dict: """Backup extractor using public Invidious instances with DoH resolution""" instances = [ "https://invidious.nerdvpn.de", "https://invidious.f5.si", "https://yt.chocolatemoo53.com", "https://inv.thepixora.com", "https://invidious.tiekoetter.com" ] for inst in instances: parsed = urllib.parse.urlparse(inst) hostname = parsed.hostname if hostname: resolve_hostname_doh(hostname) # Pre-resolve to bypass local DNS failures! try: api_url = f"{inst}/api/v1/videos/{video_id}" req = urllib.request.Request(api_url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}) with urllib.request.urlopen(req, timeout=10) as response: data = json.loads(response.read().decode('utf-8')) # Format into our standard response formats = [] # Muxed streams for fmt in data.get("formatStreams", []): formats.append({ "format_id": str(fmt.get("itag", "18")), "resolution": fmt.get("resolution", "360p"), "ext": fmt.get("container", "mp4"), "filesize": None, "vcodec": "avc1", "acodec": "aac", "hdr": "SDR", "type": "muxed", "url": fmt.get("url"), "fps": 30.0 }) # Video only streams for fmt in data.get("adaptiveFormats", []): mime = fmt.get("type", "") if "video" in mime: is_hdr = "hdr" in mime.lower() or "hdr" in fmt.get("qualityLabel", "").lower() formats.append({ "format_id": str(fmt.get("itag", "137")), "resolution": fmt.get("qualityLabel", "1080p"), "ext": "mp4" if "mp4" in mime else "webm", "filesize": int(fmt.get("contentLength", 0)) if fmt.get("contentLength") else None, "vcodec": fmt.get("encoding", "avc1"), "acodec": "none", "hdr": "HDR" if is_hdr else "SDR", "type": "video_only", "url": fmt.get("url"), "fps": float(fmt.get("fps", 30)) }) elif "audio" in mime: formats.append({ "format_id": str(fmt.get("itag", "140")), "resolution": f"{int(fmt.get('bitrate', 128000) / 1000)}kbps", "ext": "m4a" if "mp4" in mime else "webm", "filesize": int(fmt.get("contentLength", 0)) if fmt.get("contentLength") else None, "vcodec": "none", "acodec": fmt.get("encoding", "aac"), "hdr": "SDR", "type": "audio_only", "url": fmt.get("url"), "fps": None }) return { "id": data.get("videoId"), "title": data.get("title"), "thumbnail": data.get("videoThumbnails", [{}])[-1].get("url") if data.get("videoThumbnails") else f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg", "duration": data.get("lengthSeconds"), "uploader": data.get("author"), "description": data.get("description"), "formats": formats } except Exception as e: print(f"Invidious backup instance {inst} failed: {e}") continue raise Exception("All backup instances failed") @app.get("/info") def get_info(url: str = Query(..., description="YouTube video URL")): # 1. Primary Extractor: NewPipeExtractor v0.26.2 try: res = subprocess.run( ["java", "-jar", JAR_PATH, url], capture_output=True, text=True, timeout=30 ) if res.returncode == 0: details = json.loads(res.stdout) if "error" not in details: return details else: print(f"NewPipeExtractor returned error: {details['error']}") else: print(f"NewPipeExtractor process failed: {res.stderr}") except Exception as e: print(f"Failed to run NewPipeExtractor: {e}") # 2. Backup Extractor: Public Invidious APIs with DoH print("Falling back to backup extractor APIs...") video_id = extract_video_id(url) if video_id: try: return fetch_from_invidious_backup(video_id) except Exception as e: print(f"Backup extractor failed: {e}") # 3. Ultimate Fallback: oEmbed via noembed.com noembed_data = fetch_from_noembed(url, video_id) if noembed_data: return noembed_data raise HTTPException(status_code=400, detail=f"All extractors failed. NewPipeExtractor error, Backup error: {str(e)}") else: raise HTTPException(status_code=400, detail="Invalid YouTube URL and NewPipeExtractor failed.") @app.get("/download") def download_video( url: str = Query(..., description="YouTube video URL"), format_id: str = Query(..., description="Format ID (e.g., 137 or 137+140)"), background_tasks: BackgroundTasks = BackgroundTasks() ): temp_dir = tempfile.gettempdir() unique_id = str(uuid.uuid4()) # Fetch details first to get URLs and title try: details = get_info(url) except Exception as e: raise HTTPException(status_code=400, detail=f"Failed to fetch video details: {e}") title = details.get("title", "video").replace("/", "_").replace("\\", "_") formats = details.get("formats", []) # Check if we need to merge video and audio (format_id like '137+140') if "+" in format_id: v_id, a_id = format_id.split("+") v_fmt = next((f for f in formats if f["format_id"] == v_id), None) a_fmt = next((f for f in formats if f["format_id"] == a_id), None) if not v_fmt or not a_fmt: raise HTTPException(status_code=400, detail="Requested format IDs not found") v_url = v_fmt["url"] a_url = a_fmt["url"] ext = v_fmt["ext"] output_file = os.path.join(temp_dir, f"end_{unique_id}_{title}.{ext}") # Run ffmpeg to merge streams on-the-fly! ffmpeg_cmd = [ "ffmpeg", "-y", "-ss", "00:00:00", "-i", v_url, "-ss", "00:00:00", "-i", a_url, "-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy", "-c:a", "aac", "-shortest", output_file ] try: res = subprocess.run(ffmpeg_cmd, capture_output=True, text=True, timeout=180) if res.returncode != 0: print(f"ffmpeg error: {res.stderr}") raise Exception("ffmpeg merge failed") except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to merge streams: {e}") background_tasks.add_task(cleanup_file, output_file) return FileResponse( path=output_file, filename=f"{title}.{ext}", media_type="application/octet-stream" ) else: # Single stream download (muxed or audio only) fmt = next((f for f in formats if f["format_id"] == format_id), None) if not fmt: raise HTTPException(status_code=400, detail="Requested format ID not found") stream_url = fmt["url"] ext = fmt["ext"] # Download the single stream output_file = os.path.join(temp_dir, f"end_{unique_id}_{title}.{ext}") try: # Stream download using urllib req = urllib.request.Request(stream_url, headers={'User-Agent': 'Mozilla/5.0'}) with urllib.request.urlopen(req) as response, open(output_file, 'wb') as out_file: out_file.write(response.read()) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to download stream: {e}") background_tasks.add_task(cleanup_file, output_file) return FileResponse( path=output_file, filename=f"{title}.{ext}", media_type="application/octet-stream" )