RAG Over a YouTube Channel: RSS to Cited Answers

Building RAG over a YouTube channel means answering "what has this creator said about X, and in which video?" — across everything they've published. The pipeline is four steps: discover videos via RSS, fetch transcripts, chunk and embed, answer with attribution. Here's each step in runnable Python, including the parts most writeups skip: quota handling and what citation honestly looks like without timestamps.
Step 1: video IDs from the channel RSS feed
YouTube still ships a real RSS feed per channel, no API key required:
import requests
import xml.etree.ElementTree as ET
CHANNEL_ID = "UCxxxxxxxxxxxxxxxxxxxxxx"
NS = "{http://www.youtube.com/xml/schemas/2015}"
feed = requests.get(
f"https://www.youtube.com/feeds/videos.xml?channel_id={CHANNEL_ID}",
timeout=30,
).text
video_ids = [el.text for el in ET.fromstring(feed).iter(f"{NS}videoId")]
Caveat: the feed only returns the ~15 most recent uploads. For an active channel, run this on a schedule and accumulate IDs in a file or table; the back catalog beyond that requires the YouTube Data API or a one-time manual list. For keeping an index fresh, though, RSS is perfect — it's also the backbone of the channel-to-newsletter pipeline.
Step 2: fetch transcripts, respect the quota
import time
API_KEY = "yt_your_key_here"
def fetch(video_id: str) -> dict | None:
resp = requests.get(
"https://youtube2text.org/api/transcribe",
params={"url": f"https://www.youtube.com/watch?v={video_id}"},
headers={"x-api-key": API_KEY},
timeout=90,
)
if resp.status_code == 404:
return None # TRANSCRIPT_UNAVAILABLE — no captions, skip
if resp.status_code == 429:
time.sleep(resp.json()["error"].get("retryAfterSeconds", 30))
return fetch(video_id)
resp.raise_for_status()
return resp.json()["result"]
transcripts = [t for vid in video_ids if (t := fetch(vid))]
Do the math on your plan before looping over a channel: the free tier is 5 videos a month, which covers a proof of concept and nothing else. Basic ($5.99) buys 50, pro ($9.99) buys 500 — pro is the realistic floor for indexing a channel plus its ongoing uploads.
Step 3: chunk and embed
Any vector store works; Chroma keeps the example short. Auto-captions often lack punctuation, so I chunk by word count instead of sentences:
import chromadb
def chunk(text: str, size: int = 300, overlap: int = 50):
words = text.split()
for i in range(0, len(words), size - overlap):
yield " ".join(words[i:i + size])
client = chromadb.PersistentClient(path="./channel_index")
col = client.get_or_create_collection("channel")
for t in transcripts:
for i, piece in enumerate(chunk(t["content"])):
col.add(
ids=[f'{t["videoId"]}-{i}'],
documents=[piece],
metadatas=[{"videoId": t["videoId"], "title": t["title"]}],
)
Step 4: answer with citations — honest version
The API returns plain text without timestamps, so I'll be straight with you: you cannot cite "at 23:41" from this data. What you can do — and what's usually enough — is chunk-level video attribution: every retrieved chunk knows its videoId and title, so answers cite which video said it, with a working link.
hits = col.query(query_texts=["what's their take on electric trucks?"], n_results=5)
context = "\n\n".join(
f'[{m["title"]}] (https://www.youtube.com/watch?v={m["videoId"]})\n{doc}'
for doc, m in zip(hits["documents"][0], hits["metadatas"][0])
)
prompt = (
"Answer using only the excerpts below. After each claim, "
f"cite the video title in brackets.\n\n{context}\n\nQuestion: ..."
)
Feed prompt to whatever LLM you like — a local model works fine here, same setup as the Ollama summarization pipe.
Assembly notes
Framework people: this maps one-to-one onto LlamaIndex or LangChain if you'd rather not hand-roll the chunking. Either way the transcript layer is identical, and it's the only part with a meter on it — grab a key at youtube2text.org/app/keys and start with the free tier to validate the pipeline before pointing it at 300 videos.