Search: server-known videos first with YouTube results appended as they arrive; On server badge on cards the server holds a copy of

This commit is contained in:
Jonathan Sykes
2026-10-15 19:46:16 +08:00
parent 1383e89a25
commit 450dc80488
4 changed files with 49 additions and 10 deletions

View File

@@ -455,8 +455,24 @@ const searchLibrary = async (q) => {
// This server's own library first — it still answers when YouTube is unreachable.
try { return (await notesDb.listUploads({ q, limit: 20, listedOnly: true })).map(uploads.card); } catch { return []; }
};
const withYoutubeUrl = card => card && /^[\w-]{11}$/.test(card.id || '')
? { ...card, url: `https://www.youtube.com/watch?v=${card.id}` } : card;
// Ids the server holds a ready copy of, refreshed at most once a minute, so
// search results can say "On server" without a query per card.
let serverCopies = { at: 0, ids: new Set(), loading: null };
async function serverCopyIds() {
if (Date.now() - serverCopies.at < 60_000) return serverCopies.ids;
serverCopies.loading ||= notesDb.db.execute("SELECT video_id FROM media_cache WHERE status = 'ready'")
.then((r) => { serverCopies = { at: Date.now(), ids: new Set(r.rows.map((x) => x.video_id)), loading: null }; })
.catch(() => { serverCopies.loading = null; serverCopies.at = Date.now(); });
await serverCopies.loading;
return serverCopies.ids;
}
let copyIds = new Set();
const withYoutubeUrl = card => {
if (!card) return card;
const out = /^[\w-]{11}$/.test(card.id || '') ? { ...card, url: `https://www.youtube.com/watch?v=${card.id}` } : { ...card };
if (copyIds.has(card.id) || String(card.id || '').startsWith('upl_')) out.onServer = true;
return out;
};
// GET /api/search/local?q=<query> — videos this server already knows (from every
// earlier search) plus its own uploads; answers in milliseconds so the app can
@@ -464,7 +480,8 @@ const withYoutubeUrl = card => card && /^[\w-]{11}$/.test(card.id || '')
app.get('/api/search/local', async (c) => {
const q = (c.req.query('q') || '').trim();
if (!q) return c.json({ ok: false, error: 'empty query' }, 400);
const [mine, known] = await Promise.all([searchLibrary(q), searchCacheDb.searchVideos(q).catch(() => [])]);
const [mine, known, ids] = await Promise.all([searchLibrary(q), searchCacheDb.searchVideos(q).catch(() => []), serverCopyIds()]);
copyIds = ids;
return c.json({ ok: true, results: [...mine, ...known].map(withYoutubeUrl), local: true });
});
@@ -478,6 +495,7 @@ app.get('/api/search', async (c) => {
if (!q) return c.json({ ok: false, error: 'empty query' }, 400);
const key = q.toLowerCase();
const mine = await searchLibrary(q);
copyIds = await serverCopyIds();
const answer = (yt, extra = {}) => {
const body = JSON.stringify({ ok: true, results: [...mine, ...yt].map(withYoutubeUrl), ...extra });
const headers = { 'Content-Type': 'application/json; charset=utf-8', Vary: 'Accept-Encoding' };