Commit graph

648 commits

Author SHA1 Message Date
npeter83
93bca8ba74 Merge: promote dev to prod 2026-07-09 02:37:31 +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
c53a65d9d4 merge: Plex search ephemeral state + empty-with-filters hint (dev-only) 2026-07-09 02:30:57 +02:00
npeter83
de29a20d7e merge: Plex player copyts rework + full player UAT round (dev-only) 2026-07-09 02:30:44 +02:00
npeter83
488e809f2b fix(plex): keep play state honest across a seek that restarts the session
Seeking to a spot outside the current remux window restarts the hls.js
session, which detaches/re-attaches the media and fires 'emptied' — that
silently flips the element to paused WITHOUT a 'pause' event. Because the
play flag only tracked 'play'/'pause', the button kept showing 'playing'
while the video had actually stopped, and on a slow (proxied) load the
post-manifest play() could be rejected, leaving it stuck paused until a
click on the video.

Root cause confirmed by instrumenting the media events during a seek:
emptied(paused=true) with no accompanying pause event.

Fix: resync the play flag from video.paused on the settling events
(emptied/canplay/seeked) so the button can never contradict reality;
preserve the pre-seek play/pause intent through the session restart
(loadSession takes resumePlay); and retry play once on 'canplay' so a
rejected early play() still resumes. Verified on :8080 — backward seek
(session restart), forward seek (native) and manual pause all keep the
icon in sync (0 desync samples) and resume correctly.
2026-07-09 02:20:04 +02:00
npeter83
2659991d84 fix(plex): seek tooltip tracks cursor under scale; tidy the tuning menu
- The seek-bar hover tooltip positioned itself with a px `left` derived
  from the scaled getBoundingClientRect, so inside the transform:scale
  player it got re-scaled and drifted right of the cursor (only 0 was
  right). Position it by fraction (% of the bar) instead — it now sits
  under the cursor at any point, like the volume tooltip already did.
- The tuning (gear) menu tabs wrapped to two lines at the +25% scale.
  Widen the menu, keep tab labels on one line (whitespace-nowrap), and
  shorten the labels: "Sync offset" → "Offsets", "Subtitle style" →
  "Subtitles" (all three languages).
2026-07-09 01:32:04 +02:00
npeter83
23070279a3 feat(plex): player UAT round 2 — mouse-back, +25% UI, seek drag, volume revert
- Mouse/browser Back button now behaves like Backspace/Esc: while a
  menu, the shortcuts sheet, the info overlay or an auto-skip countdown
  is up, Back closes/cancels that (via a history entry) instead of
  dropping straight to the grid; with nothing open it still leaves.
