From 5e85e3d6289dd70aca2f7b3848efb3847075ede5 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 14 Dec 2025 00:06:54 +0000 Subject: [PATCH] rss: title/description templating; closes #1047 also closes #1053, a PR which inspired this commit heavily (slightly different approach for flexibility and performance) Co-authored-by: Dawson Jeane --- copyparty/__main__.py | 4 +++- copyparty/cfg.py | 10 +++++++++- copyparty/httpcli.py | 28 +++++++++++++++++++++------- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index ccbc92ec..e13f6f92 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1701,7 +1701,9 @@ def add_rss(ap): ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental) (volflag=rss)") ap2.add_argument("--rss-nf", metavar="HITS", type=int, default=250, help="default number of files to return (url-param 'nf')") ap2.add_argument("--rss-fext", metavar="E,E", type=u, default="", help="default list of file extensions to include (url-param 'fext'); blank=all") - ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files") + ap2.add_argument("--rss-sort", metavar="ORD", type=u, default="m", help="default sort order (url-param 'sort'); [\033[32mm\033[0m]=last-modified [\033[32mu\033[0m]=upload-time [\033[32mn\033[0m]=filename [\033[32ms\033[0m]=filesize; Uppercase=oldest-first. Note that upload-time is 0 for non-uploaded files (volflag=rss_sort)") + ap2.add_argument("--rss-fmt-t", metavar="TXT", type=u, default="{fname}", help="title format (url-param 'rss_fmt_t') (volflag=rss_fmt_t)") + ap2.add_argument("--rss-fmt-d", metavar="TXT", type=u, default="{artist} - {title}", help="description format (url-param 'rss_fmt_d') (volflag=rss_fmt_d)") def add_db_general(ap, hcores): diff --git a/copyparty/cfg.py b/copyparty/cfg.py index d83b47f9..138495dc 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -131,6 +131,9 @@ def vf_vmap() -> dict[str, str]: "readmes", "mv_retry", "rm_retry", + "rss_sort", + "rss_fmt_t", + "rss_fmt_d", "shr_who", "sort", "tail_fd", @@ -393,6 +396,12 @@ flagcats = { "tail_tmax=30": "kill connection after 30 sec", "tail_who=2": "restrict ?tail access (1=admins,2=authed,3=everyone)", }, + "rss": { + "rss": "allow '?rss' URL suffix (experimental)", + "rss_sort=m": "default sort-order (m/u/n/s)", + "rss_fmt_t={fname}": "default title-format", + "rss_fmt_d={album},{.tn}": "default description-format", + }, "others": { "dots": "allow all users with read-access to\nenable the option to show dotfiles in listings", "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', @@ -400,7 +409,6 @@ flagcats = { "dk=8": 'generates per-directory accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes', "dks": "per-directory accesskeys allow browsing into subdirs", "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so', - "rss": "allow '?rss' URL suffix (experimental)", "rmagic": "expensive analysis for mimetype accuracy", "shr_who=auth": "who can create shares? no/auth/a", "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name", diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 2a36320e..059a37da 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -173,6 +173,7 @@ RE_MHOST = re.compile(r"^[][0-9a-zA-Z.:_-]+$") # match faster >=18ch RE_K = re.compile(r"[^0-9a-zA-Z_-]") # search faster <=17ch RE_HR = re.compile(r"[<>\"'&]") RE_MDV = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[Mm][Dd])$") +RE_RSS_KW = re.compile(r"(\{[^} ]+\})") UPARAM_CC_OK = set("doc move tree".split()) @@ -1556,18 +1557,31 @@ class HttpCli(object): ap = "" use_magic = "rmagic" in self.vn.flags + tpl_t = self.uparam.get("fmt_t") or self.vn.flags["rss_fmt_t"] + tpl_d = self.uparam.get("fmt_d") or self.vn.flags["rss_fmt_d"] + kw_t = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_t)] + kw_d = [[x, x[1:-1]] for x in RE_RSS_KW.findall(tpl_d)] + for i in hits: if use_magic: ap = os.path.join(self.vn.realpath, i["rp"]) + tags = i["tags"] iurl = html_escape("%s%s" % (baseurl, i["rp"]), True, True) - title = unquotep(i["rp"].split("?")[0].split("/")[-1]) - title = html_escape(title, True, True) - tag_t = str(i["tags"].get("title") or "") - tag_a = str(i["tags"].get("artist") or "") - desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a) - desc = html_escape(desc, True, True) if desc else title - mime = html_escape(guess_mime(title, ap)) + fname = tags["fname"] = unquotep(i["rp"].split("?")[0].split("/")[-1]) + title = tpl_t + desc = tpl_d + for zs1, zs2 in kw_t: + title = title.replace(zs1, str(tags.get(zs2, ""))) + for zs1, zs2 in kw_d: + desc = desc.replace(zs1, str(tags.get(zs2, ""))) + title = html_escape(title.strip(), True, True) + if desc.strip(" -,"): + desc = html_escape(desc.strip(), True, True) + else: + desc = title + + mime = html_escape(guess_mime(fname, ap)) lmod = formatdate(max(0, i["ts"])) zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"]) zs = (