The catalog prune and the subscription import have long refused to reconcile against an empty listing that contradicts what we hold (should_prune: a 200 with no items is a blip far more often than a user who removed everything). Two mirrors did not: the YouTube playlist mirrors (dropped every mirror) and the Plex collections (dropped every collection with its user-set `editable` flag, and every member's keys — the guard landed in plex/sync.py with the previous commit). Both now hold the same rule; test_empty_listing_guards covers each way (seen red). sync/maintenance (19% covered) gets a lifecycle test: recover, grace, delete + one notification per user, live stream kept, abandoned premiere. It found the rolling revalidation re-fetching the same videos until `checked` reached the batch (15000 by default) whenever fewer videos were available — quota spent on answers it already had; a run now skips what it has checked.
112 lines
3.8 KiB
Python
112 lines
3.8 KiB
Python
"""An empty answer is not an emptied library (E32 S2).
|
|
|
|
Every mirror here reconciles: what the remote no longer lists is removed locally. The catalog
|
|
(`_prune_stale`) and the subscriptions (`should_prune`) had long refused to do that on an empty
|
|
listing that contradicts what we hold — a 200 with no items is a blip far more often than a user
|
|
who removed everything. Two mirrors did not: the Plex collections (a row carries the user-set
|
|
`editable` flag, and every member its keys) and the YouTube playlist mirrors. These hold both.
|
|
"""
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.models import Playlist, PlexCollection, PlexItem, PlexLibrary
|
|
from app.plex import sync as plex_sync
|
|
from app.sync import playlists as plsync
|
|
|
|
|
|
class _Plex:
|
|
def __init__(self, collections: list[dict], children: dict[str, list[dict]]):
|
|
self._collections = collections
|
|
self._children = children
|
|
|
|
def collections(self, section_key):
|
|
return self._collections
|
|
|
|
def collection_children(self, rk):
|
|
return self._children.get(rk, [])
|
|
|
|
|
|
def _library_with_a_collection(db):
|
|
lib = PlexLibrary(plex_key="4", title="Movies", kind="movie")
|
|
db.add(lib)
|
|
db.flush()
|
|
db.add(PlexCollection(rating_key="c1", library_id=lib.id, title="Bond", editable=True))
|
|
movie = PlexItem(
|
|
rating_key="m1", library_id=lib.id, kind="movie", title="Dr. No", collection_keys=["c1"]
|
|
)
|
|
db.add(movie)
|
|
db.commit()
|
|
return lib, movie
|
|
|
|
|
|
def test_an_empty_collection_listing_keeps_the_collections_and_their_members(db):
|
|
lib, movie = _library_with_a_collection(db)
|
|
stats = {"collections": 0}
|
|
plex_sync._sync_collections(db, _Plex([], {}), lib, stats)
|
|
db.commit()
|
|
db.expire_all()
|
|
col = db.execute(select(PlexCollection).where(PlexCollection.rating_key == "c1")).scalar_one()
|
|
assert col.editable is True
|
|
assert db.get(PlexItem, movie.id).collection_keys == ["c1"]
|
|
|
|
|
|
def test_a_listing_with_something_in_it_still_prunes_what_it_no_longer_has(db):
|
|
lib, movie = _library_with_a_collection(db)
|
|
stats = {"collections": 0}
|
|
other = {"ratingKey": "c2", "title": "Alien"}
|
|
plex_sync._sync_collections(db, _Plex([other], {"c2": []}), lib, stats)
|
|
db.commit()
|
|
db.expire_all()
|
|
keys = set(db.execute(select(PlexCollection.rating_key)).scalars())
|
|
assert keys == {"c2"}
|
|
assert db.get(PlexItem, movie.id).collection_keys is None
|
|
|
|
|
|
class _YT:
|
|
def __init__(self, playlists: list[dict]):
|
|
self._playlists = playlists
|
|
|
|
def __call__(self, *_):
|
|
return self
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_):
|
|
return False
|
|
|
|
def iter_my_playlists(self):
|
|
return self._playlists
|
|
|
|
def iter_playlist_items_with_ids(self, _ytid):
|
|
return []
|
|
|
|
def iter_my_playlist_video_ids(self, _ytid):
|
|
return []
|
|
|
|
|
|
def test_an_empty_playlist_answer_keeps_the_youtube_mirrors(db, make_user, monkeypatch):
|
|
user = make_user()
|
|
db.add(Playlist(user_id=user.id, name="Mixes", source="youtube", yt_playlist_id="PL1"))
|
|
db.commit()
|
|
monkeypatch.setattr(plsync, "YouTubeClient", _YT([]))
|
|
plsync.pull_user_playlists(db, user)
|
|
db.expire_all()
|
|
names = db.execute(select(Playlist.name).where(Playlist.user_id == user.id)).scalars().all()
|
|
assert "Mixes" in names
|
|
|
|
|
|
def test_a_playlist_answer_with_something_in_it_still_drops_a_vanished_mirror(
|
|
db, make_user, monkeypatch
|
|
):
|
|
user = make_user()
|
|
db.add(Playlist(user_id=user.id, name="Mixes", source="youtube", yt_playlist_id="PL1"))
|
|
db.commit()
|
|
monkeypatch.setattr(plsync, "YouTubeClient", _YT([{"id": "PL2", "title": "New"}]))
|
|
plsync.pull_user_playlists(db, user)
|
|
db.expire_all()
|
|
ytids = set(
|
|
db.execute(select(Playlist.yt_playlist_id).where(Playlist.source == "youtube")).scalars()
|
|
)
|
|
assert "PL1" not in ytids
|