feat(search): live YouTube search UI
Surface live YouTube search in the existing feed, triggered explicitly so the expensive API call is never per-keystroke. - Header: the search box still filters the local catalog as you type; Enter or a YouTube button escalates the term to a live search (hidden for the demo account). - Feed: a dedicated infinite query renders results in the same VirtualFeed cards + in-app player, under a banner with a back button and a quota note. No auto-paginate (each page spends 100 units) — an explicit 'Load more (uses quota)' button instead; quota/limit errors (incl. 429) shown inline. The empty local feed offers a 'Search YouTube for <q>' CTA. - Library: a 'Search-discovered' toggle reveals search-ingested videos (hidden by default); sent as exclude_search_discovered. - Admin: search_daily_limit_per_user config field; new videos_search quota label. - All new strings translated in HU/EN/DE.
This commit is contained in:
parent
9b1bdb6b42
commit
546be57963
16 changed files with 240 additions and 15 deletions
|
|
@ -141,6 +141,9 @@ export interface FeedFilters {
|
|||
includeShorts: boolean;
|
||||
includeLive: boolean;
|
||||
show: string;
|
||||
// Library (scope "all") only: when set, also show videos that entered the catalog via a
|
||||
// live YouTube search. Default (falsy) hides that search-discovered "noise".
|
||||
showSearchDiscovered?: boolean;
|
||||
channelId?: string;
|
||||
channelName?: string;
|
||||
maxAgeDays?: number;
|
||||
|
|
@ -306,6 +309,8 @@ function filterParams(f: FeedFilters): URLSearchParams {
|
|||
if (f.dateTo) p.set("published_before", f.dateTo);
|
||||
if (f.minDuration != null) p.set("min_duration", String(f.minDuration));
|
||||
if (f.maxDuration != null) p.set("max_duration", String(f.maxDuration));
|
||||
// Only relevant in Library scope; the backend ignores it for "my". Default = hide.
|
||||
p.set("exclude_search_discovered", String(!f.showSearchDiscovered));
|
||||
return p;
|
||||
}
|
||||
|
||||
|
|
@ -558,6 +563,14 @@ export const api = {
|
|||
req(`/api/feed?${feedQuery(f, cursor, limit)}`),
|
||||
feedCount: (f: FeedFilters): Promise<{ count: number }> =>
|
||||
req(`/api/feed/count?${filterParams(f).toString()}`),
|
||||
// Live YouTube search: results are materialised into the catalog and returned as feed cards.
|
||||
// `quiet` so the YT-search panel can render quota/limit errors (incl. 429) inline instead of
|
||||
// popping the global modal. `cursor` is the YouTube nextPageToken (each page spends 100 units).
|
||||
searchYoutube: (q: string, cursor: string | null): Promise<FeedResponse> => {
|
||||
const p = new URLSearchParams({ q });
|
||||
if (cursor) p.set("cursor", cursor);
|
||||
return req(`/api/search/youtube?${p.toString()}`, {}, { quiet: true });
|
||||
},
|
||||
facets: (f: FeedFilters): Promise<{ counts: Record<string, number> }> =>
|
||||
req(`/api/facets?${filterParams(f).toString()}`),
|
||||
// idempotent: setting a state / saving a progress checkpoint is safe to replay, so these
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue