feat(filters): filter the feed by several channels at once (OR)
Pick any number of channels; they OR together and every other filter applies across the lot — "the last week's unwatched from these three". `channelId`/`channelName` are replaced outright by `channelIds: string[]`, no legacy reader (standing rule: no back-compat unless asked — a saved view written against the old field simply loses its channel filter). The wire and the share URL move with it: repeated `channel_ids` params, `?channel=a,b`. Backend ORs via `Video.channel_id.in_()`; the count endpoint shares the same params object. Two things the migration exposed: - SavedViewsWidget fed DB blobs straight into serializers that now index an array — the FeedFilters type says nothing about what an older build wrote, so the blob is coerced where it enters. It white-screened the app before this. - Chips carried channelName to label themselves. Without it they resolve names from the channel cache, so they now fall back to the id, not a shared "This channel" that would render N identical chips while the cache loads.
This commit is contained in:
@@ -116,7 +116,7 @@ def _filtered_query(
|
||||
*,
|
||||
tags: list[int],
|
||||
tag_mode: str,
|
||||
channel_id: str | None,
|
||||
channel_ids: list[str],
|
||||
q: str | None,
|
||||
min_duration: int | None,
|
||||
max_duration: int | None,
|
||||
@@ -186,7 +186,7 @@ def _filtered_query(
|
||||
# opened an un-subscribed channel's page) is visible only to users who explored or
|
||||
# subscribe to its channel, so one user's curiosity never leaks into everyone else's
|
||||
# catalog. Applied in BOTH scopes; the channel page reaches its ephemeral videos by
|
||||
# asking for scope=all + channel_id (the channel is in this accessible set).
|
||||
# asking for scope=all + channel_ids (the channel is in this accessible set).
|
||||
accessible_explore = (
|
||||
select(Subscription.channel_id).where(Subscription.user_id == user.id)
|
||||
).union(
|
||||
@@ -227,8 +227,9 @@ def _filtered_query(
|
||||
else: # "organic" (default): subscriptions only
|
||||
query = query.where(subscribed)
|
||||
|
||||
if channel_id:
|
||||
query = query.where(Video.channel_id == channel_id)
|
||||
if channel_ids:
|
||||
# OR across the picked channels; every other filter still applies on top.
|
||||
query = query.where(Video.channel_id.in_(channel_ids))
|
||||
if min_duration is not None:
|
||||
query = query.where(Video.duration_seconds >= min_duration)
|
||||
if max_duration is not None:
|
||||
@@ -354,7 +355,7 @@ def _to_tsquery_str(q: str) -> str | None:
|
||||
def _feed_params(
|
||||
tags: list[int] = Query(default=[]),
|
||||
tag_mode: str = "or",
|
||||
channel_id: str | None = None,
|
||||
channel_ids: list[str] = Query(default=[]),
|
||||
q: str | None = None,
|
||||
min_duration: int | None = None,
|
||||
max_duration: int | None = None,
|
||||
@@ -371,7 +372,7 @@ def _feed_params(
|
||||
return {
|
||||
"tags": tags,
|
||||
"tag_mode": tag_mode,
|
||||
"channel_id": channel_id,
|
||||
"channel_ids": channel_ids,
|
||||
"q": q,
|
||||
"min_duration": min_duration,
|
||||
"max_duration": max_duration,
|
||||
|
||||
Reference in New Issue
Block a user