feat(search): ephemeral results, count selector, blocklist, new-first ordering (backend)
- Live-search results stay ephemeral: the discovery-cleanup job now also reclaims un-kept via_search videos (no watch state / not playlisted / channel not subscribed) after a grace period (search_grace_days), and POST /api/search/clear discards a given result set 'as if never added' (drops the user's search-finds + deletes the now-orphaned, un-kept videos). Admin POST /api/admin/purge-discovery runs it on demand (grace 0). - Count selector: GET /api/search/youtube gains a 'limit' — the free scrape source pages through continuations until that many results are gathered (no manual load-more); the API source stays one ≤50 page (cost). - Per-user channel blocklist (migration 0034 blocked_channels): block/unblock/list endpoints; blocked channels' videos are dropped from live search before ingest and hidden from the feed / Library / explore / channel page; explore refuses a blocked channel. - New-first ordering: results you already have (subscribed channel, watched/in-progress/saved/ playlisted) sink below genuinely-new discoveries, preserving relevance within each group.
This commit is contained in:
parent
77707a3cbe
commit
f27f31b6db
10 changed files with 396 additions and 102 deletions
|
|
@ -143,3 +143,38 @@ def purge_unkept_explored(db: Session) -> dict:
|
|||
"videos_deleted": videos_deleted,
|
||||
"channels_deleted": channels_deleted,
|
||||
}
|
||||
|
||||
|
||||
def purge_unkept_search(db: Session, grace_days: int | None = None) -> dict:
|
||||
"""Remove live-search result videos nobody kept, after a grace period — "as if never added".
|
||||
|
||||
A `via_search` video is KEPT (and spared) when anyone has watched / in-progress / hidden it
|
||||
(a VideoState), saved/playlisted it (a PlaylistItem), or subscribes to its channel (it's then
|
||||
organically owned). A bare SearchFind does NOT count — it's the ephemeral "shown in search"
|
||||
marker itself — so a result you only glanced at is reclaimed. Deleting the video cascades its
|
||||
SearchFinds. `grace_days=0` purges immediately (the admin "purge now" path); None uses config.
|
||||
0 quota."""
|
||||
grace = sysconfig.get_int(db, "search_grace_days") if grace_days is None else grace_days
|
||||
cutoff = _now() - timedelta(days=grace)
|
||||
state_exists = select(VideoState.id).where(VideoState.video_id == Video.id).exists()
|
||||
pl_exists = select(PlaylistItem.id).where(PlaylistItem.video_id == Video.id).exists()
|
||||
sub_exists = (
|
||||
select(Subscription.id).where(Subscription.channel_id == Video.channel_id).exists()
|
||||
)
|
||||
deleted = db.execute(
|
||||
delete(Video).where(
|
||||
Video.via_search.is_(True),
|
||||
Video.created_at < cutoff,
|
||||
~state_exists,
|
||||
~pl_exists,
|
||||
~sub_exists,
|
||||
)
|
||||
).rowcount
|
||||
db.commit()
|
||||
return {"search_videos_deleted": deleted}
|
||||
|
||||
|
||||
def purge_ephemeral(db: Session) -> dict:
|
||||
"""The scheduler's discovery-cleanup pass: reclaim un-kept explored channels AND un-kept
|
||||
live-search result videos in one run."""
|
||||
return {**purge_unkept_explored(db), **purge_unkept_search(db)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue