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.
This commit is contained in:
parent
6734b6ab22
commit
5906576a13
3 changed files with 69 additions and 1 deletions
|
|
@ -223,6 +223,28 @@ def register_routes(bp):
|
|||
logger.exception("discover languages get/set failed")
|
||||
return jsonify({"languages": ["en"]})
|
||||
|
||||
@bp.route("/discover/providers-pref", methods=["GET", "POST"])
|
||||
def video_discover_providers_pref():
|
||||
"""The user's subscribed streaming services (TMDB provider ids) — drives the
|
||||
'On your streaming services' rail. POST {providers:[8,9]} (or '8,9'); GET returns them."""
|
||||
from . import get_video_db
|
||||
db = get_video_db()
|
||||
try:
|
||||
if request.method == "POST":
|
||||
body = request.get_json(silent=True) or {}
|
||||
p = body.get("providers")
|
||||
if isinstance(p, list):
|
||||
val = ",".join(str(c).strip() for c in p if str(c).strip())
|
||||
else:
|
||||
val = ",".join(c.strip() for c in str(p or "").split(",") if c.strip())
|
||||
db.set_setting("discover_providers", val)
|
||||
return jsonify({"success": True, "providers": [c for c in val.split(",") if c]})
|
||||
raw = db.get_setting("discover_providers", "") or ""
|
||||
return jsonify({"providers": [c.strip() for c in raw.split(",") if c.strip()]})
|
||||
except Exception:
|
||||
logger.exception("discover providers-pref failed")
|
||||
return jsonify({"providers": []})
|
||||
|
||||
@bp.route("/discover/genres", methods=["GET"])
|
||||
def video_discover_genres():
|
||||
"""Genre id→name maps for both kinds (powers the genre rails + filter)."""
|
||||
|
|
@ -257,6 +279,8 @@ def register_routes(bp):
|
|||
year = request.args.get("year") or None
|
||||
decade = request.args.get("decade") or None
|
||||
providers = request.args.get("providers") or None
|
||||
if providers and "," in providers:
|
||||
providers = providers.replace(",", "|") # TMDB with_watch_providers OR-join
|
||||
sort = request.args.get("sort") or "popularity.desc"
|
||||
lang = (request.args.get("lang") or "").strip() or None # explicit (foreign rail)
|
||||
hide_owned = (request.args.get("hide_owned") or "") in ("1", "true", "yes")
|
||||
|
|
|
|||
|
|
@ -1063,6 +1063,17 @@
|
|||
<button class="vdsc-lang" type="button" data-lang="de">DE</button>
|
||||
<button class="vdsc-lang" type="button" data-lang="it">IT</button>
|
||||
</div>
|
||||
<div class="vdsc-langs" data-vdsc-myprov aria-label="My streaming services" title="Your streaming services power the 'On your streaming services' rail">
|
||||
<span class="vdsc-langs-label">📺</span>
|
||||
<button class="vdsc-lang" type="button" data-prov="8">Netflix</button>
|
||||
<button class="vdsc-lang" type="button" data-prov="9">Prime</button>
|
||||
<button class="vdsc-lang" type="button" data-prov="337">Disney+</button>
|
||||
<button class="vdsc-lang" type="button" data-prov="1899">Max</button>
|
||||
<button class="vdsc-lang" type="button" data-prov="350">Apple TV+</button>
|
||||
<button class="vdsc-lang" type="button" data-prov="15">Hulu</button>
|
||||
<button class="vdsc-lang" type="button" data-prov="531">Paramount+</button>
|
||||
<button class="vdsc-lang" type="button" data-prov="386">Peacock</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vdsc-chips" data-vdsc-chipset="genre" aria-label="Genre">
|
||||
<button class="vdsc-chip vdsc-chip--on" type="button" data-val="">All genres</button>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
var state = {
|
||||
loaded: false, wired: false, mode: 'shelves',
|
||||
genres: { movie: [], show: [] }, taste: { movie: [], show: [] },
|
||||
myProviders: [], // saved streaming services -> 'On your streaming services' rail
|
||||
io: null,
|
||||
hero: { items: [], idx: 0, timer: null },
|
||||
cat: { title: '', q: '', page: 1, paginates: true, busy: false, hasMore: false },
|
||||
|
|
@ -105,6 +106,11 @@
|
|||
var id = gs[name.toLowerCase()];
|
||||
if (id != null) { out.push({ title: 'More ' + name + ' shows', q: 'kind=show&genre=' + id + '&sort=popularity.desc' }); }
|
||||
});
|
||||
// 'On your streaming services' — only when the user has set their subscriptions.
|
||||
if (state.myProviders && state.myProviders.length) {
|
||||
out.push({ title: 'On your streaming services',
|
||||
q: 'kind=movie&providers=' + state.myProviders.join(',') + '&sort=popularity.desc' });
|
||||
}
|
||||
out = out.concat(CURATED);
|
||||
GENRE_RAILS.forEach(function (name) {
|
||||
var id = gm[name.toLowerCase()];
|
||||
|
|
@ -701,6 +707,31 @@
|
|||
}).then(function () { reloadRails(); }).catch(function () { /* ignore */ });
|
||||
});
|
||||
}
|
||||
// 'My streaming services' preference — multi-select; drives the 'On your services' rail.
|
||||
var provWrap = $('[data-vdsc-myprov]');
|
||||
if (provWrap && !provWrap._wired) {
|
||||
provWrap._wired = 1;
|
||||
fetch('/api/video/discover/providers-pref', { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
var on = {}; ((d && d.providers) || []).forEach(function (c) { on[String(c)] = 1; });
|
||||
state.myProviders = (d && d.providers) || [];
|
||||
provWrap.querySelectorAll('.vdsc-lang').forEach(function (b) {
|
||||
b.classList.toggle('vdsc-lang--on', !!on[b.getAttribute('data-prov')]);
|
||||
});
|
||||
}).catch(function () { /* chips stay off */ });
|
||||
provWrap.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.vdsc-lang'); if (!btn) return;
|
||||
btn.classList.toggle('vdsc-lang--on');
|
||||
var provs = Array.prototype.map.call(provWrap.querySelectorAll('.vdsc-lang--on'),
|
||||
function (b) { return b.getAttribute('data-prov'); });
|
||||
state.myProviders = provs;
|
||||
fetch('/api/video/discover/providers-pref', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ providers: provs }),
|
||||
}).then(function () { reloadRails(); }).catch(function () { /* ignore */ });
|
||||
});
|
||||
}
|
||||
// Infinite scroll: a sentinel near the grid bottom pulls the next page.
|
||||
var sentinel = $('[data-vdsc-sentinel]');
|
||||
if (sentinel && AUTO) {
|
||||
|
|
@ -726,11 +757,13 @@
|
|||
function loadMeta() {
|
||||
var jget = function (u) { return fetch(u, { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); };
|
||||
Promise.all([jget('/api/video/discover/genres'), jget('/api/video/discover/taste')])
|
||||
Promise.all([jget('/api/video/discover/genres'), jget('/api/video/discover/taste'),
|
||||
jget('/api/video/discover/providers-pref')])
|
||||
.then(function (res) {
|
||||
var g = res[0] || {}, t = res[1] || {};
|
||||
state.genres = { movie: g.movie || [], show: g.show || [] };
|
||||
state.taste = { movie: t.movie || [], show: t.show || [] };
|
||||
state.myProviders = (res[2] && res[2].providers) || [];
|
||||
// Genres are a static TMDB endpoint — empty means TMDB isn't set up.
|
||||
if (!state.genres.movie.length && !state.genres.show.length) { showEmpty(); return; }
|
||||
renderGenreChips();
|
||||
|
|
|
|||
Loading…
Reference in a new issue