92 Commits
Author SHA1 Message Date
peter dfa610a059 chore: bump VERSION to 0.58.1 (ship Gitea-URL change to prod) 2026-07-30 05:25:21 +02:00
peter 929a63b8ec chore(release): 0.58.0 — landing refresh + R7.5 API layer 2026-07-29 18:28:51 +02:00
peter 81d0943fdc fix(watch): drop the Web Audio reroute, keep Media Session for iOS background audio (0.57.2)
Reported: on iPad Brave a shared video's audio still stopped when backgrounded, while youtube.com
keeps playing. Root cause: routing the element's audio through createMediaElementSource pulls it out
of the normal media pipeline the browser's background-playback feature (and iOS) hooks into, and iOS
then suspends the AudioContext on background — the opposite of the goal. YouTube uses a plain media
element + Media Session and lets the browser background it. So drop the Web Audio graph entirely and
keep only the Media Session metadata + play/pause handlers, leaving native background handling in
charge. Frontend-only.
2026-07-28 03:29:48 +02:00
peter 9187ae56b1 fix(share): use the stable YouTube hqdefault thumbnail for OG on YT shares (0.57.1)
Facebook's scraper rejected our self-hosted poster as "Corrupted Image": the poster is an
ffmpeg-produced JPEG (non-standard marker order) that browsers/iOS/Twitter decode fine but FB's
stricter processor won't. For a YouTube source, point og:image at the video's stable, non-expiring
`i.ytimg.com/vi/<id>/hqdefault.jpg` (a Google-encoded JPEG FB decodes reliably) built from the id in
the stored thumbnail — instead of the stored `sqp`-signed variant, which can expire. Non-YouTube
sources keep the poster fallback (fine everywhere but FB). Declares 480×360 so FB includes it on the
first async scrape. og.py only — no schema change.
2026-07-28 03:20:31 +02:00
peter b367bc3126 chore(release): 0.57.0 — downloads & channels fixpack
Downloads UI quick wins, categorized download-failure dialog, channel full-history + honest
stored/total count, press-and-hold + inline-editable priority, iOS background audio on the shared
watch page, and Facebook OG image-dimension fix. Adds migration 0061 (download_jobs.error_code).
2026-07-28 02:59:04 +02:00
peter 175e49b361 chore(release): 0.56.0 — R7 frontend API layer 2026-07-28 00:18:39 +02:00
peter c86a15c252 chore(release): 0.55.0 — R6 API contract & transaction ownership 2026-07-26 21:25:36 +02:00
peter 475bd7fef8 chore(release): 0.54.0 — R5 data integrity (finalize version + release notes) 2026-07-26 01:40:43 +02:00
peter 58f836bcd8 fix(plex): stop /facets 500 on a metadata-poor library
A mirrored Shows library whose items have no genres stored `genres` as JSON
`null` — SQLAlchemy's JSONB persists a Python None as a JSON scalar, not SQL
NULL. /facets unnested it with jsonb_array_elements_text(), which raises
"cannot extract elements from a scalar", so a single bare library 500'd the
whole Plex module on open (the grid loads, the filter sidebar's facets call dies).

- Guard the /facets genre unnest with jsonb_typeof(genres)='array', mirroring
  the guard people.py already applies to the same columns.
- Persist the JSONB tag columns (genres/directors/cast_names/collection_keys on
  plex_items and plex_shows) with none_as_null=True, so an empty list round-trips
  as SQL NULL and self-heals on the next sync — closing the trap for any future
  unnest.

Verified against the real prod row (DumaTV show, genres = JSON null): the
unguarded query errors, the guarded one returns cleanly.
2026-07-25 18:22:07 +02:00
peter 08049d2aa5 release: 0.53.0 — R4 auth funnel & abuse hardening 2026-07-23 04:27:20 +02:00
peter 745ba10f2e release: 0.52.0 — R3 honest states (loading/error across the app) 2026-07-22 04:51:32 +02:00
peter 56111ceadf release: 0.51.1 — shell-safe names, re-download title refresh, GC orphan sweep 2026-07-22 03:18:36 +02:00
peter bce6175983 release: 0.51.0 — downloads correctness & UX (iOS playability, 720p, re-download, ASCII names) 2026-07-22 02:52:01 +02:00
peter 51781819db release: 0.50.0 — R1 quick-win sweep
Ships R1 (S1 backend safety, S2 frontend interaction, S3 i18n/a11y) plus the
code-review follow-ups (filename byte-cap, demo deep_requested guard, release-notes
highlight fallback). Mixed epic, so a real user-facing note: download-overwrite +
long-title fixes, Escape layering, Plex light-theme menus, toast timer, unsubscribe
error, modal-close i18n, toast aria-live; keyboard + hardening under chores.
2026-07-22 00:31:47 +02:00
peter 9445e4d71a release: 0.49.0 — R2 guardrails & test net
Internal-only epic (gates, tests, compiler strictness); no user-facing change, so
the release note is chores-only. Bundles: the siftlode check/publish gate
(tsc/eslint/prettier/knip/vitest/pytest) with a hard pre-publish gate + e2e
freshness guard, ESLint flat config + one-time Prettier pass, the backend pytest
harness (isolated test image), frontend vitest growth, the e2e scroll-restore fix,
and noUncheckedIndexedAccess (+ the zero-fallout compiler flags).
2026-07-21 04:30:51 +02:00
peter 855ae0c995 release: 0.48.0 — playlist column headers, and sorting stops rewriting the list 2026-07-21 00:46:41 +02:00
peter 742cf2df73 release: 0.47.0 — relative timestamps fixed, and localized by the platform 2026-07-20 22:31:30 +02:00
peter 758e655642 release: 0.46.0 — channel manager overhaul (table/cards, About, shared sort, paging) 2026-07-20 01:31:15 +02:00
peter 97611be9c4 release: 0.45.1 — player & share fixes 2026-07-19 06:08:06 +02:00
peter 86a6d696b8 release: 0.45.0 — feed view modes + Plex cast-explore
Cuts the release for the unshipped stack since 0.44.0: E3 feed view modes, the
Platform refactor (S1–S6b, internal), and the Plex cast-explore epic. VERSION
0.44.0 -> 0.45.0; user-facing releaseNotes entry.
2026-07-19 04:58:36 +02:00
peter 303c896d87 chore(release): 0.44.0 — peek-open panels, active-filter chips, channel filter, sync chip 2026-07-16 01:21:55 +02:00
peter 9d9c6894f4 chore(release): 0.43.1 — sidebar no longer latches open after a portaled menu 2026-07-15 20:29:41 +02:00
peter 44926f1c23 chore(release): 0.43.0 — nav overlay sidebar, account chrome, opt-in glass tuner 2026-07-14 21:41:44 +02:00
peter 88e6b6a2a0 chore(release): 0.42.0 — glass-over-image look + Starship/Matrix schemes 2026-07-12 22:28:50 +02:00
peter 08919199c2 chore(release): 0.41.0 — admin audit log + channel About enrichment/fixes 2026-07-12 17:57:39 +02:00
peter 2c4d4fa193 chore(release): 0.40.0 — deferred-bugs closeout 2026-07-12 06:54:49 +02:00
peter 216200eebf release: v0.39.1
OAuth scope-clobber fix (sign-out/in no longer drops YouTube access) + admin
access-request email accuracy/multi-admin + auth loose-ends (register/reset timing,
messages_ws dedup, revoke decrypt).
2026-07-12 04:57:06 +02:00
peter 9d3624d51b release: v0.39.0
Auth security round: SB3 (email tokens out of URL query → fragment), SA4 (server-side
session revocation + 'Log out other sessions'), SA3 (trusted-proxy X-Forwarded-For so
rate limits can't be bypassed via a forged header).
2026-07-12 03:49:55 +02:00
peter c7ef5a87b2 release: v0.38.0
Plex filter-sidebar collapse + infinite-scroll fix; the whole code-hygiene sweep
(Phases 1-4: dead-code/duplication cleanup, watch_sync/player fixes, guardrails).
2026-07-12 02:26:42 +02:00
peter 2bdccfb8c7 release: v0.37.0 — unified library, series view, adaptive filters, collections picker 2026-07-11 03:50:34 +02:00
peter 8279f259bb revert(player): drop the experimental HD-unlock fullscreen flash
User-tested on prod: the flash fires (fullscreen → back to small) but the source
stays 360p — YouTube doesn't switch quality within the flash window and we can't set
it via the (dead) API, so the unlock has no effect. Give up on forcing windowed HD.
Restore PlayerModal to its 0.36.2 state (native-menu-yield + scroll-anywhere volume
kept). Also prune the failed HD-quality attempts from the release notes (the flash
and the transform-revert notes, plus the hollow 'higher quality' claim in 0.36.0) so
there's no user-facing trace of an effort that yielded nothing.
2026-07-10 18:14:32 +02:00
peter aac03e2c2b chore(release): 0.36.3 2026-07-10 17:30:50 +02:00
peter 967364b938 chore(release): 0.36.2 2026-07-10 16:56:24 +02:00
peter 3a25f8341f chore(release): 0.36.1 2026-07-10 16:27:43 +02:00
peter bab1b3361a chore(release): 0.36.0 2026-07-10 15:52:34 +02:00
peter 1dd42f06a2 chore(release): 0.35.0 — two-way Plex watch sync (Phase B + C)
Completes the two-way Plex ↔ Siftlode watch-state sync: Phase B (Siftlode→Plex push, immediate)
and Phase C (incremental history/on-deck pull + daily full reconcile incl. un-watch). Also the
auto-advance watched-mark id-race fix.
2026-07-10 00:47:55 +02:00
peter 9ecb3e6e81 chore(release): 0.34.2 — watch-sync toggle UX polish 2026-07-09 15:34:52 +02:00
peter 980f228561 chore(release): 0.34.1 — Plex watch import concurrency fix 2026-07-09 15:19:17 +02:00
peter b67a14bd13 chore(release): 0.34.0 — Plex watch-state import (Phase A) 2026-07-09 14:43:22 +02:00
peter ccd5f21921 chore(release): 0.33.1 — channel page network-error fix 2026-07-09 09:43:08 +02:00
peter ad733c4422 chore(release): v0.33.0
Plex player (copyts playback, resume, personalization, skip intro/credits,
audio/subtitle switching, seek drag, projected finish time, +25% UI) and
Plex search improvements (own ephemeral state; empty-with-filters hint).
2026-07-09 02:32:46 +02:00
peter 1e9d58555a fix(share): let link-preview crawlers fetch /watch/ pages (robots.txt)
Shared links didn't unfurl in Messenger: robots.txt disallowed /watch/ for ALL
user-agents, so facebookexternalhit honoured it and got a blocked (403) fetch —
the OG tags were correct but never read. Add an allow group for the link-preview
bots (facebookexternalhit, Twitterbot, Discordbot, WhatsApp, Slack, Telegram,
LinkedIn, …) so they can fetch share pages; the wildcard still keeps /watch/ out
of search indexes. Release v0.32.1.
2026-07-07 22:56:38 +02:00
peter 977c09c43d chore(release): v0.32.0 2026-07-07 22:29:05 +02:00
peter 6cd18dc17f fix(downloads): clip long titles to fit display_name (VARCHAR 255)
A download whose source title exceeded 255 chars (e.g. a recipe baked into a
Facebook video's title) failed with a StringDataRightTruncation: the worker
auto-fills an empty display_name from the title, which overflows the 255-char
column. Clip the title to 255 at every point it flows into display_name (worker
completion paths + enqueue); the full title is untouched on the asset. Pre-existing
latent bug, surfaced by an unusually long title.

Release v0.31.1.
2026-07-07 21:40:15 +02:00
peter d73cfb16a3 chore(release): v0.31.0 2026-07-07 20:19:39 +02:00
peter cb6fb60b08 feat(plex): Playlists Phase 1 — per-user ordered watch-lists (Siftlode-native)
Personal ordered lists of Plex items, kept in Siftlode's own DB (works for users
without a Plex account, like watch-state) — the "your own lists" counterpart to
shared collections. Plex-direction sync is a later phase (plex_rating_key
reserved).

Backend: migration 0048 (plex_playlists + plex_playlist_items with position),
PlexPlaylist/PlexPlaylistItem models, and per-user CRUD endpoints under
/api/plex/playlists (list [+?contains for the add dialog], create [seeded],
detail [ordered cards], rename, delete, add/remove item, reorder). _leaf_card
handles the movie/episode mix. Frontend: "Add to playlist" dialog from the movie
info page (all users), a Playlists section in PlexSidebar (list + create),
PlexPlaylistView (reorder up/down, remove, rename, delete, Play all), and
PlexPlayer gained an optional `queue` so play-through follows the list order
(prev/next + auto-advance). i18n en/hu/de. Verified end-to-end on localdev
(backend CRUD + the create→add→view→play-through UI flow).
2026-07-06 19:05:12 +02:00
peter cd2175ccc3 feat(plex): Collections Phase 2 — admin collection editing (write-back to Plex)
Admins can curate Plex collections from a movie's info page ("Collections"
button → PlexCollectionEditor dialog): create a collection (seeded with the
movie), add/remove the movie to/from editable collections, delete a collection,
and "take over" an existing plain Plex collection (mark it editable). All writes
go to Plex (POST/PUT/DELETE via new PlexClient methods) and are reflected on
every client; a targeted single-collection re-sync (sync.resync_collection /
delete_collection_local) updates the local mirror without the full ~4-min sync.

Gating (per the design decisions): editing is ADMIN-ONLY (collections are shared
library-wide); only plain manual collections are editable — smart + external
auto-lists (IMDb/TMDb/…) are always read-only (can_edit = editable && !smart &&
source=="collection"). New admin endpoints under /api/plex/collections
(create/items add+remove/rename/delete/editable). Verified end-to-end incl. the
live Plex write API (create/add/rename/remove/delete all 200, self-cleaned) and
the editor UI (create + delete with confirm) on localdev.
2026-07-06 17:33:16 +02:00
peter 8853b73dda fix(plex): play subtitles as WebVTT tracks (external sidecar subs no longer crash)
Selecting a subtitle restarted the HLS session with `-map 0:s:{ord}`, assuming an
EMBEDDED stream. Films whose subs are external sidecar .srt files (Plex reports
them, but they aren't in the mkv) matched no stream; the master-playlist's
declared subtitle group then made ffmpeg fail → "Playback couldn't start".

Subtitles now go through a new GET /api/plex/subtitle/{rk}/{ord} → text/vtt
(external subs fetched from Plex via the stream key + SRT→VTT; embedded text subs
extracted with ffmpeg; image subs → 415), served as native <video><track> that
the browser overlays. So choosing/switching a subtitle is instant with NO session
restart, and stream.py drops all subtitle muxing (`-sn`, no master playlist).
Image-based subs (PGS/VobSub) are marked text=false and hidden in the picker.
Verified on prod's Nymphomaniac Vol. II: HU sidecar → 1693 WebVTT cues, no crash.
2026-07-06 08:45:30 +02:00
peter c1a3848271 improvement(plex): serve right-sized poster/art images via Plex transcode
Posters/art were proxied at full resolution (many 1-7MB, some 9.5MB) into ≤176px
grid/info cells — heavy to fetch + decode, so fast scrolling lagged 1-2s even
with a warm cache (the cache removed the Plex round-trip, not the image weight).

Now PlexClient.image_bytes optionally hits Plex's /photo/:/transcode to resize
server-side; the /image endpoint requests thumb=400x600, art=1280x720 and keys
the cache by width (old full-size files orphaned, re-cached small). Measured:
a poster 5.5MB->50KB, art 8.1MB->137KB, at 400x600 (crisp on 2x DPR). Grid
scroll is near-instant and the disk cache shrinks ~8x.
2026-07-06 08:04:13 +02:00
peter e0c7e21850 fix(plex): image proxy no longer exhausts the DB connection pool
A collections page fires 100+ concurrent poster requests. Both image proxies
(/image and /person-image) authenticated via current_user + Depends(get_db),
holding a pooled DB connection for the whole request — including the slow Plex/
CDN fetch. Under the burst, the 15-connection pool was exhausted → QueuePool
checkout timeouts → 502s → slow, partially-loaded grids on prod.

Fix: authenticate these two high-fan-out endpoints with a signed-session check
(no DB user-load), serve disk-cache hits with zero DB access, and on a cold miss
open a short session only to resolve the image key + Plex config, releasing it
BEFORE the fetch (image_bytes uses no DB). Also raise the pool (20 + 30 overflow)
as headroom above the sync-endpoint threadpool.
2026-07-06 07:14:28 +02:00