Commit graph

74 commits

Author SHA1 Message Date
npeter83
50cef1a552 chore(release): 0.36.3 2026-07-10 17:30:50 +02:00
npeter83
86ef4ad64c chore(release): 0.36.2 2026-07-10 16:56:24 +02:00
npeter83
efd7a3d09d chore(release): 0.36.1 2026-07-10 16:27:43 +02:00
npeter83
cf204e18a8 chore(release): 0.36.0 2026-07-10 15:52:34 +02:00
npeter83
a2de72420b 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
npeter83
57f521191e chore(release): 0.34.2 — watch-sync toggle UX polish 2026-07-09 15:34:52 +02:00
npeter83
cbe203391e chore(release): 0.34.1 — Plex watch import concurrency fix 2026-07-09 15:19:17 +02:00
npeter83
b09963be2b chore(release): 0.34.0 — Plex watch-state import (Phase A) 2026-07-09 14:43:22 +02:00
npeter83
8675e24663 chore(release): 0.33.1 — channel page network-error fix 2026-07-09 09:43:08 +02:00
npeter83
65b4949245 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
npeter83
6232a66878 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
npeter83
b63b3334d6 fix(share): self-host og:image so link previews stay reliable
A remote thumbnail as og:image is unreliable for the crawler — Facebook's signed
CDN URL expires (so an old shared link's image later vanishes) and cross-origin
hotlinking is flaky. Record a self-hosted poster for EVERY download (ensure_poster
returns the existing <base>.jpg thumbnail sidecar, or cuts a frame) and point
og:image at our own /api/public/watch/{token}/poster.jpg. Backfill now fills
poster_path for all existing downloads, not just thumbnail-less ones.
2026-07-07 22:43:18 +02:00
npeter83
8b02b41059 chore(release): v0.32.0 2026-07-07 22:29:05 +02:00
npeter83
33fad8911b 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
npeter83
1b691cdc5f chore(release): v0.31.0 2026-07-07 20:19:39 +02:00
npeter83
25197ed817 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
npeter83
8291f06525 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
npeter83
c8c027d0fc 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
npeter83
23fc67cb64 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
npeter83
6dece18ae8 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
npeter83
f037a4563c feat(plex): info-page polish — per-source strip toggles, glassy look, scroll restore
From UAT feedback on the collections info page:
- Per-source show/hide toggles in the customize menu, one per collection-strip
  type present on the item (Collections/IMDb/TMDb/…), persisted per account.
- Customize menu closes on Escape / outside click (reuses useDismiss).
- Preserve the info-page scroll position when returning from "Browse collection".
- Glassy refresh: content floats as .glass/.glass-card panels over a faint FIXED
  art backdrop on the <main> scroller (HTPC-style, toggleable); frosted menu;
  wider ~90% layout. Uses the existing glass surface system.
2026-07-06 06:50:03 +02:00
npeter83
418a874851 feat(plex): collections — sync, browse, filter, and info-page strips (Phase 1, read)
Backend (migration 0047_plex_collections): a plex_collections table mirrors every Plex
collection (card metadata + smart flag + an editable flag reserved for Phase 2); membership
is stored as GIN-indexed collection_keys on member movies (plex_items) and shows (plex_shows).
The background sync (plex_sync) now fetches each library's collections + their children and
rebuilds membership — so ALL reads are local (Plex's dual-language collection queries are slow;
this trades a ~4-min background sync for instant reads). New /api/plex/collections endpoint;
/browse gains a combinable  filter; item_detail returns the movie's collection
'strips' (sibling titles as playable cards, smallest/most-specific collection first) — all pure
local lookups.

Frontend: PlexSidebar gains a searchable Collection picker + an active-collection chip;
PlexInfo renders the collection strips (playable posters + 'Browse collection' → sets the
filter); the collection is part of PlexFilters (persisted). i18n en/hu/de.

Phase 2 (create/edit collections with write-back to Plex) is separate.
2026-07-06 02:25:48 +02:00
npeter83
280c62dda8 feat(plex): search cast & crew names + clickable person cards
Backend (migration 0046_plex_people_search): a new people_text column (cast +
director names, de-duped) is folded into the generated search_vector at weight B,
so the top search box finds titles by an actor/director name and ranks them above
summary-only mentions. New /api/plex/people endpoint returns the cast/crew matching
the term (name prefix or word-start) with a film count + a headshot (pulled from one
representative film's live metadata; image bytes disk-cached).

Frontend: PlexBrowse shows matching people as virtual cards above the grid; clicking
one adds them to the actor/director filter (multi-value, from the C1 work) and clears
the search box so you land on exactly that person's films. Answers the 'why does
"drew" match Rambo?' confusion — it was matching the word in the synopsis; now names
are searchable. i18n en/hu/de.

⚠️ Prod needs a Plex re-sync after deploy to populate people_text (search_vector
regenerates automatically once the column has data).
2026-07-06 00:56:01 +02:00
npeter83
1982cfa7b9 feat(plex): sort direction, genre any/all, multi-person filters; fix back-nav + player stop
UAT follow-ups on the Plex filter epic:
- Sort now has an asc/desc toggle (sort_dir), applied to any sort field.
- Genre multi-select gains an Any/All mode (genre_mode: OR vs AND containment).
- Director/actor/studio become multi-value: people AND (titles featuring all selected),
  studios OR; clicking them on the info page stacks (unions) instead of replacing, and
  the sidebar shows each as a removable 'Active' chip.
- fix(history): clicking a metadata filter on the info page now pushes a fresh grid
  entry instead of history.back(), so browser Back returns to the info page rather than
  leaving the Plex module.
- fix(player): fully tear down the <video> + hls on unmount and guard late play() calls,
  so backing out of a just-started video no longer leaves audio playing in the background.
i18n en/hu/de (match any/all, sort direction).
2026-07-06 00:15:31 +02:00
npeter83
eefd7e3abd feat(plex): expanded metadata filters, clickable info page, image disk cache
Backend (migration 0045_plex_filter_meta): plex_items gains rating (audienceRating
~IMDb), content_rating, studio, originally_available_at, and GIN-indexed genres /
directors / cast_names — all mirrored from the cheap section listing (no per-item API
calls; they also seed a future watch-habit recommender). /browse gains genre / content-
rating / year / rating / duration / added-within / director / actor / studio filters
(@> containment, GIN) + sort by year|rating|duration|release; new /facets endpoint
returns available genres+ratings (with counts) and the year/rating/duration bounds. A
thin on-disk image cache (.plex-img-cache) serves posters/art/cast photos from local
disk after first fetch (~7-14x faster repeat loads).

Frontend: PlexSidebar grows the full filter set (facet-driven genre/age chips, rating
steps, year range inputs, duration buckets, added-within, active people/studio chips,
clear-all); filters persist per-account as one JSON blob. PlexInfo metadata (year,
genre, director, cast, studio, IMDb score) is clickable → sets the matching filter and
returns to the filtered grid (page variant only; the in-player overlay stays read-only
so a stray click can't stop playback). i18n en/hu/de.
2026-07-05 23:45:55 +02:00
npeter83
690e17611c feat(plex): rich media info — info page, in-player info overlay, IMDb + cast photos
Backend (no migration): item_detail now also returns IMDb score + id/url (from the
Plex Rating/Guid arrays), content rating, genres, director(s), studio, tagline, and
cast as {name, role, photo}. New host-whitelisted /person-image proxy serves cast
photos from Plex's public metadata CDN (keeps third-party requests off the browser).

Frontend: reusable PlexInfo component in two forms — a full info page opened from a
card's 'i' button (history subview, art backdrop, Play/Resume + watch controls) and a
lean overlay in the player toggled with 'I' (video keeps playing). Two per-user view
prefs (faint backdrop, cast row) persisted in preferences. Mark-unwatched / clear-resume
cover the watched-reset gap. i18n en/hu/de.
2026-07-05 22:57:18 +02:00
npeter83
38c4bee869 feat(plex): player HTPC polish — Stop button, keyboard help, time-remaining + clock, scroll-restore
- Explicit Stop button (stops playback + returns to the library).
- Keyboard cheat-sheet overlay toggled with 'H' (movie hides the episode-nav row).
- Backspace = stop & back to feed (HTPC-remote convention); closes the help first.
- Time-remaining readout beside the seek bar + a live wall clock in the top bar
  (both fade with the controls).
- PlexBrowse restores the grid scroll position when returning from the player, so
  Back lands on the same card instead of the top of the library.
- New plex.player i18n keys (stop/help/keys.*) in en/hu/de.
2026-07-05 21:27:02 +02:00
npeter83
c84cb22e5a fix(player): freeze the launch queue so Loop All + Next/Previous use the original list
The player's queue came live from the feed query, which refetches under a
watch-state filter as each video is auto-marked watched — steadily shrinking
the list mid-session. Loop "All" could then never wrap back to the first
video (earlier items had dropped out), and prev/next drifted. Snapshot the
queue once at mount (useState lazy init) and step through that frozen list for
the whole session; the live prop is ignored thereafter. Supersedes the earlier
active-item pin with a list-level freeze that also fixes Loop All.
2026-07-05 20:50:05 +02:00
npeter83
0fff5ad35d fix(player): keep playing the current video when it leaves the live feed queue
Auto-marking a video watched near its end invalidates the feed query; the
refetched/reordered queue can no longer contain the playing item, so
findIndex returned -1 and the active item fell back to queue[0] — silently
jumping playback to an unrelated video and defeating Loop "One" (and
auto-advance:off). Pin the last resolved video in a ref and keep playing it
when it drops out of the queue instead of snapping to queue[0].
2026-07-05 19:49:39 +02:00
npeter83
2ee4d563f6 release: v0.23.1 — clearer Plex playback errors + admin media-mount check 2026-07-05 18:58:43 +02:00
npeter83
3e1460759c release: v0.23.0 — Plex integration (browse/search + rich player) 2026-07-05 06:28:15 +02:00
npeter83
401fe90256 docs(release): note player auto-advance + loop in v0.22.3 2026-07-05 00:21:15 +02:00
npeter83
597cec6517 docs(release): note the saved-view persistence fix in v0.22.3 2026-07-04 23:42:40 +02:00
npeter83
7ea91ae42c docs(release): note player stepping + native controls in v0.22.3 2026-07-04 22:26:22 +02:00
npeter83
5d56fbed99 chore(release): v0.22.3 — source URL reference + adaptive watch player 2026-07-04 21:26:08 +02:00
npeter83
1c5a58b4de docs(release): note code-splitting in v0.22.2 2026-07-04 19:44:19 +02:00
npeter83
e612e09c4e docs(release): note a11y improvements in v0.22.2 2026-07-04 19:13:35 +02:00
npeter83
3911ab48f3 chore(release): v0.22.2 — landing perf + SEO polish 2026-07-04 18:19:07 +02:00
npeter83
95984a7ce4 chore(release): v0.22.1 — iOS/Safari-compatible downloads 2026-07-04 17:30:02 +02:00
npeter83
abedca5b8c chore(release): v0.22.0 — Download Center worker/sidecar in all composes + docs
Prep the Download Center epic (Phase 1 + editor + share) for prod/self-host:
- Dockerfile: create /downloads owned by appuser so a named-volume mount is writable (prod Linux).
- docker-compose.{home,selfhost,yml}: add the 'worker' (yt-dlp/ffmpeg job loop) + 'bgutil-pot'
  (PO-token) services + a downloads mount (DOWNLOAD_ROOT, WORKER_ENABLED). Media defaults to a
  named volume; DOWNLOAD_HOST_PATH points it at a host dir (e.g. a Plex-readable folder).
- README / docs/self-hosting.md / .env.example / install.{sh,ps1}: document the Download Center,
  the two extra containers, and DOWNLOAD_HOST_PATH.
- VERSION 0.22.0 + releaseNotes entry.
2026-07-04 06:31:31 +02:00
npeter83
9ca90f515c chore(release): 0.21.0 2026-07-02 02:19:25 +02:00
npeter83
b7c79cbe53 chore(release): 0.20.2 2026-07-01 14:30:50 +02:00
npeter83
381794d9ae chore(release): 0.20.1 2026-07-01 12:07:58 +02:00
npeter83
b8e1c9386e chore(release): 0.20.0 2026-07-01 11:28:56 +02:00
npeter83
669994cc85 chore(release): 0.19.0 2026-07-01 02:37:07 +02:00
npeter83
a4abfa402f chore(release): 0.18.0 2026-06-29 23:41:53 +02:00
npeter83
b1ed706cab chore(release): 0.17.0 — maintenance epic + messaging account-switch fix 2026-06-29 01:12:22 +02:00
npeter83
7a97fef33c chore(release): 0.16.2 — access-requests UX + back-navigation fixes 2026-06-26 01:42:12 +02:00
npeter83
6286dcc3c2 chore(release): 0.16.1 — notification context + YouTube API egress proxy 2026-06-26 00:36:46 +02:00
npeter83
557f2cef9c chore(release): 0.16.0 — end-to-end encrypted real-time messaging 2026-06-26 00:01:54 +02:00