From f6f9fc5a454d6964a8b0aee0b47ab9c62c01a28f Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 2 Nov 2021 02:59:37 +0100 Subject: [PATCH] add audio transcoder --- copyparty/__main__.py | 3 +++ copyparty/httpcli.py | 1 + copyparty/th_cli.py | 10 +++++++- copyparty/th_srv.py | 39 ++++++++++++++++++++++++++----- copyparty/web/browser.html | 1 + copyparty/web/browser.js | 47 ++++++++++++++++++++++++++++++++++---- 6 files changed, 89 insertions(+), 12 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index db5d63a0..a10b6170 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -416,6 +416,9 @@ def run_argparse(argv, formatter): ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age") 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 = ap.add_argument_group('transcoding options') + ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding") + ap2 = ap.add_argument_group('general db options') ap2.add_argument("-e2d", action="store_true", help="enable up2k database") ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index d0ce8dd1..477edd76 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -2072,6 +2072,7 @@ class HttpCli(object): "def_hcols": [], "have_up2k_idx": ("e2d" in vn.flags), "have_tags_idx": ("e2t" in vn.flags), + "have_acode": (not self.args.no_acode), "have_mv": (not self.args.no_mv), "have_del": (not self.args.no_del), "have_zip": (not self.args.no_zip), diff --git a/copyparty/th_cli.py b/copyparty/th_cli.py index 687cbbb3..c64f921f 100644 --- a/copyparty/th_cli.py +++ b/copyparty/th_cli.py @@ -26,8 +26,16 @@ class ThumbCli(object): if is_vid and self.args.no_vthumb: return None + want_opus = fmt == "opus" is_au = ext in FMT_FFA - if is_au and self.args.no_athumb: + if is_au: + if want_opus: + if self.args.no_acode: + return None + else: + if self.args.no_athumb: + return None + elif want_opus: return None if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]: diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index dacf1b4f..4358ceaf 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -51,7 +51,7 @@ except: # ffmpeg -formats FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm" FMT_FFV = "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" -FMT_FFA = "aac m4a ogg opus flac alac mp3 mp2 ac3 dts wma wav aif aiff au amr gsm ape tak tta wv" +FMT_FFA = "aac m4a ogg opus flac alac mp3 mp2 ac3 dts wma ra wav aif aiff au amr gsm ape tak tta wv" if HAVE_HEIF: FMT_PIL += " heif heifs heic heics" @@ -90,9 +90,10 @@ def thumb_path(histpath, rem, mtime, fmt): h = hashlib.sha512(fsenc(fn)).digest() fn = base64.urlsafe_b64encode(h).decode("ascii")[:24] - return "{}/th/{}/{}.{:x}.{}".format( - histpath, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg" - ) + if fmt != "opus": + fmt = "webp" if fmt == "w" else "jpg" + + return "{}/th/{}/{}.{:x}.{}".format(histpath, rd, fn, int(mtime), fmt) class ThumbSrv(object): @@ -207,7 +208,10 @@ class ThumbSrv(object): elif ext in FMT_FFV: fun = self.conv_ffmpeg elif ext in FMT_FFA: - fun = self.conv_spec + if tpath.endswith(".opus"): + fun = self.conv_opus + else: + fun = self.conv_spec if fun: try: @@ -346,7 +350,6 @@ class ThumbSrv(object): def conv_spec(self, abspath, tpath): ret, _ = ffprobe(abspath) - if "ac" not in ret: raise Exception("not audio") @@ -381,6 +384,30 @@ class ThumbSrv(object): cmd += [fsenc(tpath)] self._run_ff(cmd) + def conv_opus(self, abspath, tpath): + if self.args.no_acode: + raise Exception("disabled in server config") + + ret, _ = ffprobe(abspath) + if "ac" not in ret: + raise Exception("not audio") + + # 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"libopus", + b"-b:a", b"128k", + fsenc(tpath) + ] + # fmt: on + + self._run_ff(cmd) + def poke(self, tdir): if not self.poke_cd.poke(tdir): return diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index 4570f5ef..bf46ef8f 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -130,6 +130,7 @@ def_hcols = {{ def_hcols|tojson }}, have_up2k_idx = {{ have_up2k_idx|tojson }}, have_tags_idx = {{ have_tags_idx|tojson }}, + have_acode = {{ have_acode|tojson }}, have_mv = {{ have_mv|tojson }}, have_del = {{ have_del|tojson }}, have_unpost = {{ have_unpost|tojson }}, diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index e9ac30ae..c8f4f3b6 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -320,6 +320,14 @@ var mpl = (function () { '📂 next-folder' + '' + + (have_acode ? ( + '

