diff --git a/public/app.js b/public/app.js index cb0f448..42ccb6f 100644 --- a/public/app.js +++ b/public/app.js @@ -207,10 +207,11 @@ function viewSearch() { if (term.length < 2) { res.innerHTML = ''; return; } res.innerHTML = '
Searching…
'; const enc = encodeURIComponent; - const [files, libs, music] = await Promise.allSettled([ + const [files, libs, music, ddg] = await Promise.allSettled([ sf.token() ? sf.req(`/api2/search/?q=${enc(term)}&per_page=20`) : Promise.reject(new Error('signed out')), sf.token() ? sf.req('/api2/repos/') : Promise.reject(new Error('signed out')), jfetch('/api/plex/status').then(st => st.server ? px.get(`/hubs/search?query=${enc(term)}&limit=8`) : Promise.reject(new Error('no server'))), + jfetch('/api/ddg?q=' + enc(term)), ]); if (mySeq !== seq || !location.hash.startsWith('#/search')) return; const sec = (title, html) => html ? `Could not search Seafile files right now.
' : ''; - res.innerHTML = sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + note || 'No results
'; - if (!libHtml && !fileHtml && !musicHtml) res.innerHTML = 'No results
' + note; + const d = ddg.status === 'fulfilled' ? ddg.value : {}; + const card = (d.text || d.answer) ? `${esc(d.text)}
` : ''}${d.url ? `Read more` : ''}No results
' + note; res.querySelectorAll('button[data-f]').forEach(b => b.onclick = () => { const [r, p, n, d] = JSON.parse(b.dataset.f); openFile(r, p, n, d); }); res.querySelectorAll('button[data-t]').forEach(b => b.onclick = () => playQueue([JSON.parse(b.dataset.t)], 0)); } diff --git a/server.js b/server.js index 4a2ffd2..3c07fa6 100644 --- a/server.js +++ b/server.js @@ -270,6 +270,28 @@ async function plexProxy(req, res) { console.log(`[data] ${dir}: ${count('passkeys.json')} passkeys, ${count('calendars.json')} calendars`); } +// ---- DuckDuckGo instant answers for the Search tab (a short summary when DDG has one; many queries return nothing) +const ddgCache = new Map(); +async function ddgAnswer(req, res) { + const q = (new URL(req.url, 'http://x').searchParams.get('q') || '').trim().slice(0, 200); + if (q.length < 2) return json(res, 200, {}); + const hit = ddgCache.get(q.toLowerCase()); + if (hit && Date.now() - hit.t < 10 * 60e3) return json(res, 200, hit.v); + try { + const u = new URL('https://api.duckduckgo.com/'); + u.search = new URLSearchParams({ q, format: 'json', no_html: '1', skip_disambig: '1', t: 'nehub' }); + const r = await fetch(u, { signal: AbortSignal.timeout(6000), headers: { Accept: 'application/json' } }); + const d = await r.json(); + const v = {}; + if (d.AbstractText) Object.assign(v, { heading: d.Heading, text: d.AbstractText.slice(0, 600), source: d.AbstractSource, url: d.AbstractURL, image: d.Image ? 'https://duckduckgo.com' + d.Image : '' }); + else if (d.Definition) Object.assign(v, { heading: d.Heading || q, text: d.Definition.slice(0, 600), source: d.DefinitionSource, url: d.DefinitionURL }); + if (d.Answer && typeof d.Answer === 'string') v.answer = d.Answer.slice(0, 200); + if (ddgCache.size > 200) ddgCache.clear(); + ddgCache.set(q.toLowerCase(), { t: Date.now(), v }); + json(res, 200, v); + } catch { json(res, 200, {}); } // a failed lookup must never break Search +} + if (!JWT_SECRET) console.warn('WARNING: ONLYOFFICE_JWT_SECRET not set. In-app editing only works if the Document Server has JWT disabled, and save callbacks are unauthenticated. Testing only.'); http.createServer((req, res) => { @@ -289,6 +311,7 @@ http.createServer((req, res) => { } if (req.method === 'POST' && req.url === '/api/oo/config') return ooConfig(req, res); if (req.method === 'POST' && req.url.startsWith('/api/oo/callback')) return ooCallback(req, res); + if (pathname === '/api/ddg') return ddgAnswer(req, res); if (req.url.startsWith('/api/cal/')) return cal.handle(req, res); if (req.url.startsWith('/api/plex/')) return plexApi(req, res); if (req.url.startsWith('/plex/') && req.method === 'GET') return plexProxy(req, res);