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.
46 lines
1.6 KiB
Python
46 lines
1.6 KiB
Python
"""live YouTube search: provenance flags
|
|
|
|
Revision ID: 0028_live_search_provenance
|
|
Revises: 0027_message_e2ee
|
|
Create Date: 2026-06-29
|
|
|
|
Adds provenance flags so videos (and channels) that entered the catalog only because a live
|
|
YouTube search surfaced them can be told apart from organically-synced content and hidden from
|
|
the Library by default:
|
|
- videos.via_search — this video first arrived via search
|
|
- channels.from_search — this channel exists only because search surfaced one of its videos
|
|
Both default false (every existing row is organic), indexed for the feed's exclude filter.
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "0028_live_search_provenance"
|
|
down_revision: Union[str, None] = "0027_message_e2ee"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.add_column(
|
|
"videos",
|
|
sa.Column(
|
|
"via_search", sa.Boolean(), nullable=False, server_default="false"
|
|
),
|
|
)
|
|
op.create_index("ix_videos_via_search", "videos", ["via_search"])
|
|
op.add_column(
|
|
"channels",
|
|
sa.Column(
|
|
"from_search", sa.Boolean(), nullable=False, server_default="false"
|
|
),
|
|
)
|
|
op.create_index("ix_channels_from_search", "channels", ["from_search"])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("ix_channels_from_search", table_name="channels")
|
|
op.drop_column("channels", "from_search")
|
|
op.drop_index("ix_videos_via_search", table_name="videos")
|
|
op.drop_column("videos", "via_search")
|