From e91ded61bbbc7f0a4aadec0d815e8e28fd38f916 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Mon, 15 Jun 2026 04:06:14 +0200 Subject: [PATCH] feat(feed): add scope=all to browse the whole shared catalog The feed query was always scoped to the user's own non-hidden subscriptions via an INNER JOIN on subscriptions. Add a scope param: scope=my (default) keeps that behaviour; scope=all LEFT-joins the subscription instead, so every video in the shared catalog shows while per-channel priority still resolves for channels the user is subscribed to. Per-user watch state stays private via the VideoState outer join in both modes. The priority sort is made null-safe (coalesce to 0) since unsubscribed channels have no subscription row in all-mode. --- backend/app/routes/feed.py | 66 +++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/backend/app/routes/feed.py b/backend/app/routes/feed.py index 6134e23..fc0b8b3 100644 --- a/backend/app/routes/feed.py +++ b/backend/app/routes/feed.py @@ -66,33 +66,49 @@ def _filtered_query( include_shorts: bool, include_live: bool, show: str, + scope: str = "my", ) -> tuple[Select, object]: """Build the feed query (joins + all WHERE filters), shared by /feed and /feed/count. - Returns the column-bearing select plus the watch-status expression for sorting.""" + Returns the column-bearing select plus the watch-status expression for sorting. + + `scope="my"` (default) restricts the feed to the user's own non-hidden subscriptions. + `scope="all"` shows every video in the shared catalog (any user's ingested channels); + the subscription is then only LEFT-joined so per-channel priority still resolves for + channels the user happens to be subscribed to. Per-user watch state stays private in + either mode via the VideoState outer join.""" state = aliased(VideoState) status_expr = func.coalesce(state.status, "new") position_expr = func.coalesce(state.position_seconds, 0) - query = ( - select( - Video.id, - Video.title, - Video.channel_id, - Channel.title.label("channel_title"), - Channel.thumbnail_url.label("channel_thumbnail"), - Channel.handle.label("channel_handle"), - Video.published_at, - Video.thumbnail_url, - Video.duration_seconds, - Video.view_count, - Video.is_short, - Video.live_status, - status_expr.label("status"), - position_expr.label("position_seconds"), + query = select( + Video.id, + Video.title, + Video.channel_id, + Channel.title.label("channel_title"), + Channel.thumbnail_url.label("channel_thumbnail"), + Channel.handle.label("channel_handle"), + Video.published_at, + Video.thumbnail_url, + Video.duration_seconds, + Video.view_count, + Video.is_short, + Video.live_status, + status_expr.label("status"), + position_expr.label("position_seconds"), + ).join(Channel, Channel.id == Video.channel_id) + + if scope == "all": + # Whole shared catalog; subscription is optional (only for priority sort). + query = query.outerjoin( + Subscription, + and_( + Subscription.channel_id == Video.channel_id, + Subscription.user_id == user.id, + ), ) - .join(Channel, Channel.id == Video.channel_id) + else: # Only channels this user is subscribed to (and hasn't hidden). - .join( + query = query.join( Subscription, and_( Subscription.channel_id == Video.channel_id, @@ -100,7 +116,9 @@ def _filtered_query( Subscription.hidden.is_(False), ), ) - .outerjoin(state, and_(state.video_id == Video.id, state.user_id == user.id)) + + query = query.outerjoin( + state, and_(state.video_id == Video.id, state.user_id == user.id) ) if channel_id: @@ -199,6 +217,7 @@ def _feed_params( include_shorts: bool = False, include_live: bool = False, show: str = "unwatched", + scope: str = "my", ) -> dict: return { "tags": tags, @@ -214,6 +233,7 @@ def _feed_params( "include_shorts": include_shorts, "include_live": include_live, "show": show, + "scope": scope, } @@ -226,7 +246,8 @@ SORTS = { "title": func.lower(Video.title).asc().nulls_last(), "subscribers": Channel.subscriber_count.desc().nulls_last(), # Your per-channel priority (set in the channel manager), newest first within a tier. - "priority": Subscription.priority.desc(), + # coalesce keeps it null-safe in "all" scope where unsubscribed channels have no row. + "priority": func.coalesce(Subscription.priority, 0).desc(), } @@ -243,7 +264,8 @@ def get_feed( query, _status = _filtered_query(db, user, **params) if sort == "priority": query = query.order_by( - Subscription.priority.desc(), Video.published_at.desc().nulls_last() + func.coalesce(Subscription.priority, 0).desc(), + Video.published_at.desc().nulls_last(), ) else: order = SORTS.get(sort)