ztoolx commited on
Commit
5847526
·
verified ·
1 Parent(s): a3436de
Files changed (1) hide show
  1. main.py +69 -129
main.py CHANGED
@@ -1,6 +1,6 @@
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
- from curl_cffi import requests as cffi_requests
4
  import re
5
  import json
6
  from pydantic import BaseModel
@@ -35,148 +35,85 @@ def parse_caption_xml(xml_string: str) -> str:
35
  full_text = full_text.replace("'", "'").replace(""", '"').replace("&", "&").replace("'", "'")
36
  return full_text
37
 
38
- def extract_captions_from_data(data: dict) -> str:
39
- caption_tracks = data.get("captions", {}).get("playerCaptionsTracklistRenderer", {}).get("captionTracks", [])
40
- if not caption_tracks:
41
- raise Exception("No captions found")
42
-
43
- selected_track = next((t for t in caption_tracks if t.get("languageCode") == "en"), caption_tracks[0])
44
- caption_url = selected_track.get("baseUrl")
45
- if not caption_url:
46
- raise Exception("Caption URL missing")
47
-
48
- caption_response = cffi_requests.get(caption_url, impersonate="chrome120", timeout=8)
49
- if caption_response.status_code != 200:
50
- raise Exception(f"Caption fetch failed: {caption_response.status_code}")
51
-
52
- full_text = parse_caption_xml(caption_response.text)
53
- if not full_text.strip():
54
- raise Exception("Caption XML empty")
55
- return full_text
56
-
57
- # ========================================================
58
- # METHOD 1: HTML Page Scrape (Exactly like Node.js package)
59
- # ========================================================
60
- def method1_html_scrape(video_id: str) -> str:
61
- print(f"Trying Method 1: HTML Scrape for {video_id}...")
62
  headers = {
63
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
64
- "Accept": "text/html,application/xhtml+xml",
65
- "Accept-Language": "en-US,en;q=0.9",
66
- "Cookie": "CONSENT=YES+1; SOCS=CAESHAgBEhJnd3NfMjAyMzEwMjUtMF9SQzIaAmVuIAEaBgiA-LaoBg"
67
- }
68
- response = cffi_requests.get(f"https://www.youtube.com/watch?v={video_id}", headers=headers, impersonate="chrome120", timeout=8)
69
- if response.status_code != 200:
70
- raise Exception(f"HTML fetch failed: {response.status_code}")
71
-
72
- match = re.search(r'ytInitialPlayerResponse\s*=\s*(\{.*?\})\s*;', response.text, re.DOTALL)
73
- if not match:
74
- raise Exception("ytInitialPlayerResponse not found in HTML")
75
-
76
- data = json.loads(match.group(1))
77
- return extract_captions_from_data(data)
78
-
79
- # ========================================================
80
- # METHOD 2: InnerTube API (MWEB - Mobile Web)
81
- # ========================================================
82
- def method2_innertube_mweb(video_id: str) -> str:
83
- print(f"Trying Method 2: InnerTube MWEB for {video_id}...")
84
- payload = {
85
- "context": {
86
- "client": {
87
- "clientName": "MWEB",
88
- "clientVersion": "2.20241210.00.00",
89
- "hl": "en", "gl": "US"
90
- }
91
- },
92
- "videoId": video_id
93
  }
94
- headers = {"Content-Type": "application/json", "Referer": "https://m.youtube.com/"}
95
- response = cffi_requests.post("https://www.youtube.com/youtubei/v1/player?prettyPrint=false", json=payload, headers=headers, impersonate="chrome120", timeout=8)
96
  if response.status_code != 200:
97
- raise Exception(f"MWEB API failed: {response.status_code}")
98
- return extract_captions_from_data(response.json())
99
 
100
- # ========================================================
101
- # METHOD 3: InnerTube API (TV Embedded)
102
- # ========================================================
103
- def method3_innertube_tv(video_id: str) -> str:
104
- print(f"Trying Method 3: InnerTube TV for {video_id}...")
105
- payload = {
106
- "context": {
107
- "client": {
108
- "clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
109
- "clientVersion": "2.0",
110
- "hl": "en", "gl": "US"
111
- },
112
- "thirdParty": {"embedUrl": "https://www.google.com"}
113
- },
114
- "videoId": video_id
115
- }
116
- headers = {"Content-Type": "application/json", "Referer": "https://www.youtube.com/"}
117
- response = cffi_requests.post("https://www.youtube.com/youtubei/v1/player?prettyPrint=false", json=payload, headers=headers, impersonate="chrome120", timeout=8)
118
- if response.status_code != 200:
119
- raise Exception(f"TV API failed: {response.status_code}")
120
- return extract_captions_from_data(response.json())
121
-
122
- # ========================================================
123
- # METHOD 4: Invidious API (Public Proxy)
124
- # ========================================================
125
- def method4_invidious(video_id: str) -> str:
126
- print(f"Trying Method 4: Invidious Proxies for {video_id}...")
127
- instances = [
128
- "https://inv.nadeko.net",
129
- "https://invidious.privacyredirect.com",
130
- "https://iv.ggtyler.dev"
131
- ]
132
- for host in instances:
133
- try:
134
- res = cffi_requests.get(f"{host}/api/v1/captions/{video_id}?label=English", impersonate="chrome120", timeout=5)
135
- if res.status_code != 200: continue
136
- data = res.json()
137
- if not data.get("captions"): continue
138
-
139
- caption = next((c for c in data["captions"] if "English" in c.get("label", "")), data["captions"][0])
140
- caption_url = caption.get("url")
141
- if not caption_url: continue
142
- if caption_url.startswith("/"): caption_url = f"{host}{caption_url}"
143
-
144
- cap_res = cffi_requests.get(caption_url, impersonate="chrome120", timeout=5)
145
- if cap_res.status_code != 200: continue
146
-
147
- texts = re.findall(r'<text[^>]*>([^<]*)</text>', cap_res.text)
148
- if not texts: continue
149
- return " ".join(texts).replace("&#39;", "'")
150
- except Exception:
151
- continue
152
- raise Exception("All Invidious instances failed")
153
-
154
- # ========================================================
155
- # MAIN ROUTE HANDLER
156
- # ========================================================
157
  @app.post("/api/transcript")
158
  async def get_transcript(req: TranscriptRequest):
159
  video_id = extract_video_id(req.url)
160
  if not video_id:
161
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
162
 
163
- methods = [
164
- method1_html_scrape,
165
- method2_innertube_mweb,
166
- method3_innertube_tv,
167
- method4_invidious
168
  ]
169
 
170
- for method in methods:
171
  try:
172
- full_text = method(video_id)
173
- print(f"✅ Success with {method.__name__}! Length: {len(full_text)} chars")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  return {"text": full_text}
 
175
  except Exception as e:
176
- print(f"❌ {method.__name__} failed: {e}")
177
  continue
178
 
179
- raise HTTPException(status_code=500, detail="Failed to fetch transcript after trying all methods. YouTube may be blocking this server IP entirely.")
180
 
181
  # ========================================================
182
  # TRANSLATE API
@@ -188,12 +125,15 @@ async def translate_text(req: TranslateRequest):
188
 
189
  try:
190
  url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={req.targetLang}&dt=t"
191
- res = cffi_requests.post(url,
192
- headers={"Content-Type": "application/x-www-form-urlencoded"},
193
- data=f"q={cffi_requests.utils.quote(req.text)}",
194
- impersonate="chrome120",
 
 
195
  timeout=15
196
  )
 
197
  if res.status_code != 200:
198
  raise Exception(f"Google API returned {res.status_code}")
199
 
 
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
+ import requests
4
  import re
5
  import json
6
  from pydantic import BaseModel
 
35
  full_text = full_text.replace("&amp;#39;", "'").replace("&amp;quot;", '"').replace("&amp;amp;", "&").replace("&#39;", "'")
36
  return full_text
37
 
38
+ def fetch_via_proxy(proxy_base: str, target_url: str, timeout: int = 15) -> requests.Response:
39
+ """Fetches a URL through a public CORS/Web proxy."""
40
+ encoded_url = requests.utils.quote(target_url, safe='')
41
+ proxy_url = f"{proxy_base}{encoded_url}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  headers = {
43
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
44
+ "Accept": "text/html,application/json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
+ response = requests.get(proxy_url, headers=headers, timeout=timeout)
 
47
  if response.status_code != 200:
48
+ raise Exception(f"Proxy returned status {response.status_code}")
49
+ return response
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  @app.post("/api/transcript")
52
  async def get_transcript(req: TranscriptRequest):
53
  video_id = extract_video_id(req.url)
54
  if not video_id:
55
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
56
 
57
+ # List of reliable public web proxies
58
+ proxies = [
59
+ "https://api.allorigins.win/raw?url=",
60
+ "https://corsproxy.io/?",
61
+ "https://api.codetabs.com/v1/proxy?quest="
62
  ]
63
 
64
+ for proxy in proxies:
65
  try:
66
+ print(f"Trying proxy {proxy} for video {video_id}...")
67
+
68
+ # 1. Fetch YouTube Watch Page HTML via Proxy
69
+ yt_url = f"https://www.youtube.com/watch?v={video_id}"
70
+ response = fetch_via_proxy(proxy, yt_url, timeout=15)
71
+ html = response.text
72
+
73
+ # 2. Find the ytInitialPlayerResponse JSON in the HTML
74
+ match = re.search(r'ytInitialPlayerResponse\s*=\s*(\{.*?\})\s*;', html, re.DOTALL)
75
+ if not match:
76
+ print(f"Failed to find player data via {proxy}")
77
+ continue
78
+
79
+ data = json.loads(match.group(1))
80
+
81
+ # Check playability
82
+ playability = data.get("playabilityStatus", {})
83
+ if playability.get("status") == "ERROR":
84
+ raise Exception(playability.get("reason", "Video unavailable"))
85
+
86
+ # 3. Extract caption tracks
87
+ caption_tracks = data.get("captions", {}).get("playerCaptionsTracklistRenderer", {}).get("captionTracks", [])
88
+ if not caption_tracks:
89
+ print(f"No captions found via {proxy}")
90
+ continue
91
+
92
+ # Prefer English, fallback to first available
93
+ selected_track = next((t for t in caption_tracks if t.get("languageCode") == "en"), caption_tracks[0])
94
+ caption_url = selected_track.get("baseUrl")
95
+ if not caption_url:
96
+ continue
97
+
98
+ print(f"Found captions, fetching XML via proxy...")
99
+
100
+ # 4. Fetch the actual caption XML via the same proxy
101
+ caption_response = fetch_via_proxy(proxy, caption_url, timeout=10)
102
+
103
+ # 5. Parse the XML into plain text
104
+ full_text = parse_caption_xml(caption_response.text)
105
+
106
+ if not full_text.strip():
107
+ continue
108
+
109
+ print(f"✅ Successfully fetched transcript via {proxy}. Length: {len(full_text)} chars")
110
  return {"text": full_text}
111
+
112
  except Exception as e:
113
+ print(f"❌ Proxy {proxy} failed: {e}")
114
  continue
115
 
116
+ raise HTTPException(status_code=500, detail="Failed to fetch transcript. All proxy methods failed or video has no captions.")
117
 
118
  # ========================================================
119
  # TRANSLATE API
 
125
 
126
  try:
127
  url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={req.targetLang}&dt=t"
128
+ res = requests.post(url,
129
+ headers={
130
+ "Content-Type": "application/x-www-form-urlencoded",
131
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
132
+ },
133
+ data=f"q={requests.utils.quote(req.text)}",
134
  timeout=15
135
  )
136
+
137
  if res.status_code != 200:
138
  raise Exception(f"Google API returned {res.status_code}")
139