video enrichment: add Trakt (community audience rating) — full worker parity
First of the new enrichment services, wired to the SAME standard as the rest: - TraktWorker backfill (core/video/enrichment/backfill.py): looks a title up by its IMDb id (?extended=full) and gap-fills the community rating + vote count. Registered in build_backfill_workers. - DB: trakt_rating/trakt_votes/trakt_status/trakt_attempted columns + _BACKFILL / _BACKFILL_COLS registration; detail payloads return trakt_rating/votes. - Connections tab: Trakt service frame (Client ID field + Test button), wired into video-settings.js load/save/bindings and the /enrichment/config GET+POST. - Worker-manager modal + orb: WORKERS registry entry (red ★) + status-poll SERVICES list, so it gets the same orb animation + per-service matched/pending/error card. - Detail page: a 'Trakt 8.2' rating chip alongside IMDb/RT/Metacritic (+ CSS). 5 new tests + 2 fixed-set assertions updated; 249 video tests green, ruff clean.
This commit is contained in:
parent
c55a4fa10e
commit
314e587094
11 changed files with 162 additions and 6 deletions
|
|
@ -53,6 +53,7 @@ def register_routes(bp):
|
||||||
# Backfill-worker keys (free, optional) + no-key toggles.
|
# Backfill-worker keys (free, optional) + no-key toggles.
|
||||||
"fanart_api_key": db.get_setting("fanart_api_key") or "",
|
"fanart_api_key": db.get_setting("fanart_api_key") or "",
|
||||||
"opensubtitles_api_key": db.get_setting("opensubtitles_api_key") or "",
|
"opensubtitles_api_key": db.get_setting("opensubtitles_api_key") or "",
|
||||||
|
"trakt_api_key": db.get_setting("trakt_api_key") or "",
|
||||||
"ryd_enabled": (db.get_setting("ryd_enabled") or "1") == "1",
|
"ryd_enabled": (db.get_setting("ryd_enabled") or "1") == "1",
|
||||||
"sponsorblock_enabled": (db.get_setting("sponsorblock_enabled") or "1") == "1",
|
"sponsorblock_enabled": (db.get_setting("sponsorblock_enabled") or "1") == "1",
|
||||||
"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1",
|
"billboard_autoplay": (db.get_setting("billboard_autoplay") or "1") == "1",
|
||||||
|
|
@ -88,6 +89,7 @@ def register_routes(bp):
|
||||||
put_key("tvdb_api_key")
|
put_key("tvdb_api_key")
|
||||||
put_key("fanart_api_key")
|
put_key("fanart_api_key")
|
||||||
put_key("opensubtitles_api_key")
|
put_key("opensubtitles_api_key")
|
||||||
|
put_key("trakt_api_key")
|
||||||
# No-key worker on/off toggles (read live by the worker — no rebuild needed).
|
# No-key worker on/off toggles (read live by the worker — no rebuild needed).
|
||||||
for flag in ("ryd_enabled", "sponsorblock_enabled"):
|
for flag in ("ryd_enabled", "sponsorblock_enabled"):
|
||||||
if flag in body:
|
if flag in body:
|
||||||
|
|
|
||||||
|
|
@ -486,8 +486,75 @@ class OpenSubtitlesWorker(VideoBackfillWorker):
|
||||||
return self.db.backfill_breakdown("opensubtitles")
|
return self.db.backfill_breakdown("opensubtitles")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Trakt (free API key) — community audience rating + vote count ─────────────
|
||||||
|
class TraktWorker(VideoBackfillWorker):
|
||||||
|
BASE = "https://api.trakt.tv"
|
||||||
|
|
||||||
|
def __init__(self, db):
|
||||||
|
super().__init__(db, "trakt", "Trakt", interval=1.0)
|
||||||
|
|
||||||
|
def _key(self):
|
||||||
|
return (self.db.get_setting("trakt_api_key") or "").strip()
|
||||||
|
|
||||||
|
def _enabled(self):
|
||||||
|
return bool(self._key())
|
||||||
|
|
||||||
|
def _headers(self):
|
||||||
|
return {"trakt-api-key": self._key(), "trakt-api-version": "2",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
if not self._key():
|
||||||
|
return (False, "No Trakt API key")
|
||||||
|
try:
|
||||||
|
j = _http_get_json(self.BASE + "/shows/trending", {"limit": 1}, headers=self._headers())
|
||||||
|
return (j is not None, "Trakt key OK" if j is not None else "No response")
|
||||||
|
except _Unauthorized:
|
||||||
|
return (False, "Trakt rejected the API key (check the Client ID)")
|
||||||
|
except Exception as e:
|
||||||
|
return (False, str(e))
|
||||||
|
|
||||||
|
def next_item(self):
|
||||||
|
return self.db.backfill_next("trakt")
|
||||||
|
|
||||||
|
def fetch(self, item):
|
||||||
|
# Trakt accepts an IMDb id directly as the {id} slug; ?extended=full carries
|
||||||
|
# the community rating + vote count on the summary.
|
||||||
|
if not self._key():
|
||||||
|
return None
|
||||||
|
imdb = str(item.get("imdb_id") or "").strip()
|
||||||
|
if not imdb.lower().startswith("tt"):
|
||||||
|
return None
|
||||||
|
typ = "movies" if item["kind"] == "movie" else "shows"
|
||||||
|
j = _http_get_json(self.BASE + "/" + typ + "/" + imdb, {"extended": "full"},
|
||||||
|
headers=self._headers())
|
||||||
|
if not isinstance(j, dict):
|
||||||
|
return None
|
||||||
|
out = {}
|
||||||
|
rating = j.get("rating")
|
||||||
|
if isinstance(rating, (int, float)) and rating > 0:
|
||||||
|
out["trakt_rating"] = round(float(rating), 1)
|
||||||
|
votes = j.get("votes")
|
||||||
|
if isinstance(votes, int) and votes > 0:
|
||||||
|
out["trakt_votes"] = votes
|
||||||
|
return out or None
|
||||||
|
|
||||||
|
def record_ok(self, item, data):
|
||||||
|
self.db.backfill_mark("trakt", item["kind"], item["id"], "ok", columns=data)
|
||||||
|
|
||||||
|
def record_empty(self, item):
|
||||||
|
self.db.backfill_mark("trakt", item["kind"], item["id"], "not_found")
|
||||||
|
|
||||||
|
def record_error(self, item):
|
||||||
|
self.db.backfill_mark("trakt", item["kind"], item["id"], "error")
|
||||||
|
|
||||||
|
def breakdown(self):
|
||||||
|
return self.db.backfill_breakdown("trakt")
|
||||||
|
|
||||||
|
|
||||||
def build_backfill_workers(db) -> dict:
|
def build_backfill_workers(db) -> dict:
|
||||||
"""All backfill workers, keyed by service id, for the engine registry."""
|
"""All backfill workers, keyed by service id, for the engine registry."""
|
||||||
return {w.service: w for w in (
|
return {w.service: w for w in (
|
||||||
RydWorker(db), SponsorBlockWorker(db), FanartWorker(db), OpenSubtitlesWorker(db),
|
RydWorker(db), SponsorBlockWorker(db), FanartWorker(db), OpenSubtitlesWorker(db),
|
||||||
|
TraktWorker(db),
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -87,11 +87,18 @@ _BACKFILL = {
|
||||||
"show": ("shows", "subs_status", "subs_attempted",
|
"show": ("shows", "subs_status", "subs_attempted",
|
||||||
"(imdb_id IS NOT NULL OR tmdb_id IS NOT NULL)"),
|
"(imdb_id IS NOT NULL OR tmdb_id IS NOT NULL)"),
|
||||||
},
|
},
|
||||||
|
"trakt": {
|
||||||
|
"movie": ("movies", "trakt_status", "trakt_attempted", "imdb_id IS NOT NULL"),
|
||||||
|
"show": ("shows", "trakt_status", "trakt_attempted", "imdb_id IS NOT NULL"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
# Columns each backfill service may gap-fill (whitelist; never clobbers server data).
|
# Columns each backfill service may gap-fill (whitelist; never clobbers server data).
|
||||||
|
# A worker visits each item once (status IS NULL), so these NULL columns are written
|
||||||
|
# on that single pass.
|
||||||
_BACKFILL_COLS = {
|
_BACKFILL_COLS = {
|
||||||
"fanart": {"logo_url", "backdrop_url", "poster_url", "clearart_url", "banner_url"},
|
"fanart": {"logo_url", "backdrop_url", "poster_url", "clearart_url", "banner_url"},
|
||||||
"opensubtitles": {"subtitle_langs"},
|
"opensubtitles": {"subtitle_langs"},
|
||||||
|
"trakt": {"trakt_rating", "trakt_votes"},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Columns ensured on existing DBs (ALTER TABLE ADD COLUMN; idempotent).
|
# Columns ensured on existing DBs (ALTER TABLE ADD COLUMN; idempotent).
|
||||||
|
|
@ -148,6 +155,11 @@ _COLUMN_MIGRATIONS = [
|
||||||
("movies", "subs_status", "TEXT"), ("movies", "subs_attempted", "TEXT"),
|
("movies", "subs_status", "TEXT"), ("movies", "subs_attempted", "TEXT"),
|
||||||
("shows", "subtitle_langs", "TEXT"),
|
("shows", "subtitle_langs", "TEXT"),
|
||||||
("shows", "subs_status", "TEXT"), ("shows", "subs_attempted", "TEXT"),
|
("shows", "subs_status", "TEXT"), ("shows", "subs_attempted", "TEXT"),
|
||||||
|
# Trakt community rating backfill (by imdb id) — a distinct audience score + vote count
|
||||||
|
("movies", "trakt_rating", "REAL"), ("movies", "trakt_votes", "INTEGER"),
|
||||||
|
("movies", "trakt_status", "TEXT"), ("movies", "trakt_attempted", "TEXT"),
|
||||||
|
("shows", "trakt_rating", "REAL"), ("shows", "trakt_votes", "INTEGER"),
|
||||||
|
("shows", "trakt_status", "TEXT"), ("shows", "trakt_attempted", "TEXT"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1560,6 +1572,7 @@ class VideoDatabase:
|
||||||
"first_air_date": show["first_air_date"], "last_air_date": show["last_air_date"],
|
"first_air_date": show["first_air_date"], "last_air_date": show["last_air_date"],
|
||||||
"imdb_rating": show["imdb_rating"], "rt_rating": show["rt_rating"],
|
"imdb_rating": show["imdb_rating"], "rt_rating": show["rt_rating"],
|
||||||
"metacritic": show["metacritic"],
|
"metacritic": show["metacritic"],
|
||||||
|
"trakt_rating": show["trakt_rating"], "trakt_votes": show["trakt_votes"],
|
||||||
"genres": genres, "cast": credits["cast"], "crew": credits["crew"],
|
"genres": genres, "cast": credits["cast"], "crew": credits["crew"],
|
||||||
"tmdb_id": show["tmdb_id"], "tvdb_id": show["tvdb_id"], "imdb_id": show["imdb_id"],
|
"tmdb_id": show["tmdb_id"], "tvdb_id": show["tvdb_id"], "imdb_id": show["imdb_id"],
|
||||||
"has_poster": bool(show["poster_url"]), "has_backdrop": bool(show["backdrop_url"]),
|
"has_poster": bool(show["poster_url"]), "has_backdrop": bool(show["backdrop_url"]),
|
||||||
|
|
@ -2608,6 +2621,7 @@ class VideoDatabase:
|
||||||
"content_rating": m["content_rating"], "tagline": m["tagline"],
|
"content_rating": m["content_rating"], "tagline": m["tagline"],
|
||||||
"rating": m["rating"], "rating_critic": m["rating_critic"], "genres": genres,
|
"rating": m["rating"], "rating_critic": m["rating_critic"], "genres": genres,
|
||||||
"imdb_rating": m["imdb_rating"], "rt_rating": m["rt_rating"], "metacritic": m["metacritic"],
|
"imdb_rating": m["imdb_rating"], "rt_rating": m["rt_rating"], "metacritic": m["metacritic"],
|
||||||
|
"trakt_rating": m["trakt_rating"], "trakt_votes": m["trakt_votes"],
|
||||||
"cast": credits["cast"], "crew": credits["crew"],
|
"cast": credits["cast"], "crew": credits["crew"],
|
||||||
"tmdb_id": m["tmdb_id"], "imdb_id": m["imdb_id"],
|
"tmdb_id": m["tmdb_id"], "imdb_id": m["imdb_id"],
|
||||||
"has_poster": bool(m["poster_url"]), "has_backdrop": bool(m["backdrop_url"]),
|
"has_poster": bool(m["poster_url"]), "has_backdrop": bool(m["backdrop_url"]),
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,7 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
|
||||||
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": "",
|
"tmdb_api_key": "", "tvdb_api_key": "", "omdb_api_key": "",
|
||||||
"fanart_api_key": "", "opensubtitles_api_key": "",
|
"fanart_api_key": "", "opensubtitles_api_key": "", "trakt_api_key": "",
|
||||||
"ryd_enabled": True, "sponsorblock_enabled": True,
|
"ryd_enabled": True, "sponsorblock_enabled": True,
|
||||||
"billboard_autoplay": True, "watch_region": "US"}
|
"billboard_autoplay": True, "watch_region": "US"}
|
||||||
client.post("/api/video/enrichment/config",
|
client.post("/api/video/enrichment/config",
|
||||||
|
|
@ -344,7 +344,7 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
|
||||||
"billboard_autoplay": False, "watch_region": "gb"})
|
"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",
|
"tmdb_api_key": "abc", "tvdb_api_key": "xyz", "omdb_api_key": "om",
|
||||||
"fanart_api_key": "fa", "opensubtitles_api_key": "os",
|
"fanart_api_key": "fa", "opensubtitles_api_key": "os", "trakt_api_key": "",
|
||||||
"ryd_enabled": False, "sponsorblock_enabled": True,
|
"ryd_enabled": False, "sponsorblock_enabled": True,
|
||||||
"billboard_autoplay": False, "watch_region": "GB"}
|
"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"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import pytest
|
||||||
|
|
||||||
from database.video_database import VideoDatabase
|
from database.video_database import VideoDatabase
|
||||||
from core.video.enrichment.backfill import (
|
from core.video.enrichment.backfill import (
|
||||||
RydWorker, SponsorBlockWorker, FanartWorker, OpenSubtitlesWorker,
|
RydWorker, SponsorBlockWorker, FanartWorker, OpenSubtitlesWorker, TraktWorker,
|
||||||
VideoBackfillWorker, _RateLimited, _Unauthorized, build_backfill_workers,
|
VideoBackfillWorker, _RateLimited, _Unauthorized, build_backfill_workers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -178,7 +178,51 @@ def test_get_stats_shape_matches_matcher_worker(db):
|
||||||
|
|
||||||
|
|
||||||
def test_build_backfill_workers_set(db):
|
def test_build_backfill_workers_set(db):
|
||||||
assert set(build_backfill_workers(db)) == {"ryd", "sponsorblock", "fanart", "opensubtitles"}
|
assert set(build_backfill_workers(db)) == {"ryd", "sponsorblock", "fanart", "opensubtitles", "trakt"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Trakt (community rating backfill, keyed on imdb id) ────────────────────────
|
||||||
|
def test_trakt_queue_keyed_on_imdb(db):
|
||||||
|
mid = db.upsert_movie("plex", {"server_id": "m1", "title": "Fight Club", "year": 1999})
|
||||||
|
assert db.backfill_next("trakt") is None # no imdb id → not queued
|
||||||
|
with db.connect() as c:
|
||||||
|
c.execute("UPDATE movies SET imdb_id='tt0137523' WHERE id=?", (mid,))
|
||||||
|
c.commit()
|
||||||
|
nxt = db.backfill_next("trakt")
|
||||||
|
assert nxt["kind"] == "movie" and nxt["imdb_id"] == "tt0137523"
|
||||||
|
|
||||||
|
|
||||||
|
def test_trakt_worker_records_rating(db):
|
||||||
|
mid = db.upsert_movie("plex", {"server_id": "m1", "title": "M"})
|
||||||
|
with db.connect() as c:
|
||||||
|
c.execute("UPDATE movies SET imdb_id='tt1' WHERE id=?", (mid,))
|
||||||
|
c.commit()
|
||||||
|
w = TraktWorker(db)
|
||||||
|
w.fetch = lambda item: {"trakt_rating": 8.2, "trakt_votes": 1234}
|
||||||
|
assert w.process_one() is True
|
||||||
|
with db.connect() as c:
|
||||||
|
r = c.execute("SELECT trakt_rating, trakt_votes, trakt_status FROM movies WHERE id=?", (mid,)).fetchone()
|
||||||
|
assert r["trakt_rating"] == 8.2 and r["trakt_votes"] == 1234 and r["trakt_status"] == "ok"
|
||||||
|
assert db.backfill_breakdown("trakt")["movie"]["matched"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_trakt_fetch_parses_summary(db, monkeypatch):
|
||||||
|
db.set_setting("trakt_api_key", "client-id")
|
||||||
|
import core.video.enrichment.backfill as bf
|
||||||
|
monkeypatch.setattr(bf, "_http_get_json",
|
||||||
|
lambda url, params=None, headers=None, timeout=12: {"rating": 8.234, "votes": 5000})
|
||||||
|
w = TraktWorker(db)
|
||||||
|
out = w.fetch({"kind": "movie", "imdb_id": "tt0137523"})
|
||||||
|
assert out == {"trakt_rating": 8.2, "trakt_votes": 5000} # rounded to 1dp
|
||||||
|
|
||||||
|
|
||||||
|
def test_trakt_fetch_needs_imdb_and_key(db, monkeypatch):
|
||||||
|
import core.video.enrichment.backfill as bf
|
||||||
|
monkeypatch.setattr(bf, "_http_get_json", lambda *a, **k: {"rating": 9})
|
||||||
|
w = TraktWorker(db)
|
||||||
|
assert w.fetch({"kind": "movie", "imdb_id": "tt1"}) is None # no key configured
|
||||||
|
db.set_setting("trakt_api_key", "k")
|
||||||
|
assert w.fetch({"kind": "movie", "imdb_id": "550"}) is None # not a tt-id
|
||||||
|
|
||||||
|
|
||||||
def test_backfill_module_imports_nothing_from_music():
|
def test_backfill_module_imports_nothing_from_music():
|
||||||
|
|
|
||||||
|
|
@ -5566,6 +5566,24 @@
|
||||||
<button class="test-button" type="button" data-video-test-service="opensubtitles">Test OpenSubtitles</button>
|
<button class="test-button" type="button" data-video-test-service="opensubtitles">Test OpenSubtitles</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="api-service-frame stg-service" data-video-service="trakt">
|
||||||
|
<div class="stg-service-header" onclick="toggleStgService(this)">
|
||||||
|
<span class="stg-service-dot" style="color: #ed1c24;"></span>
|
||||||
|
<h4 class="service-title">Trakt (Audience Rating)</h4>
|
||||||
|
<span class="stg-service-chevron">›</span>
|
||||||
|
</div>
|
||||||
|
<div class="stg-service-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API Key (Client ID):</label>
|
||||||
|
<input type="password" id="trakt-api-key" placeholder="Trakt Client ID">
|
||||||
|
</div>
|
||||||
|
<div class="callback-info">
|
||||||
|
<div class="callback-help">Create a free API app at <a href="https://trakt.tv/oauth/applications" target="_blank" style="color: #ed1c24;">trakt.tv</a> and paste its <strong>Client ID</strong>.</div>
|
||||||
|
<div class="callback-help">Adds Trakt's community audience score (and vote count) to movies & shows.</div>
|
||||||
|
</div>
|
||||||
|
<button class="test-button" type="button" data-video-test-service="trakt">Test Trakt</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="api-service-frame stg-service" data-video-service="ytextras">
|
<div class="api-service-frame stg-service" data-video-service="ytextras">
|
||||||
<div class="stg-service-header" onclick="toggleStgService(this)">
|
<div class="stg-service-header" onclick="toggleStgService(this)">
|
||||||
<span class="stg-service-dot" style="color: #ef4444;"></span>
|
<span class="stg-service-dot" style="color: #ef4444;"></span>
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,11 @@
|
||||||
items.push('<span class="vd-rt vd-rt--mc vd-rt--mc-' + cls + '">' +
|
items.push('<span class="vd-rt vd-rt--mc vd-rt--mc-' + cls + '">' +
|
||||||
'<span class="vd-rt-tag">MC</span>' + d.metacritic + '</span>');
|
'<span class="vd-rt-tag">MC</span>' + d.metacritic + '</span>');
|
||||||
}
|
}
|
||||||
|
if (d.trakt_rating) {
|
||||||
|
var tv = d.trakt_votes ? ' title="' + esc(d.trakt_votes) + ' Trakt votes"' : '';
|
||||||
|
items.push('<span class="vd-rt vd-rt--trakt"' + tv + '><span class="vd-rt-tag">Trakt</span>' +
|
||||||
|
(Math.round(d.trakt_rating * 10) / 10) + '</span>');
|
||||||
|
}
|
||||||
host.innerHTML = items.join('');
|
host.innerHTML = items.join('');
|
||||||
host.hidden = !items.length;
|
host.hidden = !items.length;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
{ id: 'opensubtitles', name: 'OpenSubtitles', color: '#22a079', rgb: '34, 160, 121', kinds: ['movie', 'show'], glyph: '💬' },
|
{ id: 'opensubtitles', name: 'OpenSubtitles', color: '#22a079', rgb: '34, 160, 121', kinds: ['movie', 'show'], glyph: '💬' },
|
||||||
{ id: 'ryd', name: 'YouTube Votes', color: '#ef4444', rgb: '239, 68, 68', kinds: ['video'], glyph: '👍' },
|
{ id: 'ryd', name: 'YouTube Votes', color: '#ef4444', rgb: '239, 68, 68', kinds: ['video'], glyph: '👍' },
|
||||||
{ id: 'sponsorblock', name: 'SponsorBlock', color: '#00b4a0', rgb: '0, 180, 160', kinds: ['video'], glyph: '⏭' },
|
{ id: 'sponsorblock', name: 'SponsorBlock', color: '#00b4a0', rgb: '0, 180, 160', kinds: ['video'], glyph: '⏭' },
|
||||||
|
{ id: 'trakt', name: 'Trakt', color: '#ed1c24', rgb: '237, 28, 36', kinds: ['movie', 'show'], glyph: '★' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function workerDef(id) {
|
function workerDef(id) {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
// 'enrichment:<svc>') — including the standalone YouTube date enricher — so the
|
// 'enrichment:<svc>') — including the standalone YouTube date enricher — so the
|
||||||
// browser never polls /api/video/enrichment/<svc>/status.
|
// browser never polls /api/video/enrichment/<svc>/status.
|
||||||
var SERVICES = ['tmdb', 'tvdb', 'omdb', 'youtube',
|
var SERVICES = ['tmdb', 'tvdb', 'omdb', 'youtube',
|
||||||
'fanart', 'opensubtitles', 'ryd', 'sponsorblock'];
|
'fanart', 'opensubtitles', 'ryd', 'sponsorblock', 'trakt'];
|
||||||
|
|
||||||
function onVideoSide() {
|
function onVideoSide() {
|
||||||
return document.body.getAttribute('data-side') === 'video';
|
return document.body.getAttribute('data-side') === 'video';
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,8 @@
|
||||||
if (fa && d.fanart_api_key != null) fa.value = d.fanart_api_key;
|
if (fa && d.fanart_api_key != null) fa.value = d.fanart_api_key;
|
||||||
var sub = document.getElementById('opensubtitles-api-key');
|
var sub = document.getElementById('opensubtitles-api-key');
|
||||||
if (sub && d.opensubtitles_api_key != null) sub.value = d.opensubtitles_api_key;
|
if (sub && d.opensubtitles_api_key != null) sub.value = d.opensubtitles_api_key;
|
||||||
|
var trakt = document.getElementById('trakt-api-key');
|
||||||
|
if (trakt && d.trakt_api_key != null) trakt.value = d.trakt_api_key;
|
||||||
var ryd = document.getElementById('video-ryd-enabled');
|
var ryd = document.getElementById('video-ryd-enabled');
|
||||||
if (ryd && d.ryd_enabled != null) ryd.checked = !!d.ryd_enabled;
|
if (ryd && d.ryd_enabled != null) ryd.checked = !!d.ryd_enabled;
|
||||||
var sb = document.getElementById('video-sponsorblock-enabled');
|
var sb = document.getElementById('video-sponsorblock-enabled');
|
||||||
|
|
@ -247,6 +249,7 @@
|
||||||
var o = document.getElementById('omdb-api-key');
|
var o = document.getElementById('omdb-api-key');
|
||||||
var fa = document.getElementById('fanart-api-key');
|
var fa = document.getElementById('fanart-api-key');
|
||||||
var sub = document.getElementById('opensubtitles-api-key');
|
var sub = document.getElementById('opensubtitles-api-key');
|
||||||
|
var trakt = document.getElementById('trakt-api-key');
|
||||||
var ryd = document.getElementById('video-ryd-enabled');
|
var ryd = document.getElementById('video-ryd-enabled');
|
||||||
var sb = document.getElementById('video-sponsorblock-enabled');
|
var sb = document.getElementById('video-sponsorblock-enabled');
|
||||||
return fetch(CONFIG_URL, {
|
return fetch(CONFIG_URL, {
|
||||||
|
|
@ -257,6 +260,7 @@
|
||||||
omdb_api_key: o ? o.value : '',
|
omdb_api_key: o ? o.value : '',
|
||||||
fanart_api_key: fa ? fa.value : '',
|
fanart_api_key: fa ? fa.value : '',
|
||||||
opensubtitles_api_key: sub ? sub.value : '',
|
opensubtitles_api_key: sub ? sub.value : '',
|
||||||
|
trakt_api_key: trakt ? trakt.value : '',
|
||||||
ryd_enabled: ryd ? ryd.checked : true,
|
ryd_enabled: ryd ? ryd.checked : true,
|
||||||
sponsorblock_enabled: sb ? sb.checked : true,
|
sponsorblock_enabled: sb ? sb.checked : true,
|
||||||
})
|
})
|
||||||
|
|
@ -301,7 +305,7 @@
|
||||||
}
|
}
|
||||||
// Enrichment keys save on blur/change (turns the workers on).
|
// Enrichment keys save on blur/change (turns the workers on).
|
||||||
['tmdb-api-key', 'tvdb-api-key', 'omdb-api-key',
|
['tmdb-api-key', 'tvdb-api-key', 'omdb-api-key',
|
||||||
'fanart-api-key', 'opensubtitles-api-key',
|
'fanart-api-key', 'opensubtitles-api-key', 'trakt-api-key',
|
||||||
'video-ryd-enabled', 'video-sponsorblock-enabled'].forEach(function (id) {
|
'video-ryd-enabled', 'video-sponsorblock-enabled'].forEach(function (id) {
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
if (el) el.addEventListener('change', function () { saveKeys(); });
|
if (el) el.addEventListener('change', function () { saveKeys(); });
|
||||||
|
|
|
||||||
|
|
@ -787,6 +787,7 @@ a.vd-prov:hover img, a.vd-prov:hover .vd-prov-ph { border-color: rgba(var(--vd-a
|
||||||
.vd-rt--mc-good .vd-rt-tag { background: #6c3; color: #000; }
|
.vd-rt--mc-good .vd-rt-tag { background: #6c3; color: #000; }
|
||||||
.vd-rt--mc-mid .vd-rt-tag { background: #fc3; color: #000; }
|
.vd-rt--mc-mid .vd-rt-tag { background: #fc3; color: #000; }
|
||||||
.vd-rt--mc-bad .vd-rt-tag { background: #f00; color: #fff; }
|
.vd-rt--mc-bad .vd-rt-tag { background: #f00; color: #fff; }
|
||||||
|
.vd-rt--trakt .vd-rt-tag { background: #ed1c24; color: #fff; }
|
||||||
|
|
||||||
/* ── OMDb worker uses a ★ glyph (no clean brand logo) ─────────────────────── */
|
/* ── OMDb worker uses a ★ glyph (no clean brand logo) ─────────────────────── */
|
||||||
.video-enrich-glyph {
|
.video-enrich-glyph {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue