feat(feed): /api/facets endpoint for contextual tag counts
Add a facets endpoint that returns per-tag channel counts for the current filter context (scope, channel, date, content type, search, watch state, and the other category's tags). Each category is counted with its own selections ignored — standard drill-down faceting — by a new exclude_tag_category param threaded into _filtered_query, so selecting one topic doesn't zero out the other topics. Count is distinct channels with a matching video, keeping the channel-count chip semantics. Reuses the feed's filter query so both stay in lockstep.
This commit is contained in:
parent
cf2b49a179
commit
17e64156b8
1 changed files with 41 additions and 0 deletions
|
|
@ -67,6 +67,7 @@ def _filtered_query(
|
||||||
include_live: bool,
|
include_live: bool,
|
||||||
show: str,
|
show: str,
|
||||||
scope: str = "my",
|
scope: str = "my",
|
||||||
|
exclude_tag_category: str | None = None,
|
||||||
) -> 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.
|
||||||
|
|
@ -153,6 +154,11 @@ def _filtered_query(
|
||||||
by_category: dict[str, list[int]] = {}
|
by_category: dict[str, list[int]] = {}
|
||||||
for tag_id, category in cat_rows:
|
for tag_id, category in cat_rows:
|
||||||
by_category.setdefault(category, []).append(tag_id)
|
by_category.setdefault(category, []).append(tag_id)
|
||||||
|
# Facet counting drops the category being counted so its own chips don't zero each
|
||||||
|
# other out (standard drill-down faceting: a category's count ignores its own
|
||||||
|
# selections but still honours the other categories' filters).
|
||||||
|
if exclude_tag_category:
|
||||||
|
by_category.pop(exclude_tag_category, None)
|
||||||
visible = or_(ChannelTag.user_id.is_(None), ChannelTag.user_id == user.id)
|
visible = or_(ChannelTag.user_id.is_(None), ChannelTag.user_id == user.id)
|
||||||
for category, ids in by_category.items():
|
for category, ids in by_category.items():
|
||||||
if category == "topic" and tag_mode == "and" and len(ids) > 1:
|
if category == "topic" and tag_mode == "and" and len(ids) > 1:
|
||||||
|
|
@ -294,6 +300,41 @@ def get_feed_count(
|
||||||
return {"count": total or 0}
|
return {"count": total or 0}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/facets")
|
||||||
|
def get_facets(
|
||||||
|
params: dict = Depends(_feed_params),
|
||||||
|
user: User = Depends(current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
) -> dict:
|
||||||
|
"""Per-tag channel counts for the *current* filter context, so the sidebar can show
|
||||||
|
live counts and drop chips that no longer match anything. Each category is counted with
|
||||||
|
every other filter applied (scope, channel, date, content type, search, watch state,
|
||||||
|
and the other category's tags) but its own selections ignored — standard drill-down
|
||||||
|
faceting, so selecting one topic doesn't zero out the rest of the topics.
|
||||||
|
|
||||||
|
The count is the number of distinct channels that have at least one video in the current
|
||||||
|
view, matching the existing channel-count chip semantics. JSON object keys are strings
|
||||||
|
(tag ids)."""
|
||||||
|
visible = or_(ChannelTag.user_id.is_(None), ChannelTag.user_id == user.id)
|
||||||
|
counts: dict[int, int] = {}
|
||||||
|
for category in ("language", "topic"):
|
||||||
|
base, _status = _filtered_query(
|
||||||
|
db, user, **{**params, "exclude_tag_category": category}
|
||||||
|
)
|
||||||
|
channels = base.with_only_columns(Video.channel_id).distinct().subquery()
|
||||||
|
rows = db.execute(
|
||||||
|
select(ChannelTag.tag_id, func.count(func.distinct(ChannelTag.channel_id)))
|
||||||
|
.select_from(channels)
|
||||||
|
.join(ChannelTag, ChannelTag.channel_id == channels.c.channel_id)
|
||||||
|
.join(Tag, and_(Tag.id == ChannelTag.tag_id, Tag.category == category))
|
||||||
|
.where(visible)
|
||||||
|
.group_by(ChannelTag.tag_id)
|
||||||
|
).all()
|
||||||
|
for tag_id, count in rows:
|
||||||
|
counts[tag_id] = count
|
||||||
|
return {"counts": counts}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/videos/{video_id}/state")
|
@router.post("/videos/{video_id}/state")
|
||||||
def set_video_state(
|
def set_video_state(
|
||||||
video_id: str,
|
video_id: str,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue