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 "",
|
"tvdb_api_key": db.get_setting("tvdb_api_key") or "",
|
||||||
"omdb_api_key": db.get_setting("omdb_api_key") or "",
|
"omdb_api_key": db.get_setting("omdb_api_key") or "",
|
||||||
"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1",
|
"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"])
|
@bp.route("/prefs", methods=["GET"])
|
||||||
|
|
@ -54,7 +55,10 @@ def register_routes(bp):
|
||||||
# Lightweight UI prefs for the detail page (no API keys).
|
# Lightweight UI prefs for the detail page (no API keys).
|
||||||
from . import get_video_db
|
from . import get_video_db
|
||||||
db = 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"])
|
@bp.route("/enrichment/config", methods=["POST"])
|
||||||
def video_enrichment_config_save():
|
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 "")
|
db.set_setting("tvdb_api_key", body.get("tvdb_api_key") or "")
|
||||||
if "billboard_autoplay" in body:
|
if "billboard_autoplay" in body:
|
||||||
db.set_setting("billboard_autoplay", "1" if body.get("billboard_autoplay") else "0")
|
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:
|
if "omdb_api_key" in body:
|
||||||
new_key = body.get("omdb_api_key") or ""
|
new_key = body.get("omdb_api_key") or ""
|
||||||
changed = new_key != (db.get_setting("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})
|
"poster": (self.POSTER_W + it["poster_path"]) if it.get("poster_path") else None})
|
||||||
return out[:20]
|
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
|
"""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
|
library detail payload but with direct image URLs (so the same detail UI
|
||||||
renders it). Seasons carry counts; episodes load lazily per season."""
|
renders it). Seasons carry counts; episodes load lazily per season."""
|
||||||
|
|
@ -503,7 +503,7 @@ class TMDBClient:
|
||||||
for p in cmeta.get("cast") or []],
|
for p in cmeta.get("cast") or []],
|
||||||
"crew": [{"name": p["name"], "job": p.get("job"), "tmdb_id": p.get("tmdb_id")}
|
"crew": [{"name": p["name"], "job": p.get("job"), "tmdb_id": p.get("tmdb_id")}
|
||||||
for p in cmeta.get("crew") or []],
|
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"])
|
self._fill_collection(out["_extras"])
|
||||||
if kind == "movie":
|
if kind == "movie":
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ class VideoEnrichmentEngine:
|
||||||
for w in self.workers.values():
|
for w in self.workers.values():
|
||||||
w.restore_paused()
|
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):
|
def _cache_get(self, key):
|
||||||
return self._cache.get(key)
|
return self._cache.get(key)
|
||||||
|
|
||||||
|
|
@ -171,11 +177,12 @@ class VideoEnrichmentEngine:
|
||||||
info = (self.db.movie_match_info(item_id) if kind == "movie"
|
info = (self.db.movie_match_info(item_id) if kind == "movie"
|
||||||
else self.db.show_match_info(item_id))
|
else self.db.show_match_info(item_id))
|
||||||
if info and info.get("tmdb_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)
|
cached = self._cache_get(key)
|
||||||
if cached is None:
|
if cached is None:
|
||||||
try:
|
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)
|
self._cache_put(key, cached)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("item_extras failed for %s %s", kind, item_id)
|
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)
|
lib_id = self.db.library_id_for_tmdb(kind, tmdb_id)
|
||||||
if lib_id:
|
if lib_id:
|
||||||
return {"redirect": {"source": "library", "kind": kind, "id": 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:
|
if cached is not None:
|
||||||
return dict(cached)
|
return dict(cached)
|
||||||
try:
|
try:
|
||||||
d = w.client.full_detail(kind, tmdb_id)
|
d = w.client.full_detail(kind, tmdb_id, region=region)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("tmdb_detail failed for %s %s", kind, tmdb_id)
|
logger.exception("tmdb_detail failed for %s %s", kind, tmdb_id)
|
||||||
return None
|
return None
|
||||||
|
|
@ -336,7 +344,7 @@ class VideoEnrichmentEngine:
|
||||||
d["episode_total"] = sum(s["episode_total"] for s in seasons)
|
d["episode_total"] = sum(s["episode_total"] for s in seasons)
|
||||||
d["episode_owned"] = 0
|
d["episode_owned"] = 0
|
||||||
self._fill_tmdb_ratings(d)
|
self._fill_tmdb_ratings(d)
|
||||||
self._cache_put(("detail", kind, tmdb_id), d)
|
self._cache_put(("detail", kind, tmdb_id, region), d)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def _fill_tmdb_ratings(self, d) -> None:
|
def _fill_tmdb_ratings(self, d) -> None:
|
||||||
|
|
|
||||||
|
|
@ -268,14 +268,17 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
|
||||||
client = app.test_client()
|
client = app.test_client()
|
||||||
try:
|
try:
|
||||||
assert client.get("/api/video/enrichment/config").get_json() == {
|
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",
|
client.post("/api/video/enrichment/config",
|
||||||
json={"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om",
|
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() == {
|
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 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:
|
finally:
|
||||||
videoapi._video_db = None
|
videoapi._video_db = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ def test_engine_tmdb_detail_redirects_when_owned(db):
|
||||||
|
|
||||||
class Tmdb:
|
class Tmdb:
|
||||||
enabled = True
|
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()})
|
eng = VideoEnrichmentEngine(db, {"tmdb": Tmdb()})
|
||||||
assert eng.tmdb_detail("movie", 77) == {"redirect": {"source": "library", "kind": "movie", "id": mid}}
|
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):
|
def test_engine_tmdb_detail_assembles_show(db):
|
||||||
class Tmdb:
|
class Tmdb:
|
||||||
enabled = True
|
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,
|
return {"kind": "show", "tmdb_id": tid, "title": "Loki", "imdb_id": None,
|
||||||
"poster_url": "http://p", "backdrop_url": None, "cast": [], "crew": [],
|
"poster_url": "http://p", "backdrop_url": None, "cast": [], "crew": [],
|
||||||
"_extras": {"similar": [{"title": "X", "tmdb_id": 5, "kind": "show"}]},
|
"_extras": {"similar": [{"title": "X", "tmdb_id": 5, "kind": "show"}]},
|
||||||
|
|
@ -643,7 +643,7 @@ def test_item_extras_caches_tmdb_call(db, monkeypatch):
|
||||||
calls = []
|
calls = []
|
||||||
class Tmdb:
|
class Tmdb:
|
||||||
enabled = True
|
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()})
|
eng = VideoEnrichmentEngine(db, {"tmdb": Tmdb()})
|
||||||
a = eng.item_extras("movie", mid)
|
a = eng.item_extras("movie", mid)
|
||||||
b = eng.item_extras("movie", mid)
|
b = eng.item_extras("movie", mid)
|
||||||
|
|
|
||||||
|
|
@ -5205,6 +5205,30 @@
|
||||||
<div class="callback-info">
|
<div class="callback-info">
|
||||||
<div class="callback-help">Plays a muted trailer behind the hero after a moment (uses more bandwidth).</div>
|
<div class="callback-help">Plays a muted trailer behind the hero after a moment (uses more bandwidth).</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -501,6 +501,12 @@
|
||||||
}
|
}
|
||||||
ps.hidden = !html;
|
ps.hidden = !html;
|
||||||
ph.innerHTML = 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.
|
// 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]');
|
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;
|
if (o && d.omdb_api_key != null) o.value = d.omdb_api_key;
|
||||||
var ap = document.getElementById('video-billboard-autoplay');
|
var ap = document.getElementById('video-billboard-autoplay');
|
||||||
if (ap && d.billboard_autoplay != null) ap.checked = !!d.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 */ });
|
.catch(function () { /* ignore */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAutoplay() {
|
function savePrefs() {
|
||||||
var ap = document.getElementById('video-billboard-autoplay');
|
var ap = document.getElementById('video-billboard-autoplay');
|
||||||
|
var wr = document.getElementById('video-watch-region');
|
||||||
fetch(CONFIG_URL, {
|
fetch(CONFIG_URL, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
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 */ });
|
}).catch(function () { /* ignore */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +146,9 @@
|
||||||
if (el) el.addEventListener('change', saveKeys);
|
if (el) el.addEventListener('change', saveKeys);
|
||||||
});
|
});
|
||||||
var autoplay = document.getElementById('video-billboard-autoplay');
|
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).
|
// Per-connection Test buttons (same behaviour as music's testConnection).
|
||||||
var testBtns = document.querySelectorAll('[data-video-test-service]');
|
var testBtns = document.querySelectorAll('[data-video-test-service]');
|
||||||
for (var k = 0; k < testBtns.length; k++) {
|
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 { flex-direction: column; padding-left: 14px; }
|
||||||
.vd-ep-extra-still { width: 100%; }
|
.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