Agon Agent commited on
Commit
590641c
·
1 Parent(s): 7abbf94

Enhance extract_video_id to support shorts and implement DNS-over-HTTPS fallback with socket-level DNS cache override

Browse files
Files changed (1) hide show
  1. app.py +100 -21
app.py CHANGED
@@ -5,6 +5,8 @@ import json
5
  import subprocess
6
  import urllib.request
7
  import urllib.parse
 
 
8
  from fastapi import FastAPI, HTTPException, Query, BackgroundTasks
9
  from fastapi.responses import FileResponse
10
  from fastapi.middleware.cors import CORSMiddleware
@@ -21,6 +23,36 @@ app.add_middleware(
21
 
22
  JAR_PATH = "extractor-cli.jar"
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  def cleanup_file(filepath: str):
25
  try:
26
  if os.path.exists(filepath):
@@ -34,28 +66,70 @@ def read_root():
34
  return {"message": "END Downloader API is running", "status": "ok", "extractor": "NewPipeExtractor v0.26.2"}
35
 
36
  def extract_video_id(url: str) -> str:
37
- if "youtu.be" in url:
38
- return url.split("/")[-1].split("?")[0]
39
- elif "youtube.com" in url:
40
- parsed = urllib.parse.urlparse(url)
41
- q = urllib.parse.parse_qs(parsed.query)
42
- if 'v' in q:
43
- return q['v'][0]
44
  return ""
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  def fetch_from_invidious_backup(video_id: str) -> dict:
47
- """Backup extractor using public Invidious instances"""
48
  instances = [
49
- "https://inv.tux.im",
50
- "https://invidious.projectsegfau.lt",
51
  "https://invidious.nerdvpn.de",
52
- "https://yewtu.be",
53
- "https://invidious.flokinet.to"
 
 
54
  ]
55
  for inst in instances:
 
 
 
 
 
56
  try:
57
  api_url = f"{inst}/api/v1/videos/{video_id}"
58
- req = urllib.request.Request(api_url, headers={'User-Agent': 'Mozilla/5.0'})
59
  with urllib.request.urlopen(req, timeout=10) as response:
60
  data = json.loads(response.read().decode('utf-8'))
61
 
@@ -65,7 +139,7 @@ def fetch_from_invidious_backup(video_id: str) -> dict:
65
  # Muxed streams
66
  for fmt in data.get("formatStreams", []):
67
  formats.append({
68
- "format_id": fmt.get("itag", "18"),
69
  "resolution": fmt.get("resolution", "360p"),
70
  "ext": fmt.get("container", "mp4"),
71
  "filesize": None,
@@ -83,10 +157,10 @@ def fetch_from_invidious_backup(video_id: str) -> dict:
83
  if "video" in mime:
84
  is_hdr = "hdr" in mime.lower() or "hdr" in fmt.get("qualityLabel", "").lower()
85
  formats.append({
86
- "format_id": fmt.get("itag", "137"),
87
  "resolution": fmt.get("qualityLabel", "1080p"),
88
  "ext": "mp4" if "mp4" in mime else "webm",
89
- "filesize": int(fmt.get("contentLength", 0)) or None,
90
  "vcodec": fmt.get("encoding", "avc1"),
91
  "acodec": "none",
92
  "hdr": "HDR" if is_hdr else "SDR",
@@ -96,10 +170,10 @@ def fetch_from_invidious_backup(video_id: str) -> dict:
96
  })
97
  elif "audio" in mime:
98
  formats.append({
99
- "format_id": fmt.get("itag", "140"),
100
  "resolution": f"{int(fmt.get('bitrate', 128000) / 1000)}kbps",
101
  "ext": "m4a" if "mp4" in mime else "webm",
102
- "filesize": int(fmt.get("contentLength", 0)) or None,
103
  "vcodec": "none",
104
  "acodec": fmt.get("encoding", "aac"),
105
  "hdr": "SDR",
@@ -111,7 +185,7 @@ def fetch_from_invidious_backup(video_id: str) -> dict:
111
  return {
112
  "id": data.get("videoId"),
113
  "title": data.get("title"),
114
- "thumbnail": data.get("videoThumbnails", [{}])[-1].get("url"),
115
  "duration": data.get("lengthSeconds"),
116
  "uploader": data.get("author"),
117
  "description": data.get("description"),
@@ -124,7 +198,7 @@ def fetch_from_invidious_backup(video_id: str) -> dict:
124
 
125
  @app.get("/info")
126
  def get_info(url: str = Query(..., description="YouTube video URL")):
127
- # Primary Extractor: NewPipeExtractor v0.26.2
128
  try:
129
  res = subprocess.run(
130
  ["java", "-jar", JAR_PATH, url],
@@ -143,13 +217,18 @@ def get_info(url: str = Query(..., description="YouTube video URL")):
143
  except Exception as e:
144
  print(f"Failed to run NewPipeExtractor: {e}")
145
 
146
- # Backup Extractor: Public Invidious APIs
147
  print("Falling back to backup extractor APIs...")
148
  video_id = extract_video_id(url)
149
  if video_id:
150
  try:
151
  return fetch_from_invidious_backup(video_id)
152
  except Exception as e:
 
 
 
 
 
153
  raise HTTPException(status_code=400, detail=f"All extractors failed. NewPipeExtractor error, Backup error: {str(e)}")
154
  else:
155
  raise HTTPException(status_code=400, detail="Invalid YouTube URL and NewPipeExtractor failed.")
 
5
  import subprocess
6
  import urllib.request
7
  import urllib.parse
8
+ import re
9
+ import socket
10
  from fastapi import FastAPI, HTTPException, Query, BackgroundTasks
11
  from fastapi.responses import FileResponse
12
  from fastapi.middleware.cors import CORSMiddleware
 
23
 
24
  JAR_PATH = "extractor-cli.jar"
25
 
26
+ # --- Global DoH DNS Cache Override ---
27
+ dns_cache = {}
28
+ original_getaddrinfo = socket.getaddrinfo
29
+
30
+ def custom_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
31
+ if host in dns_cache:
32
+ return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (dns_cache[host], port))]
33
+ return original_getaddrinfo(host, port, family, type, proto, flags)
34
+
35
+ socket.getaddrinfo = custom_getaddrinfo
36
+
37
+ def resolve_hostname_doh(hostname: str) -> str:
38
+ if hostname in ["google.com", "huggingface.co", "github.com", "noembed.com", "dns.google"]:
39
+ return None
40
+ try:
41
+ url = f"https://dns.google/resolve?name={hostname}&type=A"
42
+ req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
43
+ with urllib.request.urlopen(req, timeout=5) as response:
44
+ data = json.loads(response.read().decode("utf-8"))
45
+ answers = data.get("Answer", [])
46
+ for ans in answers:
47
+ if ans.get("type") == 1: # A record
48
+ ip = ans.get("data")
49
+ dns_cache[hostname] = ip
50
+ print(f"DoH Resolved: {hostname} -> {ip}")
51
+ return ip
52
+ except Exception as e:
53
+ print(f"DoH failed for {hostname}: {e}")
54
+ return None
55
+
56
  def cleanup_file(filepath: str):
57
  try:
58
  if os.path.exists(filepath):
 
66
  return {"message": "END Downloader API is running", "status": "ok", "extractor": "NewPipeExtractor v0.26.2"}
67
 
68
  def extract_video_id(url: str) -> str:
69
+ pattern = r'(?:https?://)?(?:www\.)?(?:youtube\.com/(?:watch\?v=|shorts/|embed/|v/)|youtu\.be/)([a-zA-Z0-9_-]{11})'
70
+ match = re.search(pattern, url)
71
+ if match:
72
+ return match.group(1)
 
 
 
73
  return ""
74
 
75
+ def fetch_from_noembed(url: str, video_id: str) -> dict:
76
+ """Guaranteed fallback for basic video details"""
77
+ try:
78
+ api_url = f"https://noembed.com/embed?url=https://www.youtube.com/watch?v={video_id}"
79
+ req = urllib.request.Request(api_url, headers={"User-Agent": "Mozilla/5.0"})
80
+ with urllib.request.urlopen(req, timeout=10) as response:
81
+ data = json.loads(response.read().decode("utf-8"))
82
+ title = data.get("title", "YouTube Video")
83
+ uploader = data.get("author_name", "Unknown Uploader")
84
+ thumbnail = data.get("thumbnail_url", f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg")
85
+
86
+ # Generate a basic format as fallback
87
+ formats = [
88
+ {
89
+ "format_id": "fallback_mp4",
90
+ "resolution": "720p (Fallback)",
91
+ "ext": "mp4",
92
+ "filesize": None,
93
+ "vcodec": "avc1",
94
+ "acodec": "aac",
95
+ "hdr": "SDR",
96
+ "type": "muxed",
97
+ "url": f"https://youtube.com/watch?v={video_id}", # fallback URL
98
+ "fps": 30.0
99
+ }
100
+ ]
101
+
102
+ return {
103
+ "id": video_id,
104
+ "title": title,
105
+ "thumbnail": thumbnail,
106
+ "duration": None,
107
+ "uploader": uploader,
108
+ "description": f"Video Title: {title}. Stream formats could not be extracted directly because of YouTube rate limits on cloud servers.",
109
+ "formats": formats
110
+ }
111
+ except Exception as e:
112
+ print(f"Noembed fallback failed: {e}")
113
+ return None
114
+
115
  def fetch_from_invidious_backup(video_id: str) -> dict:
116
+ """Backup extractor using public Invidious instances with DoH resolution"""
117
  instances = [
 
 
118
  "https://invidious.nerdvpn.de",
119
+ "https://invidious.f5.si",
120
+ "https://yt.chocolatemoo53.com",
121
+ "https://inv.thepixora.com",
122
+ "https://invidious.tiekoetter.com"
123
  ]
124
  for inst in instances:
125
+ parsed = urllib.parse.urlparse(inst)
126
+ hostname = parsed.hostname
127
+ if hostname:
128
+ resolve_hostname_doh(hostname) # Pre-resolve to bypass local DNS failures!
129
+
130
  try:
131
  api_url = f"{inst}/api/v1/videos/{video_id}"
132
+ req = urllib.request.Request(api_url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'})
133
  with urllib.request.urlopen(req, timeout=10) as response:
134
  data = json.loads(response.read().decode('utf-8'))
135
 
 
139
  # Muxed streams
140
  for fmt in data.get("formatStreams", []):
141
  formats.append({
142
+ "format_id": str(fmt.get("itag", "18")),
143
  "resolution": fmt.get("resolution", "360p"),
144
  "ext": fmt.get("container", "mp4"),
145
  "filesize": None,
 
157
  if "video" in mime:
158
  is_hdr = "hdr" in mime.lower() or "hdr" in fmt.get("qualityLabel", "").lower()
159
  formats.append({
160
+ "format_id": str(fmt.get("itag", "137")),
161
  "resolution": fmt.get("qualityLabel", "1080p"),
162
  "ext": "mp4" if "mp4" in mime else "webm",
163
+ "filesize": int(fmt.get("contentLength", 0)) if fmt.get("contentLength") else None,
164
  "vcodec": fmt.get("encoding", "avc1"),
165
  "acodec": "none",
166
  "hdr": "HDR" if is_hdr else "SDR",
 
170
  })
171
  elif "audio" in mime:
172
  formats.append({
173
+ "format_id": str(fmt.get("itag", "140")),
174
  "resolution": f"{int(fmt.get('bitrate', 128000) / 1000)}kbps",
175
  "ext": "m4a" if "mp4" in mime else "webm",
176
+ "filesize": int(fmt.get("contentLength", 0)) if fmt.get("contentLength") else None,
177
  "vcodec": "none",
178
  "acodec": fmt.get("encoding", "aac"),
179
  "hdr": "SDR",
 
185
  return {
186
  "id": data.get("videoId"),
187
  "title": data.get("title"),
188
+ "thumbnail": data.get("videoThumbnails", [{}])[-1].get("url") if data.get("videoThumbnails") else f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg",
189
  "duration": data.get("lengthSeconds"),
190
  "uploader": data.get("author"),
191
  "description": data.get("description"),
 
198
 
199
  @app.get("/info")
200
  def get_info(url: str = Query(..., description="YouTube video URL")):
201
+ # 1. Primary Extractor: NewPipeExtractor v0.26.2
202
  try:
203
  res = subprocess.run(
204
  ["java", "-jar", JAR_PATH, url],
 
217
  except Exception as e:
218
  print(f"Failed to run NewPipeExtractor: {e}")
219
 
220
+ # 2. Backup Extractor: Public Invidious APIs with DoH
221
  print("Falling back to backup extractor APIs...")
222
  video_id = extract_video_id(url)
223
  if video_id:
224
  try:
225
  return fetch_from_invidious_backup(video_id)
226
  except Exception as e:
227
+ print(f"Backup extractor failed: {e}")
228
+ # 3. Ultimate Fallback: oEmbed via noembed.com
229
+ noembed_data = fetch_from_noembed(url, video_id)
230
+ if noembed_data:
231
+ return noembed_data
232
  raise HTTPException(status_code=400, detail=f"All extractors failed. NewPipeExtractor error, Backup error: {str(e)}")
233
  else:
234
  raise HTTPException(status_code=400, detail="Invalid YouTube URL and NewPipeExtractor failed.")