cache /api/search results for 3 minutes to skip repeat yt-dlp spawns

This commit is contained in:
Jonathan Sykes
2026-09-30 00:17:44 +08:00
parent 4f0ee45980
commit 6ff9f86bd8

View File

@@ -353,11 +353,25 @@ app.get('/api/version', (c) =>
)
);
// Same query repeats a lot (users re-searching, several devices, the
// landing-page chips) and yt-dlp's own search takes several seconds — cache
// the combined (library + YouTube) result for a short TTL. Same shape as
// resolveStreams()' streamCache below, just keyed by normalized query text
// instead of videoId. Only a full success is cached, so a transient yt-dlp
// failure still gets retried on the next request.
const SEARCH_CACHE_MAX = 100;
const SEARCH_CACHE_TTL_MS = 3 * 60_000;
const searchCache = new Map(); // lowercased q -> { results, expiresAt }
// GET /api/search?q=<query>
app.get('/api/search', async (c) => {
const q = (c.req.query('q') || '').trim();
if (!q) return c.json({ ok: false, error: 'empty query' }, 400);
const cacheKey = q.toLowerCase();
const cached = searchCache.get(cacheKey);
if (cached && Date.now() < cached.expiresAt) return c.json({ ok: true, results: cached.results });
// This server's own library first — and it still answers when YouTube
// (yt-dlp) is unreachable or rate-limited.
let mine = [];
@@ -368,7 +382,10 @@ app.get('/api/search', async (c) => {
'--dump-json', '--flat-playlist',
'--no-warnings', '--ignore-errors',
]);
return c.json({ ok: true, results: [...mine, ...parseCards(out)] });
const results = [...mine, ...parseCards(out)];
if (searchCache.size >= SEARCH_CACHE_MAX) searchCache.delete(searchCache.keys().next().value);
searchCache.set(cacheKey, { results, expiresAt: Date.now() + SEARCH_CACHE_TTL_MS });
return c.json({ ok: true, results });
} catch (err) {
if (mine.length) return c.json({ ok: true, results: mine, youtubeError: err.message });
return c.json({ ok: false, error: err.message }, 500);