video: where-to-watch region (no longer hardcoded US)
A saved 'Where-to-watch region' picker in Settings → Detail Pages (19 common regions, default US). The engine reads it for the providers in extras + tmdb_detail (region in the cache key), and the detail page labels the section 'Where to Watch · <region>' so you know which market you're seeing.
This commit is contained in:
parent
b8de46d2ad
commit
b2adc63a6a
9 changed files with 81 additions and 18 deletions
|
|
@ -47,6 +47,7 @@ def register_routes(bp):
|
|||
"tvdb_api_key": db.get_setting("tvdb_api_key") or "",
|
||||
"omdb_api_key": db.get_setting("omdb_api_key") or "",
|
||||
"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1",
|
||||
"watch_region": (db.get_setting("watch_region") or "US").upper(),
|
||||
})
|
||||
|
||||
@bp.route("/prefs", methods=["GET"])
|
||||
|
|
@ -54,7 +55,10 @@ def register_routes(bp):
|
|||
# Lightweight UI prefs for the detail page (no API keys).
|
||||
from . import get_video_db
|
||||
db = get_video_db()
|
||||
return jsonify({"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1"})
|
||||
return jsonify({
|
||||
"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1",
|
||||
"watch_region": (db.get_setting("watch_region") or "US").upper(),
|
||||
})
|
||||
|
||||
@bp.route("/enrichment/config", methods=["POST"])
|
||||
def video_enrichment_config_save():
|
||||
|
|
@ -67,6 +71,9 @@ def register_routes(bp):
|
|||
db.set_setting("tvdb_api_key", body.get("tvdb_api_key") or "")
|
||||
if "billboard_autoplay" in body:
|
||||
db.set_setting("billboard_autoplay", "1" if body.get("billboard_autoplay") else "0")
|
||||
if "watch_region" in body:
|
||||
region = (body.get("watch_region") or "US").strip().upper()[:2] or "US"
|
||||
db.set_setting("watch_region", region)
|
||||
if "omdb_api_key" in body:
|
||||
new_key = body.get("omdb_api_key") or ""
|
||||
changed = new_key != (db.get_setting("omdb_api_key") or "")
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ class TMDBClient:
|
|||
"poster": (self.POSTER_W + it["poster_path"]) if it.get("poster_path") else None})
|
||||
return out[:20]
|
||||
|
||||
def full_detail(self, kind, tmdb_id):
|
||||
def full_detail(self, kind, tmdb_id, region="US"):
|
||||
"""Complete detail for a TMDB title NOT in the library — shaped like the
|
||||
library detail payload but with direct image URLs (so the same detail UI
|
||||
renders it). Seasons carry counts; episodes load lazily per season."""
|
||||
|
|
@ -503,7 +503,7 @@ class TMDBClient:
|
|||
for p in cmeta.get("cast") or []],
|
||||
"crew": [{"name": p["name"], "job": p.get("job"), "tmdb_id": p.get("tmdb_id")}
|
||||
for p in cmeta.get("crew") or []],
|
||||
"_extras": self._parse_extras(kind, dr),
|
||||
"_extras": self._parse_extras(kind, dr, region),
|
||||
}
|
||||
self._fill_collection(out["_extras"])
|
||||
if kind == "movie":
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ class VideoEnrichmentEngine:
|
|||
for w in self.workers.values():
|
||||
w.restore_paused()
|
||||
|
||||
def _region(self):
|
||||
try:
|
||||
return (self.db.get_setting("watch_region") or "US").upper()
|
||||
except Exception:
|
||||
return "US"
|
||||
|
||||
def _cache_get(self, key):
|
||||
return self._cache.get(key)
|
||||
|
||||
|
|
@ -171,11 +177,12 @@ class VideoEnrichmentEngine:
|
|||
info = (self.db.movie_match_info(item_id) if kind == "movie"
|
||||
else self.db.show_match_info(item_id))
|
||||
if info and info.get("tmdb_id"):
|
||||
key = ("extras", kind, info["tmdb_id"])
|
||||
region = self._region()
|
||||
key = ("extras", kind, info["tmdb_id"], region)
|
||||
cached = self._cache_get(key)
|
||||
if cached is None:
|
||||
try:
|
||||
cached = w.client.extras(kind, info["tmdb_id"]) or {}
|
||||
cached = w.client.extras(kind, info["tmdb_id"], region=region) or {}
|
||||
self._cache_put(key, cached)
|
||||
except Exception:
|
||||
logger.exception("item_extras failed for %s %s", kind, item_id)
|
||||
|
|
@ -304,11 +311,12 @@ class VideoEnrichmentEngine:
|
|||
lib_id = self.db.library_id_for_tmdb(kind, tmdb_id)
|
||||
if lib_id:
|
||||
return {"redirect": {"source": "library", "kind": kind, "id": lib_id}}
|
||||
cached = self._cache_get(("detail", kind, tmdb_id))
|
||||
region = self._region()
|
||||
cached = self._cache_get(("detail", kind, tmdb_id, region))
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
try:
|
||||
d = w.client.full_detail(kind, tmdb_id)
|
||||
d = w.client.full_detail(kind, tmdb_id, region=region)
|
||||
except Exception:
|
||||
logger.exception("tmdb_detail failed for %s %s", kind, tmdb_id)
|
||||
return None
|
||||
|
|
@ -336,7 +344,7 @@ class VideoEnrichmentEngine:
|
|||
d["episode_total"] = sum(s["episode_total"] for s in seasons)
|
||||
d["episode_owned"] = 0
|
||||
self._fill_tmdb_ratings(d)
|
||||
self._cache_put(("detail", kind, tmdb_id), d)
|
||||
self._cache_put(("detail", kind, tmdb_id, region), d)
|
||||
return d
|
||||
|
||||
def _fill_tmdb_ratings(self, d) -> None:
|
||||
|
|
|
|||
|
|
@ -268,14 +268,17 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
|
|||
client = app.test_client()
|
||||
try:
|
||||
assert client.get("/api/video/enrichment/config").get_json() == {
|
||||
"tmdb_api_key": "", "tvdb_api_key": "", "omdb_api_key": "", "billboard_autoplay": True}
|
||||
"tmdb_api_key": "", "tvdb_api_key": "", "omdb_api_key": "",
|
||||
"billboard_autoplay": True, "watch_region": "US"}
|
||||
client.post("/api/video/enrichment/config",
|
||||
json={"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om",
|
||||
"billboard_autoplay": False})
|
||||
"billboard_autoplay": False, "watch_region": "gb"})
|
||||
assert client.get("/api/video/enrichment/config").get_json() == {
|
||||
"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om", "billboard_autoplay": False}
|
||||
"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om",
|
||||
"billboard_autoplay": False, "watch_region": "GB"}
|
||||
assert db.get_setting("tmdb_api_key") == "abc" and db.get_setting("omdb_api_key") == "om"
|
||||
assert client.get("/api/video/prefs").get_json() == {"billboard_autoplay": False}
|
||||
assert client.get("/api/video/prefs").get_json() == {
|
||||
"billboard_autoplay": False, "watch_region": "GB"}
|
||||
finally:
|
||||
videoapi._video_db = None
|
||||
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ def test_engine_tmdb_detail_redirects_when_owned(db):
|
|||
|
||||
class Tmdb:
|
||||
enabled = True
|
||||
def full_detail(self, kind, tid): raise AssertionError("must not fetch an owned title")
|
||||
def full_detail(self, kind, tid, region="US"): raise AssertionError("must not fetch an owned title")
|
||||
eng = VideoEnrichmentEngine(db, {"tmdb": Tmdb()})
|
||||
assert eng.tmdb_detail("movie", 77) == {"redirect": {"source": "library", "kind": "movie", "id": mid}}
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ def test_engine_tmdb_detail_redirects_when_owned(db):
|
|||
def test_engine_tmdb_detail_assembles_show(db):
|
||||
class Tmdb:
|
||||
enabled = True
|
||||
def full_detail(self, kind, tid):
|
||||
def full_detail(self, kind, tid, region="US"):
|
||||
return {"kind": "show", "tmdb_id": tid, "title": "Loki", "imdb_id": None,
|
||||
"poster_url": "http://p", "backdrop_url": None, "cast": [], "crew": [],
|
||||
"_extras": {"similar": [{"title": "X", "tmdb_id": 5, "kind": "show"}]},
|
||||
|
|
@ -643,7 +643,7 @@ def test_item_extras_caches_tmdb_call(db, monkeypatch):
|
|||
calls = []
|
||||
class Tmdb:
|
||||
enabled = True
|
||||
def extras(self, kind, tid): calls.append(tid); return {"keywords": ["x"]}
|
||||
def extras(self, kind, tid, region="US"): calls.append(tid); return {"keywords": ["x"]}
|
||||
eng = VideoEnrichmentEngine(db, {"tmdb": Tmdb()})
|
||||
a = eng.item_extras("movie", mid)
|
||||
b = eng.item_extras("movie", mid)
|
||||
|
|
|
|||
|
|
@ -5205,6 +5205,30 @@
|
|||
<div class="callback-info">
|
||||
<div class="callback-help">Plays a muted trailer behind the hero after a moment (uses more bandwidth).</div>
|
||||
</div>
|
||||
<label class="vid-pref-row vid-pref-row--select">
|
||||
<span>Where-to-watch region</span>
|
||||
<select id="video-watch-region" data-video-pref="watch_region">
|
||||
<option value="US">United States</option>
|
||||
<option value="GB">United Kingdom</option>
|
||||
<option value="CA">Canada</option>
|
||||
<option value="AU">Australia</option>
|
||||
<option value="IE">Ireland</option>
|
||||
<option value="NZ">New Zealand</option>
|
||||
<option value="DE">Germany</option>
|
||||
<option value="FR">France</option>
|
||||
<option value="ES">Spain</option>
|
||||
<option value="IT">Italy</option>
|
||||
<option value="NL">Netherlands</option>
|
||||
<option value="SE">Sweden</option>
|
||||
<option value="NO">Norway</option>
|
||||
<option value="DK">Denmark</option>
|
||||
<option value="BR">Brazil</option>
|
||||
<option value="MX">Mexico</option>
|
||||
<option value="IN">India</option>
|
||||
<option value="JP">Japan</option>
|
||||
<option value="KR">South Korea</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -501,6 +501,12 @@
|
|||
}
|
||||
ps.hidden = !html;
|
||||
ph.innerHTML = html;
|
||||
if (!ps.hidden) {
|
||||
loadPrefs(function (p) {
|
||||
var h = ps.querySelector('.vd-section-h');
|
||||
if (h) h.textContent = 'Where to Watch' + (p && p.watch_region ? ' · ' + p.watch_region : '');
|
||||
});
|
||||
}
|
||||
}
|
||||
// Franchise / collection (movies) — the other films in the set.
|
||||
var cs = q('[data-vd-collection-section]'), ch = q('[data-vd-collection]'), ct = q('[data-vd-collection-title]');
|
||||
|
|
|
|||
|
|
@ -77,15 +77,21 @@
|
|||
if (o && d.omdb_api_key != null) o.value = d.omdb_api_key;
|
||||
var ap = document.getElementById('video-billboard-autoplay');
|
||||
if (ap && d.billboard_autoplay != null) ap.checked = !!d.billboard_autoplay;
|
||||
var wr = document.getElementById('video-watch-region');
|
||||
if (wr && d.watch_region) wr.value = d.watch_region;
|
||||
})
|
||||
.catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
function saveAutoplay() {
|
||||
function savePrefs() {
|
||||
var ap = document.getElementById('video-billboard-autoplay');
|
||||
var wr = document.getElementById('video-watch-region');
|
||||
fetch(CONFIG_URL, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ billboard_autoplay: ap ? ap.checked : true })
|
||||
body: JSON.stringify({
|
||||
billboard_autoplay: ap ? ap.checked : true,
|
||||
watch_region: wr ? wr.value : 'US',
|
||||
})
|
||||
}).catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +146,9 @@
|
|||
if (el) el.addEventListener('change', saveKeys);
|
||||
});
|
||||
var autoplay = document.getElementById('video-billboard-autoplay');
|
||||
if (autoplay) autoplay.addEventListener('change', saveAutoplay);
|
||||
if (autoplay) autoplay.addEventListener('change', savePrefs);
|
||||
var region = document.getElementById('video-watch-region');
|
||||
if (region) region.addEventListener('change', savePrefs);
|
||||
// Per-connection Test buttons (same behaviour as music's testConnection).
|
||||
var testBtns = document.querySelectorAll('[data-video-test-service]');
|
||||
for (var k = 0; k < testBtns.length; k++) {
|
||||
|
|
|
|||
|
|
@ -1458,3 +1458,10 @@ a.vd-guest:hover .vd-guest-name { color: rgb(var(--vd-accent-rgb)); }
|
|||
.vd-ep-extra { flex-direction: column; padding-left: 14px; }
|
||||
.vd-ep-extra-still { width: 100%; }
|
||||
}
|
||||
|
||||
.vid-pref-row--select { justify-content: space-between; cursor: default; }
|
||||
.vid-pref-row--select select {
|
||||
background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
color: var(--text-primary, #fff); border-radius: 8px; padding: 7px 10px; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
.vid-pref-row--select select option { background: #16161c; color: #fff; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue