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.
This commit is contained in:
parent
e7b1a239b4
commit
28fe3d2b0b
3 changed files with 56 additions and 16 deletions
|
|
@ -176,6 +176,29 @@ def register_routes(bp):
|
||||||
tr = None
|
tr = None
|
||||||
return jsonify({"trailer": tr or None})
|
return jsonify({"trailer": tr or None})
|
||||||
|
|
||||||
|
@bp.route("/discover/languages", methods=["GET", "POST"])
|
||||||
|
def video_discover_languages():
|
||||||
|
"""Get/set the preferred original-languages for general rails (ISO-639-1 codes).
|
||||||
|
POST {languages: ['en','ko']} (or 'en,ko'); GET returns the current list."""
|
||||||
|
from . import get_video_db
|
||||||
|
db = get_video_db()
|
||||||
|
try:
|
||||||
|
if request.method == "POST":
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
langs = body.get("languages")
|
||||||
|
if isinstance(langs, list):
|
||||||
|
val = ",".join(str(c).strip().lower() for c in langs if str(c).strip())
|
||||||
|
else:
|
||||||
|
val = ",".join(c.strip().lower() for c in str(langs or "").split(",") if c.strip())
|
||||||
|
db.set_setting("discover_languages", val or "en")
|
||||||
|
return jsonify({"success": True,
|
||||||
|
"languages": [c for c in (val or "en").split(",") if c]})
|
||||||
|
raw = db.get_setting("discover_languages", "en") or "en"
|
||||||
|
return jsonify({"languages": [c.strip() for c in raw.split(",") if c.strip()]})
|
||||||
|
except Exception:
|
||||||
|
logger.exception("discover languages get/set failed")
|
||||||
|
return jsonify({"languages": ["en"]})
|
||||||
|
|
||||||
@bp.route("/discover/genres", methods=["GET"])
|
@bp.route("/discover/genres", methods=["GET"])
|
||||||
def video_discover_genres():
|
def video_discover_genres():
|
||||||
"""Genre id→name maps for both kinds (powers the genre rails + filter)."""
|
"""Genre id→name maps for both kinds (powers the genre rails + filter)."""
|
||||||
|
|
@ -211,8 +234,19 @@ def register_routes(bp):
|
||||||
decade = request.args.get("decade") or None
|
decade = request.args.get("decade") or None
|
||||||
providers = request.args.get("providers") or None
|
providers = request.args.get("providers") or None
|
||||||
sort = request.args.get("sort") or "popularity.desc"
|
sort = request.args.get("sort") or "popularity.desc"
|
||||||
lang = (request.args.get("lang") or "").strip() or None # original-language filter
|
lang = (request.args.get("lang") or "").strip() or None # explicit (foreign rail)
|
||||||
hide_owned = (request.args.get("hide_owned") or "") in ("1", "true", "yes")
|
hide_owned = (request.args.get("hide_owned") or "") in ("1", "true", "yes")
|
||||||
|
# Preferred original-languages (multi) for GENERAL/curated rails — so the feeds
|
||||||
|
# aren't flooded with foreign titles (e.g. Bollywood in Popular/Trending). A rail
|
||||||
|
# with an explicit `lang` (a dedicated foreign rail) bypasses this. Default 'en'.
|
||||||
|
prefer_langs = None
|
||||||
|
if not lang:
|
||||||
|
try:
|
||||||
|
from . import get_video_db
|
||||||
|
raw = get_video_db().get_setting("discover_languages", "en") or "en"
|
||||||
|
prefer_langs = {c.strip().lower() for c in raw.split(",") if c.strip()} or None
|
||||||
|
except Exception:
|
||||||
|
prefer_langs = {"en"}
|
||||||
|
|
||||||
def fetch(p):
|
def fetch(p):
|
||||||
if key == "trending":
|
if key == "trending":
|
||||||
|
|
@ -224,11 +258,11 @@ def register_routes(bp):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
items, seen = [], set()
|
items, seen = [], set()
|
||||||
# Hiding owned + a huge library means most popular titles are already owned, so
|
# When filtering (hide-owned or language), page DEEPER and drop items server-side
|
||||||
# page DEEPER and drop owned server-side until the rail has enough un-owned to
|
# until the rail has ~enough to look full — instead of returning a half-empty rail.
|
||||||
# look full (instead of the client CSS-hiding most of a 2-page batch to nothing).
|
need_fill = hide_owned or bool(prefer_langs)
|
||||||
target = 24 if hide_owned else 0
|
target = 24 if need_fill else 0
|
||||||
max_pages = 8 if hide_owned else pages
|
max_pages = 8 if need_fill else pages
|
||||||
for offset in range(max_pages):
|
for offset in range(max_pages):
|
||||||
batch = fetch(page + offset) or []
|
batch = fetch(page + offset) or []
|
||||||
for it in batch:
|
for it in batch:
|
||||||
|
|
@ -238,13 +272,17 @@ def register_routes(bp):
|
||||||
seen.add(dk)
|
seen.add(dk)
|
||||||
if hide_owned and it.get("library_id") is not None:
|
if hide_owned and it.get("library_id") is not None:
|
||||||
continue
|
continue
|
||||||
|
if prefer_langs:
|
||||||
|
ol = (it.get("original_language") or "").lower()
|
||||||
|
if ol and ol not in prefer_langs:
|
||||||
|
continue # known foreign language not in your preference
|
||||||
items.append(it)
|
items.append(it)
|
||||||
if key == "trending" or not batch:
|
if key == "trending" or not batch:
|
||||||
break # trending is a fixed list; empty batch = TMDB ran out
|
break # trending is a fixed list; empty batch = TMDB ran out
|
||||||
if target and len(items) >= target:
|
if target and len(items) >= target:
|
||||||
break # enough un-owned collected
|
break # enough collected
|
||||||
if not hide_owned and offset + 1 >= pages:
|
if not target and offset + 1 >= pages:
|
||||||
break # normal mode: respect the requested page count
|
break # no filtering: respect the requested page count
|
||||||
return jsonify({"items": items, "page": page})
|
return jsonify({"items": items, "page": page})
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("discover list failed (key=%s)", key)
|
logger.exception("discover list failed (key=%s)", key)
|
||||||
|
|
|
||||||
|
|
@ -482,6 +482,8 @@ class TMDBClient:
|
||||||
"year": ((it.get("release_date") if is_movie else it.get("first_air_date")) or "")[:4] or None,
|
"year": ((it.get("release_date") if is_movie else it.get("first_air_date")) or "")[:4] or None,
|
||||||
"rating": it.get("vote_average") or None,
|
"rating": it.get("vote_average") or None,
|
||||||
"overview": it.get("overview") or None,
|
"overview": it.get("overview") or None,
|
||||||
|
"original_language": it.get("original_language") or None, # for the language filter
|
||||||
|
"popularity": it.get("popularity") or None, # for blended ranking
|
||||||
"poster": (self.POSTER_W + it["poster_path"]) if it.get("poster_path") else None,
|
"poster": (self.POSTER_W + it["poster_path"]) if it.get("poster_path") else None,
|
||||||
"backdrop": (self.BACKDROP_W + it["backdrop_path"]) if it.get("backdrop_path") else None,
|
"backdrop": (self.BACKDROP_W + it["backdrop_path"]) if it.get("backdrop_path") else None,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -72,10 +72,10 @@
|
||||||
'talk': '148, 163, 184', 'news': '148, 163, 184',
|
'talk': '148, 163, 184', 'news': '148, 163, 184',
|
||||||
};
|
};
|
||||||
var DECADE_RAILS = [
|
var DECADE_RAILS = [
|
||||||
{ title: 'Best of the 2010s', q: 'kind=movie&decade=2010&sort=vote_average.desc&lang=en' },
|
{ 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&lang=en' },
|
{ title: '2000s Favorites', q: 'kind=movie&decade=2000&sort=vote_average.desc' },
|
||||||
{ title: '’90s Classics', q: 'kind=movie&decade=1990&sort=vote_average.desc&lang=en' },
|
{ title: '’90s Classics', q: 'kind=movie&decade=1990&sort=vote_average.desc' },
|
||||||
{ title: 'Retro ’80s', q: 'kind=movie&decade=1980&sort=vote_average.desc&lang=en' },
|
{ title: 'Retro ’80s', q: 'kind=movie&decade=1980&sort=vote_average.desc' },
|
||||||
];
|
];
|
||||||
// Dedicated foreign-language rails so non-English titles live HERE rather than
|
// 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).
|
// leaking into the general genre/decade rails (which are pinned to lang=en).
|
||||||
|
|
@ -93,17 +93,17 @@
|
||||||
// personalized first — seeded from what you actually own
|
// personalized first — seeded from what you actually own
|
||||||
(state.taste.movie || []).slice(0, 3).forEach(function (name) {
|
(state.taste.movie || []).slice(0, 3).forEach(function (name) {
|
||||||
var id = gm[name.toLowerCase()];
|
var id = gm[name.toLowerCase()];
|
||||||
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; }
|
if (id != null) { out.push({ title: 'Because you like ' + name, q: 'kind=movie&genre=' + id + '&sort=popularity.desc' }); used['m:' + name.toLowerCase()] = 1; }
|
||||||
});
|
});
|
||||||
(state.taste.show || []).slice(0, 2).forEach(function (name) {
|
(state.taste.show || []).slice(0, 2).forEach(function (name) {
|
||||||
var id = gs[name.toLowerCase()];
|
var id = gs[name.toLowerCase()];
|
||||||
if (id != null) { out.push({ title: 'More ' + name + ' shows', q: 'kind=show&genre=' + id + '&sort=popularity.desc&lang=en' }); }
|
if (id != null) { out.push({ title: 'More ' + name + ' shows', q: 'kind=show&genre=' + id + '&sort=popularity.desc' }); }
|
||||||
});
|
});
|
||||||
out = out.concat(CURATED);
|
out = out.concat(CURATED);
|
||||||
GENRE_RAILS.forEach(function (name) {
|
GENRE_RAILS.forEach(function (name) {
|
||||||
var id = gm[name.toLowerCase()];
|
var id = gm[name.toLowerCase()];
|
||||||
if (id != null && !used['m:' + name.toLowerCase()])
|
if (id != null && !used['m:' + name.toLowerCase()])
|
||||||
out.push({ title: name, q: 'kind=movie&genre=' + id + '&sort=popularity.desc&lang=en' });
|
out.push({ title: name, q: 'kind=movie&genre=' + id + '&sort=popularity.desc' });
|
||||||
});
|
});
|
||||||
return out.concat(DECADE_RAILS).concat(FOREIGN_RAILS);
|
return out.concat(DECADE_RAILS).concat(FOREIGN_RAILS);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue