opds: add opensearch support (#1287)

tested with Moon Reader and Koreader; based on:
https://specs.opds.io/opds-1.2#3-search
https://github.com/koreader/koreader/pull/7380

Signed-off-by: Brandon Philips <brandon@ifup.org>
This commit is contained in:
Brandon Philips
2026-02-11 13:47:24 -08:00
committed by GitHub
parent d44ea24530
commit 84e687a00d
5 changed files with 72 additions and 3 deletions

View File

@@ -7412,12 +7412,70 @@ class HttpCli(object):
dirs.sort(key=itemgetter("name"))
if is_opds:
# OpenSearch Description format requires a full-qualified URL and a "Short Name" under 16 characters
# which will be the longname truncated in the template.
# Relevant specs:
# https://specs.opds.io/opds-1.2#3-search
# https://developer.mozilla.org/en-US/docs/Web/XML/Guides/OpenSearch
if "osd" in self.uparam:
j2a["longname"] = "%s %s" % (self.args.bname, self.vpath)
j2a["search_url"] = self.args.SRS + vpath
xml = self.j2s("opds_osd", **j2a)
self.reply(
xml.encode("utf-8"), mime="application/opensearchdescription+xml"
)
return True
if "q" in self.uparam:
q = self.uparam["q"]
idx = self.conn.get_u2idx()
if not idx:
raise Pebkac(500, "indexer not available")
# generate a raw query similar to web interface for multiple words
r = " and ".join(("name like *%s*" % (x,)) for x in q.split())
hits, _, _ = idx.search(self.uname, [self.vn], r, 1000)
files = []
dirs = []
prefix = quotep(vpath + "/" if vpath else "")
for h in hits:
rp = h["rp"]
if not rp.startswith(prefix):
continue
zd = datetime.fromtimestamp(h["ts"], UTC)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year,
zd.month,
zd.day,
zd.hour,
zd.minute,
zd.second,
)
item = {
"href": self.args.SRS + rp,
"name": unquotep(rp[len(prefix) :].split("?")[0]),
"sz": h["sz"],
"dt": dt,
"ts": h["ts"],
}
files.append(item)
# exclude files which don't match --opds-exts
allowed_exts = vf.get("opds_exts") or self.args.opds_exts
if allowed_exts:
files = [
x for x in files if x["name"].rsplit(".", 1)[-1] in allowed_exts
]
j2a["opds_osd"] = "%s%s?opds&osd" % (self.args.SRS, quotep(vpath))
for item in dirs:
href = item["href"]
href += ("&" if "?" in href else "?") + "opds"

View File

@@ -188,6 +188,7 @@ class HttpSrv(object):
]
self.j2 = {x: env.get_template(x + ".html") for x in jn}
self.j2["opds"] = env.get_template("opds.xml")
self.j2["opds_osd"] = env.get_template("opds_osd.xml")
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
if self.args.ipu:

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<link rel="search"
href="{{ opds_osd | e }}"
type="application/opensearchdescription+xml"/>
{%- for d in dirs %}
<entry>
<title>{{ d.name }}</title>
<title>{{ d.name | e }}</title>
<link rel="subsection"
href="{{ d.href | e }}"
type="application/atom+xml;profile=opds-catalog"/>
@@ -11,7 +14,7 @@
{%- endfor %}
{%- for f in files %}
<entry>
<title>{{ f.name }}</title>
<title>{{ f.name | e }}</title>
<updated>{{ f.iso8601 }}</updated>
<link rel="http://opds-spec.org/acquisition"
href="{{ f.href | e }}"
@@ -28,4 +31,4 @@
{%- endif %}
</entry>
{%- endfor %}
</feed>
</feed>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>{{ longname | truncate(16) | e }}</ShortName>
<Description>{{ longname | e }}</Description>
<Url type="application/atom+xml;profile=opds-catalog" template="{{ search_url }}?opds&amp;q={searchTerms}"/>
</OpenSearchDescription>

View File

@@ -102,6 +102,7 @@ copyparty/web/mde.html,
copyparty/web/mde.js,
copyparty/web/msg.html,
copyparty/web/opds.xml,
copyparty/web/opds_osd.xml,
copyparty/web/rups.css,
copyparty/web/rups.html,
copyparty/web/rups.js,