fix(sync): a channel with no uploads backfills as done, not as a failure

YouTube answers the uploads list of a channel with no uploads as
playlistNotFound. The recent and deep backfills raised on it, so the channel
was never stamped and was picked again, charged a quota unit and logged an
ERROR on every run. Both now read it as an empty last page, as explore already
does; any other failure still raises.
This commit is contained in:
2026-10-04 04:51:25 +02:00
parent 0314c4cce2
commit 40b27f2982
2 changed files with 68 additions and 5 deletions
+48 -1
View File
@@ -8,8 +8,9 @@ import httpx
import pytest
from app.auth import has_read_scope
from app.models import OAuthToken
from app.models import Channel, OAuthToken
from app.sync.explore import explore_ingest_page
from app.sync.videos import backfill_channel_deep, backfill_channel_recent
from app.youtube import client as yt
from app.youtube.client import YouTubeError, refusal_reason
@@ -57,6 +58,52 @@ def test_a_channel_without_uploads_explores_as_an_empty_page():
assert explore_ingest_page(None, _Yt(), channel) == {"ingested": 0, "next_page_token": None}
class _NoUploads:
"""A client whose every uploads read answers "playlist not found" (a channel with no uploads)."""
def __init__(self):
self.calls = 0
def get_playlist_items(self, *_):
self.calls += 1
raise YouTubeError("m", reason="playlistNotFound", path="playlistItems")
def test_a_channel_without_uploads_backfills_as_done_and_is_not_charged_again(db):
# The recent pass stamped nothing on this answer, so the channel was picked — and charged a
# quota unit, with an ERROR traceback — on every backfill run, forever (E32 review round 3).
ch = Channel(id="UCempty", title="Empty", uploads_playlist_id="UUempty")
db.add(ch)
db.commit()
yt = _NoUploads()
assert backfill_channel_recent(db, yt, ch) == 0
db.refresh(ch)
assert ch.recent_synced_at is not None and ch.backfill_done and ch.backfill_cursor is None
assert backfill_channel_deep(db, yt, ch) == 0
assert yt.calls == 1 # the deep pass has nothing left to ask
def test_a_deep_backfill_whose_uploads_vanish_ends_instead_of_failing(db):
ch = Channel(id="UCgone", title="Gone", uploads_playlist_id="UUgone", backfill_cursor="page2")
db.add(ch)
db.commit()
assert backfill_channel_deep(db, _NoUploads(), ch) == 0
db.refresh(ch)
assert ch.backfill_done and ch.backfill_cursor is None
def test_any_other_backfill_failure_still_raises(db):
class _Yt:
def get_playlist_items(self, *_):
raise YouTubeError("m", reason="quotaExceeded", path="playlistItems")
ch = Channel(id="UCq", title="Q", uploads_playlist_id="UUq")
db.add(ch)
db.commit()
with pytest.raises(YouTubeError):
backfill_channel_recent(db, _Yt(), ch)
def test_any_other_explore_failure_still_raises():
class _Yt:
def get_playlist_items(self, *_):