Spaces:
Sleeping
Sleeping
Agon Agent
Enhance extract_video_id to support shorts and implement DNS-over-HTTPS fallback with socket-level DNS cache override
590641c | 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}") | |
| 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") | |
| 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.") | |
| 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" | |
| ) | |