mirror of
				https://github.com/9001/copyparty.git
				synced 2025-11-04 05:43:17 +00:00 
			
		
		
		
	OPDS Support (#779)
* add OPDS support * add `?opds` to devnotes.md * send content-disposition for opds downloads
This commit is contained in:
		
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							@@ -1039,6 +1039,27 @@ url parameters:
 | 
			
		||||
  * `a` = filesize
 | 
			
		||||
  * 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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1437,6 +1437,10 @@ def add_smb(ap):
 | 
			
		||||
    ap2.add_argument("--smbvv", action="store_true", help="verboser")
 | 
			
		||||
    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):
 | 
			
		||||
    ap2 = ap.add_argument_group("handlers (see --help-handlers)")
 | 
			
		||||
@@ -1865,6 +1869,7 @@ def run_argparse(
 | 
			
		||||
    add_webdav(ap)
 | 
			
		||||
    add_tftp(ap)
 | 
			
		||||
    add_smb(ap)
 | 
			
		||||
    add_opds(ap)
 | 
			
		||||
    add_safety(ap)
 | 
			
		||||
    add_salt(ap, fk_salt, dk_salt, ah_salt)
 | 
			
		||||
    add_optouts(ap)
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
 | 
			
		||||
        "og",
 | 
			
		||||
        "og_no_head",
 | 
			
		||||
        "og_s_title",
 | 
			
		||||
        "opds",
 | 
			
		||||
        "rand",
 | 
			
		||||
        "reflink",
 | 
			
		||||
        "rmagic",
 | 
			
		||||
@@ -106,6 +107,7 @@ def vf_vmap() -> dict[str, str]:
 | 
			
		||||
        "og_title_i",
 | 
			
		||||
        "og_tpl",
 | 
			
		||||
        "og_ua",
 | 
			
		||||
        "opds_exts",
 | 
			
		||||
        "put_ck",
 | 
			
		||||
        "put_name",
 | 
			
		||||
        "mv_retry",
 | 
			
		||||
@@ -332,6 +334,10 @@ flagcats = {
 | 
			
		||||
        "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",
 | 
			
		||||
    },
 | 
			
		||||
    "opds": {
 | 
			
		||||
        "opds": "enable OPDS",
 | 
			
		||||
        "opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
 | 
			
		||||
    },
 | 
			
		||||
    "textfiles": {
 | 
			
		||||
        "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",
 | 
			
		||||
 
 | 
			
		||||
@@ -6278,7 +6278,7 @@ class HttpCli(object):
 | 
			
		||||
 | 
			
		||||
        add_og = "og" in vn.flags
 | 
			
		||||
        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
 | 
			
		||||
            elif vn.flags["og_ua"]:
 | 
			
		||||
                add_og = vn.flags["og_ua"].search(self.ua)
 | 
			
		||||
@@ -6483,6 +6483,7 @@ class HttpCli(object):
 | 
			
		||||
 | 
			
		||||
        url_suf = self.urlq({}, ["k"])
 | 
			
		||||
        is_ls = "ls" in self.uparam
 | 
			
		||||
        is_opds = "opds" in self.uparam
 | 
			
		||||
        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")):
 | 
			
		||||
@@ -6493,6 +6494,13 @@ class HttpCli(object):
 | 
			
		||||
        if "b" in self.uparam:
 | 
			
		||||
            tpl = "browser2"
 | 
			
		||||
            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
 | 
			
		||||
        ls_ret = {
 | 
			
		||||
@@ -6622,10 +6630,13 @@ class HttpCli(object):
 | 
			
		||||
        dirs = []
 | 
			
		||||
        files = []
 | 
			
		||||
        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:
 | 
			
		||||
            base = ""
 | 
			
		||||
            href = fn
 | 
			
		||||
            if not is_ls and not is_js and not self.trailing_slash and vpath:
 | 
			
		||||
            if use_abs_url:
 | 
			
		||||
                base = "/" + vpath + "/"
 | 
			
		||||
                href = base + fn
 | 
			
		||||
 | 
			
		||||
@@ -6725,6 +6736,7 @@ class HttpCli(object):
 | 
			
		||||
            self.cookies.get("idxh") == "y"
 | 
			
		||||
            and "ls" not in self.uparam
 | 
			
		||||
            and "v" not in self.uparam
 | 
			
		||||
            and not is_opds
 | 
			
		||||
        ):
 | 
			
		||||
            idx_html = set(["index.htm", "index.html"])
 | 
			
		||||
            for item in files:
 | 
			
		||||
@@ -6893,6 +6905,51 @@ class HttpCli(object):
 | 
			
		||||
 | 
			
		||||
        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:
 | 
			
		||||
            j2a["ls0"] = cgv["ls0"] = {
 | 
			
		||||
                "dirs": dirs,
 | 
			
		||||
 
 | 
			
		||||
@@ -187,6 +187,7 @@ class HttpSrv(object):
 | 
			
		||||
            "svcs",
 | 
			
		||||
        ]
 | 
			
		||||
        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")
 | 
			
		||||
 | 
			
		||||
        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=t` | list files/folders at URL as plaintext |
 | 
			
		||||
| 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 | `?b` | list files/folders at URL as simplified HTML |
 | 
			
		||||
| GET | `?tree=.` | list one level of subdirectories inside URL |
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user