fix(search): every related and live-search request takes a slot, and a failed one gives it back

A related list's cursor cost nothing, and a cursor is a free string: a replayed one read and
enriched without end. Every request now takes a slot of the user's daily cap — taken under a lock
before the work (`quota.claim_action`), so requests sent together cannot all pass the last free
one, and given back when the request fails outright (`quota.refund_action`). The live search's
scrape source takes its slots the same way.

A limit of 0 turns the feature off: a distinct 409 `related_disabled` / `search_disabled`, and
`/api/me` says `related_enabled` / `search_enabled` so the UI can leave it out.

A videos.list that fails no longer reads as an empty page: `ingest_candidates` raises, and the
search and the related list answer the refusal (the shared budget spent says "quota") instead of
"no results". The two routes' page loops are one (`_gather`).

`clear` deletes only what the caller found: a related list's rows carry no find, and any user
could name them.
This commit is contained in:
2026-10-25 00:32:26 +02:00
parent 7d45c4f59f
commit b04b51c909
14 changed files with 556 additions and 165 deletions
+1 -1
View File
@@ -56,7 +56,7 @@
},
"related_daily_limit_per_user": {
"label": "Related videos — daily limit per user",
"hint": "Max related-video lists a user can open per day. They come from YouTube's website and spend no search quota; the videos in a list are looked up for 1 unit per 50. A pure anti-abuse limit. Set 0 to turn related videos off."
"hint": "Max related-video requests a user can make per day — opening a list and each Show more count one. They come from YouTube's website and spend no search quota; looking up the videos in one costs up to about 8 units (up to 4 pages, each a video and a channel lookup). A failed one doesn't count. Set 0 to turn related videos off."
},
"search_daily_limit_per_user": {
"label": "Live search — daily limit per user",
+2
View File
@@ -188,7 +188,9 @@
"quota_spent": "Today's shared YouTube budget is spent. Try again tomorrow.",
"recipient_required": "A recipient email is required",
"registration_closed": "Registration is currently closed",
"related_disabled": "Related videos are turned off on this instance.",
"related_daily_limit": "You've opened today's limit of related lists ({{limit}}). Try again tomorrow.",
"search_disabled": "Live YouTube search is turned off on this instance.",
"search_daily_limit": "You've reached today's YouTube search limit ({{limit}}). Try again tomorrow.",
"search_term_required": "Enter something to search for",
"session_gone": "The playback session ended",
+1 -1
View File
@@ -56,7 +56,7 @@
},
"related_daily_limit_per_user": {
"label": "Kapcsolódó videók — napi limit / felhasználó",
"hint": "Hány kapcsolódóvideó-listát nyithat meg egy felhasználó naponta. A listák a YouTube weboldaláról jönnek, keresési kvótát nem fogyasztanak; a bennük lévő videók lekérése 50-enként 1 egység. Tiszta visszaélés-elleni limit. 0 = a kapcsolódó videók kikapcsolva."
"hint": "Hányszor kérhet kapcsolódó videókat egy felhasználó naponta — egy lista megnyitása és minden „Mutass többet” egynek számít. A YouTube weboldaláról jönnek, keresési kvótát nem fogyasztanak; a bennük lévő videók lekérése kérésenként legfeljebb kb. 8 egység (legfeljebb 4 oldal, mindegyik egy videó- és egy csatornalekérés). A sikertelen nem számít. 0 = a kapcsolódó videók kikapcsolva."
},
"search_daily_limit_per_user": {
"label": "Élő keresés — napi limit / felhasználó",
+2
View File
@@ -188,7 +188,9 @@
"quota_spent": "A mai közös YouTube-keret elfogyott. Próbáld holnap.",
"recipient_required": "Adj meg egy címzett e-mail címet",
"registration_closed": "A regisztráció jelenleg zárva van",
"related_disabled": "A kapcsolódó videók ki vannak kapcsolva ezen a példányon.",
"related_daily_limit": "Elérted a mai kapcsolódó listák korlátját ({{limit}}). Próbáld holnap.",
"search_disabled": "Az élő YouTube-keresés ki van kapcsolva ezen a példányon.",
"search_daily_limit": "Elérted a mai YouTube-keresési korlátot ({{limit}}). Próbáld holnap.",
"search_term_required": "Írj be egy keresőkifejezést",
"session_gone": "A lejátszási munkamenet véget ért",