Commit graph

25 commits

Author SHA1 Message Date
BoulderBadgeDad
877a86c9b0 Video Discover: split Top 10 today into Movies + TV Shows (Netflix-style)
The mixed /trending/all chart is movie-heavy — TV was nearly absent (only
1 of the top 10 was a show). Split it like Netflix: 'Top 10 today' now holds
two ranked rails, Movies and TV Shows, from the dedicated /trending/movie/day
and /trending/tv/day charts (full 10 of each).

- client.trending(window, kind): single-type charts force the kind into
  _disc_map (those endpoints omit media_type).
- engine.trending(window, kind): kind in the cache key.
- /discover/list: key=trending_movies_today / trending_tv_today, both treated
  as single un-paged charts; inherit the same hide-owned + ranked rendering.
- frontend: one 'Top 10 today' group, two ranked shelves titled Movies / TV
  Shows (group header carries the 'Top 10' framing).
2026-06-24 08:00:13 -07:00
BoulderBadgeDad
e7552d3c62 Video Discover: fix orphaned Top 10 numerals when hiding owned + lift 'New' up the page
Bug: with 'Hide owned' on, an owned title in the Top 10 (e.g. House of the
Dragon) had its card hidden by the global .vdsc-hide-owned rule while its big
rank numeral stayed — a blank gap with just a number. The ranked rail fetched
the true chart (ignoring hide_owned) precisely to keep the chart intact, which
collided with that CSS. Fix: ranked rail now honours hide_owned at fetch time
(owned dropped server-side, ranks stay contiguous 1-N), plus a CSS safety net
exempting ranked cards from the hide rule so a numeral can never be stranded.
When hide-owned is OFF the true chart still shows owned with the 'In Library'
ribbon.

Ordering: moved 'New & noteworthy' above the async collection/taste groups so
the two always-visible discovery rows (Top 10 + New) anchor the top of the
page, matching how streaming services surface new releases.
2026-06-24 07:33:58 -07:00
BoulderBadgeDad
24b79dc29e Video Discover: sticky 'jump to section' nav for the deep rail stack
The page is now a long stack of rail groups (For you / Top 10 / New /
Trending / Mood / Studios / Genres / More). A sticky chip bar lets you
jump straight to any section — Netflix/Disney-style category nav. Chips
stay in lockstep with the groups: async-only groups (foryou/collection/
taste) reveal their chip when filled; pruned groups hide theirs. Lives
inside the shelves host so it auto-hides in the Browse-all grid view.
2026-06-23 23:58:53 -07:00
BoulderBadgeDad
a275cbfb7a Video Discover: Top 10 ranked rail + New/Mood/Studios sections + NEW badge
Best-in-class discovery pass (Netflix/Disney-class):
- iconic ranked 'Top 10 today' rail (daily trending, big outlined rank
  numerals, true global chart via lang=any, single un-paged fetch)
- 'New & noteworthy' (date-windowed new releases + theatrical pipeline)
- 'By mood' (genre AND-combos w/ vote floors: feel-good, mind-bending…)
- 'From the studios' (Pixar/Ghibli/A24/Marvel/DreamWorks via company ids)
- 'Something different' (quick watches / epics / family) merged with gems
- 'NEW' flag on un-owned current-year cards
Preserves async-loader group ids (foryou/collection/taste). Empty rails
self-prune; wrong studio ids just drop their rail.
2026-06-23 23:55:30 -07:00
BoulderBadgeDad
82cbc22577 video discover: organise rails into fixed, labelled sections (stable order)
The three async personalized loaders each did insertAdjacentHTML('afterbegin'), so 'Recommended
for you' / 'More like X' / gap rails landed in whatever order their fetch finished — the top of
the page reshuffled every load, and taste-based rails were scattered through generic ones.

Now Discover renders 6 authored groups in a fixed order, each with a header:
  For you · Finish your collection · More of what you like · Trending & popular ·
  Browse by genre · Hidden gems & more
