diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 01216722..6b23ae19 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1514,6 +1514,7 @@ def add_thumbnail(ap): ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern") + ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec") # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://github.com/libvips/libvips # https://stackoverflow.com/a/47612661 diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index f5f5c223..0c8a5ab4 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -2227,7 +2227,7 @@ class AuthSrv(object): if vf not in vol.flags: vol.flags[vf] = getattr(self.args, ga) - zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who" + zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who" for k in zs.split(): if k in vol.flags: vol.flags[k] = int(vol.flags[k]) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index 3a7a205b..4fa73e72 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -112,6 +112,7 @@ def vf_vmap() -> dict[str, str]: "tail_tmax", "tail_who", "tcolor", + "th_spec_p", "txt_eol", "unlist", "u2abort", @@ -264,6 +265,7 @@ flagcats = { "th3x": "3x resolution (y/n/fy/fn)", "convt": "convert-to-image timeout in seconds", "aconvt": "convert-to-audio timeout in seconds", + "th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always", "ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s", }, "handlers\n(better explained in --help-handlers)": { diff --git a/copyparty/mtag.py b/copyparty/mtag.py index 43e9dd75..29750733 100644 --- a/copyparty/mtag.py +++ b/copyparty/mtag.py @@ -208,7 +208,7 @@ def au_unpk( def ffprobe( abspath: str, timeout: int = 60 -) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]: +) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: cmd = [ b"ffprobe", b"-hide_banner", @@ -222,8 +222,17 @@ def ffprobe( return parse_ffprobe(so) -def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]: - """ffprobe -show_format -show_streams""" +def parse_ffprobe( + txt: str, +) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]], list[Any], dict[str, Any]]: + """ + txt: output from ffprobe -show_format -show_streams + returns: + * normalized tags + * original/raw tags + * list of streams + * format props + """ streams = [] fmt = {} g = {} @@ -316,7 +325,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ ret[rk] = v1 if ret.get("vc") == "ansi": # shellscript - return {}, {} + return {}, {}, [], {} for strm in streams: for sk, sv in strm.items(): @@ -365,7 +374,7 @@ def parse_ffprobe(txt: str) -> tuple[dict[str, tuple[int, Any]], dict[str, list[ zero = int("0") zd = {k: (zero, v) for k, v in ret.items()} - return zd, md + return zd, md, streams, fmt def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]: @@ -706,7 +715,7 @@ class MTag(object): if not bos.path.isfile(abspath): return {} - ret, md = ffprobe(abspath, self.args.mtag_to) + ret, md, _, _ = ffprobe(abspath, self.args.mtag_to) if self.args.mtag_vv: for zd in (ret, dict(md)): diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 2522f3a6..86c3c469 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -612,7 +612,7 @@ class ThumbSrv(object): def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) - ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) + ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if not ret: return @@ -623,6 +623,17 @@ class ThumbSrv(object): dur = ret[".dur"][1] if ".dur" in ret else 4 seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] + self._ffmpeg_im(abspath, tpath, fmt, vn, seek, b"0:v:0") + + def _ffmpeg_im( + self, + abspath: str, + tpath: str, + fmt: str, + vn: VFS, + seek: list[bytes], + imap: bytes, + ) -> None: scale = "scale={0}:{1}:force_original_aspect_ratio=" if "f" in fmt: scale += "decrease,setsar=1:1" @@ -641,7 +652,7 @@ class ThumbSrv(object): cmd += seek cmd += [ b"-i", fsenc(abspath), - b"-map", b"0:v:0", + b"-map", imap, b"-vf", bscale, b"-frames:v", b"1", b"-metadata:s:v:0", b"rotate=0", @@ -710,7 +721,7 @@ class ThumbSrv(object): raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: - ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) + ret, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in ret: raise Exception("not audio") @@ -769,11 +780,31 @@ class ThumbSrv(object): else: atomic_move(self.log, wtpath, tpath, vn.flags) + def conv_emb_cv( + self, abspath: str, tpath: str, fmt: str, vn: VFS, strm: dict[str, Any] + ) -> None: + self.wait4ram(0.2, tpath) + self._ffmpeg_im( + abspath, tpath, fmt, vn, [], b"0:" + strm["index"].encode("ascii") + ) + def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: - ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) + ret, raw, strms, ctnr = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in ret: raise Exception("not audio") + want_spec = vn.flags.get("th_spec_p", 1) + if want_spec < 2: + for strm in strms: + if ( + strm.get("codec_type") == "video" + and strm.get("DISPOSITION:attached_pic") == "1" + ): + return self.conv_emb_cv(abspath, tpath, fmt, vn, strm) + + if not want_spec: + raise Exception("spectrograms forbidden by volflag") + fext = abspath.split(".")[-1].lower() # https://trac.ffmpeg.org/ticket/10797 @@ -859,7 +890,7 @@ class ThumbSrv(object): raise Exception("disabled in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") @@ -897,7 +928,7 @@ class ThumbSrv(object): raise Exception("flac not permitted in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") @@ -922,7 +953,7 @@ class ThumbSrv(object): raise Exception("wav not permitted in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, _, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") @@ -957,7 +988,7 @@ class ThumbSrv(object): raise Exception("disabled in server config") self.wait4ram(0.2, tpath) - tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + tags, rawtags, _, _ = ffprobe(abspath, int(vn.flags["convt"] / 2)) if "ac" not in tags: raise Exception("not audio") diff --git a/tests/util.py b/tests/util.py index 14031829..546851b4 100644 --- a/tests/util.py +++ b/tests/util.py @@ -155,7 +155,7 @@ class Cfg(Namespace): ex = "gid uid" ka.update(**{k: -1 for k in ex.split()}) - ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz unp_who" + ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate th_spec_p u2abort u2j u2sz unp_who" ka.update(**{k: 1 for k in ex.split()}) ex = "ac_convt au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"