1 # YouTube Processing
2
3 How to extract transcripts, metadata, and audio from YouTube videos for the podcast marketing pipeline.
4
5 ## Table of Contents
6
7 - [URL Handling](#url-handling) — Recognize and normalize YouTube URLs
8 - [Caption Extraction](#caption-extraction) — Fetch auto-generated or manual subtitles (preferred, no API key)
9
10 - [Audio Download & Transcription](#audio-download--transcription) — Fallback when no captions exist
11 - [Metadata Extraction](#metadata-extraction) — Title, channel, description, duration
12
13 - [Integration with Content Pipeline](#integration-with-content-pipeline) — How YouTube input feeds into the standard pipeline
14
15 ## URL Handling
16
17 YouTube URLs come in several formats. Normalize to a video ID before processing:
18
19 - `https://www.youtube.com/watch?v=VIDEO_ID`
20 - `https://youtu.be/VIDEO_ID`
21
22 - `https://www.youtube.com/embed/VIDEO_ID`
23 - `https://m.youtube.com/watch?v=VIDEO_ID`
24
25 - URLs with extra parameters: `&t=120`, `&list=PLxxx`, `&si=xxx`
26
27 ```python
28
29 import re
30
31 def extract_video_id(url: str) -> str | None:
32
33 patterns = [
34
35 r"(?:youtube\\com/watch\\v=|youtu\\be/|youtube\\com/embed/)([a-zA-Z0-9_-]{11})",
36
37 ]
38
39 for pattern in patterns:
40
41 match = re.search(pattern, url)
42
43 if match:
44
45 return match.group(1)
46
47 return None
48
49 ```
50
51 ## Caption Extraction
52
53 The preferred approach — fast, free, and doesn't require downloading the full audio.
54
55 ### Using youtube-transcript-api
56
57 ```bash
58
59 pip install youtube-transcript-api
60
61 ```
62
63 ```python
64
65 from youtube_transcript_api import YouTubeTranscriptApi
66
67 def get_youtube_transcript(video_id: str, languages: list[str] | None = None) -> dict:
68
69 """Fetch transcript from YouTube's caption system.
70
71 Tries manually uploaded captions first, then auto-generated.
72
73 Returns a normalized transcript dict compatible with the content pipeline.
74
75 Args:
76
77 video_id: The 11-character YouTube video ID.
78
79 languages: Preferred languages in order. Defaults to ["en"].
80
81 Returns:
82
83 dict with "text" (full transcript string) and "segments"
84
85 (list of {start, end, text} dicts).
86
87 Raises:
88
89 Exception if no captions are available in any language.
90
91 """
92
93 if languages is None:
94
95 languages = ["en"]
96
97 try:
98
99 transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
100
101 transcript = None
102
103 try:
104
105 transcript = transcript_list.find_manually_created_transcript(languages)
106
107 except Exception:
108
109 try:
110
111 transcript = transcript_list.find_generated_transcript(languages)
112
113 except Exception:
114
115 pass
116
117 if transcript is None:
118
119 available = list(transcript_list)
120
121 if available:
122
123 transcript = available[0]
124
125 if transcript.language_code not in languages:
126
127 transcript = transcript.translate("en")
128
129 if transcript is None:
130
131 raise Exception(f"No captions available for video {video_id}")
132
133 entries = transcript.fetch()
134
135 segments = []
136
137 for entry in entries:
138
139 segments.append({
140
141 "start": entry.start,
142
143 "end": entry.start + entry.duration,
144
145 "text": entry.text.strip(),
146
147 })
148
149 full_text = " ".join(seg["text"] for seg in segments)
150
151 return {"text": full_text, "segments": segments}
152
153 except Exception as e:
154
155 raise Exception(f"Failed to get transcript for {video_id}: {e}")
156
157 ```
158
159 ### Caption Quality Notes
160
161 - **Manually uploaded captions** are highest quality — proper punctuation, speaker labels sometimes included.
162 - **Auto-generated captions** are decent for clear speech but may have errors with technical terms, names, and acronyms. The content generation pipeline should handle minor transcription errors gracefully.
163
164 - **No captions available** — some videos have captions disabled. Fall back to audio download + Whisper transcription.
165
166 ## Audio Download & Transcription
167
168 Fallback when YouTube captions are unavailable. Uses yt-dlp to download the audio track.
169
170 ### Setup
171
172 ```bash
173
174 pip install yt-dlp
175
176 ```
177
178 yt-dlp is actively maintained and handles YouTube's frequent changes. Avoid using the deprecated `youtube-dl`.
179
180 ### Audio Download
181
182 ```python
183
184 import yt_dlp
185
186 import os
187
188 def download_youtube_audio(video_id: str, output_dir: str = "/tmp") -> str:
189
190 """Download audio from a YouTube video.
191
192 Returns the path to the downloaded audio file.
193
194 """
195
196 os.makedirs(output_dir, exist_ok=True)
197
198 output_path = os.path.join(output_dir, f"{video_id}.mp3")
199
200 ydl_opts = {
201
202 "format": "bestaudio/best",
203
204 "postprocessors": [{
205
206 "key": "FFmpegExtractAudio",
207
208 "preferredcodec": "mp3",
209
210 "preferredquality": "128",
211
212 }],
213
214 "outtmpl": os.path.join(output_dir, f"{video_id}.%(ext)s"),
215
216 "quiet": True,
217
218 "no_warnings": True,
219
220 }
221
222 with yt_dlp.YoutubeDL(ydl_opts) as ydl:
223
224 ydl.download([f"https://www.youtube.com/watch?v={video_id}"])
225
226 return output_path
227
228 ```
229
230 After downloading, pass the audio file to `whisper_transcribe.transcribe_episode()` from the existing transcription script.
231
232 ## Metadata Extraction
233
234 Extract video metadata without downloading the full video:
235
236 ```python
237
238 import yt_dlp
239
240 def get_youtube_metadata(video_id: str) -> dict:
241
242 """Extract metadata from a YouTube video.
243
244 Returns title, channel, description, duration, publish date,
245
246 and thumbnail URL.
247
248 """
249
250 ydl_opts = {
251
252 "quiet": True,
253
254 "no_warnings": True,
255
256 "skip_download": True,
257
258 }
259
260 with yt_dlp.YoutubeDL(ydl_opts) as ydl:
261
262 info = ydl.extract_info(
263
264 f"https://www.youtube.com/watch?v={video_id}",
265
266 download=False,
267
268 )
269
270 return {
271
272 "title": info.get("title", "Untitled"),
273
274 "channel": info.get("channel", info.get("uploader", "Unknown")),
275
276 "description": info.get("description", ""),
277
278 "duration_seconds": info.get("duration", 0),
279
280 "publish_date": info.get("upload_date", ""),
281
282 "thumbnail": info.get("thumbnail", ""),
283
284 "view_count": info.get("view_count", 0),
285
286 "url": f"https://www.youtube.com/watch?v={video_id}",
287
288 "tags": info.get("tags", []),
289
290 }
291
292 ```
293
294 ## Integration with Content Pipeline
295
296 When processing a YouTube link, the output maps to the standard pipeline like this:
297
298 1. **Episode metadata** comes from YouTube video metadata (title, channel as host/guest, description, publish date).
299 2. **Transcript** comes from captions or Whisper transcription — same format as other input modes.
300
301 3. **Episode link** in generated content should point to the YouTube video URL instead of a podcast player link.
302 4. **Guest name** — try extracting from the video title (common patterns: "Interview with X", "X on Y", "feat. X") or from the video description. Falls back to the channel name.
303
304 The content pipeline doesn't need to know the source was YouTube — once you have a transcript and metadata dict, everything works identically.
305
306 ### Recommended Workflow
307
308 ```python
309
310 from youtube_extractor import extract_video_id, get_youtube_metadata
311
312 video_id = extract_video_id(user_provided_url)
313
314 metadata = get_youtube_metadata(video_id)
315
316 try:
317
318 transcript = get_youtube_transcript(video_id)
319
320 except Exception:
321
322 audio_path = download_youtube_audio(video_id)
323
324 transcript = transcribe_episode(audio_path)
325
326 # Now pass transcript + metadata to the content generation pipeline
327
328 ```