mirror of
https://github.com/9001/copyparty.git
synced 2025-09-30 13:42:27 -06:00
OPDS Support (#779)
* add OPDS support * add `?opds` to devnotes.md * send content-disposition for opds downloads
This commit is contained in:
parent
e9ca36fa88
commit
6dbd9901b2
21
README.md
21
README.md
|
@ -1039,6 +1039,27 @@ url parameters:
|
||||||
* `a` = filesize
|
* `a` = filesize
|
||||||
* uppercase = reverse-sort; `M` = oldest file first
|
* uppercase = reverse-sort; `M` = oldest file first
|
||||||
|
|
||||||
|
# opds feeds
|
||||||
|
|
||||||
|
browse and download files from your e-book reader
|
||||||
|
|
||||||
|
enabled with the `opds` volflag or `--opds` global option
|
||||||
|
|
||||||
|
add `?opds` to the end of the url you would like to browse, then input that in your opds client.
|
||||||
|
for example: `https://copyparty.example/books/?opds`.
|
||||||
|
|
||||||
|
to log in with a password, enter it into either of the username or password fields in your client.
|
||||||
|
|
||||||
|
- if you've enabled `--usernames`, then you need to enter both username and password .
|
||||||
|
|
||||||
|
note: some clients (e.g. Moon+ Reader) will not send the password when downloading cover images, which will
|
||||||
|
cause your ip to be banned by copyparty. to work around this, you can grant the [`g` permission](#accounts-and-volumes)
|
||||||
|
to unauthenticated requests and enable [filekeys](#filekeys) to prevent guessing filenames. for example:
|
||||||
|
`-vbooks:books:r,ed:g:c,fk,opds`
|
||||||
|
|
||||||
|
by default, not all file types will be listed in opds feeds. to change this, add the extension to
|
||||||
|
`--opds-exts` (volflag: `opds_exts`), or empty the list to list everything
|
||||||
|
|
||||||
|
|
||||||
## recent uploads
|
## recent uploads
|
||||||
|
|
||||||
|
|
|
@ -1437,6 +1437,10 @@ def add_smb(ap):
|
||||||
ap2.add_argument("--smbvv", action="store_true", help="verboser")
|
ap2.add_argument("--smbvv", action="store_true", help="verboser")
|
||||||
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
||||||
|
|
||||||
|
def add_opds(ap):
|
||||||
|
ap2 = ap.add_argument_group("OPDS options")
|
||||||
|
ap2.add_argument("--opds", action="store_true", help="enable opds -- allows e-book readers to browse and download files (volflag=opds)")
|
||||||
|
ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)")
|
||||||
|
|
||||||
def add_handlers(ap):
|
def add_handlers(ap):
|
||||||
ap2 = ap.add_argument_group("handlers (see --help-handlers)")
|
ap2 = ap.add_argument_group("handlers (see --help-handlers)")
|
||||||
|
@ -1865,6 +1869,7 @@ def run_argparse(
|
||||||
add_webdav(ap)
|
add_webdav(ap)
|
||||||
add_tftp(ap)
|
add_tftp(ap)
|
||||||
add_smb(ap)
|
add_smb(ap)
|
||||||
|
add_opds(ap)
|
||||||
add_safety(ap)
|
add_safety(ap)
|
||||||
add_salt(ap, fk_salt, dk_salt, ah_salt)
|
add_salt(ap, fk_salt, dk_salt, ah_salt)
|
||||||
add_optouts(ap)
|
add_optouts(ap)
|
||||||
|
|
|
@ -52,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
|
||||||
"og",
|
"og",
|
||||||
"og_no_head",
|
"og_no_head",
|
||||||
"og_s_title",
|
"og_s_title",
|
||||||
|
"opds",
|
||||||
"rand",
|
"rand",
|
||||||
"reflink",
|
"reflink",
|
||||||
"rmagic",
|
"rmagic",
|
||||||
|
@ -106,6 +107,7 @@ def vf_vmap() -> dict[str, str]:
|
||||||
"og_title_i",
|
"og_title_i",
|
||||||
"og_tpl",
|
"og_tpl",
|
||||||
"og_ua",
|
"og_ua",
|
||||||
|
"opds_exts",
|
||||||
"put_ck",
|
"put_ck",
|
||||||
"put_name",
|
"put_name",
|
||||||
"mv_retry",
|
"mv_retry",
|
||||||
|
@ -332,6 +334,10 @@ flagcats = {
|
||||||
"og_no_head": "you want to add tags manually with og_tpl",
|
"og_no_head": "you want to add tags manually with og_tpl",
|
||||||
"og_ua": "if defined: only send OG html if useragent matches this regex",
|
"og_ua": "if defined: only send OG html if useragent matches this regex",
|
||||||
},
|
},
|
||||||
|
"opds": {
|
||||||
|
"opds": "enable OPDS",
|
||||||
|
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
|
||||||
|
},
|
||||||
"textfiles": {
|
"textfiles": {
|
||||||
"md_no_br": "newline only on double-newline or two tailing spaces",
|
"md_no_br": "newline only on double-newline or two tailing spaces",
|
||||||
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
|
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
|
||||||
|
|
|
@ -6278,7 +6278,7 @@ class HttpCli(object):
|
||||||
|
|
||||||
add_og = "og" in vn.flags
|
add_og = "og" in vn.flags
|
||||||
if add_og:
|
if add_og:
|
||||||
if "th" in self.uparam or "raw" in self.uparam:
|
if "th" in self.uparam or "raw" in self.uparam or "opds" in self.uparam:
|
||||||
add_og = False
|
add_og = False
|
||||||
elif vn.flags["og_ua"]:
|
elif vn.flags["og_ua"]:
|
||||||
add_og = vn.flags["og_ua"].search(self.ua)
|
add_og = vn.flags["og_ua"].search(self.ua)
|
||||||
|
@ -6483,6 +6483,7 @@ class HttpCli(object):
|
||||||
|
|
||||||
url_suf = self.urlq({}, ["k"])
|
url_suf = self.urlq({}, ["k"])
|
||||||
is_ls = "ls" in self.uparam
|
is_ls = "ls" in self.uparam
|
||||||
|
is_opds = "opds" in self.uparam
|
||||||
is_js = self.args.force_js or self.cookies.get("js") == "y"
|
is_js = self.args.force_js or self.cookies.get("js") == "y"
|
||||||
|
|
||||||
if not is_ls and not add_og and self.ua.startswith(("curl/", "fetch")):
|
if not is_ls and not add_og and self.ua.startswith(("curl/", "fetch")):
|
||||||
|
@ -6493,6 +6494,13 @@ class HttpCli(object):
|
||||||
if "b" in self.uparam:
|
if "b" in self.uparam:
|
||||||
tpl = "browser2"
|
tpl = "browser2"
|
||||||
is_js = False
|
is_js = False
|
||||||
|
elif is_opds:
|
||||||
|
# Display directory listing as OPDS v1.2 catalog feed
|
||||||
|
if not (self.args.opds or "opds" in self.vn.flags):
|
||||||
|
raise Pebkac(405, "OPDS is disabled in server config")
|
||||||
|
if not self.can_read:
|
||||||
|
raise Pebkac(401, "OPDS requires read permission")
|
||||||
|
is_js = is_ls = False
|
||||||
|
|
||||||
vf = vn.flags
|
vf = vn.flags
|
||||||
ls_ret = {
|
ls_ret = {
|
||||||
|
@ -6622,10 +6630,13 @@ class HttpCli(object):
|
||||||
dirs = []
|
dirs = []
|
||||||
files = []
|
files = []
|
||||||
ptn_hr = RE_HR
|
ptn_hr = RE_HR
|
||||||
|
use_abs_url = is_opds or (
|
||||||
|
not is_ls and not is_js and not self.trailing_slash and vpath
|
||||||
|
)
|
||||||
for fn in ls_names:
|
for fn in ls_names:
|
||||||
base = ""
|
base = ""
|
||||||
href = fn
|
href = fn
|
||||||
if not is_ls and not is_js and not self.trailing_slash and vpath:
|
if use_abs_url:
|
||||||
base = "/" + vpath + "/"
|
base = "/" + vpath + "/"
|
||||||
href = base + fn
|
href = base + fn
|
||||||
|
|
||||||
|
@ -6725,6 +6736,7 @@ class HttpCli(object):
|
||||||
self.cookies.get("idxh") == "y"
|
self.cookies.get("idxh") == "y"
|
||||||
and "ls" not in self.uparam
|
and "ls" not in self.uparam
|
||||||
and "v" not in self.uparam
|
and "v" not in self.uparam
|
||||||
|
and not is_opds
|
||||||
):
|
):
|
||||||
idx_html = set(["index.htm", "index.html"])
|
idx_html = set(["index.htm", "index.html"])
|
||||||
for item in files:
|
for item in files:
|
||||||
|
@ -6893,6 +6905,51 @@ class HttpCli(object):
|
||||||
|
|
||||||
dirs.sort(key=itemgetter("name"))
|
dirs.sort(key=itemgetter("name"))
|
||||||
|
|
||||||
|
if is_opds:
|
||||||
|
url_base = "%s://%s%s" % (
|
||||||
|
"https" if self.is_https else "http",
|
||||||
|
self.host,
|
||||||
|
self.args.SR,
|
||||||
|
)
|
||||||
|
# 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
|
||||||
|
]
|
||||||
|
for item in dirs:
|
||||||
|
href = url_base + item["href"]
|
||||||
|
href += ("&" if "?" in href else "?") + "opds"
|
||||||
|
item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"))
|
||||||
|
|
||||||
|
for item in files:
|
||||||
|
href = url_base + item["href"]
|
||||||
|
href += ("&" if "?" in href else "?") + "dl"
|
||||||
|
item["iso8601"] = "%sZ" % (item["dt"].replace(" ", "T"))
|
||||||
|
|
||||||
|
if "rmagic" in self.vn.flags:
|
||||||
|
ap = "%s/%s" % (fsroot, item["name"])
|
||||||
|
item["mime"] = guess_mime(item["name"], ap)
|
||||||
|
else:
|
||||||
|
item["mime"] = guess_mime(item["name"])
|
||||||
|
|
||||||
|
# Make sure we can actually generate JPEG thumbnails
|
||||||
|
if (
|
||||||
|
not self.args.th_no_jpg
|
||||||
|
and self.thumbcli
|
||||||
|
and "dthumb" not in dbv.flags
|
||||||
|
and "dithumb" not in dbv.flags
|
||||||
|
):
|
||||||
|
item["jpeg_thumb_href"] = href + "&th=jf"
|
||||||
|
item["jpeg_thumb_href_hires"] = item["jpeg_thumb_href"] + "3"
|
||||||
|
|
||||||
|
j2a["files"] = files
|
||||||
|
j2a["dirs"] = dirs
|
||||||
|
html = self.j2s("opds", **j2a)
|
||||||
|
mime = "application/atom+xml;profile=opds-catalog"
|
||||||
|
self.reply(html.encode("utf-8", "replace"), mime=mime)
|
||||||
|
return True
|
||||||
|
|
||||||
if is_js:
|
if is_js:
|
||||||
j2a["ls0"] = cgv["ls0"] = {
|
j2a["ls0"] = cgv["ls0"] = {
|
||||||
"dirs": dirs,
|
"dirs": dirs,
|
||||||
|
|
|
@ -187,6 +187,7 @@ class HttpSrv(object):
|
||||||
"svcs",
|
"svcs",
|
||||||
]
|
]
|
||||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||||
|
self.j2["opds"] = env.get_template("opds.xml")
|
||||||
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
||||||
|
|
||||||
if self.args.ipu:
|
if self.args.ipu:
|
||||||
|
|
31
copyparty/web/opds.xml
Normal file
31
copyparty/web/opds.xml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
{%- for d in dirs %}
|
||||||
|
<entry>
|
||||||
|
<title>{{ d.name }}</title>
|
||||||
|
<link rel="subsection"
|
||||||
|
href="{{ d.href | e }}"
|
||||||
|
type="application/atom+xml;profile=opds-catalog"/>
|
||||||
|
<updated>{{ d.iso8601 }}</updated>
|
||||||
|
</entry>
|
||||||
|
{%- endfor %}
|
||||||
|
{%- for f in files %}
|
||||||
|
<entry>
|
||||||
|
<title>{{ f.name }}</title>
|
||||||
|
<updated>{{ f.iso8601 }}</updated>
|
||||||
|
<link rel="http://opds-spec.org/acquisition"
|
||||||
|
href="{{ f.href | e }}"
|
||||||
|
type="{{ f.mime }}"/>
|
||||||
|
{%- if f.jpeg_thumb_href != None %}
|
||||||
|
<link rel="http://opds-spec.org/image/thumbnail"
|
||||||
|
href="{{ f.jpeg_thumb_href | e }}"
|
||||||
|
type="image/jpeg"/>
|
||||||
|
{%- endif %}
|
||||||
|
{%- if f.jpeg_thumb_href_hires != None %}
|
||||||
|
<link rel="http://opds-spec.org/image"
|
||||||
|
href="{{ f.jpeg_thumb_href_hires | e }}"
|
||||||
|
type="image/jpeg"/>
|
||||||
|
{%- endif %}
|
||||||
|
</entry>
|
||||||
|
{%- endfor %}
|
||||||
|
</feed>
|
|
@ -165,6 +165,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||||
| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |
|
| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |
|
||||||
| GET | `?ls=t` | list files/folders at URL as plaintext |
|
| GET | `?ls=t` | list files/folders at URL as plaintext |
|
||||||
| GET | `?ls=v` | list files/folders at URL, terminal-formatted |
|
| GET | `?ls=v` | list files/folders at URL, terminal-formatted |
|
||||||
|
| GET | `?opds` | list files/folders at URL as opds feed, for e-readers |
|
||||||
| GET | `?lt` | in listings, use symlink timestamps rather than targets |
|
| GET | `?lt` | in listings, use symlink timestamps rather than targets |
|
||||||
| GET | `?b` | list files/folders at URL as simplified HTML |
|
| GET | `?b` | list files/folders at URL as simplified HTML |
|
||||||
| GET | `?tree=.` | list one level of subdirectories inside URL |
|
| GET | `?tree=.` | list one level of subdirectories inside URL |
|
||||||
|
|
Loading…
Reference in a new issue