thumbnails: try FFmpeg for images too

This commit is contained in:
ed 2022-04-11 10:38:57 +02:00
parent f096f3ef81
commit fd9d0e433d
6 changed files with 67 additions and 38 deletions

View file

@ -174,7 +174,7 @@ feature summary
* ☑ image gallery with webm player * ☑ image gallery with webm player
* ☑ textfile browser with syntax hilighting * ☑ textfile browser with syntax hilighting
* ☑ [thumbnails](#thumbnails) * ☑ [thumbnails](#thumbnails)
* ☑ ...of images using Pillow and/or pyvips * ☑ ...of images using Pillow, pyvips, or FFmpeg
* ☑ ...of videos using FFmpeg * ☑ ...of videos using FFmpeg
* ☑ ...of audio (spectrograms) using FFmpeg * ☑ ...of audio (spectrograms) using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually) * ☑ 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) ![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`) 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) * or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
enable [thumbnails](#thumbnails) of... 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` * **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
* **HEIF pictures:** `pyvips` or `pyheif-pillow-opener` (requires Linux or a C compiler) * **HEIF pictures:** `pyvips` or `ffmpeg` or `pyheif-pillow-opener` (requires Linux or a C compiler)
* **AVIF pictures:** `pyvips` or `pillow-avif-plugin` * **AVIF pictures:** `pyvips` or `ffmpeg` or `pillow-avif-plugin`
* **JPEG XL pictures:** `pyvips` * **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` `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`

View file

@ -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") 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://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips # 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-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-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") 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")

View file

@ -70,8 +70,11 @@ class HttpSrv(object):
self.cb_ts = 0 self.cb_ts = 0
self.cb_v = 0 self.cb_v = 0
x = self.broker.put(True, "thumbsrv.getcfg") try:
self.th_cfg = x.get() x = self.broker.put(True, "thumbsrv.getcfg")
self.th_cfg = x.get()
except:
pass
env = jinja2.Environment() env = jinja2.Environment()
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web")) env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))

View file

@ -91,17 +91,15 @@ class SvcHub(object):
if not args.no_thumb: if not args.no_thumb:
m = "decoder preference: {}".format(", ".join(self.args.th_dec)) m = "decoder preference: {}".format(", ".join(self.args.th_dec))
self.log("thumb", m) 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) self.thumbsrv = ThumbSrv(self)
else: 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)) msg = msg.format(" " * 37, os.path.basename(sys.executable))
self.log("thumb", msg, c=3) self.log("thumb", msg, c=3)

View file

@ -18,13 +18,19 @@ class ThumbCli(object):
# cache on both sides for less broker spam # cache on both sides for less broker spam
self.cooldown = Cooldown(self.args.th_poke) 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.thumbable = c["thumbable"]
self.fmt_pil = c["pil"] self.fmt_pil = c["pil"]
self.fmt_vips = c["vips"] self.fmt_vips = c["vips"]
self.fmt_ffi = c["ffi"]
self.fmt_ffv = c["ffv"] self.fmt_ffv = c["ffv"]
self.fmt_ffa = c["ffa"] 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) d = next((x for x in self.args.th_dec if x in ("vips", "pil")), None)
self.can_webp = HAVE_WEBP or d == "vips" self.can_webp = HAVE_WEBP or d == "vips"
@ -54,6 +60,8 @@ class ThumbCli(object):
is_img = not is_vid and not is_au 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"]: if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]:
return os.path.join(ptop, rem) return os.path.join(ptop, rem)
@ -64,7 +72,7 @@ class ThumbCli(object):
if ( if (
self.args.th_no_webp self.args.th_no_webp
or (is_img and not self.can_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" fmt = "j"
@ -74,15 +82,23 @@ class ThumbCli(object):
return None return None
tpath = thumb_path(histpath, rem, mtime, fmt) 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 ret = None
try: abort = False
st = bos.stat(tpath) for tp in tpaths:
if st.st_size: try:
ret = tpath st = bos.stat(tp)
else: if st.st_size:
return None ret = tpath = tp
except: fmt = ret.rsplit(".")[1]
pass else:
abort = True
except:
pass
if ret: if ret:
tdir = os.path.dirname(tpath) tdir = os.path.dirname(tpath)
@ -96,5 +112,8 @@ class ThumbCli(object):
return ret return ret
if abort:
return None
x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime, fmt) x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime, fmt)
return x.get() return x.get()

View file

@ -122,10 +122,16 @@ class ThumbSrv(object):
t.daemon = True t.daemon = True
t.start() t.start()
self.fmt_pil = {x: True for x in self.args.th_r_pil.split(",")} self.fmt_pil, self.fmt_vips, self.fmt_ffi, self.fmt_ffv, self.fmt_ffa = [
self.fmt_vips = {x: True for x in self.args.th_r_vips.split(",")} {x: True for x in y.split(",")}
self.fmt_ffv = {x: True for x in self.args.th_r_ffv.split(",")} for y in [
self.fmt_ffa = {x: True for x in self.args.th_r_ffa.split(",")} 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: if not HAVE_HEIF:
for f in "heif heifs heic heics".split(" "): for f in "heif heifs heic heics".split(" "):
@ -144,8 +150,8 @@ class ThumbSrv(object):
self.thumbable.update(self.fmt_vips) self.thumbable.update(self.fmt_vips)
if "ff" in self.args.th_dec: if "ff" in self.args.th_dec:
self.thumbable.update(self.fmt_ffv) for t in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
self.thumbable.update(self.fmt_ffa) self.thumbable.update(t)
def log(self, msg, c=0): def log(self, msg, c=0):
self.log_func("thumb", msg, c) self.log_func("thumb", msg, c)
@ -212,6 +218,7 @@ class ThumbSrv(object):
"thumbable": self.thumbable, "thumbable": self.thumbable,
"pil": self.fmt_pil, "pil": self.fmt_pil,
"vips": self.fmt_vips, "vips": self.fmt_vips,
"ffi": self.fmt_ffi,
"ffv": self.fmt_ffv, "ffv": self.fmt_ffv,
"ffa": self.fmt_ffa, "ffa": self.fmt_ffa,
} }
@ -233,7 +240,7 @@ class ThumbSrv(object):
fun = self.conv_pil fun = self.conv_pil
elif lib == "vips" and ext in self.fmt_vips: elif lib == "vips" and ext in self.fmt_vips:
fun = self.conv_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 fun = self.conv_ffmpeg
elif lib == "ff" and ext in self.fmt_ffa: elif lib == "ff" and ext in self.fmt_ffa:
if tpath.endswith(".opus") or tpath.endswith(".caf"): if tpath.endswith(".opus") or tpath.endswith(".caf"):
@ -337,8 +344,8 @@ class ThumbSrv(object):
def conv_ffmpeg(self, abspath, tpath): def conv_ffmpeg(self, abspath, tpath):
ret, _ = ffprobe(abspath) ret, _ = ffprobe(abspath)
ext = abspath.rsplit(".")[-1] ext = abspath.rsplit(".")[-1].lower()
if ext in ["h264", "h265"]: if ext in ["h264", "h265"] or ext in self.fmt_ffi:
seek = [] seek = []
else: else:
dur = ret[".dur"][1] if ".dur" in ret else 4 dur = ret[".dur"][1] if ".dur" in ret else 4
@ -393,7 +400,7 @@ class ThumbSrv(object):
c = "1;30" c = "1;30"
m = "FFmpeg failed (probably a corrupt video file):\n" 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 "Error selecting an encoder" in serr
or "Automatic encoder selection failed" in serr or "Automatic encoder selection failed" in serr
or "Default encoder for format webp" in serr or "Default encoder for format webp" in serr