Each async loader fills its own group's body (deterministic position) instead of racing to the
top; async-only groups (gaps) stay hidden until filled, and a group whose rails all drop out is
pruned. Extracted lazyShelfHtml/filledShelfHtml/shelfNav helpers; lazy-load, stagger, gen-guard,
see-all and scroll arrows all unchanged.
2026-06-23 10:39:41 -07:00
BoulderBadgeDad
4f24ac2733 video discover: stop duplicate 'Recommended for you' rows stacking on rail rebuild
Toggling service/language chips calls reloadRails(), which clears the shelves synchronously
but the personalized loaders (foryou/gaps/morelike) fetch async and insertAdjacentHTML afterbegin.
Rapid re-toggles let superseded in-flight fetches prepend anyway -> N 'Recommended for you' rows
piled up. Each rebuild now bumps state.railGen and every loader bails if its captured gen is
stale before prepending; chip-driven rebuilds are also debounced (350ms) so multi-select
coalesces into one rebuild. The 'On your streaming services' rail itself was building fine -
it was just buried under the duplicates.
2026-06-23 10:00:52 -07:00
BoulderBadgeDad
1feb34621e video discover: cascade rail cards in instead of one block flash
Each rail showed a shimmer skeleton then revealed the whole row in a single fade — on a
big library with hide-owned on (which pages deep server-side) that reads as a long blank then
a pop. Cards now stagger in left-to-right via a per-card --i index (capped) + a 'backwards'
fill-mode keyframe (so the entrance animation releases and :hover transforms still work).
Applies to lazy rails (fillShelf) and the prepended personalized rows (staggerWithin).
2026-06-23 09:32:18 -07:00
BoulderBadgeDad
73395b9668 video discover: give the Browse panel its own language filter (self-contained search)
The Browse-all grid silently inherited the global rail language preference, so users
couldn't ad-hoc browse foreign cinema without changing their homepage prefs. Added a
language chipset to the Browse panel (auto-wired via the generic chip handler -> state.sel.lang)
and the grid now always sends lang= : a real code filters, 'any' opts OUT of the rail
preference entirely. Route treats lang=any as 'no language filter'. Added a 'Browse the full
catalog' eyebrow so the panel reads as a self-contained search, parallel to 'Across Discover'.

Hide-owned and the saved 'My services' pref stay single/global by design (they apply page-wide);
the Browse panel's provider chips remain its own grid filter.
2026-06-23 09:26:22 -07:00
BoulderBadgeDad
5906576a13 video discover phase 3: 'On your streaming services' rail
A saved streaming-services preference drives a personalized 'On your streaming services' rail:
- GET/POST /discover/providers-pref (TMDB provider ids); /discover/list OR-joins multiple
  providers (comma->pipe) into with_watch_providers.
- A 📺 multi-select in the toolbar (Netflix/Prime/Disney+/Max/Apple TV+/Hulu/Paramount+/Peacock);
  selecting services saves the preference and rebuilds the rails.
- The rail (high priority, after taste) appears once you've picked services, showing what's
  streaming on yours.
2026-06-23 00:33:35 -07:00
BoulderBadgeDad
6734b6ab22 video discover phase 4 (UI): 'Not interested' card button + ignore-list modal
- A ✕ 'Not interested' button on every un-owned Discover card (hover) — adds to the ignore list
  and fades the card out instantly.
- A '🚫 Ignore List' button top-right of the hero opens a vibey glassmorphic modal: a header
  explaining what it is, a search box to hide any movie/show directly (TMDB search), and a poster
  grid of everything hidden with one-click 'Un-hide'. Empty state guides the user.
- Card button + modal both POST /discover/ignore; ignored titles vanish from every rail (via
  _stamp_owned). Video-only, additive.
2026-06-23 00:29:46 -07:00
BoulderBadgeDad
d019e3aca1 video discover: hidden-gems rails (highly-rated, non-blockbuster)
Add 'Hidden Gems' (movies) + 'Critically Acclaimed Shows' rails — vote_average.desc with the
backend's vote_count floor filtering out single-vote noise, and the language preference applied.
Slot them above the decade/foreign rails.
2026-06-23 00:13:19 -07:00
BoulderBadgeDad
f1b6fd5e31 video discover: language preference UI (multi-select chips)
A 🌐 multi-select chip row in the Discover toolbar (EN/KO/JA/ES/FR/HI/DE/IT) to pick which
original languages appear in the general/curated rails. Loads the current preference from
/discover/languages, toggling a chip POSTs the new set and rebuilds the rails (never empty —
at least one stays on). Extracted reloadRails() (now shared by the hide-owned toggle + language
chips). Default EN, so the rails are English unless you opt more in.
2026-06-23 00:11:56 -07:00
BoulderBadgeDad
28fe3d2b0b video discover: preferred-languages filter (keep Bollywood etc. out of general feeds)
The general/curated rails (Popular/Trending/Top Rated + genre/decade) pull TMDB's GLOBAL lists,
flooding feeds with foreign-language titles (Bollywood). Add a multi-language preference:
- _disc_map now carries original_language (+ popularity) on each item.
- discover_languages setting (default 'en'); /discover/list post-filters general/curated rails
  to it (dropping known non-preferred-language titles) and pages deeper to keep rails full.
  Rails with an explicit lang (the dedicated foreign rails) bypass the filter.
- GET/POST /discover/languages to read/set the preference.
- Removed the hardcoded lang=en on general rails (the setting drives it now).
Default 'en' immediately fixes the Bollywood flood; UI to pick languages next.
2026-06-23 00:09:51 -07:00
BoulderBadgeDad
e7b1a239b4 video discover phase 2: blended 'Recommended for you' wall
A single personalized wall aggregating TMDB recommendations across many of your owned titles
(random_owned_titles seeds), ranked by consensus — a title recommended by more of your library
ranks higher (ties by rating then popularity), owned + seed titles excluded.
- core/video/discovery_recs.py: pure blend_recommendations (dedup/consensus/exclude), 7 tests.
- /api/video/discover/foryou aggregates ~12 seeds' recommendations.
- loadForYou() prepends the 'Recommended for you' rail on top of the stack; re-runs on the
  hide-owned toggle.
2026-06-23 00:05:26 -07:00
BoulderBadgeDad
d984895cba video discover: keep foreign titles out of general rails + fix hide-owned sparse rows
Two Discover UX issues:
- Foreign-language titles leaked into the general genre/decade rails. Added an
  original-language filter (with_original_language) through client.discover -> discover_filter
  -> /discover/list (?lang=); the genre/decade/'because you like' rails now pin lang=en, and a
  handful of dedicated foreign rails (Korean/Japanese/Spanish/French/Hindi) house non-English.
- 'Hide owned' + a huge library = nearly-empty rails (a 2-page batch was mostly owned, then
  CSS-hidden to almost nothing). /discover/list now takes hide_owned=1: it drops owned
  server-side and pages DEEPER (up to 8) until a rail has ~24 un-owned. fillShelf passes
  hide_owned when the toggle's on; toggling re-renders the rails (+ personalized rows) instead
  of just CSS-hiding cards.
2026-06-22 23:59:21 -07:00
BoulderBadgeDad
a8ce359cb7 video discover phase 1e: 'what am I missing' gap rails on the discover page
loadGaps() fetches /discover/gaps and prepends the gap rails ('Complete the <franchise>',
'More from <director/creator>') above the rail stack, mirroring loadMoreLike. Cards are the
standard un-owned TMDB cards — already actionable (VideoGet add-to-watchlist/get), so a
missing franchise entry or director film is one click from your queue. Best-effort/additive.
2026-06-22 23:30:05 -07:00
BoulderBadgeDad
246c650db4 Discover: fix grid pagination + session cache + responsive pass
Pagination (regression): the IntersectionObserver sentinel only fires on
intersection *changes*, so a short first page kept it on-screen and it never
re-fired — stuck at 20 — and I'd hidden the Load more button whenever IO exists,
leaving no fallback. Now:
- Load more button is always shown while there's more (the reliable control).
- Auto-load is self-correcting: track sentinel visibility and, after each load,
  pull again via rAF if it's still on-screen (rAF lets the observer update first
  so a cached category doesn't load every page at once).
- page increment moved inside loadGrid so the button + sentinel can't double-bump.

