9.0 KiB
t-youtube — Text-Based YouTube Digest
Overview
A self-hosted YouTube reader that syncs your subscriptions, generates AI summaries, and displays them in a Svelte dashboard. Designed to work around GFW restrictions in China and YouTube IP blocks on cloud providers.
Architecture
┌─────────────────────────┐
│ Oracle Tokyo VPS │
│ (free-amd-caddy) │
│ │
│ ┌──────────────────┐ │
│ │ Caddy (port 443) │ │
│ │ reverse_proxy to │────┼─── Cloudflare ─── Internet
│ │ NAS via Tailscale │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Cron (every 6h) │ │
│ │ fetch_and_sync.sh │────┼─── YouTube Data API
│ └──────────────────┘ │
└──────────┬───────────────┘
│ Tailscale (DERP relay)
┌──────────┴───────────────┐
│ NAS ds224plus │
│ (100.78.131.124) │
│ │
│ ┌───────────────────┐ │
│ │ Caddy (port 8443) │ │
│ │ Let's Encrypt via │ │
│ │ Cloudflare DNS │ │
│ └───────────────────┘ │
│ │
│ ┌───────────────────┐ │
│ │ Authelia (9092) │ │
│ │ SSO + 2FA │ │
│ └───────────────────┘ │
│ │
│ ┌───────────────────┐ │
│ │ t-youtube (8001) │ │
│ │ FastAPI + Svelte │ │
│ │ SQLite DB │ │
│ └───────────────────┘ │
│ │
│ ┌───────────────────┐ │
│ │ Gitea (3300) │ │
│ │ git.jimmygan.com │ │
│ └───────────────────┘ │
└──────────────────────────┘
Data Flow
- Fetch: VPS cron runs every 6h → YouTube Data API v3 (subscriptions list) + RSS feeds → new video metadata → local SQLite DB
- Summarize: VPS reads new videos from DB → calls Qwen35B on MacBook (port 8085 via Tailscale) → title-only summaries → saves to DB
- Sync: VPS
catDB → SSH to NAS →sudo tee→ overwrites NAS DB → restarts Docker container - Serve: NAS Docker (FastAPI + Svelte) → serves dashboard to browser → Authelia SSO in front
Deployment
VPS (Oracle Tokyo)
Files in /tmp/yt-fetch/:
| File | Purpose |
|---|---|
fetch_and_sync.sh |
Entry point — fetch → summarize → sync |
fetcher.py |
YouTube Data API + RSS hybrid fetcher |
summarize_new.py |
Title-based summarization via Qwen |
db.py |
SQLite schema |
auth.py |
OAuth credential management |
token.pickle |
YouTube OAuth token |
cookies.txt |
YouTube cookies (for transcript fallback) |
Cron: 0 */6 * * * bash /tmp/yt-fetch/fetch_and_sync.sh
NAS
Docker container at /volume1/docker/t-youtube/:
docker-compose.yml— port 8001:8000, env vars for DeepSeek/Qwen.env—DEEPSEEK_API_KEY,API_TOKENdata/t_youtube.db— SQLite database (synced from VPS)
MacBook
Qwen models run via llama-server:
- Port 8080: Qwen3.6-27B-Q8 (27B params, ~30 t/s, 256K ctx)
- Port 8085: Qwen3.6-35B-A3B-Q8 (35B MoE, 3.5B active, faster)
- Both with
--spec-type draft-mtpfor speculative decoding
Configuration
Environment Variables
| Variable | Default | Description |
|---|---|---|
DEEPSEEK_API_KEY |
— | DeepSeek API key for summarization |
DEEPSEEK_BASE_URL |
https://api.deepseek.com |
API endpoint |
DEEPSEEK_MODEL |
deepseek-chat |
Model name |
API_TOKEN |
— | Bearer token for dashboard write ops |
AGENT_ENABLED |
false |
Enable agent queue worker |
QWEN_BASE_URL |
http://localhost:8080 |
Local Qwen endpoint |
HTTP_PROXY |
— | Proxy for fetcher/summarizer (China bypass) |
AGENT_POLL_INTERVAL |
30 |
Agent queue poll seconds |
AGENT_MAX_CONCURRENT |
3 |
Agent queue max workers |
Authelia Bypass
API routes at /api/* bypass Authelia via resources: ['^/api/'] in Authelia access_control.yaml. Frontend (/) requires SSO login.
API Endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/health |
GET | None | Health check |
/api/config |
GET | None | Auth config (token required?) |
/api/videos |
GET | None | List videos (status, channel, limit params) |
/api/videos/fetch |
POST | Bearer | Trigger subscription sync |
/api/videos/{id}/summarize |
POST | Bearer | Trigger single video summary |
/api/videos/{id}/status |
POST | Bearer | Update status (pending/read/skipped) |
/api/notes |
GET | None | List all notes |
/api/notes/{id} |
GET/POST | Bearer (POST) | Get/save note |
/api/notes/migrate |
POST | Bearer | Migrate localStorage notes to server |
/api/agent-queue |
GET/POST | Bearer (POST) | List/add agent queue items |
/api/channels |
GET | None | List distinct channels |
/api/auto_categories.json |
GET | None | Channel category mapping |
Sprint Plan
Sprint 1 ✅ (Current — Deployed)
- Agent queue worker (disabled by default)
- Proxy support for fetcher + summarizer
- Auto-mark-read on navigate away (not timer)
- VPS fetch + summarize cron
- run_tests.sh fixes
- All 2,674 videos summarized (title-based)
Sprint 2 — UX Overhaul (Next)
- Full-text search across video titles
- Batch select → mark read/skip
- Infinite scroll / pagination
- Display duration + view count
- Transcript accordion in detail panel
Sprint 3 — Polish & Monitoring
- Stats dashboard (videos/day, channel health, sync lag)
- Auto-purge old read videos (configurable TTL)
- Broken channel detection (404 on RSS)
- Cron failure alerts via Telegram bot
- Scheduled DB backup to MacBook
Sprint 4 — Code Quality
- Split
main.pyinto routers - Cursor/offset pagination on API
- Rate limiting middleware
- aiosqlite connection pooling
Known Issues
Transcripts Blocked
YouTube transcripts inaccessible from all our locations:
- Oracle Cloud VPS: YouTube blocks cloud provider IPs
- MacBook (China): GFW blocks YouTube API endpoints
- YouTube Data API captions.download: 403 Forbidden for non-owned videos
Workaround: Title + description-based summaries via Qwen. Full transcripts need a Japanese residential proxy ($3-5/month).
auth.jimmygan.com from LAN
Accessing auth.jimmygan.com from LAN shows Immich (photos.jimmygan.com) instead of Authelia. Caused by NAS port 443 routing — DSM nginx still owns port 443 and routes Immich for unmatched domains.
Gitea SSL
Uses Let's Encrypt staging certificates (acme-staging) — browsers may show untrusted cert warnings.
Development
Local Setup
cd ~/repos/t-youtube
# Backend
cd backend && python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
python3 main.py # runs on :8000
# Frontend
cd frontend && npm install && npm run dev # runs on :5173 with API proxy
Testing
cd ~/repos/t-youtube && bash run_tests.sh
# 23 tests across db, fetcher, summarizer, main, agent_worker
Deploy to NAS
cd ~/repos/t-youtube
git add -A && git commit -m "..." && git push gitea main:master
ssh nas 'cd /volume1/docker/t-youtube && git pull origin master && docker compose up -d --build'