- Whole player UI is 25% larger for lean-back/HTPC legibility, applied
  as transform: scale(1.25) on an 80vw x 80vh root (fills the viewport
  exactly). transform, not zoom — under zoom, event clientX and
  getBoundingClientRect fall into different spaces and the seek/volume/
  hover math breaks; transform keeps them aligned. Skip buttons ride
  this scale (reverted their separate enlargement so they're +25% too).
- Seek bar: drag the head to scrub, not just click-to-jump; the head
  previews the target and the seek commits on release (pointer capture),
  so a long drag doesn't restart the stream on every step.
- Volume bar: dropped the loudness gradient — back to the plain accent
  fill; the 0-100 hover value tooltip stays.
- Harden both bars' pointer-capture calls (try/catch) so a capture
  failure can't abort the click/drag.
2026-07-09 01:12:10 +02:00
npeter83
dcdf48a811 fix(plex): Esc/Backspace actually cancels the auto-skip countdown
Cancelling only blanked skipProgress and flagged the marker — it never
stopped the running interval, so the next tick (~100ms later) re-set the
progress and the skip fired anyway. Hold the interval id in a ref and
clear it on cancel (and null it on completion/cleanup), so Esc/Backspace
truly stops the countdown; the Skip button stays for a manual skip.
2026-07-09 00:37:13 +02:00
npeter83
089eab76e4 fix(plex): player UAT round — resume, back, subtitles, skip, ETA, volume
- Resume: closing with Back now leaves the resume point correct. The
  position was saved server-side, but reopening the item in the same
  session read react-query's cached detail (old position); patch that
  cache on unmount so a reopen resumes where you left off (F5 was fine —
  empty cache after a reload).
- Back cascade: the on-screen Back arrow and Backspace now close an open
  tuning/tracks menu, then the shortcuts sheet, then the info overlay,
  and only leave the player when nothing is layered on top (it used to
  jump straight to the feed with a panel still open).
- Subtitle shadow: replace the single 0-offset blur (invisible even at
  max) with an eight-direction outline plus glow that scales with the
  slider — legible over any frame.
- Skip Intro/Credits: ~25% larger button and text; add a crisp bottom
  progress bar to the auto-skip countdown; and freeze the countdown while
  the video is paused so it no longer auto-seeks a paused video.
- Time readout: append the projected finish time (wall clock + time
  remaining) in the clock's own 24h/12h format.
- Volume: replace the native slider with a bar that shows the 0-100 value
  under the cursor (like the seek bar) and a loudness gradient — blue
  (too quiet) → green (normal) → red (too loud).
2026-07-08 23:48:09 +02:00
npeter83
a005a1bcbb fix(plex): give the library search its own ephemeral state
The Plex library search box wrote to the shared, persisted feed filter
(filters.q). That leaked a Plex query into the feed search box and, being
persisted, restored a stale query after a reload — which then collided
with a persisted collection filter to produce a confusing empty grid.
Plex now has its own search state: kept across page switches within a
session, but not persisted and not shared with the feed, so a reload
starts clean.
2026-07-08 23:19:36 +02:00
npeter83
d84edebc09 fix(plex): explain an empty search when filters are also active
A Plex search that returns nothing while a collection/rating filter is
active showed a bare "No matches", hiding that the filters — not the
query — were the cause. Show how many filters are narrowing the results
and offer a one-click Clear filters inline. Translated in all three
languages.
2026-07-08 23:19:23 +02:00
npeter83
c190c0e375 feat(plex): player polish — clock overlay, sub shadow, auto-skip, seekbar hover
- Wall clock is now a Clock settings tab: show/hide, 24h↔12h (am/pm), size, colour, and
  an optional date in two fixed styles (EN "08-JUL-2026 Wed" / HU "2026-JÚL-08 Sze").
- Subtitle text-shadow (blur strength + colour) in the Subtitle tab.
- Auto-skip intro/credits: per-user toggles + a shared 0–10s delay (default 5). Entering a
  marker whose auto-skip is on runs a countdown shown as a fill on the Skip button; on
  completion it skips (intro → marker end, credits → next item / binge). Esc/Backspace during
  the countdown cancels it and does NOT navigate back. 0s = immediate.
- Seekbar hover shows a timestamp tooltip that follows the cursor.
- i18n en/hu/de for the new strings.
2026-07-08 22:38:47 +02:00
npeter83
3c622fd44c feat(plex): multi-rendition audio (client-side switch) + reliable resume-on-F5
- Multi-audio items now ship every audio track as an HLS rendition in one session
  (stream.py var_stream_map -> master.m3u8), so hls.js switches audio CLIENT-SIDE with
  no ffmpeg restart, same timeline, no gap/drift. /session gains ?multi=1 (forces HLS
  even for direct-playable multi-audio files); K is probed from the video variant seg.
- Restore the selected audio track on AUDIO_TRACKS_UPDATED, not MANIFEST_PARSED (the
  renditions aren't parsed yet on MANIFEST_PARSED, so the set was dropped -> after F5 the
  UI showed the restored track but playback stayed on the default).
- Resume position now survives F5: a pagehide keepalive beacon (plexProgressBeacon)
  saves the current position on reload/close/navigate, since React effect cleanup does
  not run on a full reload; seekTo writes the target into absRef immediately so a save
  right after a seek is accurate.
2026-07-08 22:25:03 +02:00
npeter83
0a0703b769 feat(plex): rework player timing (copyts) + player settings & personalization
Streaming / subtitle sync (the core fix):
- stream.py: add -copyts so HLS segments carry the true absolute PTS, and measure
  the real keyframe start K from seg_0 (ffprobe) -> return it as the session start.
  Fixes the seconds-long subtitle lead caused by using the requested seek offset
  instead of the keyframe ffmpeg actually lands on with video stream-copy.
- Add -noaccurate_seek so the re-encoded audio starts at the same keyframe as the
  video (was starting (X-K)s later -> seconds of silence after each seek/audio switch).
- Compensate the fixed ~1.0s lag hls.js introduces for non-zero-start copyts streams,
  folded into the session start so the clock, seeking and the subtitle shift are all
  content-accurate.
- /subtitle gains an offset param; _shift_vtt shifts absolute cues onto the session's
  zero-based clock and DROPS fully-past cue blocks (collapsing them to 0->0 made every
  past cue active at currentTime 0 on resume -> a pile-up until playback advanced).

Audio:
- /session + stream.py gain an audio A/V-sync offset (-itsoffset, full +/-, second
  input only when non-zero).

Player settings & personalization (per-account, persisted -> survive F5):
- storage.ts: useAccountPersistedObject (per-account JSON prefs blob).
- PlexPlayer: volume/mute, audio+subtitle language (index-based match, fixes the F5
  audio-revert), sync offsets, seek steps, subtitle style, auto-hide, play intent.
- Hotkeys A (cycle audio) / S (cycle subtitle), mouse-wheel volume, Ctrl+arrow fine
  seek, per-user plain/fine seek-step + auto-hide toggle.
- Subtitle appearance: size / colour / vertical position / background via ::cue + line.
- UI: split into a Tracks quick-menu + a tabbed gear panel (Sync | Playback | Subtitle);
  both dismiss on outside-click; edge-aware control tooltips.
- i18n en/hu/de for all new strings.
2026-07-08 21:35:38 +02:00
npeter83
7833a82cb8 Merge: promote dev to prod 2026-07-07 22:58:09 +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
557e9e988c Merge: promote dev to prod 2026-07-07 22:44:45 +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
cb170dfd32 feat(downloads): clickable channels + poster fallback for thumbnail-less sources
Two visual gaps for non-catalog downloads:
- Channel link: YouTube already exposes channel_url; Facebook exposes none but a
  numeric uploader_id that resolves at facebook.com/<id>. `_uploader_url` derives
  it so the auto-detected channel renders as a real clickable link.
- Poster: a source with no thumbnail (e.g. a direct reddit HLS URL) showed a
  blank image box. The worker now cuts a representative frame with ffmpeg
  (`ensure_poster`) into the `<base>.jpg` sidecar and records `poster_path`
  (migration 0050). The card, the public watch page (<video poster> + og:image),
  and link previews fall back to it via new authed + public poster endpoints.

Adds `app.downloads.backfill` (one-off, re-run-safe) to fill uploader_url
(re-extract YouTube/Facebook metadata) and posters for pre-existing downloads.
2026-07-07 22:28:49 +02:00
npeter83
374ad4ddc4 fix(ui): only close a modal when the backdrop click starts on the backdrop
Pressing the mouse inside a dialog (e.g. to select text in an input), dragging
out, and releasing on the backdrop wrongly dismissed it: the click event fires
on the common ancestor of mousedown+mouseup (the backdrop), so the outside-click
handler ran. Track whether the press started on the backdrop and only close when
it both starts and ends there. Fixes all shared-Modal dialogs at once (downloads
editor/share, confirm, Plex collections + add-to-playlist, …).
2026-07-07 22:28:33 +02:00
npeter83
5e6eaceaf0 Merge: promote dev to prod 2026-07-07 21:43:10 +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
ebe8d26620 Merge: promote dev to prod 2026-07-07 20:21:38 +02:00
npeter83
ccbcc34bd3 merge: downloads metadata editing + rich share-link previews — v0.31.0 2026-07-07 20:19:51 +02:00
npeter83
1b691cdc5f chore(release): v0.31.0 2026-07-07 20:19:39 +02:00
npeter83
8591e45747 feat(share): rich link previews (Open Graph) for /watch pages
A shared /watch/{token} link is a client-rendered SPA, so a social crawler only
saw the generic index.html — a blank link card. The server now injects per-video
Open Graph / Twitter tags (title, channel, thumbnail) into the served HTML for
that route, so links unfurl richly in Messenger and other chat apps; real
browsers ignore the extra tags and hydrate the page as usual. Password /
expired / invalid links fall back to the generic card with no metadata leak.

Also shortens the generic site description used for search engines and link
previews.
2026-07-07 20:19:30 +02:00
npeter83
8c86c6b4a8 feat(downloads): editable details with clickable channel and extra links
The edit (pencil) action now edits a download's full display metadata — title,
channel name, channel link and any number of extra reference URLs — instead of
just the name. The channel and links render as clickable links on the library
card, and the channel link is auto-filled from the source (yt-dlp channel_url)
when available. Shared watch pages resolve the same per-download overrides, so a
rename/channel/link edit is reflected on the public /watch page too, with every
link clickable.

Adds migration 0049 (media_assets.uploader_url; download_jobs.display_uploader,
display_uploader_url, extra_links) and generalizes the rename endpoint into a
metadata update with URL validation. EN/HU/DE strings included.
2026-07-07 20:19:18 +02:00
npeter83
04d35375ed merge: Plex playlist power — grouped collapsible view, drag & drop, bulk add (UAT accepted, dev-only) 2026-07-06 23:59:01 +02:00
npeter83
2ef22982bb feat(plex): grouped collapsible playlist view + drag-and-drop + bulk add
Playlist view now groups a run of episodes from the same show into a
collapsible season/show block so a whole series doesn't sprawl into an
endless flat list; movies stay standalone (larger poster card in the
accordion). Two per-account layouts — Accordion and Tree — persisted via
LS.plexPlaylistLayout; show groups start collapsed. Reorder is drag &
drop (@dnd-kit): a show block moves as one unit, episodes reorder within
their show, keyboard-draggable via KeyboardSensor. Remove works per item,
per season, or per whole show. The add-to-playlist dialog is generalised
to a single leaf or a whole group (tri-state none/some/all with an in/size
count); the show page gains add buttons for an episode, a season, and the
whole show. i18n en/hu/de.
2026-07-06 22:14:56 +02:00
npeter83
e6b22f971a feat(plex): playlist bulk add/remove + group-aware playlist cards
Add POST /playlists/{id}/items/bulk and /items/remove-bulk so a whole
season or show can be added/removed in one call (input order preserved,
duplicates skipped). Extend GET /playlists with contains_group=<rk,rk,…>
returning per-playlist group_in + a top-level group_size for the bulk
add-to-playlist dialog. Include the show's rating_key (show_id) on
playlist episode cards so the client can group a playlist by show/season.
2026-07-06 22:14:40 +02:00
npeter83
02235e5056 Merge: promote dev to prod 2026-07-06 20:22:13 +02:00
npeter83
22f36d9d7f merge: Plex Playlists Phase 1 — per-user ordered watch-lists — v0.30.0 2026-07-06 20:20:08 +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
fbf67431b6 Merge: promote dev to prod 2026-07-06 17:35:14 +02:00
npeter83
1fd3003038 merge: Plex Collections Phase 2 — admin editing/write-back — v0.29.0 2026-07-06 17:33:16 +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
fc49651eaf Merge: promote dev to prod 2026-07-06 08:47:22 +02:00
npeter83
4b053a55da merge: Plex subtitles as WebVTT tracks (fix external-sub crash) — v0.28.3 2026-07-06 08:45:30 +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
0dea635179 Merge: promote dev to prod 2026-07-06 08:06:11 +02:00
npeter83
335e4d7c76 merge: right-sized Plex poster/art images — v0.28.2 2026-07-06 08:04:13 +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
f077bcf25a Merge: promote dev to prod 2026-07-06 07:16:13 +02:00
npeter83
6df43445df merge: fix Plex image-proxy DB-pool exhaustion — v0.28.1 2026-07-06 07:14:29 +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
a3dde73670 Merge: promote dev to prod 2026-07-06 06:52:53 +02:00
npeter83
4b360f8f36 merge: Plex collections Phase 1 + info-page polish — v0.28.0 2026-07-06 06:50:21 +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
8a8087ae99 feat(plex): classify collection-strip source (franchise/imdb/tmdb/smart)
Derive a `source` bucket per collection from the already-synced title + smart
flag (no schema change, no re-sync): external list providers (IMDb/TMDb/TVDb/
Trakt) by leading token, Plex smart lists via the smart flag, everything else a
genuine "collection". Returned on each info-page collection strip so the client
can show/hide each type independently. Adds the stripSource labels (en/hu/de).
2026-07-06 06:49:43 +02:00