peter 6b0401b922 fix(logs): rule on every swallowed exception in the backend
114 exception handlers neither logged nor re-raised. The defect was never that
they are silent — plenty of them should be — it is that their silence said
nothing, so nobody could tell a decision from an oversight. Each one has now been
read. 44 got a line; the other 70 carry `# silent-ok: <why>`.

What the silent ones had been hiding, in the cases that got a line:
- A failed ffprobe sent every HDR file down the 8-bit pipeline, so playback
  degraded to a software transcode with nothing saying why; a failed audio probe
  collapsed a multi-audio file to one track and the player showed no language
  picker; a failed first-PTS probe left subtitles seconds out of sync.
- A subtitle sidecar that existed but would not parse made a PARTIAL track read as
  complete, permanently — the viewer loses every cue past the cached window.
- An unreadable crop/trim/cut-list in an edit spec was dropped, so the clip came
  out uncropped, untrimmed, or shorter than the one the user built.
- A dropped `max_height` downloaded at best quality against an explicit cap.
- A failed videos.list enrich made every candidate undroppable-from-judgement and
  therefore dropped: a working search rendered as "no results".
- An unusable password hash answered as "wrong password", locking the owner out
  and telling them they mistyped; a rotated TOKEN_ENCRYPTION_KEY turned every
  stored token into "not connected".
- A stored admin setting that would not coerce fell back to the env default while
  the settings page went on showing the saved value.
- A one-off InnerTube config failure pinned the whole process to the hard-coded
  fallback key, and searches return nothing when that goes stale.
- The HLS sweep reported 0 swept — like a clean run — when it could not list its
  own root, leaving every crashed session's segments on disk forever.

Four of them are counted rather than logged per item (dropped cues, dropped cut
list segments, unreclaimed GC files) and reported once per call, because the
alternative is one line per cue over a whole subtitle track.

ruff gains S110 and BLE001. Every blind `except Exception` that remains carries
`# noqa: BLE001 — <why>`, which is the point: catching everything is sometimes
right, and always worth a sentence. plex/sync.py's three best-effort backfill
catches now keep the traceback — a blind catch over a batch that fetches AND
writes could be Plex, the database or our own bug, and `%s` of the exception told
them apart badly. main.py's boot-time log-level read likewise.

logs.event's `code` is positional-only now: `event("x", code=...)` was a TypeError,
and `code` is exactly the word a caller reaches for when the thing that failed has
one of its own.

tests/test_swallowed_exceptions.py is the wider rule ruff cannot see: a handler
that neither logs nor re-raises must say why. Both cases mutation-checked — a
marker removed and a log line removed each fail it, naming the handler.
2026-09-18 01:20:42 +02:00
2026-09-18 00:05:46 +02:00
2026-09-18 00:05:46 +02:00
2026-09-18 00:05:46 +02:00

Siftlode

Your YouTube subscriptions, the way a feed should work. Siftlode pulls every upload from the channels you follow into one clean, filterable feed — no algorithm deciding what you see, and no Shorts or livestream noise unless you want it. Self-hosted, multi-user, and private: your data stays on your own server.

Siftlode feed

Everything expensive (channels, videos, metadata) is fetched from YouTube once and stored locally, so filtering, searching and sorting are instant and don't burn API quota. Click a video to watch it in an in-app player that resumes where you left off — or open it on youtube.com so your own ad blocker and SponsorBlock keep working.

Features

  • A readable subscription feed — sort and filter by channel, tag, language, topic, length, upload date or watch state; hide channels without unsubscribing; save filter setups as named views.
  • Search all of YouTube from the feed — live results you can play, save or add to playlists like any other video; they stay temporary and out of your catalog unless you keep one.
  • Channel pages & a channel manager — per-channel stats and uploads, priorities, and your own tags to slice the feed by.
  • Playlists with two-way YouTube sync — build them locally, keep them in sync in both directions.
  • In-app player with resume, plus keyboard/scroll controls.
  • Download Center — save videos to the server with yt-dlp in a Plex-friendly layout (format presets, per-user storage quota), trim / crop / split & join them in a built-in editor, then save to your device, share with another user, or hand out a public watch link.
  • Plex library (optional) — point Siftlode at your own Plex server to browse and play its movies and shows alongside your feed, with watch state kept in sync.
  • Multi-user with per-user private state, a shared catalog, and a fair daily API-quota guard.
  • Self-hosted & private, with a first-run web setup wizard and the interface in English and Hungarian.

