feat(search): live YouTube search backend
Add a live YouTube search that materialises results into the shared catalog so they render with the normal feed cards + in-app player and gain per-user state. - YouTubeClient.search_videos(): search.list (100 units), embeddable-only, returns flat stubs + nextPageToken; surfaces liveBroadcastContent for live filtering. - routes/search.py GET /api/search/youtube: require_human + per-user daily cap (search_daily_limit_per_user, default 70) + can_spend pre-check (429 on either); drops live/upcoming, upserts channel stubs (channels.list) + video stubs, enriches (videos.list), runs the youtube.com/shorts probe, then excludes Shorts/live and returns feed cards in relevance order with the YouTube pageToken as the cursor. - Provenance: videos.via_search / channels.from_search (migration 0028) flag search-discovered rows; the feed hides them from the Library (scope=all) by default via exclude_search_discovered, leaving the Mine feed untouched. - quota.actions_today() counts a user's per-action events today for the cap; only the search.list call is attributed VIDEOS_SEARCH so the counter is exactly 1 per search.
This commit is contained in:
parent
b1ed706cab
commit
9b1bdb6b42
9 changed files with 341 additions and 0 deletions
|
|
@ -36,6 +36,7 @@ class QuotaAction:
|
|||
VIDEOS_BACKFILL_FULL = "videos_backfill_full"
|
||||
VIDEOS_ENRICH = "videos_enrich"
|
||||
VIDEOS_LOOKUP = "videos_lookup"
|
||||
VIDEOS_SEARCH = "videos_search"
|
||||
CHANNELS_DISCOVER = "channels_discover"
|
||||
CHANNELS_SUBSCRIBE = "channels_subscribe"
|
||||
CHANNELS_UNSUBSCRIBE = "channels_unsubscribe"
|
||||
|
|
@ -109,6 +110,26 @@ def can_spend(db: Session, units: int) -> bool:
|
|||
return remaining_today(db) >= units
|
||||
|
||||
|
||||
def actions_today(db: Session, user_id: int, action: str) -> int:
|
||||
"""How many quota events of `action` this user has logged so far in the current Pacific
|
||||
day — for per-user, per-action daily caps (e.g. the live-search limit). Counts events,
|
||||
not units, so it only works for actions charged exactly once per user action."""
|
||||
from sqlalchemy import func, select # local import: keep the module's import head lean
|
||||
|
||||
return (
|
||||
db.scalar(
|
||||
select(func.count())
|
||||
.select_from(QuotaEvent)
|
||||
.where(
|
||||
QuotaEvent.user_id == user_id,
|
||||
QuotaEvent.action == action,
|
||||
QuotaEvent.created_at >= pacific_day_start_utc(),
|
||||
)
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
|
||||
def record_usage(db: Session, units: int) -> None:
|
||||
"""Atomically add `units` to today's counter (upsert) and log an attribution event."""
|
||||
if units <= 0:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue