Self-hosted multi-user YouTube subscription feed
Find a file
npeter83 3e90fe2e5b refactor(filters): stop mirroring filters into the URL; localStorage is canonical
Filters/sort/search/scope were written to the address bar on every change (a leftover
from sharing reproducible examples), giving two sources of truth. Make localStorage the
single source: drop the automatic syncUrl from setFilters/setPage. A "Share view" link
still hydrates filters on first load, after which the query is stripped from the URL
(stripUrlParams) so it stays clean. syncUrl is replaced by shareUrl (builds the link on
demand); the serializer now also round-trips scope.
2026-06-15 12:29:43 +02:00
.forgejo/workflows chore(repo): remove internal deployment plan, sanitize infra references 2026-06-14 23:20:02 +02:00
backend fix(feed): conjunctive facet counts when topic match mode is AND 2026-06-15 12:20:08 +02:00
frontend refactor(filters): stop mirroring filters into the URL; localStorage is canonical 2026-06-15 12:29:43 +02:00
scripts chore: rebrand Subfeed -> Siftlode 2026-06-14 04:40:22 +02:00
.dockerignore feat: M1 foundation — compose stack, FastAPI, Google OAuth, encrypted tokens 2026-06-11 01:01:37 +02:00
.env.example feat(auth): split base sign-in from YouTube scopes for incremental onboarding 2026-06-13 23:56:34 +02:00
.gitattributes chore: enforce LF line endings via .gitattributes (CRLF for .ps1) 2026-06-11 01:02:06 +02:00
.gitignore feat: M1 foundation — compose stack, FastAPI, Google OAuth, encrypted tokens 2026-06-11 01:01:37 +02:00
docker-compose.localdev.yml feat(version): /api/version + build-time version/commit stamping 2026-06-15 00:06:57 +02:00
docker-compose.yml feat(version): /api/version + build-time version/commit stamping 2026-06-15 00:06:57 +02:00
Dockerfile fix(version): read app version from the committed VERSION file 2026-06-15 02:22:27 +02:00
README.md chore: rebrand Subfeed -> Siftlode 2026-06-14 04:40:22 +02:00
VERSION feat(version): /api/version + build-time version/commit stamping 2026-06-15 00:06:57 +02:00

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

  1. 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.

  2. 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 match OAUTH_REDIRECT_URL in .env).
    • Put the client ID/secret into .env, and list invited emails in ALLOWED_EMAILS.
  3. Start it:

    docker compose up --build
    

    Open 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 ./pgdata bind 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 in
    

    Set YOUTUBE_API_KEY in .env so 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 .env set DATABASE_URL to the central DB (e.g. …@your-db-host:5432/subfeed), then docker compose -f docker-compose.localdev.yml up --build and 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.