Quick start (self-hosting)

You don't need to build anything — Siftlode runs from a prebuilt public image, and all configuration (your admin account, Google sign-in, email) happens in a first-run web wizard. You need Docker with the Compose plugin.

1. Get the files and run the installer:

git clone https://git.b1fr0st.eu/peter/siftlode.git
cd siftlode
./install.sh          # Windows (PowerShell):  ./install.ps1

The installer generates a private .env (secrets), pulls the image, starts the app + database, and prints a one-time setup URL like http://localhost:8080/setup?token=….

2. Finish in your browser. Open that URL and follow the wizard:

  1. Admin account — the email + password you'll sign in with.
  2. Google sign-in (optional) — paste a Google OAuth client to enable "Sign in with Google" and pulling your YouTube subscriptions. Skip it to use email + password only.
  3. Email / SMTP (optional) — for verification/notification emails. Skip it and you (the admin) simply approve new accounts yourself.

Then sign in with your admin account. That's it. See docs/self-hosting.md for the full walkthrough.

Just trying it out? Press Enter at the installer's URL prompt to run on http://localhost:8080.

Build from source (alternative)

Prefer to build the image yourself instead of pulling it:

git clone https://git.b1fr0st.eu/peter/siftlode.git
cd siftlode
cp .env.example .env
# generate the two secrets and paste them into .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())"
docker compose up --build -d      # builds from the included Dockerfile

Open http://localhost:8080 and finish in the setup wizard as above. (Set a POSTGRES_PASSWORD in .env too.)

HTTPS / public access

Port 8080 over plain HTTP is fine for a LAN or a quick trial. For public access put a reverse proxy (Caddy, Nginx, Traefik…) in front to terminate TLS, and set the public URL (the installer prompt, or OAUTH_REDIRECT_URL in .env) to your https://… address — this also marks the session cookie secure. Add that same …/auth/callback URL to your Google OAuth client's authorized redirect URIs. Behind a proxy, also set TRUSTED_PROXY_IPS so the rate limiters see the real client IP and can't be bypassed via a forged X-Forwarded-For — see docs/self-hosting.md.

Updating & backups

docker compose -f docker-compose.selfhost.yml pull   # or: docker compose pull
docker compose -f docker-compose.selfhost.yml up -d

That follows latest, which moves with every release. Set IMAGE_TAG=0.80.0 in .env to pin a version and upgrade on your own schedule; released versions are listed in CHANGELOG.md and in the app's Release Notes. The registry keeps only the newest few of those, so a pin decides when you take the next hop rather than letting you stay put forever — see docs/self-hosting.md.

Back up before upgrading. Migrations run automatically on startup and are not undone by pulling the old image back. A backup is the database dump plus your .env — TOKEN_ENCRYPTION_KEY is what decrypts the Google, SMTP and Plex secrets stored in that database, so a dump restored without it is only half an instance. Use scripts/backup.sh (or backup.ps1 on Windows) and scripts/restore.sh; the equivalent one-liners, the pinning recipe and the rollback steps are in docs/self-hosting.md.

How it works

  • Shared catalog, private state. Channels and videos are stored once and shared; each user's subscriptions, tags, playlists and watch/save/hide state are private.
  • Cheap by design. Public reads are cached locally; a shared daily quota budget and a background scheduler keep unattended syncing within YouTube's free API limits. An optional API key lets backfill run without depending on a user's OAuth token.

Tech

FastAPI + PostgreSQL (SQLAlchemy, Alembic) backend; React + Vite + Tailwind + TanStack Query frontend; packaged as a single Docker image with Docker Compose.

License

GNU AGPL-3.0. You can run, study, modify and share Siftlode; if you modify it and let other people use it over a network, they are entitled to your modified source. Copyright © 2026 Siftlode contributors.

Note

This project is developed with AI assistance.

S
Description
Self-hosted multi-user YouTube subscription feed
Readme AGPL-3.0
20 MiB
Languages
TypeScript 58.5%
Python 40.1%
CSS 0.5%
JavaScript 0.5%
Shell 0.2%
Other 0.1%