From 5d63949e9870638e6cf1590210fe6767fd6c21a3 Mon Sep 17 00:00:00 2001 From: ed Date: Fri, 28 May 2021 02:44:13 +0200 Subject: [PATCH] create webp thumbnails by default --- copyparty/__main__.py | 4 +++- copyparty/httpcli.py | 5 +++-- copyparty/ico.py | 2 +- copyparty/th_cli.py | 12 ++++++++--- copyparty/th_srv.py | 43 ++++++++++++++++++++++++++++++---------- copyparty/util.py | 3 +++ copyparty/web/browser.js | 18 ++++++++++++++++- 7 files changed, 69 insertions(+), 18 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3b3996de..ada5048b 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -253,7 +253,9 @@ def run_argparse(argv, formatter): ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res") - ap2.add_argument("--th-nocrop", action="store_true", help="dynamic height (no crop)") + ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image") + ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") + ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown") ap2.add_argument("--th-clean", metavar="SEC", type=int, default=1800, help="cleanup interval") ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 4f3ff5d4..18e45a96 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1385,10 +1385,11 @@ class HttpCli(object): if rem.startswith(".hist/up2k."): raise Pebkac(403) - if "th" in self.uparam: + th_fmt = self.uparam.get("th") + if th_fmt is not None: thp = None if self.thumbcli: - thp = self.thumbcli.get(vn.realpath, rem, int(st.st_mtime)) + thp = self.thumbcli.get(vn.realpath, rem, int(st.st_mtime), th_fmt) if thp: return self.tx_file(thp) diff --git a/copyparty/ico.py b/copyparty/ico.py index ea1eb949..f4c84e29 100644 --- a/copyparty/ico.py +++ b/copyparty/ico.py @@ -22,7 +22,7 @@ class Ico(object): c = "".join(["{:02x}".format(x) for x in c]) h = 30 - if not self.args.th_nocrop and as_thumb: + if not self.args.th_no_crop and as_thumb: w, h = self.args.th_size.split("x") h = int(100 / (float(w) / float(h))) diff --git a/copyparty/th_cli.py b/copyparty/th_cli.py index e83a8357..1e49a84a 100644 --- a/copyparty/th_cli.py +++ b/copyparty/th_cli.py @@ -13,7 +13,7 @@ class ThumbCli(object): # cache on both sides for less broker spam self.cooldown = Cooldown(self.args.th_poke) - def get(self, ptop, rem, mtime): + def get(self, ptop, rem, mtime, fmt): ext = rem.rsplit(".")[-1].lower() if ext not in THUMBABLE: return None @@ -21,7 +21,13 @@ class ThumbCli(object): if self.args.no_vthumb and ext in FMT_FF: return None - tpath = thumb_path(ptop, rem, mtime) + if fmt == "w" and self.args.th_no_webp: + fmt = "j" + + if fmt == "j" and self.args.th_no_jpg: + fmt = "w" + + tpath = thumb_path(ptop, rem, mtime, fmt) ret = None try: st = os.stat(tpath) @@ -39,5 +45,5 @@ class ThumbCli(object): return ret - x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime) + 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 09341b7a..7d6163f8 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -51,7 +51,7 @@ if HAVE_FFMPEG and HAVE_FFPROBE: THUMBABLE.update(FMT_FF) -def thumb_path(ptop, rem, mtime): +def thumb_path(ptop, rem, mtime, fmt): # base16 = 16 = 256 # b64-lc = 38 = 1444 # base64 = 64 = 4096 @@ -72,7 +72,9 @@ def thumb_path(ptop, rem, mtime): h = hashlib.sha512(fsenc(fn)).digest()[:24] fn = base64.urlsafe_b64encode(h).decode("ascii")[:24] - return "{}/.hist/th/{}/{}.{:x}.jpg".format(ptop, rd, fn, int(mtime)) + return "{}/.hist/th/{}/{}.{:x}.{}".format( + ptop, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg" + ) class ThumbSrv(object): @@ -129,8 +131,8 @@ class ThumbSrv(object): with self.mutex: return not self.nthr - def get(self, ptop, rem, mtime): - tpath = thumb_path(ptop, rem, mtime) + def get(self, ptop, rem, mtime, fmt): + tpath = thumb_path(ptop, rem, mtime, fmt) abspath = os.path.join(ptop, rem) cond = threading.Condition() with self.mutex: @@ -207,7 +209,7 @@ class ThumbSrv(object): def conv_pil(self, abspath, tpath): with Image.open(abspath) as im: - crop = not self.args.th_nocrop + crop = not self.args.th_no_crop res2 = self.res if crop: res2 = (res2[0] * 2, res2[1] * 2) @@ -222,7 +224,15 @@ class ThumbSrv(object): if im.mode not in ("RGB", "L"): im = im.convert("RGB") - im.save(tpath, quality=50) + if tpath.endswith(".webp"): + # quality 80 = pillow-default + # quality 75 = ffmpeg-default + # method 0 = pillow-default, fast + # method 4 = ffmpeg-default + # method 6 = max, slow + im.save(tpath, quality=40, method=6) + else: + im.save(tpath, quality=40) # default=75 def conv_ffmpeg(self, abspath, tpath): ret, _ = ffprobe(abspath) @@ -231,7 +241,7 @@ class ThumbSrv(object): seek = "{:.0f}".format(dur / 3) scale = "scale={0}:{1}:force_original_aspect_ratio=" - if self.args.th_nocrop: + if self.args.th_no_crop: scale += "decrease,setsar=1:1" else: scale += "increase,crop={0}:{1},setsar=1:1" @@ -249,10 +259,23 @@ class ThumbSrv(object): scale, b"-vframes", b"1", - b"-q:v", - b"6", - fsenc(tpath), ] + + if tpath.endswith(".jpg"): + cmd += [ + b"-q:v", + b"6", # default=?? + ] + else: + cmd += [ + b"-q:v", + b"50", # default=75 + b"-compression_level:v", + b"6", # default=4, 0=fast, 6=max + ] + + cmd += [fsenc(tpath)] + p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) p.communicate() diff --git a/copyparty/util.py b/copyparty/util.py index 722c653e..b7152e00 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -944,6 +944,9 @@ def guess_mime(url, fallback="application/octet-stream"): if url.endswith(".md"): return ["text/plain; charset=UTF-8"] + if url.endswith(".webp"): + return ["image/webp"] + return mimetypes.guess_type(url) or fallback diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index bc60afa5..20ef100f 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -29,6 +29,19 @@ ebi('widget').innerHTML = ( ); +var have_webp = null; +(function () { + var img = new Image(); + img.onload = function () { + have_webp = img.width > 0 && img.height > 0; + }; + img.onerror = function () { + have_webp = false; + }; + img.src = "data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA"; +})(); + + // extract songs + add play column function MPlayer() { this.id = Date.now(); @@ -816,6 +829,9 @@ var thegrid = (function () { } function loadgrid() { + if (have_webp === null) + return setTimeout(loadgrid, 50); + if (!r.dirty) return r.loadsel(); @@ -832,7 +848,7 @@ var thegrid = (function () { ihref = '/.cpr/ico/folder' } else if (r.thumbs) { - ihref += ihref.indexOf('?') === -1 ? '?th' : '&th'; + ihref += (ihref.indexOf('?') === -1 ? '?' : '&') + 'th=' + (have_webp ? 'w' : 'j'); } else { var ar = href.split('?')[0].split('.');