mirror of
https://github.com/9001/copyparty.git
synced 2025-08-18 01:22:13 -06:00
create video thumbnails
This commit is contained in:
parent
4dff726310
commit
02a856ecb4
14
README.md
14
README.md
|
@ -94,7 +94,7 @@ you may also want these, especially on servers:
|
||||||
* ☑ media player
|
* ☑ media player
|
||||||
* ✖ thumbnails
|
* ✖ thumbnails
|
||||||
* ☑ images
|
* ☑ images
|
||||||
* ✖ videos
|
* ☑ videos
|
||||||
* ✖ cache eviction
|
* ✖ cache eviction
|
||||||
* ☑ SPA (browse while uploading)
|
* ☑ SPA (browse while uploading)
|
||||||
* if you use the file-tree on the left only, not folders in the file list
|
* if you use the file-tree on the left only, not folders in the file list
|
||||||
|
@ -404,14 +404,20 @@ quick outline of the up2k protocol, see [uploading](#uploading) for the web-clie
|
||||||
|
|
||||||
* `jinja2` (is built into the SFX)
|
* `jinja2` (is built into the SFX)
|
||||||
|
|
||||||
**optional,** enables music tags:
|
|
||||||
|
## optional dependencies
|
||||||
|
|
||||||
|
enable music tags:
|
||||||
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
* either `mutagen` (fast, pure-python, skips a few tags, makes copyparty GPL? idk)
|
||||||
* 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)
|
||||||
|
|
||||||
**optional,** enables thumbnails:
|
enable image thumbnails:
|
||||||
* `Pillow` (requires py2.7 or py3.5+)
|
* `Pillow` (requires py2.7 or py3.5+)
|
||||||
|
|
||||||
**optional,** enables reading HEIF pictures:
|
enable video thumbnails:
|
||||||
|
* `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||||
|
|
||||||
|
enable reading HEIF pictures:
|
||||||
* `pyheif-pillow-opener` (requires Linux or a C compiler)
|
* `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -245,11 +245,15 @@ def run_argparse(argv, formatter):
|
||||||
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
|
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||||
ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
|
ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
|
||||||
ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
||||||
ap.add_argument("--no-thumb", action="store_true", help="disable thumbnails")
|
|
||||||
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
||||||
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
|
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
|
||||||
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
|
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('thumbnail options')
|
||||||
|
ap.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
|
||||||
|
ap.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
|
||||||
|
ap.add_argument("--thumbsz", metavar="WxH", default="420x420", help="thumbnail res")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('database options')
|
ap2 = ap.add_argument_group('database options')
|
||||||
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
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")
|
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d")
|
||||||
|
|
|
@ -14,6 +14,117 @@ if not PY2:
|
||||||
unicode = str
|
unicode = str
|
||||||
|
|
||||||
|
|
||||||
|
def have_ff(cmd):
|
||||||
|
if PY2:
|
||||||
|
cmd = (cmd + " -version").encode("ascii").split(b" ")
|
||||||
|
try:
|
||||||
|
sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return bool(shutil.which(cmd))
|
||||||
|
|
||||||
|
|
||||||
|
HAVE_FFMPEG = have_ff("ffmpeg")
|
||||||
|
HAVE_FFPROBE = have_ff("ffprobe")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ffprobe(stdout, logger):
|
||||||
|
txt = [x.rstrip("\r") for x in stdout.split("\n")]
|
||||||
|
|
||||||
|
"""
|
||||||
|
note:
|
||||||
|
tags which contain newline will be truncated on first \n,
|
||||||
|
ffprobe emits \n and spacepads the : to align visually
|
||||||
|
note:
|
||||||
|
the Stream ln always mentions Audio: if audio
|
||||||
|
the Stream ln usually has kb/s, is more accurate
|
||||||
|
the Duration ln always has kb/s
|
||||||
|
the Metadata: after Chapter may contain BPM info,
|
||||||
|
title : Tempo: 126.0
|
||||||
|
|
||||||
|
Input #0, wav,
|
||||||
|
Metadata:
|
||||||
|
date : <OK>
|
||||||
|
Duration:
|
||||||
|
Chapter #
|
||||||
|
Metadata:
|
||||||
|
title : <NG>
|
||||||
|
|
||||||
|
Input #0, mp3,
|
||||||
|
Metadata:
|
||||||
|
album : <OK>
|
||||||
|
Duration:
|
||||||
|
Stream #0:0: Audio:
|
||||||
|
Stream #0:1: Video:
|
||||||
|
Metadata:
|
||||||
|
comment : <NG>
|
||||||
|
"""
|
||||||
|
|
||||||
|
ptn_md_beg = re.compile("^( +)Metadata:$")
|
||||||
|
ptn_md_kv = re.compile("^( +)([^:]+) *: (.*)")
|
||||||
|
ptn_dur = re.compile("^ *Duration: ([^ ]+)(, |$)")
|
||||||
|
ptn_br1 = re.compile("^ *Duration: .*, bitrate: ([0-9]+) kb/s(, |$)")
|
||||||
|
ptn_br2 = re.compile("^ *Stream.*: Audio:.* ([0-9]+) kb/s(, |$)")
|
||||||
|
ptn_audio = re.compile("^ *Stream .*: Audio: ")
|
||||||
|
ptn_au_parent = re.compile("^ *(Input #|Stream .*: Audio: )")
|
||||||
|
|
||||||
|
ret = {}
|
||||||
|
md = {}
|
||||||
|
in_md = False
|
||||||
|
is_audio = False
|
||||||
|
au_parent = False
|
||||||
|
for ln in txt:
|
||||||
|
m = ptn_md_kv.match(ln)
|
||||||
|
if m and in_md and len(m.group(1)) == in_md:
|
||||||
|
_, k, v = [x.strip() for x in m.groups()]
|
||||||
|
if k != "" and v != "":
|
||||||
|
md[k] = [v]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
in_md = False
|
||||||
|
|
||||||
|
m = ptn_md_beg.match(ln)
|
||||||
|
if m and au_parent:
|
||||||
|
in_md = len(m.group(1)) + 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
au_parent = bool(ptn_au_parent.search(ln))
|
||||||
|
|
||||||
|
if ptn_audio.search(ln):
|
||||||
|
is_audio = True
|
||||||
|
|
||||||
|
m = ptn_dur.search(ln)
|
||||||
|
if m:
|
||||||
|
sec = 0
|
||||||
|
tstr = m.group(1)
|
||||||
|
if tstr.lower() != "n/a":
|
||||||
|
try:
|
||||||
|
tf = tstr.split(",")[0].split(".")[0].split(":")
|
||||||
|
for f in tf:
|
||||||
|
sec *= 60
|
||||||
|
sec += int(f)
|
||||||
|
except:
|
||||||
|
logger("invalid timestr from ffprobe: [{}]".format(tstr), c=3)
|
||||||
|
|
||||||
|
ret[".dur"] = sec
|
||||||
|
m = ptn_br1.search(ln)
|
||||||
|
if m:
|
||||||
|
ret[".q"] = m.group(1)
|
||||||
|
|
||||||
|
m = ptn_br2.search(ln)
|
||||||
|
if m:
|
||||||
|
ret[".q"] = m.group(1)
|
||||||
|
|
||||||
|
if not is_audio:
|
||||||
|
return {}, {}
|
||||||
|
|
||||||
|
ret = {k: [0, v] for k, v in ret.items()}
|
||||||
|
|
||||||
|
return ret, md
|
||||||
|
|
||||||
|
|
||||||
class MTag(object):
|
class MTag(object):
|
||||||
def __init__(self, log_func, args):
|
def __init__(self, log_func, args):
|
||||||
self.log_func = log_func
|
self.log_func = log_func
|
||||||
|
@ -35,15 +146,7 @@ class MTag(object):
|
||||||
self.get = self.get_ffprobe
|
self.get = self.get_ffprobe
|
||||||
self.prefer_mt = True
|
self.prefer_mt = True
|
||||||
# about 20x slower
|
# about 20x slower
|
||||||
if PY2:
|
self.usable = HAVE_FFPROBE
|
||||||
cmd = [b"ffprobe", b"-version"]
|
|
||||||
try:
|
|
||||||
sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
|
||||||
except:
|
|
||||||
self.usable = False
|
|
||||||
else:
|
|
||||||
if not shutil.which("ffprobe"):
|
|
||||||
self.usable = False
|
|
||||||
|
|
||||||
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
||||||
self.usable = False
|
self.usable = False
|
||||||
|
@ -226,97 +329,7 @@ class MTag(object):
|
||||||
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
r = p.communicate()
|
r = p.communicate()
|
||||||
txt = r[1].decode("utf-8", "replace")
|
txt = r[1].decode("utf-8", "replace")
|
||||||
txt = [x.rstrip("\r") for x in txt.split("\n")]
|
ret, md = parse_ffprobe(txt, self.log)
|
||||||
|
|
||||||
"""
|
|
||||||
note:
|
|
||||||
tags which contain newline will be truncated on first \n,
|
|
||||||
ffprobe emits \n and spacepads the : to align visually
|
|
||||||
note:
|
|
||||||
the Stream ln always mentions Audio: if audio
|
|
||||||
the Stream ln usually has kb/s, is more accurate
|
|
||||||
the Duration ln always has kb/s
|
|
||||||
the Metadata: after Chapter may contain BPM info,
|
|
||||||
title : Tempo: 126.0
|
|
||||||
|
|
||||||
Input #0, wav,
|
|
||||||
Metadata:
|
|
||||||
date : <OK>
|
|
||||||
Duration:
|
|
||||||
Chapter #
|
|
||||||
Metadata:
|
|
||||||
title : <NG>
|
|
||||||
|
|
||||||
Input #0, mp3,
|
|
||||||
Metadata:
|
|
||||||
album : <OK>
|
|
||||||
Duration:
|
|
||||||
Stream #0:0: Audio:
|
|
||||||
Stream #0:1: Video:
|
|
||||||
Metadata:
|
|
||||||
comment : <NG>
|
|
||||||
"""
|
|
||||||
|
|
||||||
ptn_md_beg = re.compile("^( +)Metadata:$")
|
|
||||||
ptn_md_kv = re.compile("^( +)([^:]+) *: (.*)")
|
|
||||||
ptn_dur = re.compile("^ *Duration: ([^ ]+)(, |$)")
|
|
||||||
ptn_br1 = re.compile("^ *Duration: .*, bitrate: ([0-9]+) kb/s(, |$)")
|
|
||||||
ptn_br2 = re.compile("^ *Stream.*: Audio:.* ([0-9]+) kb/s(, |$)")
|
|
||||||
ptn_audio = re.compile("^ *Stream .*: Audio: ")
|
|
||||||
ptn_au_parent = re.compile("^ *(Input #|Stream .*: Audio: )")
|
|
||||||
|
|
||||||
ret = {}
|
|
||||||
md = {}
|
|
||||||
in_md = False
|
|
||||||
is_audio = False
|
|
||||||
au_parent = False
|
|
||||||
for ln in txt:
|
|
||||||
m = ptn_md_kv.match(ln)
|
|
||||||
if m and in_md and len(m.group(1)) == in_md:
|
|
||||||
_, k, v = [x.strip() for x in m.groups()]
|
|
||||||
if k != "" and v != "":
|
|
||||||
md[k] = [v]
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
in_md = False
|
|
||||||
|
|
||||||
m = ptn_md_beg.match(ln)
|
|
||||||
if m and au_parent:
|
|
||||||
in_md = len(m.group(1)) + 2
|
|
||||||
continue
|
|
||||||
|
|
||||||
au_parent = bool(ptn_au_parent.search(ln))
|
|
||||||
|
|
||||||
if ptn_audio.search(ln):
|
|
||||||
is_audio = True
|
|
||||||
|
|
||||||
m = ptn_dur.search(ln)
|
|
||||||
if m:
|
|
||||||
sec = 0
|
|
||||||
tstr = m.group(1)
|
|
||||||
if tstr.lower() != "n/a":
|
|
||||||
try:
|
|
||||||
tf = tstr.split(",")[0].split(".")[0].split(":")
|
|
||||||
for f in tf:
|
|
||||||
sec *= 60
|
|
||||||
sec += int(f)
|
|
||||||
except:
|
|
||||||
self.log("invalid timestr from ffprobe: [{}]".format(tstr), c=3)
|
|
||||||
|
|
||||||
ret[".dur"] = sec
|
|
||||||
m = ptn_br1.search(ln)
|
|
||||||
if m:
|
|
||||||
ret[".q"] = m.group(1)
|
|
||||||
|
|
||||||
m = ptn_br2.search(ln)
|
|
||||||
if m:
|
|
||||||
ret[".q"] = m.group(1)
|
|
||||||
|
|
||||||
if not is_audio:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
ret = {k: [0, v] for k, v in ret.items()}
|
|
||||||
|
|
||||||
return self.normalize_tags(ret, md)
|
return self.normalize_tags(ret, md)
|
||||||
|
|
||||||
def get_bin(self, parsers, abspath):
|
def get_bin(self, parsers, abspath):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .th_srv import thumb_path, THUMBABLE
|
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
||||||
|
|
||||||
|
|
||||||
class ThumbCli(object):
|
class ThumbCli(object):
|
||||||
|
@ -13,9 +13,17 @@ class ThumbCli(object):
|
||||||
if ext not in THUMBABLE:
|
if ext not in THUMBABLE:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if self.args.no_vthumb and ext in FMT_FF:
|
||||||
|
return None
|
||||||
|
|
||||||
tpath = thumb_path(ptop, rem, mtime)
|
tpath = thumb_path(ptop, rem, mtime)
|
||||||
if os.path.exists(tpath):
|
try:
|
||||||
|
st = os.stat(tpath)
|
||||||
|
if st.st_size:
|
||||||
return tpath
|
return tpath
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime)
|
x = self.broker.put(True, "thumbsrv.get", ptop, rem, mtime)
|
||||||
return x.get()
|
return x.get()
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import threading
|
import threading
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
from .__init__ import PY2
|
||||||
|
from .util import fsenc, Queue
|
||||||
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, parse_ffprobe
|
||||||
|
|
||||||
|
|
||||||
|
if not PY2:
|
||||||
|
unicode = str
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
HAVE_PIL = True
|
HAVE_PIL = True
|
||||||
|
@ -17,12 +28,25 @@ try:
|
||||||
except:
|
except:
|
||||||
HAVE_PIL = False
|
HAVE_PIL = False
|
||||||
|
|
||||||
from .util import fsenc, Queue
|
|
||||||
|
|
||||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||||
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"
|
# ffmpeg -formats
|
||||||
FMT_PIL = {x: True for x in FMT_PIL.split(" ") if x}
|
FMT_PIL, FMT_FF = [
|
||||||
THUMBABLE = FMT_PIL
|
{x: True for x in y.split(" ") if x}
|
||||||
|
for y in [
|
||||||
|
"bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm",
|
||||||
|
"av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 mov 3gp mp4 ts mpegts nut ogv ogm rm vob wmv",
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
THUMBABLE = {}
|
||||||
|
|
||||||
|
if HAVE_PIL:
|
||||||
|
THUMBABLE.update(FMT_PIL)
|
||||||
|
|
||||||
|
if HAVE_FFMPEG and HAVE_FFPROBE:
|
||||||
|
THUMBABLE.update(FMT_FF)
|
||||||
|
|
||||||
|
|
||||||
def thumb_path(ptop, rem, mtime):
|
def thumb_path(ptop, rem, mtime):
|
||||||
|
@ -52,8 +76,11 @@ def thumb_path(ptop, rem, mtime):
|
||||||
class ThumbSrv(object):
|
class ThumbSrv(object):
|
||||||
def __init__(self, hub):
|
def __init__(self, hub):
|
||||||
self.hub = hub
|
self.hub = hub
|
||||||
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
|
self.res = hub.args.thumbsz.split("x")
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.busy = {}
|
self.busy = {}
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
@ -64,6 +91,22 @@ class ThumbSrv(object):
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
if not HAVE_PIL:
|
||||||
|
msg = "need Pillow to create thumbnails so please run this:\n {} -m pip install --user Pillow"
|
||||||
|
self.log(msg.format(os.path.basename(sys.executable)), c=1)
|
||||||
|
|
||||||
|
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
||||||
|
missing = []
|
||||||
|
if not HAVE_FFMPEG:
|
||||||
|
missing.append("ffmpeg")
|
||||||
|
|
||||||
|
if not HAVE_FFPROBE:
|
||||||
|
missing.append("ffprobe")
|
||||||
|
|
||||||
|
msg = "cannot create video thumbnails since some of the required programs are not available: "
|
||||||
|
msg += ", ".join(missing)
|
||||||
|
self.log(msg, c=1)
|
||||||
|
|
||||||
def log(self, msg, c=0):
|
def log(self, msg, c=0):
|
||||||
self.log_func("thumb", msg, c)
|
self.log_func("thumb", msg, c)
|
||||||
|
|
||||||
|
@ -108,10 +151,14 @@ class ThumbSrv(object):
|
||||||
with cond:
|
with cond:
|
||||||
cond.wait()
|
cond.wait()
|
||||||
|
|
||||||
if not os.path.exists(tpath):
|
try:
|
||||||
return None
|
st = os.stat(tpath)
|
||||||
|
if st.st_size:
|
||||||
return tpath
|
return tpath
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def worker(self):
|
def worker(self):
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
|
@ -125,9 +172,16 @@ class ThumbSrv(object):
|
||||||
if not os.path.exists(tpath):
|
if not os.path.exists(tpath):
|
||||||
if ext in FMT_PIL:
|
if ext in FMT_PIL:
|
||||||
fun = self.conv_pil
|
fun = self.conv_pil
|
||||||
|
elif ext in FMT_FF:
|
||||||
|
fun = self.conv_ffmpeg
|
||||||
|
|
||||||
if fun:
|
if fun:
|
||||||
|
try:
|
||||||
fun(abspath, tpath)
|
fun(abspath, tpath)
|
||||||
|
except:
|
||||||
|
self.log("{} failed on {}".format(fun.__name__, abspath), 3)
|
||||||
|
with open(tpath, "wb") as _:
|
||||||
|
pass
|
||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
subs = self.busy[tpath]
|
subs = self.busy[tpath]
|
||||||
|
@ -141,12 +195,39 @@ class ThumbSrv(object):
|
||||||
self.nthr -= 1
|
self.nthr -= 1
|
||||||
|
|
||||||
def conv_pil(self, abspath, tpath):
|
def conv_pil(self, abspath, tpath):
|
||||||
try:
|
|
||||||
with Image.open(abspath) as im:
|
with Image.open(abspath) as im:
|
||||||
if im.mode in ("RGBA", "P"):
|
if im.mode in ("RGBA", "P"):
|
||||||
im = im.convert("RGB")
|
im = im.convert("RGB")
|
||||||
|
|
||||||
im.thumbnail((256, 256))
|
im.thumbnail(self.res)
|
||||||
im.save(tpath)
|
im.save(tpath)
|
||||||
except:
|
|
||||||
pass
|
def conv_ffmpeg(self, abspath, tpath):
|
||||||
|
cmd = [b"ffprobe", b"-hide_banner", b"--", fsenc(abspath)]
|
||||||
|
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
|
r = p.communicate()
|
||||||
|
txt = r[1].decode("utf-8", "replace")
|
||||||
|
ret, _ = parse_ffprobe(txt, self.log)
|
||||||
|
|
||||||
|
dur = ret[".dur"][1]
|
||||||
|
seek = "{:.0f}".format(dur / 3)
|
||||||
|
scale = "scale=w={}:h={}:force_original_aspect_ratio=decrease"
|
||||||
|
scale = scale.format(*list(self.res)).encode("utf-8")
|
||||||
|
cmd = [
|
||||||
|
b"ffmpeg",
|
||||||
|
b"-nostdin",
|
||||||
|
b"-hide_banner",
|
||||||
|
b"-ss",
|
||||||
|
seek,
|
||||||
|
b"-i",
|
||||||
|
fsenc(abspath),
|
||||||
|
b"-vf",
|
||||||
|
scale,
|
||||||
|
b"-vframes",
|
||||||
|
b"1",
|
||||||
|
b"-q:v",
|
||||||
|
b"5",
|
||||||
|
fsenc(tpath),
|
||||||
|
]
|
||||||
|
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
|
r = p.communicate()
|
||||||
|
|
Loading…
Reference in a new issue