Search: movies and TV from Plex
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf22aba9dd
commit
05e2a2ec7b
1 file changed
+24
-5
+24
-5
@@ -25,8 +25,21 @@ async function searchMusic(term) {
|
|||||||
return items.slice(0, 16);
|
return items.slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plex movies and shows (and episodes) from the server-wide search.
|
||||||
|
async function searchVideo(term) {
|
||||||
|
const mc = await px.get(`/hubs/search?query=${encodeURIComponent(term)}&limit=12`), items = [];
|
||||||
|
for (const hub of mc.Hub || []) for (const m of hub.Metadata || []) if (['movie', 'show', 'episode'].includes(m.type)) items.push(m);
|
||||||
|
return items.slice(0, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Podgrab: the podcasts you follow, matched on title and author.
|
||||||
|
async function searchPodcasts(term) {
|
||||||
|
const list = await jfetch('/api/podcast/podcasts'), lower = term.toLowerCase();
|
||||||
|
return (Array.isArray(list) ? list : []).filter(s => ((pdF(s, 'title') || '') + ' ' + (pdF(s, 'author') || '')).toLowerCase().includes(lower)).slice(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
function viewSearch() {
|
function viewSearch() {
|
||||||
$('#view').innerHTML = `<div class="wrap"><h1>Search</h1><input type="search" id="q" placeholder="Tasks, events, files, music…" autocomplete="off" autocapitalize="off">
|
$('#view').innerHTML = `<div class="wrap"><h1>Search</h1><input type="search" id="q" placeholder="Tasks, events, files, music, movies & TV, podcasts…" autocomplete="off" autocapitalize="off">
|
||||||
<p class="sub" id="src" style="margin:8px 0 0"></p>
|
<p class="sub" id="src" style="margin:8px 0 0"></p>
|
||||||
<div class="bar" id="web"><a class="btn ghost" id="duckai" href="https://duck.ai/" target="_blank" rel="noopener">${icon('search')}Ask Duck.ai</a>
|
<div class="bar" id="web"><a class="btn ghost" id="duckai" href="https://duck.ai/" target="_blank" rel="noopener">${icon('search')}Ask Duck.ai</a>
|
||||||
<a class="btn ghost" id="ddg" href="https://duckduckgo.com/" target="_blank" rel="noopener">${icon('search')}Search the web</a></div>
|
<a class="btn ghost" id="ddg" href="https://duckduckgo.com/" target="_blank" rel="noopener">${icon('search')}Search the web</a></div>
|
||||||
@@ -41,7 +54,7 @@ function viewSearch() {
|
|||||||
const part = (st, off, label) => st === undefined ? off : st.err ? bad('error: ' + st.err) : `✓${st.n !== undefined ? ' ' + st.n : ''}`;
|
const part = (st, off, label) => st === undefined ? off : st.err ? bad('error: ' + st.err) : `✓${st.n !== undefined ? ' ' + st.n : ''}`;
|
||||||
const filesOff = !sf.token() ? link('#/files', 'sign in to search files') : '✓';
|
const filesOff = !sf.token() ? link('#/files', 'sign in to search files') : '✓';
|
||||||
const musicOff = !plexSt || !plexSt.signedIn ? link('#/music', 'sign in to search music') : !plexSt.server ? link('#/music', 'choose a Plex server') : '✓';
|
const musicOff = !plexSt || !plexSt.signedIn ? link('#/music', 'sign in to search music') : !plexSt.server ? link('#/music', 'choose a Plex server') : '✓';
|
||||||
el.innerHTML = `Searching: Tasks ${part(r.tasks, '✓')} · Events ${part(r.events, '✓')} · Files ${part(r.files, filesOff)} · Music ${part(r.music, musicOff)}`;
|
el.innerHTML = `Searching: Tasks ${part(r.tasks, '✓')} · Events ${part(r.events, '✓')} · Files ${part(r.files, filesOff)} · Music ${part(r.music, musicOff)} · Movies & TV ${part(r.video, musicOff)} · Podcasts ${part(r.pods, '✓')}`;
|
||||||
};
|
};
|
||||||
jfetch('/api/plex/status').then(st => { plexSt = st; showSources(); }).catch(() => showSources());
|
jfetch('/api/plex/status').then(st => { plexSt = st; showSources(); }).catch(() => showSources());
|
||||||
showSources();
|
showSources();
|
||||||
@@ -55,10 +68,12 @@ function viewSearch() {
|
|||||||
const enc = encodeURIComponent, lower = term.toLowerCase();
|
const enc = encodeURIComponent, lower = term.toLowerCase();
|
||||||
const hasSf = !!sf.token(), plexReady = !!(plexSt && plexSt.signedIn && plexSt.server);
|
const hasSf = !!sf.token(), plexReady = !!(plexSt && plexSt.signedIn && plexSt.server);
|
||||||
const libsP = hasSf ? sf.req('/api2/repos/') : Promise.reject(new Error('signed out'));
|
const libsP = hasSf ? sf.req('/api2/repos/') : Promise.reject(new Error('signed out'));
|
||||||
const [libs, files, music, ddg, hub] = await Promise.allSettled([
|
const [libs, files, music, video, pods, ddg, hub] = await Promise.allSettled([
|
||||||
libsP,
|
libsP,
|
||||||
libsP.then(l => searchFiles(term, l)),
|
libsP.then(l => searchFiles(term, l)),
|
||||||
plexReady ? searchMusic(term) : Promise.reject(new Error('not connected')),
|
plexReady ? searchMusic(term) : Promise.reject(new Error('not connected')),
|
||||||
|
plexReady ? searchVideo(term) : Promise.reject(new Error('not connected')),
|
||||||
|
allowed('podcast') ? searchPodcasts(term) : Promise.reject(new Error('not allowed')),
|
||||||
jfetch('/api/ddg?q=' + enc(term)),
|
jfetch('/api/ddg?q=' + enc(term)),
|
||||||
jfetch('/api/search?q=' + enc(term)),
|
jfetch('/api/search?q=' + enc(term)),
|
||||||
]);
|
]);
|
||||||
@@ -78,6 +93,8 @@ function viewSearch() {
|
|||||||
else if (m.type === 'album') musicHtml += `<a class="row" href="#/music/album/${m.ratingKey}"><img class="art" style="width:40px;height:40px;border-radius:8px" alt="" src="${px.art(m.thumb, 80)}"><span class="grow"><b>${esc(m.title)}</b><small>Album · ${esc(m.parentTitle || '')}</small></span></a>`;
|
else if (m.type === 'album') musicHtml += `<a class="row" href="#/music/album/${m.ratingKey}"><img class="art" style="width:40px;height:40px;border-radius:8px" alt="" src="${px.art(m.thumb, 80)}"><span class="grow"><b>${esc(m.title)}</b><small>Album · ${esc(m.parentTitle || '')}</small></span></a>`;
|
||||||
else if (m.type === 'track') { const tr = mapTrack(m, {}); if (tr) musicHtml += `<button class="row" data-t="${esc(JSON.stringify(tr))}"><img class="art" style="width:40px;height:40px;border-radius:8px" alt="" src="${px.art(m.thumb || m.parentThumb, 80)}"><span class="grow"><b>${esc(m.title)}</b><small>Track · ${esc(m.grandparentTitle || '')}</small></span></button>`; }
|
else if (m.type === 'track') { const tr = mapTrack(m, {}); if (tr) musicHtml += `<button class="row" data-t="${esc(JSON.stringify(tr))}"><img class="art" style="width:40px;height:40px;border-radius:8px" alt="" src="${px.art(m.thumb || m.parentThumb, 80)}"><span class="grow"><b>${esc(m.title)}</b><small>Track · ${esc(m.grandparentTitle || '')}</small></span></button>`; }
|
||||||
}
|
}
|
||||||
|
const videoHtml = (video.status === 'fulfilled' ? video.value : []).map(m => vdCard(m)).join('');
|
||||||
|
const podHtml = (pods.status === 'fulfilled' ? pods.value : []).map(s => `<a class="row" href="#/podcast/show/${enc(pdId(s))}"><img class="art" style="width:40px;height:40px;border-radius:8px" alt="" src="${pdImg('podcasts', s)}" onerror="this.style.visibility='hidden'"><span class="grow"><b>${esc(pdF(s, 'title'))}</b><small>Podcast${pdF(s, 'author') ? ' · ' + esc(pdF(s, 'author')) : ''}</small></span></a>`).join('');
|
||||||
const d = ddg.status === 'fulfilled' ? ddg.value : {};
|
const d = ddg.status === 'fulfilled' ? ddg.value : {};
|
||||||
const card = (d.text || d.answer) ? `<div class="card2" style="padding:14px;margin-top:14px;display:flex;gap:12px;align-items:flex-start">
|
const card = (d.text || d.answer) ? `<div class="card2" style="padding:14px;margin-top:14px;display:flex;gap:12px;align-items:flex-start">
|
||||||
${d.image ? `<img alt="" src="${esc(d.image)}" referrerpolicy="no-referrer" style="width:72px;height:72px;object-fit:cover;border-radius:10px;flex:none">` : ''}
|
${d.image ? `<img alt="" src="${esc(d.image)}" referrerpolicy="no-referrer" style="width:72px;height:72px;object-fit:cover;border-radius:10px;flex:none">` : ''}
|
||||||
@@ -96,11 +113,13 @@ function viewSearch() {
|
|||||||
events: hub.status === 'fulfilled' ? { n: (h.events || []).length } : { err: hub.reason && hub.reason.message },
|
events: hub.status === 'fulfilled' ? { n: (h.events || []).length } : { err: hub.reason && hub.reason.message },
|
||||||
files: !hasSf ? undefined : files.status === 'fulfilled' ? { n: found.length + libHtml.split('class="row"').length - 1 } : { err: (files.reason && files.reason.message) || (libs.reason && libs.reason.message) || 'failed' },
|
files: !hasSf ? undefined : files.status === 'fulfilled' ? { n: found.length + libHtml.split('class="row"').length - 1 } : { err: (files.reason && files.reason.message) || (libs.reason && libs.reason.message) || 'failed' },
|
||||||
music: !plexReady ? undefined : music.status === 'fulfilled' ? { n: music.value.length } : { err: music.reason && music.reason.message },
|
music: !plexReady ? undefined : music.status === 'fulfilled' ? { n: music.value.length } : { err: music.reason && music.reason.message },
|
||||||
|
pods: !allowed('podcast') ? undefined : pods.status === 'fulfilled' ? { n: pods.value.length } : { err: pods.reason && pods.reason.message },
|
||||||
|
video: !plexReady ? undefined : video.status === 'fulfilled' ? { n: video.value.length } : { err: video.reason && video.reason.message },
|
||||||
});
|
});
|
||||||
const notes = [...(h.errors || [])].map(e => `<p class="sub">${esc(e)}</p>`).join('');
|
const notes = [...(h.errors || [])].map(e => `<p class="sub">${esc(e)}</p>`).join('');
|
||||||
const linkHtml = allowed('links') ? allLinks().filter(l => (l.title + ' ' + l.url + ' ' + (l.group || '')).toLowerCase().includes(lower)).slice(0, 8).map(l => `<a class="row" href="${esc(safeUrl(l.url))}" target="_blank" rel="noopener noreferrer"><span class="ic" style="background:${linkColor(l)}">${icon('link')}</span><span class="grow"><b>${esc(l.title || l.url)}</b><small>${esc(new URL(safeUrl(l.url)).host)}${l.group ? ' · ' + esc(l.group) : ''}</small></span></a>`).join('') : '';
|
const linkHtml = allowed('links') ? allLinks().filter(l => (l.title + ' ' + l.url + ' ' + (l.group || '')).toLowerCase().includes(lower)).slice(0, 8).map(l => `<a class="row" href="${esc(safeUrl(l.url))}" target="_blank" rel="noopener noreferrer"><span class="ic" style="background:${linkColor(l)}">${icon('link')}</span><span class="grow"><b>${esc(l.title || l.url)}</b><small>${esc(new URL(safeUrl(l.url)).host)}${l.group ? ' · ' + esc(l.group) : ''}</small></span></a>`).join('') : '';
|
||||||
res.innerHTML = card + sec('Tasks', taskHtml) + sec('Links', linkHtml) + sec('Events', eventHtml) + sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + notes;
|
res.innerHTML = card + sec('Tasks', taskHtml) + sec('Links', linkHtml) + sec('Events', eventHtml) + sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + (podHtml ? sec('Podcasts', podHtml) : '') + (videoHtml ? `<h2>Movies & TV</h2><div class="vgrid">${videoHtml}</div>` : '') + notes;
|
||||||
if (!taskHtml && !linkHtml && !eventHtml && !libHtml && !fileHtml && !musicHtml && !card) res.innerHTML = '<p class="empty">No results. The line above shows what was searched.</p>' + notes;
|
if (!taskHtml && !linkHtml && !eventHtml && !libHtml && !fileHtml && !musicHtml && !videoHtml && !podHtml && !card) res.innerHTML = '<p class="empty">No results. The line above shows what was searched.</p>' + notes;
|
||||||
res.querySelectorAll('button[data-f]').forEach(b => b.onclick = () => { const [r, p, n, dd] = JSON.parse(b.dataset.f); openFile(r, p, n, dd); });
|
res.querySelectorAll('button[data-f]').forEach(b => b.onclick = () => { const [r, p, n, dd] = JSON.parse(b.dataset.f); openFile(r, p, n, dd); });
|
||||||
res.querySelectorAll('button[data-t]').forEach(b => b.onclick = () => playQueue([JSON.parse(b.dataset.t)], 0));
|
res.querySelectorAll('button[data-t]').forEach(b => b.onclick = () => playQueue([JSON.parse(b.dataset.t)], 0));
|
||||||
res.querySelectorAll('button[data-tl]').forEach(b => b.onclick = () => { store.set('task_list', b.dataset.tl); location.hash = '#/tasks'; });
|
res.querySelectorAll('button[data-tl]').forEach(b => b.onclick = () => { store.set('task_list', b.dataset.tl); location.hash = '#/tasks'; });
|
||||||
|
|||||||
Reference in new issue
Block a user