youtube-to-ebook
Transform YouTube videos into beautifully formatted ebook articles with transcripts
1How It Works & Mechanism
Four stages are linked into a single local pipeline (fetch videos → get transcripts → rewrite into magazine-style articles with Claude → package into EPUB and deliver via email); main.py simply invokes them sequentially without any branching logic.
from get_videos import main as fetch_videos from get_transcripts import get_transcripts_for_videos from write_articles import write_articles_for_videos from send_email import send_newsletter from video_tracker import filter_new_videos, mark_videos_processed, get_processed_count
Video discovery uses the YouTube Data API uploads playlist rather than search (bypassing the issue that 'search does not guarantee chronological order'), and relies on /shorts/ URL redirects to identify Shorts.
# Get the 15 most recent videos from the uploads playlist # The uploads playlist is always in exact upload order (newest first)
Transcript retrieval practically relies on a third-party commercial API (Supadata) rather than YouTube's official API or open-source libraries—code comments explicitly state this is to 'avoid YouTube bans'.
This script uses the Supadata API to fetch transcripts reliably. Unlike the youtube-transcript-api library, Supadata doesn't get blocked by YouTube.
'Magazine-style rewriting' uses a hardcoded, lengthy prompt: requiring title rewrites, targeting non-expert readers, explaining technical terms, preserving counterintuitive perspectives and memorable quotes, and explicitly mandating standalone articles readable without watching the video.
- Do NOT include phrases like "In this video" - write it as a standalone article. Assume the reader has not watched the video and has zero context about it. This article is meant to be as a replacement, not complement, for watching the video.
The final product is a genuine EPUB (ebooklib generates TOC + chapters + CSS + reading order) delivered as an attachment via Gmail SMTP_SSL on port 465, while archiving HTML/EPUB/JSON files to newsletters/.
# Create the ebook book = epub.EpubBook()
Deduplication relies on a local JSON tracker: processed video IDs are persisted to processed_videos.json, processing only unseen new videos and marking them only after the email has been successfully sent.
TRACKER_FILE = os.path.join(os.path.dirname(__file__), "processed_videos.json")
Two optional automation channels are included in the repository: macOS launchd (plist + run_newsletter.sh with log redirection) and a weekly Wednesday GitHub Actions cron schedule (requiring 4 secrets).
env: YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GMAIL_ADDRESS: ${{ secrets.GMAIL_ADDRESS }} GMAIL_APP_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }} SUPADATA_API_KEY: ${{ secrets.SUPADATA_API_KEY }}
The bundled Streamlit dashboard is implemented by 'directly modifying source code files': the channel list edits get_videos.py, writing style modifies the prompt inside write_articles.py, and scheduling edits the plist under ~/Library/LaunchAgents followed by launchctl reload.
CHANNELS_FILE = PROJECT_DIR / "get_videos.py" PROMPT_FILE = PROJECT_DIR / "write_articles.py"
2Core Capabilities
3External Dependencies
| Kind | Dependency / Endpoint |
|---|---|
| api | YouTube Data API v3(Google) |
| network | YouTube Shorts URL 探测(HEAD 请求判重定向) |
| api | Supadata 字幕 API(第三方商业服务) |
| api | Anthropic Claude API(文章改写) |
| network | Gmail SMTP(投递 EPUB 附件) |
| package | Python 依赖(含一个已不再被代码使用的库) |
| package | streamlit(dashboard 专用,未列入 requirements.txt) |
| cli | launchctl(macOS 排期,dashboard 内以子进程调用) |
| network | Google Fonts(dashboard 页面样式 @import,浏览器侧发起) |
4Recommended Governance & Evidence
- scripts/: Local script/command execution: python main.py (main pipeline; entry point specified in README/SKILL.md)
- scripts/ 或 SKILL.md: Dynamic external language package dependency installation declared
- 网络端点: External network access declared: googleapis.com YouTube Data API v3 (channels.list / playlistItems.list, with dev key)
- 本地工作目录: File I/O and artifact output: processed_videos.json (tracks processed videos within project root)
5Second-Pass Verification
- [ok] 四阶段流水线与去重逻辑 — main.py 五个 import 与方法体逐行核对;video_tracker.py 的 load/save/is_video_processed/mark_video_processed 与 processed_videos.json 路径均存在。
- [discrepancy] 字幕来自 Supadata(而非 SKILL.md 所述 youtube-transcript-api) — 代码侧唯一实现是 Supadata(get_transcripts.py 顶部注释与 SUPADATA_TRANSCRIPT_URL 常量);但 SKILL.md『Known Pitfalls』#3 仍在教 'ytt_api = YouTubeTranscriptApi(); transcript = ytt_api.fetch(video_id)',requirements.txt 也仍列 youtube-transcript-api——全仓 .py 无一处 import youtube_transcript_api。文档描述的是已被替换掉的旧实现。
- [discrepancy] 『云服务会被 YouTube 封,请本地跑』与仓库自带 Actions 工作流 — SKILL.md『### 6. Cloud Automation Blocked … Solution: Run automation locally on your Mac using launchd』、README『| Cloud servers blocked | Run locally, not GitHub Actions |』,但 .github/workflows/newsletter.yml 仍是完整的 ubuntu-latest 定时任务(cron '0 15 * * 3'),且注入 5 个 secrets。两种说法不能同时成立;从代码看 Supadata 正是为绕开该封锁而引入('Supadata doesn't get blocked by YouTube'),故云路径更可能是『能跑但不保证字幕』。
- [discrepancy] 配置入口 channels.txt 与 `python main.py --channels` — SKILL.md 命令表写 '| `python main.py --channels` | Edit channel list |'、Key Files 与 README 结构表都列 channels.txt;实际 main.py 无 argparse(全仓 .py 无 argparse 引用),频道是 get_videos.py 的 CHANNELS 常量,dashboard 通过 CHANNELS_FILE = get_videos.py 改源码。channels.txt 在仓库中不存在。
- [discrepancy] SUPADATA_API_KEY 的文档覆盖 — .env.example 只有 YOUTUBE_API_KEY / ANTHROPIC_API_KEY / GMAIL_ADDRESS / GMAIL_APP_PASSWORD 四项,而 get_transcripts.py 在无该 key 时直接返回 None(『⚠ SUPADATA_API_KEY not set in .env file』)导致流水线无字幕。文档缺失这把必需 key(CI workflow 里反而有 secrets.SUPADATA_API_KEY)。
- [discrepancy] 许可声明一致性 — README 末段写 'MIT - Use freely, modify as needed.',但 git ls-tree 无 LICENSE 文件,gh api license=null(spdx 未识别)。已在 meta.license 如实标注冲突,未按 MIT 硬写。
- [ok] 邮件投递与留档细节 — SMTP_SSL('smtp.gmail.com', 465) + server.login + sendmail;MIMEBase('application','epub+zip') 附件;save_newsletter_archive 写 html/epub/json 三件套;成功后 os.remove 临时 EPUB——与 SKILL.md『Email Delivery (Optional)』一致。
- [ok] dashboard 的破坏性写回(源码/plist) — save_channels 重写 get_videos.py 的 CHANNELS 块、风格保存重写 write_articles.py 内 prompt、save_schedule 重写 ~/Library/LaunchAgents plist 并 launchctl bootout/bootstrap —— 已在 file_writes 与 injection_surface 记入;另确认 requirements.txt 未含 streamlit(README 要求单独 pip install)。
6Verdict & Scenarios
2f7c07f82fd3668b…