fix(youtube): "this playlist no longer exists" only where the user picked that list
playlistNotFound was mapped to "gone" everywhere: exploring a channel without uploads (its uploads list answers not found) and a list deleted mid bulk pull both said a playlist no longer exists. The reason is now "gone" only on single-playlist routes; explore reads it as an empty back-catalogue, and the bulk pull skips that list and syncs the rest.
This commit is contained in:
@@ -25,7 +25,7 @@ from app.models import (
|
||||
)
|
||||
from app.sync.videos import insert_stubs, stub_from_playlist_item, apply_video_details
|
||||
from app.utils import now_utc as _now
|
||||
from app.youtube.client import YouTubeClient
|
||||
from app.youtube.client import YouTubeClient, YouTubeError, is_playlist_gone
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
@@ -81,7 +81,15 @@ def explore_ingest_page(
|
||||
the organic deep backfill) is never touched."""
|
||||
if not channel.uploads_playlist_id:
|
||||
return {"ingested": 0, "next_page_token": None}
|
||||
data = yt.get_playlist_items(channel.uploads_playlist_id, page_token)
|
||||
try:
|
||||
data = yt.get_playlist_items(channel.uploads_playlist_id, page_token)
|
||||
except YouTubeError as exc:
|
||||
# A channel with no uploads has an uploads id that YouTube answers as not found: an empty
|
||||
# back-catalogue, not a failure (and not a "playlist gone" — the user picked a channel).
|
||||
if not is_playlist_gone(exc):
|
||||
raise
|
||||
log.info("explore ingest channel=%s has no uploads playlist", channel.id)
|
||||
return {"ingested": 0, "next_page_token": None}
|
||||
stubs: list[dict] = []
|
||||
for item in data.get("items", []):
|
||||
stub = stub_from_playlist_item(item, channel.id)
|
||||
|
||||
@@ -26,7 +26,7 @@ from app.sync.playlist_merge import merge_plan
|
||||
from app.sync.runner import token_users
|
||||
from app.sync.subscriptions import blank_listing, refuse_blank_listing, vanished
|
||||
from app.sync.videos import apply_video_details, parse_dt
|
||||
from app.youtube.client import YouTubeClient, YouTubeError
|
||||
from app.youtube.client import YouTubeClient, YouTubeError, is_playlist_gone
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
# Cost of a single YouTube write op (playlists/playlistItems insert/update/delete).
|
||||
@@ -319,6 +319,24 @@ def user_sync_claim(db: Session, user_id: int) -> AbstractContextManager[bool]:
|
||||
return _advisory_claim(db, _USER_SYNC_LOCK_NS, user_id)
|
||||
|
||||
|
||||
def _mirror_listing(yt: YouTubeClient, ytid: str, count: object, pl_id: int) -> list[str] | None:
|
||||
"""A mirror's video ids from YouTube, or None when there is nothing to believe: the list was
|
||||
deleted between the pull's listing and this read (the next pull drops the mirror as vanished —
|
||||
not a reason to fail every other list, E32 review round 2), or YouTube listed no items for a
|
||||
list it counts as non-empty (`blank_listing`)."""
|
||||
try:
|
||||
ids = list(yt.iter_my_playlist_video_ids(ytid))
|
||||
except YouTubeError as exc:
|
||||
if not is_playlist_gone(exc):
|
||||
raise
|
||||
log.info("YouTube playlist of %s vanished mid-pull; skipped", pl_id)
|
||||
return None
|
||||
if blank_listing(ids, count):
|
||||
log.warning("YouTube listed no items for non-empty playlist %s; kept", pl_id)
|
||||
return None
|
||||
return ids
|
||||
|
||||
|
||||
def pull_user_playlists(db: Session, user: User) -> dict:
|
||||
"""The mirror pull itself; the caller holds `user_sync_claim`."""
|
||||
synced = 0
|
||||
@@ -377,9 +395,9 @@ def pull_user_playlists(db: Session, user: User) -> dict:
|
||||
# The user is pushing or re-pulling this very list: it writes the same rows.
|
||||
# The next scheduled run refreshes it.
|
||||
continue
|
||||
ids = list(yt.iter_my_playlist_video_ids(ytid))
|
||||
if blank_listing(ids, p.get("item_count")):
|
||||
log.warning("YouTube listed no items for non-empty playlist %s; kept", pl.id)
|
||||
ids = _mirror_listing(yt, ytid, p.get("item_count"), pl.id)
|
||||
if ids is None:
|
||||
db.rollback() # a mirror this run just created is not kept empty either
|
||||
continue
|
||||
# Ingest first — it commits — so the row lock below is held from the dirty check
|
||||
# to the commit that replaces the items.
|
||||
|
||||
Reference in New Issue
Block a user