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)
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 4e890b5..ff3dfcf 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -39,6 +39,7 @@ const DEFAULT_FILTERS: FeedFilters = {
tagMode: "or",
q: "",
sort: "newest",
+ scope: "my",
includeNormal: true,
includeShorts: false,
includeLive: false,
diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx
index 09a05cf..7380bb0 100644
--- a/frontend/src/components/Feed.tsx
+++ b/frontend/src/components/Feed.tsx
@@ -148,7 +148,9 @@ export default function Feed({
if (query.isLoading) return
{t("feed.loading")}
;
if (query.isError) return
{t("feed.loadError")}
;
if (items.length === 0) {
- if (!canRead)
+ // No YouTube connection and looking at their own (empty) subscriptions: offer both
+ // connecting YouTube and just browsing the shared library (no read scope needed).
+ if (!canRead && filters.scope === "my")
return (