From fd9d0e433db81f029fb251095f59116f3f992c39 Mon Sep 17 00:00:00 2001 From: ed Date: Mon, 11 Apr 2022 10:38:57 +0200 Subject: [PATCH] thumbnails: try FFmpeg for images too --- README.md | 13 +++++++------ copyparty/__main__.py | 5 +++-- copyparty/httpsrv.py | 7 +++++-- copyparty/svchub.py | 14 ++++++-------- copyparty/th_cli.py | 39 +++++++++++++++++++++++++++++---------- copyparty/th_srv.py | 27 +++++++++++++++++---------- 6 files changed, 67 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 3ec6c1d7..5e22694c 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ feature summary * ☑ image gallery with webm player * ☑ textfile browser with syntax hilighting * ☑ [thumbnails](#thumbnails) - * ☑ ...of images using Pillow and/or pyvips + * ☑ ...of images using Pillow, pyvips, or FFmpeg * ☑ ...of videos using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg * ☑ cache eviction (max-age; maybe max-size eventually) @@ -403,7 +403,8 @@ press `g` to toggle grid-view instead of the file listing, and `t` toggles icon ![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png) -it does static images with Pillow and/or pyvips, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are +it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are +* pyvips is 3x faster than Pillow, Pillow is 3x faster than FFmpeg audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`) @@ -1096,11 +1097,11 @@ enable music tags: * or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users) enable [thumbnails](#thumbnails) of... -* **images:** `Pillow` and/or `pyvips` (requires py2.7 or py3.5+) +* **images:** `Pillow` and/or `pyvips` and/or `ffmpeg` (requires py2.7 or py3.5+) * **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH` -* **HEIF pictures:** `pyvips` or `pyheif-pillow-opener` (requires Linux or a C compiler) -* **AVIF pictures:** `pyvips` or `pillow-avif-plugin` -* **JPEG XL pictures:** `pyvips` +* **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler) +* **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin` +* **JPEG XL pictures:** `pyvips` or `ffmpeg` `pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips` diff --git a/copyparty/__main__.py b/copyparty/__main__.py index a49b0cd5..9b515b4c 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -511,9 +511,10 @@ def run_argparse(argv, formatter): ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat for") # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://github.com/libvips/libvips - # ffmpeg -formats + # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="bmp,dib,gif,icns,ico,jpg,jpeg,jp2,jpx,pcx,png,pbm,pgm,ppm,pnm,sgi,tga,tif,tiff,webp,xbm,dds,xpm,heif,heifs,heic,heics,avif,avifs", help="image formats to decode using pillow") - ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="jpg,jpeg,jp2,jpx,jxl,tif,tiff,png,webp,heic,avif,fit,fits,fts,exr,svg,hdr,ppm,pgm,pfm,gif,nii,dzi", help="image formats to decode using pyvips") + ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="jpg,jpeg,jp2,jpx,jxl,tif,tiff,png,webp,heic,avif,fit,fits,fts,exr,svg,hdr,ppm,pgm,pfm,gif,nii", help="image formats to decode using pyvips") + ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg") ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="av1,asf,avi,flv,m4v,mkv,mjpeg,mjpg,mpg,mpeg,mpg2,mpeg2,h264,avc,mts,h265,hevc,mov,3gp,mp4,ts,mpegts,nut,ogv,ogm,rm,vob,webm,wmv", help="video formats to decode using ffmpeg") ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,m4a,ogg,opus,flac,alac,mp3,mp2,ac3,dts,wma,ra,wav,aif,aiff,au,alaw,ulaw,mulaw,amr,gsm,ape,tak,tta,wv,mpc", help="audio formats to decode using ffmpeg") diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index 7d3642ac..9c228d4a 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -70,8 +70,11 @@ class HttpSrv(object): self.cb_ts = 0 self.cb_v = 0 - x = self.broker.put(True, "thumbsrv.getcfg") - self.th_cfg = x.get() + try: + x = self.broker.put(True, "thumbsrv.getcfg") + self.th_cfg = x.get() + except: + pass env = jinja2.Environment() env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web")) diff --git a/copyparty/svchub.py b/copyparty/svchub.py index 344d1a99..96f48026 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -91,17 +91,15 @@ class SvcHub(object): if not args.no_thumb: m = "decoder preference: {}".format(", ".join(self.args.th_dec)) self.log("thumb", m) - if "vips" in self.args.th_dec: - self.thumbsrv = ThumbSrv(self) - elif "pil" in self.args.th_dec: - if not HAVE_WEBP: - msg = "disabling webp thumbnails because either libwebp is not available or your Pillow is too old" - self.log("thumb", msg, c=3) - self.log("thumb", "using pillow") + if "pil" in self.args.th_dec and not HAVE_WEBP: + msg = "disabling webp thumbnails because either libwebp is not available or your Pillow is too old" + self.log("thumb", msg, c=3) + + if self.args.th_dec: self.thumbsrv = ThumbSrv(self) else: - msg = "need Pillow and/or pyvips to create thumbnails; for example:\n{0}{1} -m pip install --user Pillow\n{0}{1} -m pip install --user pyvips\n" + msg = "need either Pillow, pyvips, or FFmpeg to create thumbnails; for example:\n{0}{1} -m pip install --user Pillow\n{0}{1} -m pip install --user pyvips\n{0}apt install ffmpeg" msg = msg.format(" " * 37, os.path.basename(sys.executable)) self.log("thumb", msg, c=3) diff --git a/copyparty/th_cli.py b/copyparty/th_cli.py index 8de4700f..b2601f30 100644 --- a/copyparty/th_cli.py +++ b/copyparty/th_cli.py @@ -18,13 +18,19 @@ class ThumbCli(object): # cache on both sides for less broker spam self.cooldown = Cooldown(self.args.th_poke) - c = hsrv.th_cfg + try: + c = hsrv.th_cfg + except: + c = {k: {} for k in ["thumbable", "pil", "vips", "ffi", "ffv", "ffa"]} + self.thumbable = c["thumbable"] self.fmt_pil = c["pil"] self.fmt_vips = c["vips"] + self.fmt_ffi = c["ffi"] self.fmt_ffv = c["ffv"] self.fmt_ffa = c["ffa"] + # defer args.th_ff_jpg, can change at runtime d = next((x for x in self.args.th_dec if x in ("vips", "pil")), None) self.can_webp = HAVE_WEBP or d == "vips" @@ -54,6 +60,8 @@ class ThumbCli(object): is_img = not is_vid and not is_au + preferred = self.args.th_dec[0] if self.args.th_dec else "" + if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]: return os.path.join(ptop, rem) @@ -64,7 +72,7 @@ class ThumbCli(object): if ( self.args.th_no_webp or (is_img and not self.can_webp) - or (not is_img and self.args.th_ff_jpg) + or (self.args.th_ff_jpg and (not is_img or preferred == "ff")) ): fmt = "j" @@ -74,15 +82,23 @@ class ThumbCli(object): return None tpath = thumb_path(histpath, rem, mtime, fmt) + tpaths = [tpath] + if fmt == "w": + # also check for jpg (maybe webp is unavailable) + tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg") + ret = None - try: - st = bos.stat(tpath) - if st.st_size: - ret = tpath - else: - return None - except: - pass + abort = False + for tp in tpaths: + try: + st = bos.stat(tp) + if st.st_size: + ret = tpath = tp + fmt = ret.rsplit(".")[1] + else: + abort = True + except: + pass if ret: tdir = os.path.dirname(tpath) @@ -96,5 +112,8 @@ class ThumbCli(object): return ret + if abort: + return None + x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime, fmt) return x.get() diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index c00c5331..e6593de3 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -122,10 +122,16 @@ class ThumbSrv(object): t.daemon = True t.start() - self.fmt_pil = {x: True for x in self.args.th_r_pil.split(",")} - self.fmt_vips = {x: True for x in self.args.th_r_vips.split(",")} - self.fmt_ffv = {x: True for x in self.args.th_r_ffv.split(",")} - self.fmt_ffa = {x: True for x in self.args.th_r_ffa.split(",")} + self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [ + {x: True for x in y.split(",")} + for y in [ + self.args.th_r_pil, + self.args.th_r_vips, + self.args.th_r_ffi, + self.args.th_r_ffv, + self.args.th_r_ffa, + ] + ] if not HAVE_HEIF: for f in "heif heifs heic heics".split(" "): @@ -144,8 +150,8 @@ class ThumbSrv(object): self.thumbable.update(self.fmt_vips) if "ff" in self.args.th_dec: - self.thumbable.update(self.fmt_ffv) - self.thumbable.update(self.fmt_ffa) + for t in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]: + self.thumbable.update(t) def log(self, msg, c=0): self.log_func("thumb", msg, c) @@ -212,6 +218,7 @@ class ThumbSrv(object): "thumbable": self.thumbable, "pil": self.fmt_pil, "vips": self.fmt_vips, + "ffi": self.fmt_ffi, "ffv": self.fmt_ffv, "ffa": self.fmt_ffa, } @@ -233,7 +240,7 @@ class ThumbSrv(object): fun = self.conv_pil elif lib == "vips" and ext in self.fmt_vips: fun = self.conv_vips - elif lib == "ff" and ext in self.fmt_ffv: + elif lib == "ff" and ext in self.fmt_ffi or ext in self.fmt_ffv: fun = self.conv_ffmpeg elif lib == "ff" and ext in self.fmt_ffa: if tpath.endswith(".opus") or tpath.endswith(".caf"): @@ -337,8 +344,8 @@ class ThumbSrv(object): def conv_ffmpeg(self, abspath, tpath): ret, _ = ffprobe(abspath) - ext = abspath.rsplit(".")[-1] - if ext in ["h264", "h265"]: + ext = abspath.rsplit(".")[-1].lower() + if ext in ["h264", "h265"] or ext in self.fmt_ffi: seek = [] else: dur = ret[".dur"][1] if ".dur" in ret else 4 @@ -393,7 +400,7 @@ class ThumbSrv(object): c = "1;30" m = "FFmpeg failed (probably a corrupt video file):\n" - if cmd[-1].endswith(b".webp") and ( + if cmd[-1].lower().endswith(b".webp") and ( "Error selecting an encoder" in serr or "Automatic encoder selection failed" in serr or "Default encoder for format webp" in serr