Files
ytplayer/server/related.js

66 lines
4.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Related discovery is bounded and independent of the deep search collector.
export function relatedQueries({ title = '', channel = '' }) {
title = String(title).replace(/\([^)]*(?:official|lyrics?|music video)[^)]*\)|\[[^\]]*(?:official|lyrics?|music video)[^\]]*\]/gi, '').replace(/\s+/g, ' ').trim().slice(0, 300);
channel = String(channel).trim().slice(0, 120);
return [...new Set([title && channel ? `${title} ${channel}` : '', title, channel].filter(Boolean))];
}
export function selectRelated(cards, id, limit = 8) {
const seen = new Set([id]);
return (Array.isArray(cards) ? cards : []).filter(card => {
if (!card || typeof card.id !== 'string' || !card.title || seen.has(card.id)) return false;
seen.add(card.id); return true;
}).slice(0, limit);
}
export function createRelatedService({ next, search, local, collect = async () => {}, decorate = async cards => cards, now = Date.now, budgetMs = 18000 }) {
const cache = new Map(), pending = new Map();
async function load(seed, { refresh = false } = {}) {
const key = JSON.stringify([seed.id, seed.title, seed.channel]);
const cached = cache.get(key);
if (!refresh && cached && now() - cached.at < cached.ttl) return cached.result;
if (pending.has(key)) return pending.get(key);
const task = (async () => {
const deadline = now() + budgetMs, details = [];
let results = [], source = 'youtube';
try { results = selectRelated(await next(seed.id, Math.min(4000, budgetMs)), seed.id); }
catch (error) { details.push(String(error.message || error).slice(0, 300)); }
const queries = relatedQueries(seed);
for (const query of queries) {
if (results.length || now() >= deadline) break;
try { results = selectRelated(await search(query, Math.max(1, deadline - now())), seed.id); if (results.length) source = 'search'; }
catch (error) { details.push(String(error.message || error).slice(0, 300)); }
}
if (!results.length) {
source = 'catalog';
// Local results still work when YouTube blocks or times out. Broad queries
// avoid requiring every word of a long video title to be in another title.
const localQueries = [...new Set([seed.channel, seed.title?.split(/\s+/).slice(0, 3).join(' '), ...queries].filter(Boolean))];
for (const query of localQueries) {
try { results = selectRelated(await local(query), seed.id); } catch { /* try another local query */ }
if (results.length) break;
}
}
if (!results.length) return { ok: false, error: 'Related videos aren’t available right now. Please try again.', detail: details.join('\n') };
// Metadata/thumbnail persistence must not discard otherwise playable cards.
try { await collect(results); } catch { /* discovery remains usable */ }
try { results = await decorate(results); } catch { /* remote thumbnails remain */ }
const result = { ok: true, results, source, ...(source === 'catalog' ? { message: 'YouTube is unavailable. Showing related videos already known to this server.' } : {}) };
if (cache.size >= 500) cache.delete(cache.keys().next().value);
cache.set(key, { result, at: now(), ttl: source === 'catalog' ? 60000 : 600000 });
return result;
})().finally(() => pending.delete(key));
pending.set(key, task); return task;
}
return { load };
}
export function registerRelatedRoute(app, service) {
app.get('/api/related', async c => {
const id = (c.req.query('videoId') || '').trim();
if (!/^[\w-]{11}$/.test(id)) return c.json({ ok: false, error: 'Invalid video id.' }, 400);
const seed = { id, title: (c.req.query('title') || '').trim().slice(0, 300), channel: (c.req.query('channel') || '').trim().slice(0, 120) };
try {
const result = await service.load(seed, { refresh: c.req.query('refresh') === '1' });
return c.json(result, result.ok ? 200 : 503);
} catch { return c.json({ ok: false, error: 'Related videos aren’t available right now. Please try again.' }, 503); }
});
}