fix(catalog): a follow from YouTube keeps an explored channel, and the list counts only what can come

A subscription pulled in from YouTube left an explored channel flagged as an exploration's, so its
later videos came in hidden from the Library with their watched marks waiting for good; a subscribe
landing while an exploration's videos came in left those flagged too. One step, settle_explored,
now holds the rule by who follows the channel now: the subscribe here, the subscription import, the
watched-channel videos and the explore page all end with it. Migration 0116 clears what the gap
left behind.

The list's "may still grow" count no longer counts a channel it already shows from a stub row, and
the comments on the cleanup's keep rule and on the in-memory daily shares say what they do.
This commit is contained in:
2026-10-24 11:29:14 +02:00
parent 33dd9ff054
commit bc8fe2cc6c
11 changed files with 242 additions and 20 deletions
+38
View File
@@ -10,6 +10,7 @@ from sqlalchemy import select, update
from app import quota
from app.catalog import service as catalog_service
from app.catalog.models import BlockedChannel, Channel, ExploredChannel, Subscription, Video
from app.db import SessionLocal
from app.reader import queries, service, takeout
from app.reader.models import TakeoutChannelDismissal, TakeoutItem, TakeoutKind
from app.routes import feed as feed_routes
@@ -318,6 +319,18 @@ def test_naming_the_channels_is_tried_again_and_meanwhile_the_list_says_it_is_no
assert queries.watched_channels_pending(db, user) == 0
def test_a_listed_channel_still_to_be_described_is_not_counted_as_one_to_come(db, make_user):
user = make_user("stub@example.com")
# A stub the catalog made when one of its videos came in: a title, no details yet.
db.add(Channel(id=_ch(1), title="From a video"))
db.commit()
_watched(db, user, 1, [LONG] * 5)
assert queries.watched_channels_to_describe(db, user) == [_ch(1)]
assert _listed(db, user) == [(_ch(1), 5)]
# It is on the list already: "the list may grow" would promise one more that cannot come.
assert queries.watched_channels_pending(db, user) == 0
def test_the_lengths_leave_the_watch_later_lookups_their_own_share(
db, make_user, youtube, monkeypatch
):
@@ -400,6 +413,31 @@ def test_a_followed_channels_watched_videos_come_in_as_the_catalogs_own(
assert db.get(Channel, _ch(1)).from_explore is False
def test_a_follow_landing_while_the_videos_come_in_leaves_none_of_them_flagged(
db, make_user, as_user, monkeypatch
):
user, follower = make_user("racer@example.com"), make_user("late@example.com")
_channel(db, 1)
_watched(db, user, 1, [LONG] * 5)
fake = _fake_ensure(calls := [])
def ensure(session, yt, ids, *, explore=False):
# Someone subscribes, in a request of their own, while YouTube answers this one.
with SessionLocal() as other:
ch = other.get(Channel, _ch(1))
catalog_service.record_subscription(other, follower, ch, "sub-late")
other.commit()
fake(session, yt, ids, explore=explore)
monkeypatch.setattr(feed_routes, "ensure_videos", ensure)
monkeypatch.setattr(feed_routes.quota, "backfill_reserve_reached", lambda: False)
assert as_user(user).post(f"/api/takeout/channels/{_ch(1)}/videos").status_code == 200
assert calls[0][1] is True # asked as the exploration's
db.expire_all()
flagged = db.scalars(select(Video.id).where(Video.via_explore.is_(True))).all()
assert (db.get(Channel, _ch(1)).from_explore, flagged) == (False, [])
def test_the_cleanup_keeps_a_channel_a_watch_history_lists_and_reclaims_its_videos(db, make_user):
user = make_user("kept@example.com")
_channel(db, 1)