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.
This commit is contained in:
parent
a68d9755a9
commit
d984895cba
4 changed files with 60 additions and 22 deletions
|
|
@ -186,6 +186,8 @@ def register_routes(bp):
|
|||
decade = request.args.get("decade") or None
|
||||
providers = request.args.get("providers") or None
|
||||
sort = request.args.get("sort") or "popularity.desc"
|
||||
lang = (request.args.get("lang") or "").strip() or None # original-language filter
|
||||
hide_owned = (request.args.get("hide_owned") or "") in ("1", "true", "yes")
|
||||
|
||||
def fetch(p):
|
||||
if key == "trending":
|
||||
|
|
@ -193,19 +195,31 @@ def register_routes(bp):
|
|||
if key:
|
||||
return eng.discover_curated(key, page=p)
|
||||
return eng.discover_filter(kind, genre=genre, year=year, decade=decade,
|
||||
providers=providers, sort_by=sort, page=p)
|
||||
providers=providers, sort_by=sort, page=p, language=lang)
|
||||
|
||||
try:
|
||||
items, seen = [], set()
|
||||
for p in range(page, page + pages):
|
||||
for it in (fetch(p) or []):
|
||||
# Hiding owned + a huge library means most popular titles are already owned, so
|
||||
# page DEEPER and drop owned server-side until the rail has enough un-owned to
|
||||
# look full (instead of the client CSS-hiding most of a 2-page batch to nothing).
|
||||
target = 24 if hide_owned else 0
|
||||
max_pages = 8 if hide_owned else pages
|
||||
for offset in range(max_pages):
|
||||
batch = fetch(page + offset) or []
|
||||
for it in batch:
|
||||
dk = (it.get("kind"), it.get("tmdb_id"))
|
||||
if dk in seen:
|
||||
continue
|
||||
seen.add(dk)
|
||||
if hide_owned and it.get("library_id") is not None:
|
||||
continue
|
||||
items.append(it)
|
||||
if key == "trending":
|
||||
break # trending is a fixed list — extra pages just repeat it
|
||||
if key == "trending" or not batch:
|
||||
break # trending is a fixed list; empty batch = TMDB ran out
|
||||
if target and len(items) >= target:
|
||||
break # enough un-owned collected
|
||||
if not hide_owned and offset + 1 >= pages:
|
||||
break # normal mode: respect the requested page count
|
||||
return jsonify({"items": items, "page": page})
|
||||
except Exception:
|
||||
logger.exception("discover list failed (key=%s)", key)
|
||||
|
|
|
|||
|
|
@ -512,9 +512,11 @@ class TMDBClient:
|
|||
return self._disc_map((r.json() or {}).get("results"), kind)
|
||||
|
||||
def discover(self, kind, *, genre=None, year=None, decade=None, providers=None,
|
||||
sort_by="popularity.desc", page=1, region="US"):
|
||||
sort_by="popularity.desc", page=1, region="US", language=None):
|
||||
"""Browse /discover/{movie,tv} filtered by genre / year / decade / streaming
|
||||
provider (``providers`` is a TMDB provider id; needs ``region``)."""
|
||||
provider (``providers`` is a TMDB provider id; needs ``region``). ``language``
|
||||
restricts to an original-language (ISO-639-1, e.g. 'en') so general rails aren't
|
||||
flooded with foreign-language titles; pass a non-English code for a foreign rail."""
|
||||
if not self.api_key:
|
||||
return []
|
||||
import requests
|
||||
|
|
@ -522,6 +524,8 @@ class TMDBClient:
|
|||
path = "/discover/movie" if is_movie else "/discover/tv"
|
||||
params = {"api_key": self.api_key, "sort_by": sort_by, "page": page,
|
||||
"include_adult": "false", "vote_count.gte": 40}
|
||||
if language:
|
||||
params["with_original_language"] = language
|
||||
if genre:
|
||||
params["with_genres"] = genre
|
||||
if providers:
|
||||
|
|
|
|||
|
|
@ -363,21 +363,21 @@ class VideoEnrichmentEngine:
|
|||
return self._stamp_owned(items)
|
||||
|
||||
def discover_filter(self, kind, *, genre=None, year=None, decade=None, providers=None,
|
||||
sort_by="popularity.desc", page=1) -> list:
|
||||
"""Browse /discover filtered by genre / year / decade / streaming provider,
|
||||
cached + owned-annotated."""
|
||||
sort_by="popularity.desc", page=1, language=None) -> list:
|
||||
"""Browse /discover filtered by genre / year / decade / streaming provider /
|
||||
original language, cached + owned-annotated."""
|
||||
w = self.workers.get("tmdb")
|
||||
if not w or not w.enabled:
|
||||
return []
|
||||
if kind not in ("movie", "show"):
|
||||
kind = "movie"
|
||||
ck = ("disc-flt", kind, genre, year, decade, providers, sort_by, page)
|
||||
ck = ("disc-flt", kind, genre, year, decade, providers, sort_by, page, language)
|
||||
items = self._cache_get(ck)
|
||||
if items is None:
|
||||
try:
|
||||
items = w.client.discover(kind, genre=genre, year=year, decade=decade,
|
||||
providers=providers, sort_by=sort_by, page=page,
|
||||
region=self._region()) or []
|
||||
region=self._region(), language=language) or []
|
||||
self._cache_put(ck, items, ttl=3600)
|
||||
except Exception:
|
||||
logger.exception("discover filter failed (%s g=%s y=%s d=%s p=%s)",
|
||||
|
|
|
|||
|
|
@ -72,10 +72,19 @@
|
|||
'talk': '148, 163, 184', 'news': '148, 163, 184',
|
||||
};
|
||||
var DECADE_RAILS = [
|
||||
{ title: 'Best of the 2010s', q: 'kind=movie&decade=2010&sort=vote_average.desc' },
|
||||
{ title: '2000s Favorites', q: 'kind=movie&decade=2000&sort=vote_average.desc' },
|
||||
{ title: '’90s Classics', q: 'kind=movie&decade=1990&sort=vote_average.desc' },
|
||||
{ title: 'Retro ’80s', q: 'kind=movie&decade=1980&sort=vote_average.desc' },
|
||||
{ title: 'Best of the 2010s', q: 'kind=movie&decade=2010&sort=vote_average.desc&lang=en' },
|
||||
{ title: '2000s Favorites', q: 'kind=movie&decade=2000&sort=vote_average.desc&lang=en' },
|
||||
{ title: '’90s Classics', q: 'kind=movie&decade=1990&sort=vote_average.desc&lang=en' },
|
||||
{ title: 'Retro ’80s', q: 'kind=movie&decade=1980&sort=vote_average.desc&lang=en' },
|
||||
];
|
||||
// Dedicated foreign-language rails so non-English titles live HERE rather than
|
||||
// leaking into the general genre/decade rails (which are pinned to lang=en).
|
||||
var FOREIGN_RAILS = [
|
||||
{ title: 'Korean Cinema', q: 'kind=movie&sort=popularity.desc&lang=ko' },
|
||||
{ title: 'Japanese Films', q: 'kind=movie&sort=popularity.desc&lang=ja' },
|
||||
{ title: 'Spanish-Language', q: 'kind=movie&sort=popularity.desc&lang=es' },
|
||||
{ title: 'French Cinema', q: 'kind=movie&sort=popularity.desc&lang=fr' },
|
||||
{ title: 'Hindi Cinema', q: 'kind=movie&sort=popularity.desc&lang=hi' },
|
||||
];
|
||||
|
||||
function buildShelfList() {
|
||||
|
|
@ -84,19 +93,19 @@
|
|||
// personalized first — seeded from what you actually own
|
||||
(state.taste.movie || []).slice(0, 3).forEach(function (name) {
|
||||
var id = gm[name.toLowerCase()];
|
||||
if (id != null) { out.push({ title: 'Because you like ' + name, q: 'kind=movie&genre=' + id + '&sort=popularity.desc' }); used['m:' + name.toLowerCase()] = 1; }
|
||||
if (id != null) { out.push({ title: 'Because you like ' + name, q: 'kind=movie&genre=' + id + '&sort=popularity.desc&lang=en' }); used['m:' + name.toLowerCase()] = 1; }
|
||||
});
|
||||
(state.taste.show || []).slice(0, 2).forEach(function (name) {
|
||||
var id = gs[name.toLowerCase()];
|
||||
if (id != null) { out.push({ title: 'More ' + name + ' shows', q: 'kind=show&genre=' + id + '&sort=popularity.desc' }); }
|
||||
if (id != null) { out.push({ title: 'More ' + name + ' shows', q: 'kind=show&genre=' + id + '&sort=popularity.desc&lang=en' }); }
|
||||
});
|
||||
out = out.concat(CURATED);
|
||||
GENRE_RAILS.forEach(function (name) {
|
||||
var id = gm[name.toLowerCase()];
|
||||
if (id != null && !used['m:' + name.toLowerCase()])
|
||||
out.push({ title: name, q: 'kind=movie&genre=' + id + '&sort=popularity.desc' });
|
||||
out.push({ title: name, q: 'kind=movie&genre=' + id + '&sort=popularity.desc&lang=en' });
|
||||
});
|
||||
return out.concat(DECADE_RAILS);
|
||||
return out.concat(DECADE_RAILS).concat(FOREIGN_RAILS);
|
||||
}
|
||||
|
||||
// ── card (mirrors the search title card: owned ribbon + get button) ───────
|
||||
|
|
@ -334,12 +343,18 @@
|
|||
}, { rootMargin: '400px 0px' });
|
||||
for (var j = 0; j < shelves.length; j++) state.io.observe(shelves[j]);
|
||||
}
|
||||
function isHideOwned() {
|
||||
var h = $('[data-vdsc-hideowned]');
|
||||
return !!(h && h.checked);
|
||||
}
|
||||
function fillShelf(shelf) {
|
||||
if (!shelf || shelf.getAttribute('data-vdsc-loaded')) return;
|
||||
shelf.setAttribute('data-vdsc-loaded', '1');
|
||||
var rail = $('[data-vdsc-rail]', shelf);
|
||||
// 2 pages (~40 items) so a rail still looks full after 'Hide owned'.
|
||||
cachedFetch(LIST_URL + '?' + shelf.getAttribute('data-vdsc-q') + '&pages=2')
|
||||
// Normal: 2 pages (~40 items). Hiding owned: let the backend page DEEPER and drop
|
||||
// owned server-side, so a huge library's rail still fills instead of CSS-hiding to ~nothing.
|
||||
var q = shelf.getAttribute('data-vdsc-q') + (isHideOwned() ? '&hide_owned=1' : '&pages=2');
|
||||
cachedFetch(LIST_URL + '?' + q)
|
||||
.then(function (d) {
|
||||
var items = (d && d.items) || [];
|
||||
if (!items.length) { shelf.remove(); return; } // drop empty shelves
|
||||
|
|
@ -507,6 +522,11 @@
|
|||
hide.addEventListener('change', function () {
|
||||
page.classList.toggle('vdsc-hide-owned', hide.checked);
|
||||
try { localStorage.setItem('vdsc_hideowned', hide.checked ? '1' : '0'); } catch (e) { /* ignore */ }
|
||||
// The rails are now server-filtered for owned (and page deeper), so rebuild
|
||||
// them with the new state instead of just CSS-hiding cards in place.
|
||||
renderShelves();
|
||||
loadMoreLike();
|
||||
loadGaps();
|
||||
});
|
||||
}
|
||||
// Infinite scroll: a sentinel near the grid bottom pulls the next page.
|
||||
|
|
|
|||
Loading…
Reference in a new issue