feat(search): drop quota wording when the live search source is scrape
The search response now reports its source; in scrape mode (zero quota) the results banner and Load-more button drop the 'uses quota' wording. Adds the search_source toggle's labels/hints and updates the per-user-limit hint to note the cost only applies to the api source. EN/HU/DE.
This commit is contained in:
parent
396e09189b
commit
1d6dfaf486
8 changed files with 35 additions and 12 deletions
|
|
@ -116,9 +116,10 @@ export default function Feed({
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Live YouTube search results. Each page spends 100 quota units, so we never auto-paginate
|
// Live YouTube search results. We never auto-paginate on scroll — the user pulls more with an
|
||||||
// on scroll — the user pulls more with an explicit button. retry:false so a 429 (quota/limit)
|
// explicit button (each api-source page spends 100 quota units; scrape-source pages are free).
|
||||||
// surfaces at once for inline display. staleTime keeps a revisited search from re-spending.
|
// retry:false so a 429 (quota/limit) surfaces at once for inline display. staleTime keeps a
|
||||||
|
// revisited search from re-spending.
|
||||||
const ytQuery = useInfiniteQuery({
|
const ytQuery = useInfiniteQuery({
|
||||||
queryKey: ["yt-search", ytSearch],
|
queryKey: ["yt-search", ytSearch],
|
||||||
queryFn: ({ pageParam }) => api.searchYoutube(ytSearch as string, pageParam as string | null),
|
queryFn: ({ pageParam }) => api.searchYoutube(ytSearch as string, pageParam as string | null),
|
||||||
|
|
@ -279,6 +280,9 @@ export default function Feed({
|
||||||
: ytQuery.isError
|
: ytQuery.isError
|
||||||
? t("feed.yt.error")
|
? t("feed.yt.error")
|
||||||
: null;
|
: null;
|
||||||
|
// The backend reports which source served the search; scrape spends no search quota, so we
|
||||||
|
// drop the quota warning + "uses quota" wording in that mode.
|
||||||
|
const zeroQuota = ytQuery.data?.pages?.[0]?.source === "scrape";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|
@ -303,7 +307,9 @@ export default function Feed({
|
||||||
<span className="text-sm font-semibold truncate">
|
<span className="text-sm font-semibold truncate">
|
||||||
{t("feed.yt.resultsFor", { query: ytSearch })}
|
{t("feed.yt.resultsFor", { query: ytSearch })}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted">{t("feed.yt.quotaNote")}</span>
|
<span className="text-xs text-muted">
|
||||||
|
{zeroQuota ? t("feed.yt.freeNote") : t("feed.yt.quotaNote")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ytQuery.isLoading ? (
|
{ytQuery.isLoading ? (
|
||||||
|
|
@ -333,7 +339,11 @@ export default function Feed({
|
||||||
disabled={ytQuery.isFetchingNextPage}
|
disabled={ytQuery.isFetchingNextPage}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl border border-border bg-card text-sm font-medium hover:border-accent hover:text-accent disabled:opacity-50 transition"
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl border border-border bg-card text-sm font-medium hover:border-accent hover:text-accent disabled:opacity-50 transition"
|
||||||
>
|
>
|
||||||
{ytQuery.isFetchingNextPage ? t("feed.loadingMore") : t("feed.yt.loadMore")}
|
{ytQuery.isFetchingNextPage
|
||||||
|
? t("feed.loadingMore")
|
||||||
|
: zeroQuota
|
||||||
|
? t("feed.yt.loadMoreFree")
|
||||||
|
: t("feed.yt.loadMore")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
"smtp_password": { "label": "SMTP-Passwort", "hint": "App-Passwort. Verschlüsselt gespeichert; nur schreibbar — wird nie wieder angezeigt." },
|
"smtp_password": { "label": "SMTP-Passwort", "hint": "App-Passwort. Verschlüsselt gespeichert; nur schreibbar — wird nie wieder angezeigt." },
|
||||||
"quota_daily_budget": { "label": "Tägliches Kontingentbudget", "hint": "YouTube-Data-API-Einheiten pro Tag (kostenloses Limit 10.000). Standard 9000 lässt Puffer." },
|
"quota_daily_budget": { "label": "Tägliches Kontingentbudget", "hint": "YouTube-Data-API-Einheiten pro Tag (kostenloses Limit 10.000). Standard 9000 lässt Puffer." },
|
||||||
"backfill_quota_reserve": { "label": "Nachlade-Kontingentreserve", "hint": "Reservierte Einheiten, damit geplantes Nachladen interaktive Syncs / Anreicherung nie aushungert." },
|
"backfill_quota_reserve": { "label": "Nachlade-Kontingentreserve", "hint": "Reservierte Einheiten, damit geplantes Nachladen interaktive Syncs / Anreicherung nie aushungert." },
|
||||||
"search_daily_limit_per_user": { "label": "Live-Suche — Tageslimit pro Nutzer", "hint": "Maximale Live-YouTube-Suchen pro Nutzer und Tag. Jede Suche kostet 100 Einheiten, das gemeinsame Budget reicht also für insgesamt nur ~80-90/Tag. 0 = Live-Suche deaktiviert." },
|
"search_daily_limit_per_user": { "label": "Live-Suche — Tageslimit pro Nutzer", "hint": "Maximale Live-YouTube-Suchen pro Nutzer und Tag. Bei der API-Quelle kostet jede Suche 100 Einheiten (das gemeinsame Budget reicht also für insgesamt nur ~80-90/Tag); bei der Scrape-Quelle ist es ein reines Missbrauchslimit. 0 = Live-Suche deaktiviert." },
|
||||||
|
"search_source": { "label": "Quelle der Live-Suche", "hint": "Woher die Ergebnisse der Live-YouTube-Suche stammen. \"scrape\" nutzt YouTubes internen Endpunkt und verbraucht kein API-Kontingent (empfohlen). \"api\" nutzt das offizielle search.list (100 Einheiten/Seite). Auf \"api\" umstellen, falls das Scraping ausfällt oder blockiert wird." },
|
||||||
"backfill_recent_max_videos": { "label": "Aktuelles Nachladen — max. Videos", "hint": "Neueste Videos pro Kanal beim ersten Durchlauf." },
|
"backfill_recent_max_videos": { "label": "Aktuelles Nachladen — max. Videos", "hint": "Neueste Videos pro Kanal beim ersten Durchlauf." },
|
||||||
"backfill_recent_max_days": { "label": "Aktuelles Nachladen — max. Alter (Tage)", "hint": "Wie weit der erste Durchlauf pro Kanal zurückreicht." },
|
"backfill_recent_max_days": { "label": "Aktuelles Nachladen — max. Alter (Tage)", "hint": "Wie weit der erste Durchlauf pro Kanal zurückreicht." },
|
||||||
"shorts_probe_max_seconds": { "label": "Shorts-Prüfung — max. Dauer (s)", "hint": "Nur Videos bis zu dieser Länge werden als mögliche Shorts geprüft." },
|
"shorts_probe_max_seconds": { "label": "Shorts-Prüfung — max. Dauer (s)", "hint": "Nur Videos bis zu dieser Länge werden als mögliche Shorts geprüft." },
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,12 @@
|
||||||
"searchFor": "Auf YouTube suchen nach „{{query}}”",
|
"searchFor": "Auf YouTube suchen nach „{{query}}”",
|
||||||
"resultsFor": "YouTube-Ergebnisse für „{{query}}”",
|
"resultsFor": "YouTube-Ergebnisse für „{{query}}”",
|
||||||
"quotaNote": "Live-Ergebnisse — verbraucht gemeinsames API-Kontingent",
|
"quotaNote": "Live-Ergebnisse — verbraucht gemeinsames API-Kontingent",
|
||||||
|
"freeNote": "Live-Ergebnisse — kein Suchkontingent verbraucht",
|
||||||
"back": "Zurück zum Feed",
|
"back": "Zurück zum Feed",
|
||||||
"searching": "Suche auf YouTube…",
|
"searching": "Suche auf YouTube…",
|
||||||
"noResults": "Keine YouTube-Videos gefunden.",
|
"noResults": "Keine YouTube-Videos gefunden.",
|
||||||
"error": "YouTube-Suche fehlgeschlagen.",
|
"error": "YouTube-Suche fehlgeschlagen.",
|
||||||
"loadMore": "Mehr laden (verbraucht Kontingent)"
|
"loadMore": "Mehr laden (verbraucht Kontingent)",
|
||||||
|
"loadMoreFree": "Mehr laden"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
"smtp_password": { "label": "SMTP password", "hint": "App password. Stored encrypted; write-only — it's never shown back." },
|
"smtp_password": { "label": "SMTP password", "hint": "App password. Stored encrypted; write-only — it's never shown back." },
|
||||||
"quota_daily_budget": { "label": "Daily quota budget", "hint": "Total YouTube Data API units to spend per day (free tier is 10,000). Default 9000 leaves headroom." },
|
"quota_daily_budget": { "label": "Daily quota budget", "hint": "Total YouTube Data API units to spend per day (free tier is 10,000). Default 9000 leaves headroom." },
|
||||||
"backfill_quota_reserve": { "label": "Backfill quota reserve", "hint": "Units kept in reserve so scheduled backfill never starves interactive syncs / enrichment." },
|
"backfill_quota_reserve": { "label": "Backfill quota reserve", "hint": "Units kept in reserve so scheduled backfill never starves interactive syncs / enrichment." },
|
||||||
"search_daily_limit_per_user": { "label": "Live search — daily limit per user", "hint": "Max live YouTube searches per user per day. Each search costs 100 units, so the shared budget only affords ~80-90/day total. Set 0 to disable live search." },
|
"search_daily_limit_per_user": { "label": "Live search — daily limit per user", "hint": "Max live YouTube searches per user per day. With the API source each search costs 100 units (so the shared budget affords only ~80-90/day total); with the scrape source it's a pure anti-abuse limit. Set 0 to disable live search." },
|
||||||
|
"search_source": { "label": "Live search source", "hint": "Where live YouTube search results come from. \"scrape\" uses YouTube's internal endpoint and spends no API quota (recommended). \"api\" uses the official search.list (100 units/page). Switch to \"api\" if scraping ever breaks or is blocked." },
|
||||||
"backfill_recent_max_videos": { "label": "Recent backfill — max videos", "hint": "Newest videos fetched per channel on the first pass." },
|
"backfill_recent_max_videos": { "label": "Recent backfill — max videos", "hint": "Newest videos fetched per channel on the first pass." },
|
||||||
"backfill_recent_max_days": { "label": "Recent backfill — max age (days)", "hint": "How far back the first pass reaches per channel." },
|
"backfill_recent_max_days": { "label": "Recent backfill — max age (days)", "hint": "How far back the first pass reaches per channel." },
|
||||||
"shorts_probe_max_seconds": { "label": "Shorts probe — max duration (s)", "hint": "Only videos at or below this length are probed as possible Shorts." },
|
"shorts_probe_max_seconds": { "label": "Shorts probe — max duration (s)", "hint": "Only videos at or below this length are probed as possible Shorts." },
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,12 @@
|
||||||
"searchFor": "Search YouTube for “{{query}}”",
|
"searchFor": "Search YouTube for “{{query}}”",
|
||||||
"resultsFor": "YouTube results for “{{query}}”",
|
"resultsFor": "YouTube results for “{{query}}”",
|
||||||
"quotaNote": "Live results — uses shared API quota",
|
"quotaNote": "Live results — uses shared API quota",
|
||||||
|
"freeNote": "Live results — no search quota used",
|
||||||
"back": "Back to feed",
|
"back": "Back to feed",
|
||||||
"searching": "Searching YouTube…",
|
"searching": "Searching YouTube…",
|
||||||
"noResults": "No YouTube videos found.",
|
"noResults": "No YouTube videos found.",
|
||||||
"error": "YouTube search failed.",
|
"error": "YouTube search failed.",
|
||||||
"loadMore": "Load more (uses quota)"
|
"loadMore": "Load more (uses quota)",
|
||||||
|
"loadMoreFree": "Load more"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
"smtp_password": { "label": "SMTP-jelszó", "hint": "Alkalmazásjelszó. Titkosítva tárolva; csak írható — soha nem jelenik meg újra." },
|
"smtp_password": { "label": "SMTP-jelszó", "hint": "Alkalmazásjelszó. Titkosítva tárolva; csak írható — soha nem jelenik meg újra." },
|
||||||
"quota_daily_budget": { "label": "Napi kvótakeret", "hint": "Naponta elkölthető YouTube Data API-egységek (az ingyenes keret 10 000). Az alap 9000 tartalékot hagy." },
|
"quota_daily_budget": { "label": "Napi kvótakeret", "hint": "Naponta elkölthető YouTube Data API-egységek (az ingyenes keret 10 000). Az alap 9000 tartalékot hagy." },
|
||||||
"backfill_quota_reserve": { "label": "Letöltési kvótatartalék", "hint": "Tartalékban tartott egységek, hogy az ütemezett letöltés ne éheztesse ki az interaktív szinkront / gazdagítást." },
|
"backfill_quota_reserve": { "label": "Letöltési kvótatartalék", "hint": "Tartalékban tartott egységek, hogy az ütemezett letöltés ne éheztesse ki az interaktív szinkront / gazdagítást." },
|
||||||
"search_daily_limit_per_user": { "label": "Élő keresés — napi limit / felhasználó", "hint": "Maximális élő YouTube-keresés felhasználónként naponta. Egy keresés 100 egységbe kerül, így a közös büdzséből összesen csak ~80-90 fér bele naponta. 0 = élő keresés kikapcsolva." },
|
"search_daily_limit_per_user": { "label": "Élő keresés — napi limit / felhasználó", "hint": "Maximális élő YouTube-keresés felhasználónként naponta. Az API-forrásnál egy keresés 100 egységbe kerül (így a közös büdzséből összesen csak ~80-90 fér bele naponta); a scrape-forrásnál ez tiszta visszaélés-elleni limit. 0 = élő keresés kikapcsolva." },
|
||||||
|
"search_source": { "label": "Élő keresés forrása", "hint": "Honnan jönnek az élő YouTube-keresés találatai. A „scrape” a YouTube belső végpontját használja és nem fogyaszt API-kvótát (ajánlott). Az „api” a hivatalos search.list-et (100 egység/oldal). Válts „api”-ra, ha a scrape valaha elromlik vagy letiltják." },
|
||||||
"backfill_recent_max_videos": { "label": "Friss letöltés — max videó", "hint": "Csatornánként az első körben letöltött legújabb videók száma." },
|
"backfill_recent_max_videos": { "label": "Friss letöltés — max videó", "hint": "Csatornánként az első körben letöltött legújabb videók száma." },
|
||||||
"backfill_recent_max_days": { "label": "Friss letöltés — max kor (nap)", "hint": "Az első kör csatornánként meddig nyúl vissza." },
|
"backfill_recent_max_days": { "label": "Friss letöltés — max kor (nap)", "hint": "Az első kör csatornánként meddig nyúl vissza." },
|
||||||
"shorts_probe_max_seconds": { "label": "Shorts-vizsgálat — max hossz (mp)", "hint": "Csak az ennél nem hosszabb videókat vizsgáljuk lehetséges Shortsként." },
|
"shorts_probe_max_seconds": { "label": "Shorts-vizsgálat — max hossz (mp)", "hint": "Csak az ennél nem hosszabb videókat vizsgáljuk lehetséges Shortsként." },
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,12 @@
|
||||||
"searchFor": "Keresés a YouTube-on: „{{query}}”",
|
"searchFor": "Keresés a YouTube-on: „{{query}}”",
|
||||||
"resultsFor": "YouTube találatok erre: „{{query}}”",
|
"resultsFor": "YouTube találatok erre: „{{query}}”",
|
||||||
"quotaNote": "Élő találatok — a közös API-kvótát fogyasztja",
|
"quotaNote": "Élő találatok — a közös API-kvótát fogyasztja",
|
||||||
|
"freeNote": "Élő találatok — nem fogyaszt keresési kvótát",
|
||||||
"back": "Vissza a hírfolyamhoz",
|
"back": "Vissza a hírfolyamhoz",
|
||||||
"searching": "Keresés a YouTube-on…",
|
"searching": "Keresés a YouTube-on…",
|
||||||
"noResults": "Nincs YouTube-találat.",
|
"noResults": "Nincs YouTube-találat.",
|
||||||
"error": "A YouTube-keresés nem sikerült.",
|
"error": "A YouTube-keresés nem sikerült.",
|
||||||
"loadMore": "Továbbiak betöltése (kvótát fogyaszt)"
|
"loadMore": "Továbbiak betöltése (kvótát fogyaszt)",
|
||||||
|
"loadMoreFree": "Továbbiak betöltése"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@ export interface FeedResponse {
|
||||||
// Opaque keyset cursor for the next page, or null when this is the last page.
|
// Opaque keyset cursor for the next page, or null when this is the last page.
|
||||||
next_cursor: string | null;
|
next_cursor: string | null;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
// Only set by the live YouTube search: "scrape" (InnerTube, no API quota) or "api"
|
||||||
|
// (search.list, 100 units/page) — lets the UI drop the quota warning in scrape mode.
|
||||||
|
source?: "scrape" | "api";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Playlist {
|
export interface Playlist {
|
||||||
|
|
@ -565,7 +568,8 @@ export const api = {
|
||||||
req(`/api/feed/count?${filterParams(f).toString()}`),
|
req(`/api/feed/count?${filterParams(f).toString()}`),
|
||||||
// Live YouTube search: results are materialised into the catalog and returned as feed cards.
|
// 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
|
// `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).
|
// popping the global modal. `cursor` is the next-page token (an InnerTube continuation token
|
||||||
|
// in scrape mode, or a Data API pageToken in api mode); the response's `source` says which.
|
||||||
searchYoutube: (q: string, cursor: string | null): Promise<FeedResponse> => {
|
searchYoutube: (q: string, cursor: string | null): Promise<FeedResponse> => {
|
||||||
const p = new URLSearchParams({ q });
|
const p = new URLSearchParams({ q });
|
||||||
if (cursor) p.set("cursor", cursor);
|
if (cursor) p.set("cursor", cursor);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue