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.
This commit is contained in:
parent
44ef725a2e
commit
e91ded61bb
1 changed files with 44 additions and 22 deletions
|
|
@ -66,33 +66,49 @@ def _filtered_query(
|
||||||
include_shorts: bool,
|
include_shorts: bool,
|
||||||
include_live: bool,
|
include_live: bool,
|
||||||
show: str,
|
show: str,
|
||||||
|
scope: str = "my",
|
||||||
) -> tuple[Select, object]:
|
) -> tuple[Select, object]:
|
||||||
"""Build the feed query (joins + all WHERE filters), shared by /feed and /feed/count.
|
"""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)
|
state = aliased(VideoState)
|
||||||
status_expr = func.coalesce(state.status, "new")
|
status_expr = func.coalesce(state.status, "new")
|
||||||
position_expr = func.coalesce(state.position_seconds, 0)
|
position_expr = func.coalesce(state.position_seconds, 0)
|
||||||
|
|
||||||
query = (
|
query = select(
|
||||||
select(
|
Video.id,
|
||||||
Video.id,
|
Video.title,
|
||||||
Video.title,
|
Video.channel_id,
|
||||||
Video.channel_id,
|
Channel.title.label("channel_title"),
|
||||||
Channel.title.label("channel_title"),
|
Channel.thumbnail_url.label("channel_thumbnail"),
|
||||||
Channel.thumbnail_url.label("channel_thumbnail"),
|
Channel.handle.label("channel_handle"),
|
||||||
Channel.handle.label("channel_handle"),
|
Video.published_at,
|
||||||
Video.published_at,
|
Video.thumbnail_url,
|
||||||
Video.thumbnail_url,
|
Video.duration_seconds,
|
||||||
Video.duration_seconds,
|
Video.view_count,
|
||||||
Video.view_count,
|
Video.is_short,
|
||||||
Video.is_short,
|
Video.live_status,
|
||||||
Video.live_status,
|
status_expr.label("status"),
|
||||||
status_expr.label("status"),
|
position_expr.label("position_seconds"),
|
||||||
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).
|
# Only channels this user is subscribed to (and hasn't hidden).
|
||||||
.join(
|
query = query.join(
|
||||||
Subscription,
|
Subscription,
|
||||||
and_(
|
and_(
|
||||||
Subscription.channel_id == Video.channel_id,
|
Subscription.channel_id == Video.channel_id,
|
||||||
|
|
@ -100,7 +116,9 @@ def _filtered_query(
|
||||||
Subscription.hidden.is_(False),
|
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:
|
if channel_id:
|
||||||
|
|
@ -199,6 +217,7 @@ def _feed_params(
|
||||||
include_shorts: bool = False,
|
include_shorts: bool = False,
|
||||||
include_live: bool = False,
|
include_live: bool = False,
|
||||||
show: str = "unwatched",
|
show: str = "unwatched",
|
||||||
|
scope: str = "my",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
return {
|
return {
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
|
|
@ -214,6 +233,7 @@ def _feed_params(
|
||||||
"include_shorts": include_shorts,
|
"include_shorts": include_shorts,
|
||||||
"include_live": include_live,
|
"include_live": include_live,
|
||||||
"show": show,
|
"show": show,
|
||||||
|
"scope": scope,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -226,7 +246,8 @@ SORTS = {
|
||||||
"title": func.lower(Video.title).asc().nulls_last(),
|
"title": func.lower(Video.title).asc().nulls_last(),
|
||||||
"subscribers": Channel.subscriber_count.desc().nulls_last(),
|
"subscribers": Channel.subscriber_count.desc().nulls_last(),
|
||||||
# Your per-channel priority (set in the channel manager), newest first within a tier.
|
# 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)
|
query, _status = _filtered_query(db, user, **params)
|
||||||
if sort == "priority":
|
if sort == "priority":
|
||||||
query = query.order_by(
|
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:
|
else:
|
||||||
order = SORTS.get(sort)
|
order = SORTS.get(sort)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue