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:
@@ -114,7 +114,7 @@ _DEAD_GRANT = "invalid_grant"
|
||||
|
||||
# What a refused YouTube action means to the person who asked for it — the three things they can
|
||||
# act on differently: wait for tomorrow's budget, reconnect the account, or try again later.
|
||||
YouTubeRefusal = Literal["quota", "reconnect", "unavailable"]
|
||||
YouTubeRefusal = Literal["quota", "reconnect", "unavailable", "gone"]
|
||||
|
||||
|
||||
def refusal_reason(exc: YouTubeError) -> YouTubeRefusal:
|
||||
@@ -126,6 +126,9 @@ def refusal_reason(exc: YouTubeError) -> YouTubeRefusal:
|
||||
the account would still read as connected, with no connect button to press."""
|
||||
if is_quota_refusal(exc):
|
||||
return "quota"
|
||||
if exc.reason == "playlistNotFound":
|
||||
# The list itself is gone (deleted on YouTube): neither a retry nor a reconnect helps.
|
||||
return "gone"
|
||||
if exc.path == "oauth2/token":
|
||||
return "reconnect" if exc.reason == _DEAD_GRANT else "unavailable"
|
||||
if not exc.path and exc.reason != "transport":
|
||||
@@ -423,16 +426,22 @@ class YouTubeClient:
|
||||
return resp.json() if resp.content else {}
|
||||
|
||||
def get_playlist_snippet(self, playlist_id: str) -> dict | None:
|
||||
"""The snippet (title/description) of one playlist, or None if it no longer
|
||||
exists. 1 unit (OAuth, so private playlists work)."""
|
||||
"""The snippet (title/description) of one playlist plus its `item_count` (what an empty
|
||||
item listing is checked against), or None if it no longer exists. 1 unit (OAuth, so
|
||||
private playlists work)."""
|
||||
data = self._get(
|
||||
"playlists",
|
||||
{"part": "snippet", "id": playlist_id, "maxResults": 1},
|
||||
{"part": "snippet,contentDetails", "id": playlist_id, "maxResults": 1},
|
||||
cost=1,
|
||||
allow_key=False,
|
||||
)
|
||||
items = data.get("items", [])
|
||||
return items[0].get("snippet") if items else None
|
||||
if not items:
|
||||
return None
|
||||
return {
|
||||
**(items[0].get("snippet") or {}),
|
||||
"item_count": items[0].get("contentDetails", {}).get("itemCount"),
|
||||
}
|
||||
|
||||
def update_playlist_title(self, playlist_id: str, title: str) -> None:
|
||||
"""Rename a playlist on YouTube. 50 units."""
|
||||
|
||||
Reference in New Issue
Block a user