Recover related videos with bounded discovery and retryable fallbacks

This commit is contained in:
Jonathan Sykes
2026-10-04 02:42:02 +08:00
parent df4acfc9f1
commit 2042c9b6fc
15 changed files with 326 additions and 12 deletions

View File

@@ -572,6 +572,9 @@ const API = {
search: (query, { refresh = false } = {}) => WEB
? webFetch(`/api/search?q=${encodeURIComponent(query)}${refresh ? '&refresh=1' : ''}`)
: call('yt.search', 'yt_search', { query }),
related: (meta, { refresh = false } = {}) => WEB && /^[\w-]{11}$/.test(meta.id)
? fetch(`/api/related?${new URLSearchParams({ videoId: meta.id, title: meta.title || '', channel: meta.channel || meta.artist || '', ...(refresh ? { refresh: '1' } : {}) })}`).then(response => response.json())
: API.search(meta.title || meta.channel, { refresh }),
// Videos the server already knows — answers fast while the real search loads.
searchLocal: (query) => WEB
? webFetch(`/api/search/local?q=${encodeURIComponent(query)}`)
@@ -11835,23 +11838,33 @@ function updateAbUI() {
// ============================================================================
// Related videos
// ============================================================================
async function loadRelated() {
if (!current || !current.meta) return;
$('relatedPanel').classList.add('hidden');
relatedVideos = [];
try {
const res = await API.search(current.meta.title);
if (!res || !res.ok || !current) return;
relatedVideos = (res.results || []).filter((v) => v.id !== current.meta.id).slice(0, 8);
renderRelated();
} catch { /* ignore */ }
let relatedState = { status: 'idle', message: '' };
const fetchRelatedVideos = RelatedVideos.createLoader({
fetchRelated: (meta, options) => API.related(meta, options),
update(state) {
if (current?.meta?.id !== state.id) return;
relatedState = state; relatedVideos = state.cards; renderRelated();
},
});
async function loadRelated(options = {}) {
if (current?.meta) return fetchRelatedVideos(current.meta, options);
}
function renderRelated() {
const panel = $('relatedPanel'), list = $('relatedList');
if (!panel || !list || !relatedVideos.length) { if (panel) panel.classList.add('hidden'); return; }
if (!panel || !list || (relatedState.status === 'idle' && !relatedVideos.length)) { if (panel) panel.classList.add('hidden'); return; }
panel.classList.remove('hidden');
list.classList.toggle('hidden', relatedCollapsed);
list.innerHTML = '';
if (relatedState.message) {
const notice = document.createElement('div'); notice.className = 'related-notice'; notice.setAttribute('role', 'status');
const message = document.createElement('p'); message.textContent = relatedState.message; notice.append(message);
if (relatedState.status === 'error') {
const retry = document.createElement('button'); retry.type = 'button'; retry.className = 'related-retry'; retry.setAttribute('aria-label', 'Retry related videos');
retry.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 11a9 9 0 1 1 2.6 7M3 3v8h8"/></svg><span>Retry</span>';
retry.onclick = () => loadRelated({ refresh: true }); notice.append(retry);
}
list.append(notice);
}
relatedVideos.forEach((v) => {
const item = document.createElement('div');
item.className = 'related-item';