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")
|
logger.exception("discover languages get/set failed")
|
||||||
return jsonify({"languages": ["en"]})
|
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"])
|
@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)."""
|
||||||
|
|
@ -257,6 +279,8 @@ def register_routes(bp):
|
||||||
year = request.args.get("year") or None
|
year = request.args.get("year") or None
|
||||||
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
|
||||||
|
if providers and "," in providers:
|
||||||
|
providers = providers.replace(",", "|") # TMDB with_watch_providers OR-join
|
||||||
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 # explicit (foreign rail)
|
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")
|
||||||
|
|
|
||||||
|
|
@ -1063,6 +1063,17 @@
|
||||||
<button class="vdsc-lang" type="button" data-lang="de">DE</button>
|
<button class="vdsc-lang" type="button" data-lang="de">DE</button>
|
||||||
<button class="vdsc-lang" type="button" data-lang="it">IT</button>
|
<button class="vdsc-lang" type="button" data-lang="it">IT</button>
|
||||||
</div>
|
</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>
|
||||||
<div class="vdsc-chips" data-vdsc-chipset="genre" aria-label="Genre">
|
<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>
|
<button class="vdsc-chip vdsc-chip--on" type="button" data-val="">All genres</button>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
var state = {
|
var state = {
|
||||||
loaded: false, wired: false, mode: 'shelves',
|
loaded: false, wired: false, mode: 'shelves',
|
||||||
genres: { movie: [], show: [] }, taste: { movie: [], show: [] },
|
genres: { movie: [], show: [] }, taste: { movie: [], show: [] },
|
||||||
|
myProviders: [], // saved streaming services -> 'On your streaming services' rail
|
||||||
io: null,
|
io: null,
|
||||||
hero: { items: [], idx: 0, timer: null },
|
hero: { items: [], idx: 0, timer: null },
|
||||||
cat: { title: '', q: '', page: 1, paginates: true, busy: false, hasMore: false },
|
cat: { title: '', q: '', page: 1, paginates: true, busy: false, hasMore: false },
|
||||||
|
|
@ -105,6 +106,11 @@
|
||||||
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' }); }
|
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);
|
out = out.concat(CURATED);
|
||||||
GENRE_RAILS.forEach(function (name) {
|
GENRE_RAILS.forEach(function (name) {
|
||||||
var id = gm[name.toLowerCase()];
|
var id = gm[name.toLowerCase()];
|
||||||
|
|
@ -701,6 +707,31 @@
|
||||||
}).then(function () { reloadRails(); }).catch(function () { /* ignore */ });
|
}).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.
|
// Infinite scroll: a sentinel near the grid bottom pulls the next page.
|
||||||
var sentinel = $('[data-vdsc-sentinel]');
|
var sentinel = $('[data-vdsc-sentinel]');
|
||||||
if (sentinel && AUTO) {
|
if (sentinel && AUTO) {
|
||||||
|
|
@ -726,11 +757,13 @@
|
||||||
function loadMeta() {
|
function loadMeta() {
|
||||||
var jget = function (u) { return fetch(u, { headers: { Accept: 'application/json' } })
|
var jget = function (u) { return fetch(u, { headers: { Accept: 'application/json' } })
|
||||||
.then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); };
|
.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) {
|
.then(function (res) {
|
||||||
var g = res[0] || {}, t = res[1] || {};
|
var g = res[0] || {}, t = res[1] || {};
|
||||||
state.genres = { movie: g.movie || [], show: g.show || [] };
|
state.genres = { movie: g.movie || [], show: g.show || [] };
|
||||||
state.taste = { movie: t.movie || [], show: t.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.
|
// Genres are a static TMDB endpoint — empty means TMDB isn't set up.
|
||||||
if (!state.genres.movie.length && !state.genres.show.length) { showEmpty(); return; }
|
if (!state.genres.movie.length && !state.genres.show.length) { showEmpty(); return; }
|
||||||
renderGenreChips();
|
renderGenreChips();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue