After read access is granted the wizard now imports the user's YouTube subscriptions automatically (with a "Building your feed…" progress state), so a new user lands on a populated feed — channels already in the shared catalog show up instantly, new ones backfill in the background. The empty feed now prompts users without read access to set up via the wizard instead of a bare message. |
||
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| frontend | ||
| scripts | ||
| .dockerignore | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| docker-compose.localdev.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| README.md | ||
Siftlode
Self-hosted, multi-user web app for browsing your own YouTube subscriptions the way you actually want: precise filtering and sorting (by language, topic, length, age, watch state…), a fast local-first feed, and one-click playback that opens the real youtube.com — so your browser's ad blocking and SponsorBlock keep working exactly as before.
Each user signs in with their own Google account (invite-only) and sees only their own subscriptions. All the expensive data (channels, videos, metadata) is fetched from YouTube once and stored locally, so filtering/searching/sorting are instant and don't burn API quota.
Status: early development.
- M1 (foundation): docker-compose stack, FastAPI backend, Google OAuth login with an email invite-list, encrypted token storage.
- M2 (ingest core): subscription import, free RSS detection, recent-first + deep backfill from the uploads playlist, enrichment (duration/stats/category, Shorts & livestream classification), a shared daily quota guard, and a background scheduler.
- M3 (auto-tagging): system tags for channel language (offline detection) and topic (from YouTube topics + dominant category), regenerated automatically; user tags are never overwritten.
- M4 (reader UI): React + Vite SPA with four color schemes (dark/light) and adjustable text size; grid/list feed scoped to your subscriptions, with faceted tag filters, content-type toggles (Normal/Shorts/Live), date range, search, sort, watch/save/hide state and per-channel filtering; clicking a video opens youtube.com. Accurate Shorts detection via the /shorts probe, a sync-status indicator with admin pause/resume, a filtered video count, and structured timestamped logging.
Requirements
- Docker + Docker Compose
- A Google Cloud project with an OAuth client (see below)
Quick start
-
Copy the env template and generate secrets:
cp .env.example .env python -c "import secrets;print('SECRET_KEY=',secrets.token_urlsafe(48))" python -c "import base64,os;print('TOKEN_ENCRYPTION_KEY=',base64.urlsafe_b64encode(os.urandom(32)).decode())"Paste the generated values into
.env. -
Create a Google OAuth client (Google Cloud Console → APIs & Services):
- Enable the YouTube Data API v3.
- OAuth consent screen: External, publishing status Testing, and add every invited Google account as a Test user (up to 100 — no app verification needed at this scale).
- Create credentials → OAuth client ID → type Web application.
- Authorized redirect URI:
http://localhost:8080/auth/callback(must matchOAUTH_REDIRECT_URLin.env). - Put the client ID/secret into
.env, and list invited emails inALLOWED_EMAILS.
-
Start it:
docker compose up --buildOpen http://localhost:8080 and sign in. Database migrations run automatically on startup.
Backup & moving to another machine
All data lives in the Postgres database — moving to another host (e.g. a Proxmox Linux
server) does not require re-fetching from YouTube. Copy your .env (keep the same
TOKEN_ENCRYPTION_KEY and Google client so stored tokens stay valid), then:
./scripts/backup.sh # -> backups/siftlode-<timestamp>.dump (Windows: scripts\backup.ps1)
./scripts/restore.sh backups/<file> # on the new host after `docker compose up`
Central server + local dev (single shared database)
For always-on operation the recommended topology is a single central Postgres running on a 24/7 host (e.g. a Proxmox LXC) that also runs the background scheduler, with every other instance (your local dev machine, a future VPS) pointing at that same database. One source of truth, no sync.
-
Server (
docker-compose.server.yml): full stack, Postgres published on the LAN, scheduler on, DB files in a host-visible./pgdatabind mount. In the LXC, from the project root:echo 'COMPOSE_FILE=docker-compose.server.yml' >> .env # so backup/restore target this stack docker compose up -d --build ./scripts/restore.sh backups/<file> # migrate existing data inSet
YOUTUBE_API_KEYin.envso unattended backfill/enrichment use the API key and don't depend on a refresh token (which expires after 7 days while the OAuth screen is in Testing). -
Local dev (
docker-compose.localdev.yml): webapp only, no local DB, no scheduler. In.envsetDATABASE_URLto the central DB (e.g.…@your-db-host:5432/subfeed), thendocker compose -f docker-compose.localdev.yml up --buildand browse http://localhost:8080.
Exactly one instance may run the scheduler. The server keeps SCHEDULER_ENABLED=true; every
other instance must be false (the compose files enforce this) to avoid double quota burn and
write races on the shared DB.
Tech
FastAPI + PostgreSQL backend, React + Vite frontend (added in a later milestone), packaged with Docker Compose.
Note
This project is developed with AI assistance.