Round 5 added a guard so a request for a segment beyond the produced region answers instead of polling out the full timeout — the route is a SYNC one, so every such wait parks a threadpool worker and, through `current_user`, a pooled DB connection. Measured against real ffmpeg, the guard could not fire where it mattered. `-hls_segment_filename seg_%v_%d.ts` substitutes the VARIANT first: a dual-audio session writes seg_0_0.ts (video) and seg_a0_0.ts / seg_a1_0.ts (the `name:aN` renditions). The regex read those as sequence-first, so every video segment parsed as segment 0 and every audio segment did not parse at all — the guard was inert on every multi-audio item, which is most of this library. `_seg()` carried the same inversion: the warm-up waited 8 s for `seg_1_0.ts`, a name ffmpeg never writes, on every multi-audio session start AND every seek-restart. The layout is now one constant plus one parser (`segment_name` / `_SEG_NAME_RE`), with a test pinning them to each other, instead of three comments that could drift — and they had drifted, all three ways. The guard is also a positive rule now. It was a blocklist, so anything that was not a single-audio segment name was waved through: `GET …/anything.m3u8` still parked a worker for 30 s, while the route's comment claimed a flood of hopeless names was what it prevented. A session produces its playlists and its segments and nothing else. `.vtt` leaves `_HLS_CT` with it — ffmpeg runs with `-sn` and subtitles are a separate route, so that extension could only ever name a file that will not arrive. Also here: - A 5000-digit segment index reached the client as a 500: `int()` on an unbounded digit run raises past 4300 digits and only OSError was caught. The name is bounded on the route and the index in the regex — the same bug class the subtitle routes were bounded for. - A session torn down between the wait and the read is a 404, not a 500. `_kill` rmtrees the directory and no lock spans those two steps; the player's recovery is keyed on the STATUS, so a 500 skipped the reclaim and ended playback with "couldn't play" instead of restarting at the playhead. The trigger is ordinary — `loadSession` awaits the new session before destroying the old hls instance. - Two comments claimed an open player checkpoints every 10 s "whether or not it is playing", which is the stated reason the 30 s finished-idle reap is safe. `flush` sends nothing once the finish latch is up, or before a position is known. The states it does cover — playing or paused mid-item — are the ones the rule needs; the comments now say that instead of more. - The route's tests leave the DB lane: the route reads nothing from the database since the auto-start was removed, and there those four regressions SKIP on a machine with no Postgres while pytest prints green. 14 tests, mutation-checked: the old parse, the old warm-up name, the blocklist, both bounds off the absurd name, the unguarded read, and the route dropping `proc` — each red on the test that names it.
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.
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:
- Admin account — the email + password you'll sign in with.
- 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.
- 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.