transcode

' + + 'flac' + + 'aac' + + 'oth' + + '
' + ) : '') + + '

tint

' + '' + '
' + @@ -335,6 +343,9 @@ var mpl = (function () { bcfg_bind(r, 'clip', 'au_npclip', false, function (v) { clmod(ebi('wtoggle'), 'np', v && mp.au); }); + bcfg_bind(r, 'ac_flac', 'ac_flac', true); + bcfg_bind(r, 'ac_aac', 'ac_aac', false); + bcfg_bind(r, 'ac_oth', 'ac_oth', true, reload_mp); ebi('au_os_ctl').onclick = function (e) { ev(e); @@ -373,6 +384,23 @@ var mpl = (function () { }; set_tint(); + r.acode = function (url) { + var c = true; + if (!have_acode) + c = false; + else if (/\.flac$/i.exec(url)) + c = r.ac_flac; + else if (/\.(aac|m4a)$/i.exec(url)) + c = r.ac_aac; + else if (re_au_native.exec(url)) + c = false; + + if (!c) + return url; + + return url + (url.indexOf('?') < 0 ? '?' : '&') + 'th=opus'; + }; + r.pp = function () { if (!r.os_ctl) return; @@ -441,6 +469,10 @@ var mpl = (function () { })(); +var re_au_native = /\.(opus|ogg|m4a|aac|mp3|wav|flac)$/i, + re_au_all = /\.(aac|m4a|ogg|opus|flac|alac|mp3|mp2|ac3|dts|wma|ra|wav|aif|aiff|au|amr|gsm|ape|tak|tta|wv)$/i; + + // extract songs + add play column function MPlayer() { var r = this; @@ -454,7 +486,7 @@ function MPlayer() { r.tracks = {}; r.order = []; - var re_audio = /\.(opus|ogg|m4a|aac|mp3|wav|flac)$/i, + var re_audio = mpl.ac_oth ? re_au_all : re_au_native, trs = QSA('#files tbody tr'); for (var a = 0, aa = trs.length; a < aa; a++) { @@ -552,6 +584,7 @@ function MPlayer() { r.preload = function (url) { var au = null; + url = mpl.acode(url); if (need_ogv_for(url)) { au = mp.au_ogvjs2; if (!au && window['OGVPlayer']) { @@ -1033,7 +1066,7 @@ var mpui = (function () { if (pos > 0 && pos > len - 10) { preloaded = mp.au.src; try { - mp.preload(ebi(mp.order[mp.order.indexOf(mp.au.tid) + 1]).href); + mp.preload(mp.tracks[mp.order[mp.order.indexOf(mp.au.tid) + 1]]); } catch (ex) { console.log("preload failed", ex); @@ -1078,7 +1111,7 @@ catch (ex) { } function need_ogv_for(url) { - return need_ogv && /\.(ogg|opus)$/i.test(url); + return need_ogv && /\.(ogg|opus)|\?th=opus/i.test(url); } @@ -1378,7 +1411,7 @@ function play(tid, is_ev, seek, call_depth) { // ogv.js breaks on .play() unless directly user-triggered var attempt_play = true; - var url = mp.tracks[tid]; + var url = mpl.acode(mp.tracks[tid]); if (need_ogv_for(url)) { var m = /.* Version\/([0-9]+)\.[0-9\.]+ Mobile\/[^ ]+ Safari\/[0-9\.]+$/.exec(navigator.userAgent), safari = m ? parseInt(m[1]) : 99; @@ -1435,7 +1468,7 @@ function play(tid, is_ev, seek, call_depth) { audio_eq.apply(); - url += (url.indexOf('?') < 0 ? '?cache' : '&cache'); + url += (url.indexOf('?') < 0 ? '?' : '&') + 'cache'; if (mp.au.src == url) mp.au.currentTime = 0; else { @@ -4450,6 +4483,10 @@ function reload_mp() { } mpl.stop(); widget.close(); + var plays = QSA('tr>td:first-child>a.play'); + for (var a = plays.length - 1; a >= 0; a--) + plays[a].parentNode.innerHTML = '-'; + mp = new MPlayer(); setTimeout(pbar.onresize, 1); }