fix(sync): an empty answer the remote's own count contradicts is not believed, one level down
E32 review round 1 (B1, B2, B3, B8). - Plex watch reconcile: a section the server SAYS is empty (totalSize 0, nothing listed) is a complete listing. It read as "cut short" forever, so one empty library stopped un-watches from ever syncing. A listing with no total at all stays incomplete (the client now returns None for it instead of 0), and a whole scan that listed nothing still clears nothing. Tests now cover completeness across two sections, both orders. - Plex collections: no members from a collection whose listing counts some (childCount) keeps the membership we hold, in the full sync and in the targeted resync after a write. - YouTube playlists: no items for a playlist whose listing counts some (itemCount) keeps the mirror in the bulk pull, and is refused as retryable in "reset to YouTube" and in the push plan/merge, where it read as "YouTube deleted them all". The rule sits beside vanished(). - A playlist deleted on YouTube answered "reconnect your account"; it now says it is gone (errors.youtube.gone, HU+EN). - The pure 3-way merge moved to sync/playlist_merge.py; playlists.py is back under 800 lines and off the size list.
This commit is contained in:
@@ -9,7 +9,7 @@ from sqlalchemy.orm import Session
|
||||
from app import logs
|
||||
from app.models import Channel, Subscription, User
|
||||
from app.utils import normalize_description
|
||||
from app.youtube.client import YouTubeClient, best_thumbnail
|
||||
from app.youtube.client import YouTubeClient, YouTubeError, best_thumbnail
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
@@ -115,7 +115,8 @@ def should_prune(fetched_count: int, existing_count: int) -> bool:
|
||||
has — taking each row's `deep_requested` flag with it — in a single run. Someone who genuinely
|
||||
unsubscribed from everything simply keeps their rows until a sync that returns something.
|
||||
|
||||
Pure so it can be tested without a database; `import_subscriptions` is the only caller."""
|
||||
Pure so it can be tested without a database; its callers are `import_subscriptions` and, through
|
||||
`vanished`, every mirror that drops what the remote no longer lists."""
|
||||
return fetched_count > 0 or existing_count == 0
|
||||
|
||||
|
||||
@@ -128,6 +129,23 @@ def vanished[K, V](existing: dict[K, V], listed: Collection[K]) -> list[V]:
|
||||
return [row for key, row in existing.items() if key not in listed]
|
||||
|
||||
|
||||
def blank_listing(listed: Collection, count: object) -> bool:
|
||||
"""The same rule one level down: no members from a container whose own listing counts some (a
|
||||
playlist's `itemCount`) is a blip, not a list emptied. Replacing or merging on it deleted every
|
||||
mirrored item and reset the merge baseline (E32 review)."""
|
||||
return not listed and isinstance(count, int) and count > 0
|
||||
|
||||
|
||||
def refuse_blank_listing(listed: Collection, count: object) -> None:
|
||||
"""`blank_listing` for a user-started pull or push: refused as retryable ("try again")."""
|
||||
if blank_listing(listed, count):
|
||||
raise YouTubeError(
|
||||
"YouTube listed no items for a playlist it counts as non-empty",
|
||||
reason="emptyAnswer",
|
||||
path="playlistItems",
|
||||
)
|
||||
|
||||
|
||||
def import_subscriptions(db: Session, user: User) -> dict:
|
||||
"""Pull all subscriptions for `user`, upsert channels + subscription links, prune
|
||||
channels the user has unsubscribed from on YouTube, and fetch channel details for
|
||||
|
||||
Reference in New Issue
Block a user