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:
2026-10-04 04:16:52 +02:00
parent 4191189b4c
commit 79704b0001
7 changed files with 117 additions and 19 deletions
+33 -2
View File
@@ -9,6 +9,7 @@ import pytest
from app.auth import has_read_scope
from app.models import OAuthToken
from app.sync.explore import explore_ingest_page
from app.youtube import client as yt
from app.youtube.client import YouTubeError, refusal_reason
@@ -27,8 +28,8 @@ from app.youtube.client import YouTubeError, refusal_reason
(YouTubeError("No refresh token; user must re-authenticate"), "reconnect"),
(YouTubeError("m", reason="transport"), "unavailable"),
(YouTubeError("m", reason="backendError", path="subscriptions"), "unavailable"),
# A playlist deleted on YouTube: neither a retry nor a reconnect helps (it said "reconnect").
(YouTubeError("m", reason="playlistNotFound", path="playlists"), "gone"),
# A missing playlist the user never named (a channel's uploads): nothing they can act on.
(YouTubeError("m", reason="playlistNotFound", path="playlistItems"), "unavailable"),
(YouTubeError("m", reason="emptyAnswer", path="playlistItems"), "unavailable"),
],
)
@@ -36,6 +37,36 @@ def test_a_failure_is_sorted_by_what_the_user_can_do(exc, reason):
assert refusal_reason(exc) == reason
def test_a_missing_playlist_is_gone_only_on_a_route_about_that_playlist():
# A playlist deleted on YouTube: neither a retry nor a reconnect helps (it said "reconnect").
# Said only where the user picked that one list — anywhere else "this playlist no longer
# exists" names nothing on their screen (E32 review round 2).
exc = YouTubeError("m", reason="playlistNotFound", path="playlists")
assert refusal_reason(exc, playlist=True) == "gone"
assert refusal_reason(exc) == "unavailable"
assert refusal_reason(YouTubeError("m", reason="quotaExceeded"), playlist=True) == "quota"
def test_a_channel_without_uploads_explores_as_an_empty_page():
# YouTube answers a channel's uploads playlist as not found when the channel has no uploads.
class _Yt:
def get_playlist_items(self, *_):
raise YouTubeError("m", reason="playlistNotFound", path="playlistItems")
channel = SimpleNamespace(id="UCempty", uploads_playlist_id="UUempty")
assert explore_ingest_page(None, _Yt(), channel) == {"ingested": 0, "next_page_token": None}
def test_any_other_explore_failure_still_raises():
class _Yt:
def get_playlist_items(self, *_):
raise YouTubeError("m", reason="quotaExceeded", path="playlistItems")
channel = SimpleNamespace(id="UCx", uploads_playlist_id="UUx")
with pytest.raises(YouTubeError):
explore_ingest_page(None, _Yt(), channel)
@pytest.mark.parametrize(
("route", "target"),
[