Channel videos: POST + one page per request (kill the giant slow-request log spam)
Each /videos batch was 3 InnerTube pages + 0.4s sleeps (>1s → tripped the slow-request WARNING) and carried the ~2KB continuation token in the URL, so every batch dumped a giant warning line — a dozen per channel open. Now it's a POST (token in the body, never the URL) fetching ONE page per call with no server sleep, so each request is fast (<1s, no warning) and the frontend's 120ms pacing keeps it polite. 17 youtube API tests green.
This commit is contained in:
parent
267f11c848
commit
66f3f97460
3 changed files with 26 additions and 32 deletions
|
|
@ -220,28 +220,22 @@ def register_routes(bp):
|
||||||
logger.exception("youtube channel detail failed for %r", channel_id)
|
logger.exception("youtube channel detail failed for %r", channel_id)
|
||||||
return jsonify({"success": False, "error": "Could not load channel"}), 500
|
return jsonify({"success": False, "error": "Could not load channel"}), 500
|
||||||
|
|
||||||
@bp.route("/youtube/channel/<channel_id>/videos", methods=["GET"])
|
@bp.route("/youtube/channel/<channel_id>/videos", methods=["POST"])
|
||||||
def video_youtube_channel_videos(channel_id):
|
def video_youtube_channel_videos(channel_id):
|
||||||
"""A batch of a channel's videos via InnerTube — the channel page streams the
|
"""ONE InnerTube page of a channel's videos — the channel page streams the
|
||||||
WHOLE catalog by calling this with the continuation token from each response
|
WHOLE catalog by re-POSTing with the continuation token from each response
|
||||||
(each page fetched once; no yt-dlp re-scan). Returns ~a hundred videos with
|
(each page fetched once; no yt-dlp re-scan). POST (not GET) keeps the giant
|
||||||
approximate upload dates (refined from the date cache) + next ``continuation``
|
continuation token out of the URL/access logs; the frontend paces the calls,
|
||||||
(null = no more)."""
|
so each is fast and never trips the slow-request warning. Returns the videos
|
||||||
|
(dates refined from cache) + next ``continuation`` (null = no more)."""
|
||||||
from . import get_video_db
|
from . import get_video_db
|
||||||
from core.video import youtube as yt
|
from core.video import youtube as yt
|
||||||
import time
|
body = request.get_json(silent=True) or {}
|
||||||
cont = (request.args.get("continuation") or "").strip() or None
|
cont = (body.get("continuation") or "").strip() or None
|
||||||
try:
|
try:
|
||||||
db = get_video_db()
|
db = get_video_db()
|
||||||
videos, token = [], cont
|
page = yt.innertube_channel_videos_page(channel_id, continuation=cont)
|
||||||
# ~3 InnerTube pages (~90 videos) per request, gently throttled.
|
videos, token = page.get("videos") or [], page.get("continuation")
|
||||||
for i in range(3):
|
|
||||||
page = yt.innertube_channel_videos_page(channel_id, continuation=token)
|
|
||||||
videos.extend(page.get("videos") or [])
|
|
||||||
token = page.get("continuation")
|
|
||||||
if not token or len(videos) >= 90:
|
|
||||||
break
|
|
||||||
time.sleep(0.2)
|
|
||||||
ids = [v.get("youtube_id") for v in videos if v.get("youtube_id")]
|
ids = [v.get("youtube_id") for v in videos if v.get("youtube_id")]
|
||||||
cached = db.get_video_dates(ids)
|
cached = db.get_video_dates(ids)
|
||||||
wished = db.youtube_video_wish_state(ids)
|
wished = db.youtube_video_wish_state(ids)
|
||||||
|
|
|
||||||
|
|
@ -660,21 +660,14 @@ def test_youtube_channel_detail_404_on_unresolvable(tmp_path, monkeypatch):
|
||||||
def test_youtube_channel_videos_batch_streams_and_merges(tmp_path, monkeypatch):
|
def test_youtube_channel_videos_batch_streams_and_merges(tmp_path, monkeypatch):
|
||||||
client, videoapi = _make_client(tmp_path)
|
client, videoapi = _make_client(tmp_path)
|
||||||
import core.video.youtube as ytmod
|
import core.video.youtube as ytmod
|
||||||
# First call (no continuation) returns two InnerTube videos + a token; a second
|
# One POST = one InnerTube page; the token rides in the body (not the URL).
|
||||||
# call with that token would return more — but the endpoint stops at the token.
|
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
def fake_page(cid, continuation=None, now=None, post=None):
|
def fake_page(cid, continuation=None, now=None, post=None):
|
||||||
calls.append(continuation)
|
calls.append(continuation)
|
||||||
if continuation is None:
|
return {"videos": [{"youtube_id": "a1", "title": "A1", "thumbnail_url": "ta", "published_at": "2020-01-01"},
|
||||||
# A full page (>=90) so the endpoint stops AT the token (one batch) rather
|
{"youtube_id": "a2", "title": "A2", "thumbnail_url": "tb", "published_at": "2019-01-01"}],
|
||||||
# than draining it; a1/a2 are the rows we assert on, the rest are filler.
|
"continuation": "NEXT" if continuation is None else None}
|
||||||
vids = [{"youtube_id": "a1", "title": "A1", "thumbnail_url": "ta", "published_at": "2020-01-01"},
|
|
||||||
{"youtube_id": "a2", "title": "A2", "thumbnail_url": "tb", "published_at": "2019-01-01"}]
|
|
||||||
vids += [{"youtube_id": "f%d" % i, "title": "F%d" % i, "thumbnail_url": "t",
|
|
||||||
"published_at": "2018-01-01"} for i in range(95)]
|
|
||||||
return {"videos": vids, "continuation": "NEXT"}
|
|
||||||
return {"videos": [], "continuation": None}
|
|
||||||
|
|
||||||
monkeypatch.setattr(ytmod, "innertube_channel_videos_page", fake_page)
|
monkeypatch.setattr(ytmod, "innertube_channel_videos_page", fake_page)
|
||||||
# Seed a cached (exact) date for a1 + wish a2 so the merge is observable.
|
# Seed a cached (exact) date for a1 + wish a2 so the merge is observable.
|
||||||
|
|
@ -682,13 +675,18 @@ def test_youtube_channel_videos_batch_streams_and_merges(tmp_path, monkeypatch):
|
||||||
videoapi._video_db.add_videos_to_wishlist({"youtube_id": "UCx", "title": "X"},
|
videoapi._video_db.add_videos_to_wishlist({"youtube_id": "UCx", "title": "X"},
|
||||||
[{"youtube_id": "a2", "title": "A2"}])
|
[{"youtube_id": "a2", "title": "A2"}])
|
||||||
|
|
||||||
r = client.get("/api/video/youtube/channel/UCx/videos").get_json()
|
r = client.post("/api/video/youtube/channel/UCx/videos", json={}).get_json()
|
||||||
assert r["success"] is True
|
assert r["success"] is True
|
||||||
assert r["continuation"] == "NEXT" # token passed back for the next batch
|
assert r["continuation"] == "NEXT" # token passed back for the next batch
|
||||||
vids = {v["youtube_id"]: v for v in r["videos"]}
|
vids = {v["youtube_id"]: v for v in r["videos"]}
|
||||||
assert vids["a1"]["published_at"] == "2020-03-15" # cached date wins over InnerTube's
|
assert vids["a1"]["published_at"] == "2020-03-15" # cached date wins over InnerTube's
|
||||||
assert vids["a1"]["wished"] is False and vids["a2"]["wished"] is True
|
assert vids["a1"]["wished"] is False and vids["a2"]["wished"] is True
|
||||||
assert calls[0] is None # first batch started from page 1
|
assert calls[0] is None # first batch started from page 1
|
||||||
|
# the streamed list is remembered for instant re-open
|
||||||
|
assert {v["youtube_id"] for v in videoapi._video_db.get_channel_videos("UCx")} == {"a1", "a2"}
|
||||||
|
# the next page is fetched by POSTing the returned token (kept out of the URL)
|
||||||
|
r2 = client.post("/api/video/youtube/channel/UCx/videos", json={"continuation": "NEXT"}).get_json()
|
||||||
|
assert r2["continuation"] is None and calls[1] == "NEXT"
|
||||||
|
|
||||||
|
|
||||||
def test_youtube_wishlist_add_single_video(tmp_path):
|
def test_youtube_wishlist_add_single_video(tmp_path):
|
||||||
|
|
|
||||||
|
|
@ -1362,9 +1362,11 @@
|
||||||
var cont = null, MAX = 2000; // safety ceiling for pathological channels
|
var cont = null, MAX = 2000; // safety ceiling for pathological channels
|
||||||
function step() {
|
function step() {
|
||||||
if (token !== ytLoadAllToken || currentId !== id || currentSource !== 'youtube') return;
|
if (token !== ytLoadAllToken || currentId !== id || currentSource !== 'youtube') return;
|
||||||
var url = '/api/video/youtube/channel/' + encodeURIComponent(id) + '/videos' +
|
// POST so the (huge) continuation token rides in the body, not the URL.
|
||||||
(cont ? '?continuation=' + encodeURIComponent(cont) : '');
|
fetch('/api/video/youtube/channel/' + encodeURIComponent(id) + '/videos', {
|
||||||
fetch(url, { headers: { 'Accept': 'application/json' } })
|
method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||||
|
body: JSON.stringify({ continuation: cont || null }),
|
||||||
|
})
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
.then(function (resp) {
|
.then(function (resp) {
|
||||||
if (token !== ytLoadAllToken || currentId !== id || currentSource !== 'youtube') return;
|
if (token !== ytLoadAllToken || currentId !== id || currentSource !== 'youtube') return;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue