The server asks DuckDuckGo's instant-answer service (summary, definition, quick answers) and Search shows it as a card above the app's own results when there is one. Lookups are cached for 10 minutes and a failure never breaks Search. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
64c91f9e0a
commit
18266267e5
2 files changed
+33
-3
No files matched your search
+10
-3
@@ -207,10 +207,11 @@ function viewSearch() {
|
||||
if (term.length < 2) { res.innerHTML = ''; return; }
|
||||
res.innerHTML = '<p class="empty">Searching…</p>';
|
||||
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 ? `<h2>${title}</h2><div class="list">${html}</div>` : '';
|
||||
@@ -234,8 +235,14 @@ function viewSearch() {
|
||||
}
|
||||
}
|
||||
const note = !found && sf.token() ? '<p class="sub">Could not search Seafile files right now.</p>' : '';
|
||||
res.innerHTML = sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + note || '<p class="empty">No results</p>';
|
||||
if (!libHtml && !fileHtml && !musicHtml) res.innerHTML = '<p class="empty">No results</p>' + note;
|
||||
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">
|
||||
${d.image ? `<img alt="" src="${esc(d.image)}" referrerpolicy="no-referrer" style="width:72px;height:72px;object-fit:cover;border-radius:10px;flex:none">` : ''}
|
||||
<div style="min-width:0"><small style="color:var(--muted)">From DuckDuckGo${d.source ? ' · ' + esc(d.source) : ''}</small>
|
||||
${d.heading ? `<div style="font-weight:700;margin-top:2px">${esc(d.heading)}</div>` : ''}${d.answer ? `<div style="font-size:18px;margin-top:2px">${esc(d.answer)}</div>` : ''}
|
||||
${d.text ? `<p style="margin:4px 0 0;font-size:14px">${esc(d.text)}</p>` : ''}${d.url ? `<a href="${esc(d.url)}" target="_blank" rel="noopener" style="color:var(--accent);font-size:14px">Read more</a>` : ''}</div></div>` : '';
|
||||
res.innerHTML = card + sec('Libraries', libHtml) + sec('Files', fileHtml) + sec('Music', musicHtml) + note;
|
||||
if (!libHtml && !fileHtml && !musicHtml && !card) res.innerHTML = '<p class="empty">No results</p>' + 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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user