add RSS feed output; closes #109
This commit is contained in:
		
							parent
							
								
									a7e2a0c981
								
							
						
					
					
						commit
						7ffd805a03
					
				
							
								
								
									
										25
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								README.md
									
									
									
									
									
								
							| @ -47,6 +47,7 @@ turn almost any device into a file server with resumable uploads/downloads using | ||||
|     * [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission) | ||||
|     * [shares](#shares) - share a file or folder by creating a temporary link | ||||
|     * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI | ||||
|     * [rss feeds](#rss-feeds) - monitor a folder with your RSS reader | ||||
|     * [media player](#media-player) - plays almost every audio format there is | ||||
|         * [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression) | ||||
|         * [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings | ||||
| @ -845,6 +846,30 @@ or a mix of both: | ||||
| the metadata keys you can use in the format field are the ones in the file-browser table header (whatever is collected with `-mte` and `-mtp`) | ||||
| 
 | ||||
| 
 | ||||
| ## rss feeds | ||||
| 
 | ||||
| monitor a folder with your RSS reader  , optionally recursive | ||||
| 
 | ||||
| must be enabled per-volume with volflag `rss` or globally with `--rss` | ||||
| 
 | ||||
| the feed includes itunes metadata for use with podcast readers such as [AntennaPod](https://antennapod.org/) | ||||
| 
 | ||||
| a feed example: https://cd.ocv.me/a/d2/d22/?rss&fext=mp3 | ||||
| 
 | ||||
| url parameters: | ||||
| 
 | ||||
| * `pw=hunter2` for password auth | ||||
| * `recursive` to also include subfolders | ||||
| * `title=foo` changes the feed title (default: folder name) | ||||
| * `fext=mp3,opus` only include mp3 and opus files (default: all) | ||||
| * `nf=30` only show the first 30 results (default: 250) | ||||
| * `sort=m` sort by mtime (file last-modified), newest first (default) | ||||
|   * `u` = upload-time; NOTE: non-uploaded files have upload-time `0` | ||||
|   * `n` = filename | ||||
|   * `a` = filesize | ||||
|   * uppercase = reverse-sort; `M` = oldest file first | ||||
| 
 | ||||
| 
 | ||||
| ## media player | ||||
| 
 | ||||
| plays almost every audio format there is  (if the server has FFmpeg installed for on-demand transcoding) | ||||
|  | ||||
| @ -1357,6 +1357,14 @@ def add_transcoding(ap): | ||||
|     ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds") | ||||
| 
 | ||||
| 
 | ||||
| def add_rss(ap): | ||||
|     ap2 = ap.add_argument_group('RSS options') | ||||
|     ap2.add_argument("--rss", action="store_true", help="enable RSS output (experimental)") | ||||
|     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") | ||||
| 
 | ||||
| 
 | ||||
| def add_db_general(ap, hcores): | ||||
|     noidx = APPLESAN_TXT if MACOS else "" | ||||
|     ap2 = ap.add_argument_group('general db options') | ||||
| @ -1526,6 +1534,7 @@ def run_argparse( | ||||
|     add_db_metadata(ap) | ||||
|     add_thumbnail(ap) | ||||
|     add_transcoding(ap) | ||||
|     add_rss(ap) | ||||
|     add_ftp(ap) | ||||
|     add_webdav(ap) | ||||
|     add_tftp(ap) | ||||
|  | ||||
| @ -46,6 +46,7 @@ def vf_bmap() -> dict[str, str]: | ||||
|         "og_no_head", | ||||
|         "og_s_title", | ||||
|         "rand", | ||||
|         "rss", | ||||
|         "xdev", | ||||
|         "xlink", | ||||
|         "xvol", | ||||
|  | ||||
| @ -131,6 +131,8 @@ LOGUES = [[0, ".prologue.html"], [1, ".epilogue.html"]] | ||||
| 
 | ||||
| READMES = [[0, ["preadme.md", "PREADME.md"]], [1, ["readme.md", "README.md"]]] | ||||
| 
 | ||||
| RSS_SORT = {"m": "mt", "u": "at", "n": "fn", "s": "sz"} | ||||
| 
 | ||||
| 
 | ||||
| class HttpCli(object): | ||||
|     """ | ||||
| @ -1201,8 +1203,146 @@ class HttpCli(object): | ||||
|         if "h" in self.uparam: | ||||
|             return self.tx_mounts() | ||||
| 
 | ||||
|         if "rss" in self.uparam: | ||||
|             return self.tx_rss() | ||||
| 
 | ||||
|         return self.tx_browser() | ||||
| 
 | ||||
|     def tx_rss(self) -> bool: | ||||
|         if self.do_log: | ||||
|             self.log("RSS  %s @%s" % (self.req, self.uname)) | ||||
| 
 | ||||
|         if not self.can_read: | ||||
|             return self.tx_404() | ||||
| 
 | ||||
|         vn = self.vn | ||||
|         if not vn.flags.get("rss"): | ||||
|             raise Pebkac(405, "RSS is disabled in server config") | ||||
| 
 | ||||
|         rem = self.rem | ||||
|         idx = self.conn.get_u2idx() | ||||
|         if not idx or not hasattr(idx, "p_end"): | ||||
|             if not HAVE_SQLITE3: | ||||
|                 raise Pebkac(500, "sqlite3 not found on server; rss is disabled") | ||||
|             raise Pebkac(500, "server busy, cannot generate rss; please retry in a bit") | ||||
| 
 | ||||
|         uv = [rem] | ||||
|         if "recursive" in self.uparam: | ||||
|             uq = "up.rd like ?||'%'" | ||||
|         else: | ||||
|             uq = "up.rd == ?" | ||||
| 
 | ||||
|         zs = str(self.uparam.get("fext", self.args.rss_fext)) | ||||
|         if zs in ("True", "False"): | ||||
|             zs = "" | ||||
|         if zs: | ||||
|             zsl = [] | ||||
|             for ext in zs.split(","): | ||||
|                 zsl.append("+up.fn like '%.'||?") | ||||
|                 uv.append(ext) | ||||
|             uq += " and ( %s )" % (" or ".join(zsl),) | ||||
| 
 | ||||
|         zs1 = self.uparam.get("sort", self.args.rss_sort) | ||||
|         zs2 = zs1.lower() | ||||
|         zs = RSS_SORT.get(zs2) | ||||
|         if not zs: | ||||
|             raise Pebkac(400, "invalid sort key; must be m/u/n/s") | ||||
| 
 | ||||
|         uq += " order by up." + zs | ||||
|         if zs1 == zs2: | ||||
|             uq += " desc" | ||||
| 
 | ||||
|         nmax = int(self.uparam.get("nf") or self.args.rss_nf) | ||||
| 
 | ||||
|         hits = idx.run_query(self.uname, [self.vn], uq, uv, False, False, nmax)[0] | ||||
| 
 | ||||
|         pw = self.ouparam.get("pw") | ||||
|         if pw: | ||||
|             q_pw = "?pw=%s" % (pw,) | ||||
|             a_pw = "&pw=%s" % (pw,) | ||||
|             for i in hits: | ||||
|                 i["rp"] += a_pw if "?" in i["rp"] else q_pw | ||||
|         else: | ||||
|             q_pw = a_pw = "" | ||||
| 
 | ||||
|         title = self.uparam.get("title") or self.vpath.split("/")[-1] | ||||
|         etitle = html_escape(title, True, True) | ||||
| 
 | ||||
|         baseurl = "%s://%s%s" % ( | ||||
|             "https" if self.is_https else "http", | ||||
|             self.host, | ||||
|             self.args.SRS, | ||||
|         ) | ||||
|         feed = "%s%s" % (baseurl, self.req[1:]) | ||||
|         efeed = html_escape(feed, True, True) | ||||
|         edirlink = efeed.split("?")[0] + q_pw | ||||
| 
 | ||||
|         ret = [ | ||||
|             """\ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/"> | ||||
| \t<channel> | ||||
| \t\t<atom:link href="%s" rel="self" type="application/rss+xml" /> | ||||
| \t\t<title>%s</title> | ||||
| \t\t<description></description> | ||||
| \t\t<link>%s</link> | ||||
| \t\t<generator>copyparty-1</generator> | ||||
| """ | ||||
|             % (efeed, etitle, edirlink) | ||||
|         ] | ||||
| 
 | ||||
|         q = "select fn from cv where rd=? and dn=?" | ||||
|         crd, cdn = rem.rsplit("/", 1) if "/" in rem else ("", rem) | ||||
|         try: | ||||
|             cfn = idx.cur[self.vn.realpath].execute(q, (crd, cdn)).fetchone()[0] | ||||
|             bos.stat(os.path.join(vn.canonical(rem), cfn)) | ||||
|             cv_url = "%s%s?th=jf%s" % (baseurl, vjoin(self.vpath, cfn), a_pw) | ||||
|             cv_url = html_escape(cv_url, True, True) | ||||
|             zs = """\ | ||||
| \t\t<image> | ||||
| \t\t\t<url>%s</url> | ||||
| \t\t\t<title>%s</title> | ||||
| \t\t\t<link>%s</link> | ||||
| \t\t</image> | ||||
| """ | ||||
|             ret.append(zs % (cv_url, etitle, edirlink)) | ||||
|         except: | ||||
|             pass | ||||
| 
 | ||||
|         for i in hits: | ||||
|             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)) | ||||
|             lmod = formatdate(i["ts"]) | ||||
|             zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"]) | ||||
|             zs = ( | ||||
|                 """\ | ||||
| \t\t<item> | ||||
| \t\t\t<guid>%s</guid> | ||||
| \t\t\t<link>%s</link> | ||||
| \t\t\t<title>%s</title> | ||||
| \t\t\t<description>%s</description> | ||||
| \t\t\t<pubDate>%s</pubDate> | ||||
| \t\t\t<enclosure url="%s" type="%s" length="%d"/> | ||||
| """ | ||||
|                 % zsa | ||||
|             ) | ||||
|             dur = i["tags"].get(".dur") | ||||
|             if dur: | ||||
|                 zs += "\t\t\t<itunes:duration>%d</itunes:duration>\n" % (dur,) | ||||
|             ret.append(zs + "\t\t</item>\n") | ||||
| 
 | ||||
|         ret.append("\t</channel>\n</rss>\n") | ||||
|         bret = "".join(ret).encode("utf-8", "replace") | ||||
|         self.reply(bret, 200, "text/xml; charset=utf-8") | ||||
|         self.log("rss: %d hits, %d bytes" % (len(hits), len(bret))) | ||||
|         return True | ||||
| 
 | ||||
|     def handle_propfind(self) -> bool: | ||||
|         if self.do_log: | ||||
|             self.log("PFIND %s @%s" % (self.req, self.uname)) | ||||
|  | ||||
| @ -95,7 +95,7 @@ class U2idx(object): | ||||
|         uv: list[Union[str, int]] = [wark[:16], wark] | ||||
| 
 | ||||
|         try: | ||||
|             return self.run_query(uname, vols, uq, uv, False, 99999)[0] | ||||
|             return self.run_query(uname, vols, uq, uv, False, True, 99999)[0] | ||||
|         except: | ||||
|             raise Pebkac(500, min_ex()) | ||||
| 
 | ||||
| @ -301,7 +301,7 @@ class U2idx(object): | ||||
|                 q += " lower({}) {} ? ) ".format(field, oper) | ||||
| 
 | ||||
|         try: | ||||
|             return self.run_query(uname, vols, q, va, have_mt, lim) | ||||
|             return self.run_query(uname, vols, q, va, have_mt, True, lim) | ||||
|         except Exception as ex: | ||||
|             raise Pebkac(500, repr(ex)) | ||||
| 
 | ||||
| @ -312,6 +312,7 @@ class U2idx(object): | ||||
|         uq: str, | ||||
|         uv: list[Union[str, int]], | ||||
|         have_mt: bool, | ||||
|         sort: bool, | ||||
|         lim: int, | ||||
|     ) -> tuple[list[dict[str, Any]], list[str], bool]: | ||||
|         if self.args.srch_dbg: | ||||
| @ -458,7 +459,8 @@ class U2idx(object): | ||||
|         done_flag.append(True) | ||||
|         self.active_id = "" | ||||
| 
 | ||||
|         ret.sort(key=itemgetter("rp")) | ||||
|         if sort: | ||||
|             ret.sort(key=itemgetter("rp")) | ||||
| 
 | ||||
|         return ret, list(taglist.keys()), lim < 0 and not clamped | ||||
| 
 | ||||
|  | ||||
| @ -122,7 +122,7 @@ class Cfg(Namespace): | ||||
|     def __init__(self, a=None, v=None, c=None, **ka0): | ||||
|         ka = {} | ||||
| 
 | ||||
|         ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_clone no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand re_dirsz smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs" | ||||
|         ex = "chpw daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink ih ihead magic hardlink_only nid nih no_acode no_athumb no_clone no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand re_dirsz rss smb srch_dbg stats uqe vague_403 vc ver write_uplog xdev xlink xvol zs" | ||||
|         ka.update(**{k: False for k in ex.split()}) | ||||
| 
 | ||||
|         ex = "dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash plain_ip" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 ed
						ed