diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
index e5fc2a13..3f84bb17 100644
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -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"
diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py
index f6cdef40..ea89a0aa 100644
--- a/copyparty/httpsrv.py
+++ b/copyparty/httpsrv.py
@@ -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:
diff --git a/copyparty/web/opds.xml b/copyparty/web/opds.xml
index fb22371c..ea89edf7 100644
--- a/copyparty/web/opds.xml
+++ b/copyparty/web/opds.xml
@@ -1,8 +1,11 @@
+
{%- for d in dirs %}
- {{ d.name }}
+ {{ d.name | e }}
@@ -11,7 +14,7 @@
{%- endfor %}
{%- for f in files %}
- {{ f.name }}
+ {{ f.name | e }}
{{ f.iso8601 }}
{%- endfor %}
-
\ No newline at end of file
+
diff --git a/copyparty/web/opds_osd.xml b/copyparty/web/opds_osd.xml
new file mode 100644
index 00000000..15393b26
--- /dev/null
+++ b/copyparty/web/opds_osd.xml
@@ -0,0 +1,6 @@
+
+
+ {{ longname | truncate(16) | e }}
+ {{ longname | e }}
+
+
diff --git a/scripts/sfx.ls b/scripts/sfx.ls
index 0b8f14f2..6a1cad2f 100644
--- a/scripts/sfx.ls
+++ b/scripts/sfx.ls
@@ -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,