From 93463bce3aecd4819b0887e2726e3372ff7e3139 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 2 Nov 2026 22:29:00 +0000 Subject: [PATCH] Ask the provider again for a favorite yt-dlp can't resolve Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN --- frontend/js/videos.js | 42 ++++++++++++++++++-- tests/smoke_favorite_formats.py | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/smoke_favorite_formats.py diff --git a/frontend/js/videos.js b/frontend/js/videos.js index 0f899a6..a783e1a 100644 --- a/frontend/js/videos.js +++ b/frontend/js/videos.js @@ -2767,6 +2767,39 @@ App.videos = App.videos || {}; } }; + // Some providers (rule34video among them) hand the listing their media + // formats themselves, and yt-dlp can't extract their pages. A favorite keeps + // no formats, so for those /api/resolve comes back empty -- ask the + // providers again instead, the way the listing did: search the channel for + // the title and take the item that is this video. + const formatsFromProvider = async function(video) { + const idHead = String(video.id || '').split(':')[0]; + const channelId = (idHead && idHead !== String(video.id) ? idHead : String(video.channel || '')).toLowerCase(); + if (!channelId) return null; + const servers = (App.storage.getConfig().servers || []).map((entry) => Object.keys(entry)[0]); + const wanted = App.favorites && App.favorites.urlKey ? App.favorites.urlKey(video.url) : video.url; + const same = (item) => item && (item.id === video.id || + (item.url && (App.favorites && App.favorites.urlKey ? App.favorites.urlKey(item.url) : item.url) === wanted)); + for (const server of servers) { + try { + const response = await fetch('/api/videos', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channel: channelId, query: video.title || '', page: 1, perPage: 24, server }) + }); + if (!response.ok) continue; + const data = await response.json(); + const found = (data && Array.isArray(data.items) ? data.items : []).find(same); + if (found && hasFormats(found)) { + return { url: found.url, formats: found.formats, http_headers: found.http_headers || {}, isLive: !!found.isLive }; + } + } catch (err) { + // Next server. + } + } + return null; + }; + App.videos.ensureFormats = function(video) { if (!video || typeof video !== 'object') return Promise.resolve(null); if (hasFormats(video.meta)) return Promise.resolve(video.meta); @@ -2793,13 +2826,14 @@ App.videos = App.videos || {}; headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: video.url }) }); - if (!response.ok) return null; - const data = await response.json(); - return hasFormats(data) ? data : null; + if (response.ok) { + const data = await response.json(); + if (hasFormats(data)) return data; + } } catch (err) { // Best-effort: playback still works through the proxy. - return null; } + return await formatsFromProvider(video); })(); metaCache.set(cacheKey, promise); } diff --git a/tests/smoke_favorite_formats.py b/tests/smoke_favorite_formats.py new file mode 100644 index 0000000..2369ff9 --- /dev/null +++ b/tests/smoke_favorite_formats.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""A favorite whose site yt-dlp can't extract still gets its formats. + +Providers like rule34video hand the listing their formats; a favorite keeps +none, and /api/resolve comes back empty for them. ensureFormats must then ask +the providers again and take the item that is this video. + + backend/main.py & + .venv/bin/python tests/smoke_favorite_formats.py +""" +import json +import sys +from playwright.sync_api import sync_playwright + +BASE = "http://127.0.0.1:5000/" +SERVER = "https://a.fixture.test" +FAV = {"key": "rule34video:4511669", "id": "rule34video:4511669", + "url": "https://rule34video.com/video/4511669/sindel-cassie-sonya/", + "title": "Sindel, Cassie, Sonya", "thumb": "", "channel": "Rule34video", + "uploader": "Nyl", "duration": 510, "isLive": False, + "favoriteDate": "2026-11-02T22:00:05.356Z"} +STATUS = {"id": "a", "name": "a", "online": True, "channels": [ + {"id": "rule34video", "name": "Rule34video", "description": "x", "options": []}]} +FMT = [{"url": "https://rule34video.com/get_file/1/x/4511669_720p.mp4/", "height": 720, "ext": "mp4"}] +asked = [] + + +def videos(route): + body = json.loads(route.request.post_data or "{}") + asked.append(body) + items = [{"id": "rule34video:1", "url": "https://rule34video.com/video/1/other/", "formats": []}, + {"id": FAV["id"], "url": FAV["url"], "formats": FMT}] + route.fulfill(status=200, content_type="application/json", body=json.dumps({"items": items})) + + +def main(): + failed = 0 + with sync_playwright() as p: + b = p.chromium.launch(args=["--no-sandbox", "--disable-gpu"]) + page = b.new_page() + page.route(f"{SERVER}/api/status", lambda r: r.fulfill( + status=200, content_type="application/json", body=json.dumps(STATUS))) + page.route("**/api/resolve", lambda r: r.fulfill( + status=200, content_type="application/json", + body='{"formats":[],"http_headers":{},"isLive":false,"url":null}')) + page.route("**/api/videos", videos) + page.goto(BASE, wait_until="domcontentloaded") + page.evaluate("""([s, f]) => { + localStorage.setItem('config', JSON.stringify({servers: [{[s]: {}}]})); + localStorage.setItem('favorites', JSON.stringify([f])); + }""", [SERVER, FAV]) + page.goto(BASE, wait_until="load") + meta = page.evaluate("""async (f) => { + const v = Object.assign({}, f); + const m = await App.videos.ensureFormats(v); + return { m, attached: !!v.meta }; + }""", FAV) + ok = bool(meta["m"]) and meta["m"]["formats"] == FMT and meta["attached"] + print(f" [{'PASS' if ok else 'FAIL'}] favorite gets the provider's formats {meta}") + failed += 0 if ok else 1 + ok = bool(asked) and asked[0]["channel"] == "rule34video" and asked[0]["server"] == SERVER + print(f" [{'PASS' if ok else 'FAIL'}] asked the right channel and server {asked[:1]}") + failed += 0 if ok else 1 + b.close() + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main())