ztoolx commited on
Commit
f1c5d90
·
verified ·
1 Parent(s): eceba31
Files changed (1) hide show
  1. main.py +83 -28
main.py CHANGED
@@ -1,8 +1,7 @@
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.middleware.cors import CORSMiddleware
3
- from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound, VideoUnavailable
4
  import re
5
- import requests
6
  from pydantic import BaseModel
7
 
8
  app = FastAPI()
@@ -28,6 +27,14 @@ def extract_video_id(url: str) -> str:
28
  match = re.search(pattern, url)
29
  return match.group(1) if match else None
30
 
 
 
 
 
 
 
 
 
31
  @app.post("/api/transcript")
32
  async def get_transcript(req: TranscriptRequest):
33
  video_id = extract_video_id(req.url)
@@ -35,36 +42,83 @@ async def get_transcript(req: TranscriptRequest):
35
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
36
 
37
  try:
38
- print(f"Attempting to fetch transcript for video_id: {video_id} using youtube-transcript-api 0.5.0")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- # Try fetching English transcript first
41
- try:
42
- print("Trying English transcript...")
43
- transcript_data = YouTubeTranscriptApi.get_transcript(video_id, languages=['en'])
44
- print("Found English transcript.")
45
- except NoTranscriptFound:
46
- print("No English found, trying any available language...")
47
- # Fallback to any language if English is not found
48
- try:
49
- transcript_data = YouTubeTranscriptApi.get_transcript(video_id)
50
- print("Found fallback language transcript.")
51
- except Exception as e:
52
- print(f"Fallback failed: {e}")
53
- raise HTTPException(status_code=404, detail="No captions found for this video.")
54
 
55
- # Combine all text lines safely
56
- full_text = " ".join([line.get('text', '') for line in transcript_data])
57
 
