ztoolx commited on
Commit
a3436de
·
verified ·
1 Parent(s): 0f0adb1
Files changed (1) hide show
  1. main.py +120 -102
main.py CHANGED
@@ -2,6 +2,7 @@ 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
  from pydantic import BaseModel
6
 
7
  app = FastAPI()
@@ -34,127 +35,148 @@ def parse_caption_xml(xml_string: str) -> str:
34
  full_text = full_text.replace("'", "'").replace(""", '"').replace("&", "&").replace("'", "'")
35
  return full_text
36
 
37
- def fetch_via_innertube(video_id: str, payload: dict) -> str:
38
- innertube_url = "https://www.youtube.com/youtubei/v1/player?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false"
39
-
40
- headers = {
41
- "Content-Type": "application/json",
42
- "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",
43
- "Origin": "https://www.youtube.com",
44
- "Referer": "https://www.youtube.com/",
45
- "Cookie": "CONSENT=YES+1; SOCS=CAESHAgBEhJnd3NfMjAyMzEwMjUtMF9SQzIaAmVuIAEaBgiA-LaoBg"
46
- }
47
-
48
- # Using chrome120 to avoid Hugging Face SSL bug with 131
49
- response = cffi_requests.post(innertube_url, json=payload, headers=headers, impersonate="chrome120", timeout=10)
50
-
51
- if response.status_code != 200:
52
- raise Exception(f"YouTube API returned status {response.status_code}")
53
-
54
- data = response.json()
55
-
56
- playability = data.get("playabilityStatus", {})
57
- if playability.get("status") == "ERROR":
58
- raise Exception(playability.get("reason", "Video unavailable"))
59
-
60
  caption_tracks = data.get("captions", {}).get("playerCaptionsTracklistRenderer", {}).get("captionTracks", [])
61
-
62
  if not caption_tracks:
63
- raise Exception("No captions found in this client context.")
64
-
65
- selected_track = None
66
- for track in caption_tracks:
67
- if track.get("languageCode") == "en":
68
- selected_track = track
69
- break
70
-
71
- if not selected_track:
72
- selected_track = caption_tracks[0]
73
-
74
  caption_url = selected_track.get("baseUrl")
75
  if not caption_url:
76
- raise Exception("Caption URL missing.")
77
 
78
- caption_response = cffi_requests.get(caption_url, impersonate="chrome120", timeout=10)
79
-
80
  if caption_response.status_code != 200:
81
- raise Exception(f"Caption fetch failed with status {caption_response.status_code}")
82
 
83
  full_text = parse_caption_xml(caption_response.text)
84
-
85
  if not full_text.strip():
86
- raise Exception("Caption XML was empty.")
87
-
88
  return full_text
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  @app.post("/api/transcript")
91
  async def get_transcript(req: TranscriptRequest):
92
  video_id = extract_video_id(req.url)
93
  if not video_id:
94
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
95
 
96
- # We will try 3 different client contexts. YouTube strips captions from WEB on cloud IPs,
97
- # but usually leaves them for TV and Android contexts.
98
- contexts = [
99
- {
100
- "name": "TV_EMBEDDED",
101
- "payload": {
102
- "context": {
103
- "client": {
104
- "clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
105
- "clientVersion": "2.0",
106
- "hl": "en",
107
- "gl": "US"
108
- },
109
- "thirdParty": {
110
- "embedUrl": "https://www.google.com"
111
- }
112
- },
113
- "videoId": video_id
114
- }
115
- },
116
- {
117
- "name": "ANDROID",
118
- "payload": {
119
- "context": {
120
- "client": {
121
- "clientName": "ANDROID",
122
- "clientVersion": "19.29.37",
123
- "androidSdkVersion": 30,
124
- "hl": "en",
125
- "gl": "US"
126
- }
127
- },
128
- "videoId": video_id
129
- }
130
- },
131
- {
132
- "name": "WEB",
133
- "payload": {
134
- "context": {
135
- "client": {
136
- "clientName": "WEB",
137
- "clientVersion": "2.20241210.00.00",
138
- "hl": "en",
139
- "gl": "US"
140
- }
141
- },
142
- "videoId": video_id
143
- }
144
- }
145
  ]
146
 
147
- for ctx in contexts:
148
  try:
149
- print(f"Trying {ctx['name']} context for video {video_id}...")
150
- full_text = fetch_via_innertube(video_id, ctx["payload"])
151
- print(f"✅ Successfully fetched transcript via {ctx['name']}. Length: {len(full_text)} chars")
152
  return {"text": full_text}
153
  except Exception as e:
154
- print(f"❌ {ctx['name']} failed: {e}")
155
  continue
156
 
157
- raise HTTPException(status_code=500, detail="Failed to fetch transcript. YouTube is actively blocking server requests or captions are disabled.")
158
 
159
  # ========================================================
160
  # TRANSLATE API
@@ -167,15 +189,11 @@ async def translate_text(req: TranslateRequest):
167
  try:
168
  url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={req.targetLang}&dt=t"
169
  res = cffi_requests.post(url,
170
- headers={
171
- "Content-Type": "application/x-www-form-urlencoded",
172
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
173
- },
174
  data=f"q={cffi_requests.utils.quote(req.text)}",
175
  impersonate="chrome120",
176
  timeout=15
177
  )
178
-
179
  if res.status_code != 200:
180
  raise Exception(f"Google API returned {res.status_code}")
181
 
 
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
7
 
8
  app = FastAPI()
 
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
 
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