fix(sync): re-enrich live videos so ended streams gain duration

enrich_pending only touches enriched_at IS NULL, so a video first seen
while live was stamped live + duration-null and never revisited — staying
'live' with no duration forever after the broadcast ended. Add refresh_live
(run after each enrich pass) that re-fetches anything still live/upcoming,
plus just-ended was_live videos that haven't got their duration yet, until
they settle. Cheap: videos.list is 1 unit per 50 ids.
This commit is contained in:
npeter83 2026-06-16 10:06:58 +02:00
parent ddaa95f2a6
commit b114dae39b
2 changed files with 52 additions and 1 deletions

View file

@ -5,7 +5,7 @@ import re
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
from sqlalchemy import func, or_, select, update
from sqlalchemy import and_, func, or_, select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.orm import Session
@ -260,6 +260,52 @@ def enrich_pending(db: Session, yt: YouTubeClient, limit: int | None = None) ->
return enriched
def refresh_live(db: Session, yt: YouTubeClient, limit: int | None = None) -> int:
"""Re-fetch videos in a transient live state so they pick up their final shape.
`enrich_pending` is one-shot (it only touches enriched_at IS NULL), but a live broadcast
is not a final state: a stream ends and becomes a normal VOD gaining a real duration
and flipping live_status to was_live and an upcoming premiere eventually goes live then
ends. So we revisit anything still live/upcoming, plus a just-ended stream that hasn't got
its duration yet (YouTube can lag a bit after the broadcast), until it settles. Cheap:
videos.list is 1 unit per 50 ids, and the live/upcoming set is normally tiny."""
limit = limit or settings.enrich_batch_size
videos = (
db.execute(
select(Video)
.where(
or_(
Video.live_status.in_(("live", "upcoming")),
and_(
Video.live_status == "was_live",
Video.duration_seconds.is_(None),
),
)
)
.limit(limit)
)
.scalars()
.all()
)
if not videos:
return 0
items = {it["id"]: it for it in yt.get_videos([v.id for v in videos])}
now = _now()
updated = 0
for video in videos:
item = items.get(video.id)
if item is None:
# The broadcast vanished (deleted/private). Stop treating it as live so it
# doesn't get refreshed forever; it just becomes an ordinary (dead) row.
video.live_status = "none"
continue
apply_video_details(video, item)
video.enriched_at = now
updated += 1
db.commit()
return updated
def run_shorts_classification(db: Session, limit: int | None = None) -> dict:
"""Finalize Shorts classification: cheaply mark non-candidates, then probe the
youtube.com/shorts URL for short-enough, non-live, enriched videos."""