58
- # Clean up HTML entities
59
- full_text = full_text.replace("'", "'").replace(""", '"').replace("&", "&").replace("'", "'")
 
 
 
 
 
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  print(f"Successfully fetched transcript. Length: {len(full_text)} chars")
62
  return {"text": full_text}
63
 
64
- except TranscriptsDisabled:
65
- raise HTTPException(status_code=403, detail="Captions are disabled for this video.")
66
- except VideoUnavailable:
67
- raise HTTPException(status_code=404, detail="Video is unavailable.")
68
  except Exception as e:
69
  error_name = type(e).__name__
70
  print(f"UNHANDLED ERROR for {video_id}: {error_name} - {str(e)}")
@@ -80,13 +134,14 @@ async def translate_text(req: TranslateRequest):
80
 
81
  try:
82
  url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={req.targetLang}&dt=t"
83
- res = requests.post(url,
84
  headers={
85
  "Content-Type": "application/x-www-form-urlencoded",
86
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
87
  },
88
- data=f"q={requests.utils.quote(req.text)}",
89
- timeout=10
 
90
  )
91
 
92
  if res.status_code != 200:
 
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
  from pydantic import BaseModel
6
 
7
  app = FastAPI()
 
27
  match = re.search(pattern, url)
28
  return match.group(1) if match else None
29
 
30
+ def parse_caption_xml(xml_string: str) -> str:
31
+ text_matches = re.findall(r'<text[^>]*>([^<]*)</text>', xml_string)
32
+ if not text_matches:
33
+ return ""
34
+ full_text = " ".join(text_matches)
35
+ full_text = full_text.replace("&amp;#39;", "'").replace("&amp;quot;", '"').replace("&amp;amp;", "&").replace("&#39;", "'")
36
+ return full_text
37
+
38
  @app.post("/api/transcript")
39
  async def get_transcript(req: TranscriptRequest):
40
  video_id = extract_video_id(req.url)
 
42
  raise HTTPException(status_code=400, detail="Invalid YouTube URL")
43
 
44
  try:
45
+ print(f"Fetching InnerTube data for video_id: {video_id} using Chrome Impersonation...")
46
+
47
+ # YouTube's internal API endpoint
48
+ innertube_url = "https://www.youtube.com/youtubei/v1/player?prettyPrint=false"
49
+
50
+ payload = {
51
+ "context": {
52
+ "client": {
53
+ "clientName": "WEB",
54
+ "clientVersion": "2.20241210.00.00",
55
+ "hl": "en",
56
+ "gl": "US"
57
+ }
58
+ },
59
+ "videoId": video_id
60
+ }
61
 
62
+ headers = {
63
+ "Content-Type": "application/json",
64
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
65
+ "Origin": "https://www.youtube.com",
66
+ "Referer": "https://www.youtube.com/",
67
+ "Cookie": "CONSENT=YES+1; SOCS=CAESHAgBEhJnd3NfMjAyMzEwMjUtMF9SQzIaAmVuIAEaBgiA-LaoBg"
68
+ }
69
+
70
+ # 1. Fetch the video player data (Impersonating Chrome to bypass SSLError)
71
+ response = cffi_requests.post(innertube_url, json=payload, headers=headers, impersonate="chrome131", timeout=10)
72
+
73
+ if response.status_code != 200:
74
+ raise HTTPException(status_code=500, detail=f"YouTube API returned status {response.status_code}")
 
75
 
76
+ data = response.json()
 
77
 
78
+ # Check if video is playable
79
+ playability = data.get("playabilityStatus", {})
80
+ if playability.get("status") == "ERROR":
81
+ raise HTTPException(status_code=404, detail=playability.get("reason", "Video unavailable"))
82
+
83
+ # 2. Extract caption tracks
84
+ caption_tracks = data.get("captions", {}).get("playerCaptionsTracklistRenderer", {}).get("captionTracks", [])
85
 
86
+ if not caption_tracks:
87
+ raise HTTPException(status_code=404, detail="No captions found for this video.")
88
+
89
+ # Prefer English, fallback to first available
90
+ selected_track = None
91
+ for track in caption_tracks:
92
+ if track.get("languageCode") == "en":
93
+ selected_track = track
94
+ break
95
+
96
+ if not selected_track:
97
+ selected_track = caption_tracks[0]
98
+
99
+ caption_url = selected_track.get("baseUrl")
100
+ if not caption_url:
101
+ raise HTTPException(status_code=404, detail="Caption URL not found.")
102
+
103
+ print(f"Fetching captions from URL using Chrome Impersonation...")
104
+
105
+ # 3. Fetch the actual caption XML (Impersonating Chrome)
106
+ caption_response = cffi_requests.get(caption_url, impersonate="chrome131", timeout=10)
107
+
108
+ if caption_response.status_code != 200:
109
+ raise HTTPException(status_code=500, detail=f"Caption fetch failed with status {caption_response.status_code}")
110
+
111
+ # 4. Parse the XML into plain text
112
+ full_text = parse_caption_xml(caption_response.text)
113
+
114
+ if not full_text.strip():
115
+ raise HTTPException(status_code=404, detail="Caption XML was empty.")
116
+
117
  print(f"Successfully fetched transcript. Length: {len(full_text)} chars")
118
  return {"text": full_text}
119
 
120
+ except HTTPException:
121
+ raise
 
 
122
  except Exception as e:
123
  error_name = type(e).__name__
124
  print(f"UNHANDLED ERROR for {video_id}: {error_name} - {str(e)}")
 
134
 
135
  try:
136
  url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl={req.targetLang}&dt=t"
137
+ res = cffi_requests.post(url,
138
  headers={
139
  "Content-Type": "application/x-www-form-urlencoded",
140
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
141
  },
142
+ data=f"q={cffi_requests.utils.quote(req.text)}",
143
+ impersonate="chrome131",
144
+ timeout=15
145
  )
146
 
147
  if res.status_code != 200: