Video enrichment: surface enriched YouTube data

- YouTube video cards show 👍/👎 like/dislike counts (from Return YouTube Dislike)
  next to views, threaded through ytEpisodeOf.
- New GET /youtube/video/<id>/segments exposes stored SponsorBlock segments so the
  player can offer skips. Update config-shape test for the new keys/toggles.
This commit is contained in:
BoulderBadgeDad 2026-06-17 23:35:59 -07:00
parent 21a1e5ddbb
commit 6f14f15dd6
3 changed files with 20 additions and 2 deletions

View file

@ -36,6 +36,14 @@ def _server():
def register_routes(bp):
@bp.route("/youtube/video/<video_id>/segments", methods=["GET"])
def video_youtube_segments(video_id):
"""SponsorBlock crowd segments stored for a video (sponsor/intro/outro/…),
so the player can offer skips. [] until the SponsorBlock worker enriches it."""
from . import get_video_db
return jsonify({"video_id": video_id,
"segments": get_video_db().youtube_video_segments(video_id)})
@bp.route("/youtube/resolve", methods=["GET"])
def video_youtube_resolve():
"""Preview a pasted channel URL without committing. Returns the channel +

View file

@ -334,12 +334,18 @@ def test_enrichment_config_save_load(tmp_path, monkeypatch):
try:
assert client.get("/api/video/enrichment/config").get_json() == {
"tmdb_api_key": "", "tvdb_api_key": "", "omdb_api_key": "",
"fanart_api_key": "", "opensubtitles_api_key": "",
"ryd_enabled": True, "sponsorblock_enabled": True,
"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",
"fanart_api_key": "fa", "opensubtitles_api_key": "os",
"ryd_enabled": False, "sponsorblock_enabled": True,
"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",
"fanart_api_key": "fa", "opensubtitles_api_key": "os",
"ryd_enabled": False, "sponsorblock_enabled": True,
"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() == {

View file

@ -975,7 +975,10 @@
: '';
var dur = ep.yt_duration ? '<span class="vd-ep-dur">' + esc(ep.yt_duration) + '</span>' : '';
var meta = [];
if (ep.view_count) { var yc = window.VideoYoutube; meta.push((yc ? yc.compactCount(ep.view_count) : ep.view_count) + ' views'); }
var yc0 = window.VideoYoutube;
if (ep.view_count) { meta.push((yc0 ? yc0.compactCount(ep.view_count) : ep.view_count) + ' views'); }
if (ep.like_count) { meta.push('👍 ' + (yc0 ? yc0.compactCount(ep.like_count) : ep.like_count)); }
if (ep.dislike_count) { meta.push('👎 ' + (yc0 ? yc0.compactCount(ep.dislike_count) : ep.dislike_count)); }
if (ep.air_date) meta.push(fmtDate(ep.air_date));
var wished = !!ep.owned;
return '<div class="vd-ep vd-ep--yt" data-vd-ep-key="' + key + '" data-vd-yt-vid="' + esc(ep.youtube_id) + '">' +
@ -1373,7 +1376,8 @@
return { episode_number: i + 1, title: v.title, overview: v.description || '',
air_date: v.published_at, owned: !!v.wished, has_still: false,
still_url: ytProx(v.thumbnail_url), youtube_id: v.youtube_id,
yt_duration: v.duration || '', view_count: v.view_count || 0 };
yt_duration: v.duration || '', view_count: v.view_count || 0,
like_count: v.like_count || 0, dislike_count: v.dislike_count || 0 };
}
function ytDurSecs(d) {
if (!d) return 0;