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).
This commit is contained in:
BoulderBadgeDad 2026-06-24 08:00:13 -07:00
parent e7552d3c62
commit 877a86c9b0
4 changed files with 31 additions and 16 deletions

View file

@ -322,7 +322,11 @@ def register_routes(bp):
if key == "trending":
return eng.trending()
if key == "trending_today":
return eng.trending(window="day") # the ranked 'Top 10 today' chart
return eng.trending(window="day") # the ranked 'Top 10 today' chart (mixed)
if key == "trending_movies_today":
return eng.trending(window="day", kind="movie") # split 'Top 10 Movies Today'
if key == "trending_tv_today":
return eng.trending(window="day", kind="show") # split 'Top 10 TV Shows Today'
if key:
return eng.discover_curated(key, page=p)
return eng.discover_filter(
@ -360,7 +364,7 @@ def register_routes(bp):
# thread-safe) so digging 20 pages deep costs ~4 page-latencies, not 20.
need_fill = hide_owned or bool(prefer_langs)
target = 20 if need_fill else 0
if key in ("trending", "trending_today"):
if key in ("trending", "trending_today", "trending_movies_today", "trending_tv_today"):
consume(fetch(page)) # a single fixed chart; never paged
elif not need_fill:
for offset in range(pages): # no filtering: respect requested page count

View file

@ -447,16 +447,22 @@ class TMDBClient:
"poster": (self.PROFILE + it["profile_path"]) if it.get("profile_path") else None})
return out
def trending(self, window="week"):
"""Trending movies + shows this week — fills the search page when idle and
the Discover hero slideshow (hence backdrops via _disc_map)."""
def trending(self, window="week", kind=None):
"""Trending titles. ``kind`` None = mixed movies + shows (/trending/all — the
search-idle filler + Discover hero slideshow, hence backdrops via _disc_map).
``kind`` 'movie' / 'show' hit the dedicated single-type charts (/trending/movie,
/trending/tv) that power the split 'Top 10 Movies / TV Shows Today' rails.
Single-type endpoints omit media_type, so the kind is forced into _disc_map."""
if not self.api_key:
return []
import requests
r = requests.get(self.BASE + "/trending/all/" + window,
params={"api_key": self.api_key}, timeout=15)
path = ("/trending/movie/" if kind == "movie"
else "/trending/tv/" if kind == "show"
else "/trending/all/") + window
r = requests.get(self.BASE + path, params={"api_key": self.api_key}, timeout=15)
r.raise_for_status()
return self._disc_map((r.json() or {}).get("results"), None)[:20]
forced = kind if kind in ("movie", "show") else None
return self._disc_map((r.json() or {}).get("results"), forced)[:20]
# ── discover (browse TMDB by curated list / genre / year / decade) ────────
def _disc_map(self, results, kind):

View file

@ -310,19 +310,21 @@ class VideoEnrichmentEngine:
return []
return self._stamp_owned(results)
def trending(self, window="week") -> list:
def trending(self, window="week", kind=None) -> list:
"""Trending titles, annotated owned/not. ``window='day'`` is the real-time
daily chart (the iconic 'Top 10 today' row); 'week' is the steadier hero/idle
feed. Order is preserved, so the caller can render rank numbers."""
daily chart (the iconic 'Top 10 today' rows); 'week' is the steadier hero/idle
feed. ``kind`` None = mixed; 'movie'/'show' = the dedicated single-type charts
(split 'Top 10 Movies / TV Shows Today'). Order is preserved for rank numbers."""
w = self.workers.get("tmdb")
if not w or not w.enabled:
return []
window = "day" if window == "day" else "week"
ck = ("trending", window)
kind = kind if kind in ("movie", "show") else None
ck = ("trending", window, kind)
cached = self._cache_get(ck)
if cached is None:
try:
cached = w.client.trending(window=window) or []
cached = w.client.trending(window=window, kind=kind) or []
self._cache_put(ck, cached, ttl=3600)
except Exception:
logger.exception("video trending failed")

View file

@ -49,8 +49,11 @@
}
// ── the rail stack (personalized + curated + genre + decade) ──────────────
// The iconic ranked "Top 10 today" row — daily TMDB trending, rendered with rank numbers.
var TOP10 = { title: 'Top 10 Today', q: 'key=trending_today&lang=any', ranked: true };
// The iconic ranked "Top 10 today" rows — split Movies / TV like Netflix, so TV
// isn't drowned out by the movie-heavy mixed chart. Daily TMDB trending, rank numbers.
// (Titles are terse because the group header already says "Top 10 today".)
var TOP10_MOVIES = { title: 'Movies', q: 'key=trending_movies_today&lang=any', ranked: true };
var TOP10_TV = { title: 'TV Shows', q: 'key=trending_tv_today&lang=any', ranked: true };
var CURATED = [
{ title: 'Trending This Week', q: 'key=trending' },
{ title: 'Popular Movies', q: 'key=popular_movies' },
@ -155,7 +158,7 @@
// keep those group ids. Order of this array IS the on-screen order.
return [
{ id: 'foryou', label: 'For you', rails: foryou },
{ id: 'topten', label: 'Top 10 today', rails: [TOP10] },
{ id: 'topten', label: 'Top 10 today', rails: [TOP10_MOVIES, TOP10_TV] },
{ id: 'new', label: 'New & noteworthy', rails: NEW_RAILS },
{ id: 'collection', label: 'Finish your collection', rails: [] },
{ id: 'taste', label: 'More of what you like', rails: taste },