Five of them sit where round 6 turned F from a Fullscreen API call into a CSS maximise: the guards around it still asked `isFullscreen`, which that pivot made permanently false. - Arrow-key volume was dead in the maximised view: the scroll-deference checked only `isFullscreenRef`, so the (now hidden but still scrollable) card kept swallowing Up/Down. Both flags now count as "the video fills the screen". Measured: 87px of scroll room on the focused card, ArrowDown moves 80 -> 75 and scrolls nothing. - The maximised view had no way out once the pointer touched the video. Reaching YouTube's controls means clicking into the cross-origin iframe, which takes keyboard focus with it, and the re-arm paths (stage mouseleave, window focus) need the pointer to leave the browser entirely while the stage fills it. So the exit now lives on screen, in our DOM: a button that un-maximises AND pulls focus back, re-arming every shortcut. - WebKit fires only `webkitfullscreenchange`/`webkitFullscreenElement`, so on Safari/iPadOS YouTube's own fullscreen never registered — no overlay yield, no focus reclaim. One `fullscreenEl()` reader, both event spellings. - The stage claimed `z-index: 9999`, the glass tuner's reserved level, for a job that only needs to beat the modal chrome it is nested in (its siblings top out at z-menu). Dropped to rail-level; nothing escapes the modal's stacking context either way. - `inset: 0` already sizes a fixed layer — the inherited `100vw`/`100vh` over-constrained it, which is how a classic scrollbar gets a horizontal overflow and a mobile URL bar hides the bottom edge (YouTube's control bar). - The shortcut hint still promised "F: fullscreen" in both locales. Backend, both "the fix stopped one layer short": - The startup HLS sweep was awaited inside lifespan, so uvicorn still served nothing until it finished — a thread doesn't help when nothing is listening yet. Fire-and-forget task instead, cancelled on shutdown. Measured with 200 planted segments: "Application startup complete" now precedes "swept 1". - An RSS poll where every channel failed returned normally, so the scheduler recorded `ok` with the outage buried in the summary string. `failed == total` now raises: a run that reached nothing is a failed run. Suites: backend 147 -> 150.
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 — results play, save and add to playlists like any other video, and are materialised into your catalog.
- 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.
- 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://forge.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://forge.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
Database migrations run automatically on startup. Your data (accounts, subscriptions, playlists, the
video catalog) lives in a Postgres volume — back it up with scripts/backup.sh (or backup.ps1 on
Windows) and restore with scripts/restore.sh.
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.
Note
This project is developed with AI assistance.
