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:
@@ -163,6 +163,33 @@ def test_a_playlist_answer_with_something_in_it_still_drops_a_vanished_mirror(
|
||||
assert "PL1" not in ytids
|
||||
|
||||
|
||||
class _YTOneGone(_YT):
|
||||
"""A list deleted on YouTube between the listing and the read of its items."""
|
||||
|
||||
def iter_my_playlist_video_ids(self, ytid):
|
||||
if ytid == "PLgone":
|
||||
raise YouTubeError("m", reason="playlistNotFound", path="playlistItems")
|
||||
return []
|
||||
|
||||
|
||||
def test_a_list_deleted_mid_pull_is_skipped_and_the_rest_still_sync(db, make_user, monkeypatch):
|
||||
# Not "this playlist no longer exists" for a bulk pull the user never named a list in, and not
|
||||
# a failure for every other list either (E32 review round 2): the next pull drops the mirror.
|
||||
user = make_user()
|
||||
db.commit()
|
||||
listing = [
|
||||
{"id": "PLgone", "title": "Gone", "item_count": 3},
|
||||
{"id": "PL2", "title": "Kept", "item_count": 0},
|
||||
]
|
||||
monkeypatch.setattr(plsync, "YouTubeClient", _YTOneGone(listing))
|
||||
assert plsync.pull_user_playlists(db, user) == {"synced": 1}
|
||||
db.expire_all()
|
||||
ytids = set(
|
||||
db.execute(select(Playlist.yt_playlist_id).where(Playlist.user_id == user.id)).scalars()
|
||||
)
|
||||
assert ytids == {"PL2"}
|
||||
|
||||
|
||||
def _mirror_with_an_item(db, user, **kw):
|
||||
db.add(Channel(id="UC_pl", title="C"))
|
||||
db.flush()
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user