From b2d48c646f7a9edf2f95806d1d4ff478540e4319 Mon Sep 17 00:00:00 2001 From: Toby Kohlhagen Date: Mon, 4 Aug 2025 00:04:09 +1000 Subject: [PATCH] Add flac transcoding option --- copyparty/__main__.py | 2 ++ copyparty/httpcli.py | 2 +- copyparty/th_srv.py | 43 ++++++++++++++++++++++++++++++++-------- copyparty/web/browser.js | 16 +++++++++++---- scripts/tl.js | 2 ++ 5 files changed, 52 insertions(+), 13 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3a3d950e..183fb916 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1449,6 +1449,8 @@ def add_transcoding(ap): ap2.add_argument("--q-mp3", metavar="QUALITY", type=u, default="q2", help="target quality for transcoding to mp3, for example [\033[32m192k\033[0m] (CBR) or [\033[32mq0\033[0m] (CQ/CRF, q0=maxquality, q9=smallest); set 0 to disable") ap2.add_argument("--no-caf", action="store_true", help="disable transcoding to caf-opus (affects iOS v12~v17), will use mp3 instead") ap2.add_argument("--no-owa", action="store_true", help="disable transcoding to webm-opus (iOS v18 and later), will use mp3 instead") + ap2.add_argument("--no-wav", action="store_true", help="disable transcoding to wav (lossless, uncompressed)") + ap2.add_argument("--no-flac", action="store_true", help="disable transcoding to flac (lossless, compressed)") ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding") ap2.add_argument("--no-bacode", action="store_true", help="disable batch audio transcoding by folder download (zip/tar)") ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after \033[33mSEC\033[0m seconds") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 51620912..db1cbcb8 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -4684,7 +4684,7 @@ class HttpCli(object): # for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]})) cfmt = "" if self.thumbcli and not self.args.no_bacode: - for zs in ("opus", "mp3", "wav", "w", "j", "p"): + for zs in ("opus", "mp3", "flac", "wav", "w", "j", "p"): if zs in self.ouparam or uarg == zs: cfmt = zs diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 8eaf9287..e8094b5a 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -50,7 +50,7 @@ HAVE_AVIF = False HAVE_WEBP = False EXTS_TH = set(["jpg", "webp", "png"]) -EXTS_AC = set(["opus", "owa", "caf", "mp3", "wav"]) +EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"]) EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split()) PTN_TS = re.compile("^-?[0-9a-f]{8,10}$") @@ -355,9 +355,10 @@ class ThumbSrv(object): tex = tpath.rsplit(".", 1)[-1] want_mp3 = tex == "mp3" want_opus = tex in ("opus", "owa", "caf") + want_flac = tex == "flac" want_wav = tex == "wav" want_png = tex == "png" - want_au = want_mp3 or want_opus or want_wav + want_au = want_mp3 or want_opus or want_flac or want_wav for lib in self.args.th_dec: can_au = lib == "ff" and ( ext in self.fmt_ffa or ext in self.fmt_ffv @@ -372,6 +373,8 @@ class ThumbSrv(object): funs.append(self.conv_opus) elif want_mp3: funs.append(self.conv_mp3) + elif want_flac: + funs.append(self.conv_flac) elif want_wav: funs.append(self.conv_wav) elif want_png: @@ -583,7 +586,7 @@ class ThumbSrv(object): self._run_ff(cmd, vn) def _run_ff(self, cmd: list[bytes], vn: VFS, oom: int = 400) -> None: - self.log((b" ".join(cmd)).decode("utf-8")) + # self.log((b" ".join(cmd)).decode("utf-8")) ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=oom) if not ret: return @@ -810,8 +813,33 @@ class ThumbSrv(object): # fmt: on self._run_ff(cmd, vn, oom=300) + def conv_flac(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: + if self.args.no_acode or self.args.no_flac: + raise Exception("disabled in server config") + + self.wait4ram(0.2, tpath) + tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2)) + if "ac" not in tags: + raise Exception("not audio") + + self.log("conv2 flac", 6) + + # fmt: off + cmd = [ + b"ffmpeg", + b"-nostdin", + b"-v", b"error", + b"-hide_banner", + b"-i", fsenc(abspath), + b"-map", b"0:a:0", + b"-c:a", b"flac", + fsenc(tpath) + ] + # fmt: on + self._run_ff(cmd, vn, oom=300) + def conv_wav(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: - if self.args.no_acode or not self.args.q_wav: + if self.args.no_acode or self.args.no_wav: raise Exception("disabled in server config") self.wait4ram(0.2, tpath) @@ -824,12 +852,12 @@ class ThumbSrv(object): bits = tags[".bprs"][1] codec = b"pcm_s32le" - if bits == 16.0: + if bits <= 16.0: codec = b"pcm_s16le" - elif bits == 24.0: + elif bits <= 24.0: codec = b"pcm_s24le" - self.log("conv2 wav-tmp", 6) + self.log("conv2 wav", 6) # fmt: off cmd = [ @@ -843,7 +871,6 @@ class ThumbSrv(object): fsenc(tpath) ] # fmt: on - print(cmd) self._run_ff(cmd, vn, oom=300) def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index bd9fbbe7..201b3588 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -306,7 +306,8 @@ var Ls = { "mt_c2owa": "opus-weba, for iOS 17.5 and newer\">owa", "mt_c2caf": "opus-caf, for iOS 11 through 17\">caf", "mt_c2mp3": "use this on very old devices\">mp3", - "mt_c2wav": "use this for uncompressed playback\">wav", + "mt_c2flac": "best sound quality\">flac", + "mt_c2wav": "uncompressed playback\">wav", "mt_c2ok": "nice, good choice", "mt_c2nd": "that's not the recommended output format for your device, but that's fine", "mt_c2ng": "your device does not seem to support this output format, but let's try anyways", @@ -6803,6 +6804,7 @@ var mpl = (function () { 'owa", "mt_c2caf": "opus-caf, for iOS 11 through 17\">caf", "mt_c2mp3": "use this on very old devices\">mp3", + "mt_c2flac": "best sound quality\">flac", + "mt_c2wav": "uncompressed playback\">wav", "mt_c2ok": "nice, good choice", "mt_c2nd": "that's not the recommended output format for your device, but that's fine", "mt_c2ng": "your device does not seem to support this output format, but let's try anyways",