Caching: cachedFetch() memoizes /discover/list responses per URL for the session
(rails + grid pages) — revisits, paging and reopening a category are instant and
don't re-hit TMDB.

Responsive: small-screen pass for the Browse panel, hero (height/title/actions
full-width), grid header wrap, and the trailer close button (kept on-screen).
Also fixed the ambient layer's 130% width that could cause horizontal scroll.
2026-06-16 15:44:36 -07:00
BoulderBadgeDad
80d504f94d Discover: meaningful filter colors + drop the asymmetric edge-fade
Colors now carry meaning instead of cycling by position:
- segments (Movies/TV, sort) → the app accent (they're modes, not categories)
- genre chips → a thematic colour per genre (Horror red, Comedy gold, Sci-Fi
  cyan, Romance pink, …) via a name→colour map; unmapped fall back to neutral
- provider chips → each service's brand colour (Netflix red, Disney+ blue, Max
  purple, Hulu green, …)
- era chips → a single warm amber; 'All/Any …' reset chips → neutral grey

Edge-fade: removed the mask-image from .vdsc-rail and .vdsc-chips. On short
filter rows only the left fade landed (dimming the first chip) while the right
fell on empty space — reading as a one-sided fade over everything.
2026-06-16 15:26:12 -07:00
BoulderBadgeDad
a1b9f8a827 Discover: restyle filter controls to the music album-detail button vibe
The fully-round accent pills + sliding-thumb segments didn't fit. Now every
filter control (kind/sort segments + genre/provider/era chips) shares the
album-detail action-button look: rounded-rect (9px), each tinted from a vibey
8-colour palette that cycles across the row (green/purple/blue/amber/pink/cyan/
coral/violet) — soft fill + border + coloured text, brightening on hover, the
selected one filling with its own colour. Dropped the sliding-thumb segmented
control + its now-dead JS (moveSeg/positionSegs).
2026-06-16 15:11:20 -07:00
BoulderBadgeDad
2b3ca12310 Discover: streaming-provider filter + infinite scroll + a11y polish
Provider filter (#4):
- client.discover() + engine.discover_filter() take a TMDB provider id and pass
  with_watch_providers + watch_region (engine._region) + flatrate. Browse gets a
  streaming-service chip row (Netflix/Prime/Disney+/Max/Apple TV+/Hulu/Paramount+/
  Peacock); the grid title reflects 'on <service>'.

Infinite scroll (#6):
- Grid paginates via an IntersectionObserver sentinel (600px lookahead) with a
  bottom spinner; the Load more button stays only as a no-IO fallback.

Polish (#7):
- Hero keyboard nav (←/→ when Discover is the visible view, ignoring inputs and
  while the trailer is open); focus-visible rings on chips/segments/cards/arrows.

Note: 'complete-the-franchise' rail (#5) needs a collection_id per movie, which
the schema doesn't store yet — deferred (would need an enrichment pass).

Tests: +1 (provider watch_region params); updated the discover_filter fake for
the new kwargs. Enrichment + API suites: 115 passed.
2026-06-16 14:55:13 -07:00
BoulderBadgeDad
a9ec025706 Discover: 'More like…' rails + in-app hero trailer
More-like rails (real personalization beyond genre):
- db.random_owned_titles() seeds from a few random owned titles (with tmdb_id);
  client.recommendations() + engine.recommendations() (cached + owned-annotated)
  fetch TMDB recs. New /discover/morelike interleaves movie/show seeds (max 3
  rails) and the page prepends them, pre-filled, above the rail stack with a
  gradient 'for you' title.

Hero trailer (cheap big visual win):
- client.video_trailer() (light /videos call, Trailer over Teaser) + engine
  .trailer() (day-cached) + /discover/trailer. Hero gets a 'Trailer' button that
  opens an in-app YouTube lightbox (autoplay, Esc/backdrop close, pauses the
  slideshow) — nothing leaves SoulSync.

Tests: +11 (recs parse/annotate/cache, trailer pick+fallback+cache, random owned
seeds owned-with-tmdb only, /discover/trailer + /discover/morelike endpoints).
Enrichment + API suites: 114 passed.
2026-06-16 14:44:44 -07:00
BoulderBadgeDad
5aa792d399 Discover: sliding segmented pills + chip polish + no-TMDB state
- Segmented controls (Kind/Sort) now have a highlight 'thumb' that springs
  between options (JS measures the active button → CSS var slide); repositions
  on click, page-show, and resize.
- Chips: brighter active gradient with a glow ring + subtle lift, smoother hover.
- No-TMDB empty state: genres are a static TMDB endpoint, so when they come back
  empty the page shows a 'Discover needs TMDB' card instead of a bare shell.
- Persist 'Hide owned' across sessions (localStorage); async image decoding on
  cards.
2026-06-16 14:21:59 -07:00
BoulderBadgeDad
8043db0ad4 Discover: redesigned Browse panel + more data per rail
Browse panel (was boring native dropdowns):
- Kind + Sort are now segmented pill controls; Genre + Era are horizontally
  scrollable, edge-faded chip rows with an accent-glow active state; primary
  'Browse all →' CTA. Genre chips rebuild when kind flips. Selection lives in
  state.sel (no <select> reads).

More data per rail (so 'Hide owned' doesn't gut a shelf):
- /discover/list gains a 'pages' param (1–3): fetches that many consecutive
  TMDB pages and concatenates them deduped in one response. Rails request
  pages=2 (~40 items); trending is a fixed list so extra pages are skipped.

Cleanup: removed dead .vdsc-filterbar/.vdsc-select CSS.

Tests: +3 (discover routes registered; multi-page concat+dedup; trending
fetched once despite pages). API suite: 22 passed.
2026-06-16 14:13:27 -07:00
BoulderBadgeDad
5c9b43a175 Discover v2: per-category pagination, personalization, hide-owned + perf
Performance:
- Batched ownership: new db.library_ids_for_tmdb() resolves a whole rail in one
  query per kind. _stamp_owned (now also used by search + trending) groups by
  kind, so a full Discover page drops from ~500 connections to a couple per rail.

Function/data:
- 'See all' on every rail opens it as a paged grid (Load more); the filter bar's
  Browse routes through the same generic category grid with a back button + title.
- Personalized 'Because you like <Genre>' rails seeded from your most-owned
  genres (new db.top_owned_genres + /discover/taste endpoint).
- 'Hide owned' toggle drops in-library titles from every rail/grid (CSS class,
  instant).

Visual vibes:
- Ambient page-top color bleed that follows the current hero slide's hue.
- Rail edge-fade mask, gentle fade-in on load, per-title hue glow on card hover.

Tests: +4 (batched id map, server scoping, one-query-per-kind stamp, top genres).
Full video enrichment + database suites: 145 passed.
2026-06-16 14:02:44 -07:00
BoulderBadgeDad
a8ebc9b8f7 Video Discover page: trending hero + lazy genre/decade rails + filter grid
A browse-everything page for TMDB titles you don't own yet.

Backend:
- TMDBClient: discover() (genre/year/decade), curated() (popular / top_rated /
  now_playing / upcoming / on_the_air / airing_today), genres(); shared
  _disc_map() that carries backdrop + overview. trending() now routes through it.
- Engine: discover_curated / discover_filter / genre_list (cached + owned-
  annotated via library_id_for_tmdb) + _stamp_owned helper.
- api/video/discover.py: /discover/hero, /discover/genres, /discover/list
  (curated key | filtered kind+genre+year+decade+sort, paged).

Frontend (video-discover.js + .vdsc-* CSS + subpage markup):
- Cross-fading trending hero slideshow (per-title hue, dots, hover-pause,
  reduced-motion safe, click -> detail).
- Deep stack of Netflix-style rails (curated + every movie genre + decades),
  each lazy-loaded on scroll via IntersectionObserver, arrow scrollers,
  reusing the search card (owned 'In Library' / 'Preview' ribbon + get button).
- Filter bar (kind / genre / decade / sort) flips into a paged 'Load more' grid.
- Cards open detail through the shared soulsync:video-open-detail event.
2026-06-16 13:35:31 -07:00