Commit graph

47 commits

Author SHA1 Message Date
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
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
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
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
5d56fbed99 chore(release): v0.22.3 — source URL reference + adaptive watch player 2026-07-04 21:26:08 +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
npeter83
c84f5d5fe5 chore(release): 0.15.0 — feed virtualization + keyset pagination 2026-06-25 20:04:25 +02:00
npeter83
231e0c708b chore: release 0.14.1 — finish subfeed→siftlode rename
Bump VERSION and add a release-notes entry for the rename cleanup so the
prod image is published under a fresh tag rather than overwriting 0.14.0.
2026-06-21 07:07:56 +02:00
npeter83
0f124dc4d3 chore(release): 0.14.0 2026-06-21 03:23:23 +02:00
npeter83
2d1d5c80ac chore(release): 0.13.0 2026-06-19 19:53:31 +02:00
npeter83
4fc7e1e7df chore(release): 0.12.0
Cut a version marker for the accumulated work staged on dev (epics 1-4): reusable
Banner; Discover total-videos column + subscribe confirm; Stats consolidation +
quota-action taxonomy; DB-backed system_config + admin Configuration page. NOT
deployed to prod yet — dev/localdev report 0.12.0, prod stays 0.11.2 until a ship.
Alembic head at this marker: 0021.
2026-06-19 13:23:33 +02:00
npeter83
262ed06b3c release: 0.11.2 2026-06-19 04:27:22 +02:00
npeter83
403a5608df release: 0.11.1 2026-06-19 04:16:23 +02:00
npeter83
d254cf4353 release: 0.11.0 2026-06-19 04:02:38 +02:00
npeter83
1e41a624ff release: 0.10.0
Explicit Save/Discard for Settings; self-resolving connection-lost notice +
automatic retry of transient gateway 502s; state-change notices gated on
server confirmation; Settings panel height tidy-up.
2026-06-19 00:07:25 +02:00
npeter83
3a0789ebe7 chore(release): v0.9.0 — notification inbox + automatic video maintenance
Bump VERSION to 0.9.0 and add the user-facing release notes for the durable
notification inbox (P1) and the maintenance/validation job that retires videos
that can no longer be played.
2026-06-18 03:22:22 +02:00
npeter83
a11a8db278 chore(release): v0.8.0 — channel columns, tag workflow, video reset, error dialog 2026-06-18 01:18:41 +02:00
npeter83
a7d087c6d3 chore(release): v0.7.0 — data-table channel manager, sidebar reorg, fixes 2026-06-17 19:17:35 +02:00
npeter83
79249104d4 chore(release): v0.6.0 — admin Background Scheduler dashboard 2026-06-16 17:46:15 +02:00
npeter83
e6c53b3f96 chore(release): v0.5.1 — graceful fallback for non-embeddable videos 2026-06-16 12:02:38 +02:00
npeter83
a76bbb5825 chore(release): v0.5.0 — demo account, clearer live videos 2026-06-16 10:23:29 +02:00
npeter83
af84cc0404 chore(release): v0.4.1 — feed toolbar + clearer counts/dates
Bump VERSION to 0.4.1 and add the 0.4.1 release-notes entry (feed view/content-type chips
above the videos, key+direction sort, exact view count + upload date on cards/player).
2026-06-16 03:18:58 +02:00
npeter83
f6efe44e37 chore(release): v0.4.0 — navigation overhaul, glass, account switch
Bump VERSION to 0.4.0 and add the 0.4.0 release-notes entry (Epic N: left nav sidebar,
multi-account switch, Settings page, in-app back, frosted-glass refresh).
2026-06-16 02:16:21 +02:00
npeter83
3143d1c1db chore(release): v0.3.0 — playlists + YouTube two-way sync
Bump VERSION to 0.3.0 and add the 0.3.0 release-notes entry (local playlists, watch
later as a playlist, YouTube mirror + two-way sync, sort + undo/redo, rail sorting).
2026-06-16 00:16:59 +02:00
npeter83
34c41f7a41 chore(release): 0.2.0 — shared library, dynamic facets, share view
Bump VERSION to 0.2.0 and add release notes covering the Mine/Library shared-feed
scope, dynamic faceted filter chips (live counts, hide non-matching, Any/All topic
match), surprise-me reshuffle, share-view links, and the deep-backfill status
clarity fixes.
2026-06-15 13:58:22 +02:00
npeter83
a93ab30fb2 feat(version): /api/version + build-time version/commit stamping
Add a VERSION file (0.1.0) and inject APP_VERSION/GIT_SHA/BUILD_DATE as Docker
build-args (both stages; Vite inlines them into the SPA). New public GET
/api/version returns app_version, git_sha, build_date and the Alembic head as the
database revision. deploy.sh and the localdev build pass the args.
2026-06-15 00:06:57 +02:00