diff --git a/public/search-view.js b/public/search-view.js index 35dd26f..90b6561 100644 --- a/public/search-view.js +++ b/public/search-view.js @@ -25,8 +25,21 @@ async function searchMusic(term) { 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() { - $('#view').innerHTML = `

Search

+ $('#view').innerHTML = `

Search

@@ -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 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') : '✓'; - 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()); showSources(); @@ -55,10 +68,12 @@ function viewSearch() { const enc = encodeURIComponent, lower = term.toLowerCase(); const hasSf = !!sf.token(), plexReady = !!(plexSt && plexSt.signedIn && plexSt.server); 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.then(l => searchFiles(term, l)), 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/search?q=' + enc(term)), ]); @@ -78,6 +93,8 @@ function viewSearch() { else if (m.type === 'album') musicHtml += `${esc(m.title)}Album · ${esc(m.parentTitle || '')}`; else if (m.type === 'track') { const tr = mapTrack(m, {}); if (tr) musicHtml += ``; } } + const videoHtml = (video.status === 'fulfilled' ? video.value : []).map(m => vdCard(m)).join(''); + const podHtml = (pods.status === 'fulfilled' ? pods.value : []).map(s => `${esc(pdF(s, 'title'))}Podcast${pdF(s, 'author') ? ' · ' + esc(pdF(s, 'author')) : ''}`).join(''); const d = ddg.status === 'fulfilled' ? ddg.value : {}; const card = (d.text || d.answer) ? `
${d.image ? `` : ''} @@ -96,11 +113,13 @@ function viewSearch() { 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' }, 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 => `

${esc(e)}

`).join(''); const linkHtml = allowed('links') ? allLinks().filter(l => (l.title + ' ' + l.url + ' ' + (l.group || '')).toLowerCase().includes(lower)).slice(0, 8).map(l => `${icon('link')}${esc(l.title || l.url)}${esc(new URL(safeUrl(l.url)).host)}${l.group ? ' · ' + esc(l.group) : ''}`).join('') : ''; - res.innerHTML = card + sec('Tasks', taskHtml) + sec('Links', linkHtml) + sec('Events', eventHtml) + sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + notes; - if (!taskHtml && !linkHtml && !eventHtml && !libHtml && !fileHtml && !musicHtml && !card) res.innerHTML = '

No results. The line above shows what was searched.

' + 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 ? `

Movies & TV

${videoHtml}
` : '') + notes; + if (!taskHtml && !linkHtml && !eventHtml && !libHtml && !fileHtml && !musicHtml && !videoHtml && !podHtml && !card) res.innerHTML = '

No results. The line above shows what was searched.

' + 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-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'; });