From 75d122de75bc98540c1608b032ad4571394d9e00 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 14 Jun 2026 19:01:04 +0200 Subject: [PATCH] fix(backfill): auto-complete full history when stored >= video_count A channel whose stored uploads already meet or exceed YouTube's advertised video_count holds its whole history, but backfill_done could stay false forever when the deep cursor never reached the end (e.g. a small channel that was never deep-requested, so the demand-driven deep job never ran). Such channels nagged as 'needs full history' despite having every video. Add reconcile_full_history() (idempotent, no quota) and run it at the end of each recent/deep backfill cycle so backfill_done self-heals. --- backend/app/sync/runner.py | 15 +++++++++++++-- backend/app/sync/videos.py | 28 +++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/backend/app/sync/runner.py b/backend/app/sync/runner.py index acefd9a..64f4e04 100644 --- a/backend/app/sync/runner.py +++ b/backend/app/sync/runner.py @@ -20,6 +20,7 @@ from app.sync.videos import ( backfill_channel_recent, enrich_pending, poll_rss_channel, + reconcile_full_history, run_shorts_classification, ) from app.youtube.client import YouTubeClient @@ -118,7 +119,12 @@ def run_recent_backfill( except Exception: db.rollback() log.exception("Recent backfill failed for channel %s", channel.id) - return {"channels_processed": processed, "videos_added": videos_added} + reconciled = reconcile_full_history(db) + return { + "channels_processed": processed, + "videos_added": videos_added, + "reconciled_full": reconciled, + } def run_deep_backfill(db: Session, max_channels: int = 10, max_pages: int = 5) -> dict: @@ -158,7 +164,12 @@ def run_deep_backfill(db: Session, max_channels: int = 10, max_pages: int = 5) - except Exception: db.rollback() log.exception("Deep backfill failed for channel %s", channel.id) - return {"channels_processed": processed, "videos_added": videos_added} + reconciled = reconcile_full_history(db) + return { + "channels_processed": processed, + "videos_added": videos_added, + "reconciled_full": reconciled, + } def estimate_deep_backfill(db: Session, channels: list[Channel]) -> dict: diff --git a/backend/app/sync/videos.py b/backend/app/sync/videos.py index 38c6cc3..e4672ed 100644 --- a/backend/app/sync/videos.py +++ b/backend/app/sync/videos.py @@ -5,7 +5,7 @@ import re from concurrent.futures import ThreadPoolExecutor from datetime import datetime, timedelta, timezone -from sqlalchemy import or_, select, update +from sqlalchemy import func, or_, select, update from sqlalchemy.dialects.postgresql import insert as pg_insert from sqlalchemy.orm import Session @@ -173,6 +173,32 @@ def backfill_channel_deep( return total +def reconcile_full_history(db: Session) -> int: + """Mark channels fully backfilled once their stored uploads meet or exceed YouTube's + advertised video_count. The uploads playlist length is normally <= the advertised + count, so 'stored >= video_count' means we already hold the whole history — even if + the deep cursor never reached the end (e.g. a small channel that was never + deep-requested, so the demand-driven deep job never ran). Without this such a channel + sits in 'needs full history' forever despite having every video. Idempotent, no API + quota; returns the number of channels flipped.""" + stored = ( + select(func.count(Video.id)) + .where(Video.channel_id == Channel.id) + .scalar_subquery() + ) + result = db.execute( + update(Channel) + .where( + Channel.backfill_done.is_(False), + Channel.video_count.is_not(None), + stored >= Channel.video_count, + ) + .values(backfill_done=True, backfill_cursor=None) + ) + db.commit() + return result.rowcount + + # --- Enrichment (videos.list) --- def _live_status(snippet: dict, live_details: dict | None) -> str: broadcast = snippet.get("liveBroadcastContent")