mirror of
https://github.com/9001/copyparty.git
synced 2026-08-15 14:53:08 -06:00
th: custom thumbnail extractors (#1602)
Signed-off-by: ed <s@ocv.me> Co-authored-by: ed <s@ocv.me>
This commit is contained in:
parent
47297475bf
commit
1864805072
52
bin/thumbs/README.md
Normal file
52
bin/thumbs/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# thumbnail extractors
|
||||||
|
|
||||||
|
extract (or generate) thumbnails for custom file formats or override default thumbnail extraction for standard media files
|
||||||
|
|
||||||
|
|
||||||
|
## usage
|
||||||
|
|
||||||
|
* load plugins with `--th-extract baz=~/dev/copyparty/bin/thumbs/randomcolor.py`
|
||||||
|
* `baz` is the file extension (may be comma-separated list of extensions)
|
||||||
|
* add multiple different extractor plugins by repeating the `--th-extract` argument
|
||||||
|
|
||||||
|
|
||||||
|
## api
|
||||||
|
|
||||||
|
### in
|
||||||
|
|
||||||
|
each plugin must define a function `main(abspath, **kwargs)`:
|
||||||
|
* `abspath` is the path to a file to extract thumbnail for
|
||||||
|
* `kwargs` currently receives these keyword arguments:
|
||||||
|
* `vn` is the VFS which contains the requested file
|
||||||
|
* `th_srv` is an instance of [copyparty/th_srv](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/th_srv.py)
|
||||||
|
|
||||||
|
### out
|
||||||
|
|
||||||
|
the `main()` function must
|
||||||
|
* return `None` or raise an `Exception` in case it is unable to extract thumbnail
|
||||||
|
* return tuple `fmt, stream, offset, whence, size` otherwise:
|
||||||
|
* `fmt: str` – format (filename extension) of the thumbnail; should be one of the image formats supported by copyparty (see `--th-r-*` options)
|
||||||
|
* `stream: IO[bytes]` – binary file-like object supporting `seek()`, `read()`, `close()` methods; should contain extracted thumbnail image
|
||||||
|
* `offset: int, whence: int` – arguments to `stream.seek()`; copyparty will call `stream.seek(offset, whence)` once before starting reading the stream contents
|
||||||
|
* `size: int | None` – number of bytes to read from the `stream`; if `size` is negative or `None`, copyparty will read until the end of the stream
|
||||||
|
|
||||||
|
|
||||||
|
### notes
|
||||||
|
|
||||||
|
it is possible to extract or generate custom thumbnails for standard media files too,
|
||||||
|
e.g. run copyparty with `--th-extract mp3,m4a,aac,flac,opus=~/dev/copyparty/bin/thumbs/my_custom_mp3_th_extractor.py` and do your magic with audio file thumbnails
|
||||||
|
|
||||||
|
in case copyparty doesn't like the `fmt` option, it will `close()` the `stream` without ever calling `seek()` or `read()`
|
||||||
|
|
||||||
|
if your scenario involves heavy computation or i/o, it is better to defer the heavy parts until the first `seek()` or `read()` to avoid redundant work in case the result gets rejected based on `fmt` value
|
||||||
|
|
||||||
|
if you provide custom `stream` object:
|
||||||
|
copyparty performs buffered reading, so expect multiple `read()` calls, respect `size` argument in `read(size)` calls, and maintain correct seek position
|
||||||
|
|
||||||
|
|
||||||
|
## examples
|
||||||
|
|
||||||
|
|
||||||
|
## some other known plugins seen on the internets
|
||||||
|
|
||||||
|
* [fpkg_thumb](https://github.com/kamaeff/copyparty-dumb-fpkgi-handler/blob/master/fpkg_thumb.py) extracts cover images from playstation4 software installation packages ("pkg" and "fpkg" files)
|
||||||
|
|
@ -777,6 +777,54 @@ def get_sects():
|
||||||
).rstrip()
|
).rstrip()
|
||||||
+ build_flags_desc(),
|
+ build_flags_desc(),
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"thumb-extractors",
|
||||||
|
"extract thumbnails from custom file formats with python scripts",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
copyparty is able to extract thumbnails from various media formats:
|
||||||
|
images, audio, video, ebooks
|
||||||
|
|
||||||
|
if you want to extract thumbnails from unsupported file formats,
|
||||||
|
you can load a plugin, which extracts image data from custom formats
|
||||||
|
|
||||||
|
load the plugin using --args; for example \033[36m
|
||||||
|
--tx-extract iso,mdf,dmg=~/party-thumb-extractors/th_diskimg.py
|
||||||
|
\033[0m
|
||||||
|
the file must define the function \033[35mmain(abspath, **kwargs)\033[0m:
|
||||||
|
\033[35mabspath\033[0m: path to the file to extract thumbnail from
|
||||||
|
\033[35mkwargs\033[0m currently have:
|
||||||
|
\033[35mvn\033[0m: the VFS which contains the requested file
|
||||||
|
\033[35mrth_srv\033[0m: the copyparty ThumbSrv instance
|
||||||
|
|
||||||
|
if `main` couldn't extract a thumbnail, it must return None;
|
||||||
|
otherwise the return value must be a tuple:
|
||||||
|
\033[35mreturn fmt, stream, offset, whence, size\033[0m
|
||||||
|
|
||||||
|
> \033[32mfmt\033[0m: thumbnail format, should be one of the formats
|
||||||
|
supported by copyparty; e.g. "png", "jpg"
|
||||||
|
|
||||||
|
> \033[32mstream\033[0m: binary file-like object containing the thumbnail,
|
||||||
|
supporting seek(), read(), and close() methods
|
||||||
|
|
||||||
|
> \033[32moffset, whence\033[0m: arguments to seek() method;
|
||||||
|
stream.seek(offset, whence) will be called once before copyparty
|
||||||
|
starts reading from the stream
|
||||||
|
|
||||||
|
> \033[32msize\033[0m: number of bytes to read ftom the stream;
|
||||||
|
if negative, copyparty will read through the end of the stream;
|
||||||
|
|
||||||
|
you can also supply extractors for standard formats to override default
|
||||||
|
thumbnail extraction behaviour; for example \033[36m
|
||||||
|
--tx-extract mp3=~/party-thumb-extractors/th_mp3.py
|
||||||
|
\033[0mwill let you extract or generate custom thumbnails for mp3 files
|
||||||
|
|
||||||
|
\033[1;35mPS!\033[0m the folder that contains the python file should ideally
|
||||||
|
not contain many other python files, and especially nothing
|
||||||
|
with filenames that overlap with modules used by copyparty
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"handlers",
|
"handlers",
|
||||||
"use plugins to handle certain events",
|
"use plugins to handle certain events",
|
||||||
|
|
@ -1838,8 +1886,19 @@ def add_thumbnail(ap):
|
||||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,kra,ora,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,kra,ora,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bcstm,bfstm,brstm,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,m4b,m4r,mdgz,mdxz,mdz,mka,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||||
|
|
||||||
|
ap2.add_argument("--th-pil-add", metavar="T", type=u, default="", help="add or remove image formats to decode using pillow listed in \033[33m--th-r-pil\033[0m; example: [\033[32mfb2,pdf,-psd,-bmp\033[0m] (add fb2 and pdf, remove psd and bmp)")
|
||||||
|
ap2.add_argument("--th-vips-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for pyvips")
|
||||||
|
ap2.add_argument("--th-raw-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for rawpy/libraw")
|
||||||
|
ap2.add_argument("--th-ffi-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg images")
|
||||||
|
ap2.add_argument("--th-ffv-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg videos")
|
||||||
|
ap2.add_argument("--th-ffa-add", metavar="T", type=u, default="", help="add or remove formats; same as \033[33m--th-pil-add\033[0m but for ffmpeg audio")
|
||||||
|
|
||||||
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
||||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub, kra=png.kra, ora=png.ora", help="audio/image formats to decompress before passing to ffmpeg")
|
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub, kra=png.kra, ora=png.ora", help="audio/image formats to decompress before passing to ffmpeg")
|
||||||
|
ap2.add_argument("--th-extract", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m list of file extensions to thumbnail using a custom plugin (a python script); example: [\033[32mmdf,iso,dmg=/handlers/th_diskimg.py\033[0m]")
|
||||||
|
ap2.add_argument("--th-extr-sz", metavar="M", type=int, default=16, help="max num megabytes to allow \033[33m--th-extract\033[0m plugins to extract from each file")
|
||||||
|
ap2.add_argument("--hot-th-extr", action="store_true", help="recompile extractors on each thumbnail extraction -- expensive but convenient when hacking on stuff")
|
||||||
|
|
||||||
|
|
||||||
def add_transcoding(ap):
|
def add_transcoding(ap):
|
||||||
|
|
|
||||||
|
|
@ -2379,6 +2379,7 @@ class AuthSrv(object):
|
||||||
vol.flags["dvthumb"] = True
|
vol.flags["dvthumb"] = True
|
||||||
vol.flags["dathumb"] = True
|
vol.flags["dathumb"] = True
|
||||||
vol.flags["dithumb"] = True
|
vol.flags["dithumb"] = True
|
||||||
|
vol.flags["dethumb"] = True
|
||||||
|
|
||||||
have_fk = False
|
have_fk = False
|
||||||
for vol in vfs.all_nodes.values():
|
for vol in vfs.all_nodes.values():
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,7 @@ flagcats = {
|
||||||
"dvthumb": "disables video thumbnails",
|
"dvthumb": "disables video thumbnails",
|
||||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||||
"dithumb": "disables image thumbnails",
|
"dithumb": "disables image thumbnails",
|
||||||
|
"dethumb": "disables custom thumbnails (--th-extract)",
|
||||||
"pngquant": "compress audio waveforms 33% better",
|
"pngquant": "compress audio waveforms 33% better",
|
||||||
"thsize": "thumbnail res; WxH",
|
"thsize": "thumbnail res; WxH",
|
||||||
"crop": "center-cropping (y/n/fy/fn)",
|
"crop": "center-cropping (y/n/fy/fn)",
|
||||||
|
|
|
||||||
|
|
@ -7893,6 +7893,10 @@ class HttpCli(object):
|
||||||
ext in self.thumbcli.fmt_pil
|
ext in self.thumbcli.fmt_pil
|
||||||
or ext in self.thumbcli.fmt_vips
|
or ext in self.thumbcli.fmt_vips
|
||||||
or ext in self.thumbcli.fmt_ffi
|
or ext in self.thumbcli.fmt_ffi
|
||||||
|
or (
|
||||||
|
ext in self.thumbcli.thumbable
|
||||||
|
and ext not in self.thumbcli.thumbable_native
|
||||||
|
)
|
||||||
)
|
)
|
||||||
is_vid = ext in self.thumbcli.fmt_ffv
|
is_vid = ext in self.thumbcli.fmt_ffv
|
||||||
is_au = ext in self.thumbcli.fmt_ffa
|
is_au = ext in self.thumbcli.fmt_ffa
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,19 @@ VER_SHARES_DB = 2
|
||||||
CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4}
|
CVE_SEVS = {"low": 1, "medium": 2, "moderate": 2, "high": 3, "critical": 4}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_th_fset(f: str, fadd: str) -> set[str]:
|
||||||
|
fset = {x for x in f.replace(" ", "").split(",")}
|
||||||
|
rmset = {""}
|
||||||
|
|
||||||
|
for x in fadd.replace(" ", "").split(","):
|
||||||
|
if x.startswith("-"):
|
||||||
|
rmset.add(x.lstrip("-"))
|
||||||
|
else:
|
||||||
|
fset.add(x)
|
||||||
|
|
||||||
|
return fset - rmset
|
||||||
|
|
||||||
|
|
||||||
class SvcHub(object):
|
class SvcHub(object):
|
||||||
"""
|
"""
|
||||||
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
|
Hosts all services which cannot be parallelized due to reliance on monolithic resources.
|
||||||
|
|
@ -441,6 +454,20 @@ class SvcHub(object):
|
||||||
zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")]
|
zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")]
|
||||||
args.au_unpk = {x[0]: x[1] for x in zlss}
|
args.au_unpk = {x[0]: x[1] for x in zlss}
|
||||||
|
|
||||||
|
args.th_r_pil = _build_th_fset(args.th_r_pil, args.th_pil_add)
|
||||||
|
args.th_r_vips = _build_th_fset(args.th_r_vips, args.th_vips_add)
|
||||||
|
args.th_r_raw = _build_th_fset(args.th_r_raw, args.th_raw_add)
|
||||||
|
args.th_r_ffi = _build_th_fset(args.th_r_ffi, args.th_ffi_add)
|
||||||
|
args.th_r_ffv = _build_th_fset(args.th_r_ffv, args.th_ffv_add)
|
||||||
|
args.th_r_ffa = _build_th_fset(args.th_r_ffa, args.th_ffa_add)
|
||||||
|
|
||||||
|
th_extract = args.th_extract or []
|
||||||
|
args.th_extract = {}
|
||||||
|
for arg in th_extract:
|
||||||
|
exts, script = arg.split("=", 1)
|
||||||
|
for ext in exts.split(","):
|
||||||
|
args.th_extract[ext.strip().lower()] = script
|
||||||
|
|
||||||
self.args.th_dec = list(decs.keys())
|
self.args.th_dec = list(decs.keys())
|
||||||
self.thumbsrv = None
|
self.thumbsrv = None
|
||||||
want_ff = False
|
want_ff = False
|
||||||
|
|
@ -478,7 +505,7 @@ class SvcHub(object):
|
||||||
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
|
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
|
||||||
raise Exception(t % (args.q_mp3,))
|
raise Exception(t % (args.q_mp3,))
|
||||||
else:
|
else:
|
||||||
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
|
zss = args.th_r_ffa | args.th_r_ffv
|
||||||
args.au_unpk = {
|
args.au_unpk = {
|
||||||
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
|
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ class ThumbCli(object):
|
||||||
self.cooldown = Cooldown(self.args.th_poke) if self.args.th_poke else None
|
self.cooldown = Cooldown(self.args.th_poke) if self.args.th_poke else None
|
||||||
|
|
||||||
self.thumbable = c["thumbable"]
|
self.thumbable = c["thumbable"]
|
||||||
|
self.thumbable_native = c["thumbable_native"]
|
||||||
self.fmt_pil = c["pil"]
|
self.fmt_pil = c["pil"]
|
||||||
self.fmt_vips = c["vips"]
|
self.fmt_vips = c["vips"]
|
||||||
self.fmt_raw = c["raw"]
|
self.fmt_raw = c["raw"]
|
||||||
|
|
@ -59,6 +60,10 @@ class ThumbCli(object):
|
||||||
if is_vid and "dvthumb" in dbv.flags:
|
if is_vid and "dvthumb" in dbv.flags:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
is_custom = ext not in self.thumbable_native
|
||||||
|
if is_custom and "dethumb" in dbv.flags:
|
||||||
|
return None
|
||||||
|
|
||||||
want_opus = fmt in EXTS_AC
|
want_opus = fmt in EXTS_AC
|
||||||
is_au = ext in self.fmt_ffa
|
is_au = ext in self.fmt_ffa
|
||||||
is_vau = want_opus and ext in self.fmt_ffv
|
is_vau = want_opus and ext in self.fmt_ffv
|
||||||
|
|
@ -76,7 +81,7 @@ class ThumbCli(object):
|
||||||
elif want_opus:
|
elif want_opus:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
is_img = not is_vid and not is_au
|
is_img = not is_vid and not is_au and not is_custom
|
||||||
if is_img and "dithumb" in dbv.flags:
|
if is_img and "dithumb" in dbv.flags:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ from .util import (
|
||||||
afsenc,
|
afsenc,
|
||||||
atomic_move,
|
atomic_move,
|
||||||
fsenc,
|
fsenc,
|
||||||
|
loadpy,
|
||||||
min_ex,
|
min_ex,
|
||||||
runcmd,
|
runcmd,
|
||||||
statdir,
|
statdir,
|
||||||
|
|
@ -323,24 +324,12 @@ class ThumbSrv(object):
|
||||||
if self.args.th_clean:
|
if self.args.th_clean:
|
||||||
Daemon(self.cleaner, "thumb.cln")
|
Daemon(self.cleaner, "thumb.cln")
|
||||||
|
|
||||||
(
|
self.fmt_pil = self.args.th_r_pil
|
||||||
self.fmt_pil,
|
self.fmt_vips = self.args.th_r_vips
|
||||||
self.fmt_vips,
|
self.fmt_raw = self.args.th_r_raw
|
||||||
self.fmt_raw,
|
self.fmt_ffi = self.args.th_r_ffi
|
||||||
self.fmt_ffi,
|
self.fmt_ffv = self.args.th_r_ffv
|
||||||
self.fmt_ffv,
|
self.fmt_ffa = self.args.th_r_ffa
|
||||||
self.fmt_ffa,
|
|
||||||
) = [
|
|
||||||
set(y.split(","))
|
|
||||||
for y in [
|
|
||||||
self.args.th_r_pil,
|
|
||||||
self.args.th_r_vips,
|
|
||||||
self.args.th_r_raw,
|
|
||||||
self.args.th_r_ffi,
|
|
||||||
self.args.th_r_ffv,
|
|
||||||
self.args.th_r_ffa,
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
if not H_PIL_HEIF:
|
if not H_PIL_HEIF:
|
||||||
for f in "heif heifs heic heics".split(" "):
|
for f in "heif heifs heic heics".split(" "):
|
||||||
|
|
@ -359,23 +348,26 @@ class ThumbSrv(object):
|
||||||
self.fmt_pil.discard(f)
|
self.fmt_pil.discard(f)
|
||||||
|
|
||||||
self.thumbable: set[str] = set()
|
self.thumbable: set[str] = set()
|
||||||
|
self.thumbable_native: set[str] = set()
|
||||||
self._build_thumbable()
|
self._build_thumbable()
|
||||||
|
|
||||||
def _build_thumbable(self) -> None:
|
def _build_thumbable(self) -> None:
|
||||||
self.thumbable.clear()
|
self.thumbable_native.clear()
|
||||||
|
|
||||||
if "pil" in self.args.th_dec:
|
if "pil" in self.args.th_dec:
|
||||||
self.thumbable |= self.fmt_pil
|
self.thumbable_native |= self.fmt_pil
|
||||||
|
|
||||||
if "vips" in self.args.th_dec:
|
if "vips" in self.args.th_dec:
|
||||||
self.thumbable |= self.fmt_vips
|
self.thumbable_native |= self.fmt_vips
|
||||||
|
|
||||||
if "raw" in self.args.th_dec:
|
if "raw" in self.args.th_dec:
|
||||||
self.thumbable |= self.fmt_raw
|
self.thumbable_native |= self.fmt_raw
|
||||||
|
|
||||||
if "ff" in self.args.th_dec:
|
if "ff" in self.args.th_dec:
|
||||||
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
|
for zss in [self.fmt_ffi, self.fmt_ffv, self.fmt_ffa]:
|
||||||
self.thumbable |= zss
|
self.thumbable_native |= zss
|
||||||
|
|
||||||
|
self.thumbable = self.thumbable_native | set(self.args.th_extract)
|
||||||
|
|
||||||
def _log(self, msg: str, c: Union[int, str] = 0) -> None:
|
def _log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||||
self.log_func("thumb", msg, c)
|
self.log_func("thumb", msg, c)
|
||||||
|
|
@ -467,6 +459,7 @@ class ThumbSrv(object):
|
||||||
def getcfg(self) -> dict[str, set[str]]:
|
def getcfg(self) -> dict[str, set[str]]:
|
||||||
return {
|
return {
|
||||||
"thumbable": self.thumbable,
|
"thumbable": self.thumbable,
|
||||||
|
"thumbable_native": self.thumbable_native,
|
||||||
"pil": self.fmt_pil,
|
"pil": self.fmt_pil,
|
||||||
"vips": self.fmt_vips,
|
"vips": self.fmt_vips,
|
||||||
"raw": self.fmt_raw,
|
"raw": self.fmt_raw,
|
||||||
|
|
@ -549,19 +542,38 @@ class ThumbSrv(object):
|
||||||
png_ok = False
|
png_ok = False
|
||||||
funs = []
|
funs = []
|
||||||
|
|
||||||
|
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_flac or want_wav
|
||||||
|
|
||||||
|
ap_extr = abspath
|
||||||
|
if (
|
||||||
|
ext in self.args.th_extract
|
||||||
|
and not want_au
|
||||||
|
and not want_png
|
||||||
|
and "dethumb" not in vn.flags
|
||||||
|
):
|
||||||
|
ap_extr = self.run_extractor(self.args.th_extract[ext], abspath, vn)
|
||||||
|
if ap_extr:
|
||||||
|
ext = ap_extr.rsplit(".", 1)[-1]
|
||||||
|
|
||||||
if ext in self.args.au_unpk:
|
if ext in self.args.au_unpk:
|
||||||
ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
|
ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
|
||||||
|
ap_unpk = au_unpk(self.log, self.args.au_unpk, ap_extr, vn)
|
||||||
|
elif ext in self.thumbable_native:
|
||||||
|
ap_unpk = ap_extr
|
||||||
else:
|
else:
|
||||||
ap_unpk = abspath
|
ap_unpk = abspath
|
||||||
|
ap_unpk = ""
|
||||||
|
|
||||||
|
if ap_extr and ap_extr != ap_unpk and ap_extr != abspath:
|
||||||
|
wunlink(self.log, ap_extr, vn.flags)
|
||||||
|
|
||||||
if ap_unpk and not bos.path.exists(tpath):
|
if ap_unpk and not bos.path.exists(tpath):
|
||||||
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_flac or want_wav
|
|
||||||
for lib in self.args.th_dec:
|
for lib in self.args.th_dec:
|
||||||
can_au = lib == "ff" and (
|
can_au = lib == "ff" and (
|
||||||
ext in self.fmt_ffa or ext in self.fmt_ffv
|
ext in self.fmt_ffa or ext in self.fmt_ffv
|
||||||
|
|
@ -669,6 +681,66 @@ class ThumbSrv(object):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.nthr -= 1
|
self.nthr -= 1
|
||||||
|
|
||||||
|
def run_extractor(self, extr: str, abspath: str, vn: VFS) -> str:
|
||||||
|
try:
|
||||||
|
mod = loadpy(extr, self.args.hot_th_extr)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("extractor import failed; " + min_ex(), 1)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
fd = 0
|
||||||
|
ret = ""
|
||||||
|
stream = None
|
||||||
|
try:
|
||||||
|
res = mod.main(abspath, vn=vn, th_srv=self)
|
||||||
|
if not res:
|
||||||
|
t = "extractor %r returned no data for file %r"
|
||||||
|
self.log(t % (extr, abspath))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
outext, stream, offset, whence, size = res
|
||||||
|
if (
|
||||||
|
outext not in self.thumbable_native
|
||||||
|
or outext in self.fmt_ffa
|
||||||
|
or outext in self.fmt_ffv
|
||||||
|
):
|
||||||
|
t = "extractor %r returned unsupported format %r"
|
||||||
|
self.log(t % (extr, outext))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
remains = size if size is not None and size >= 0 else -1
|
||||||
|
|
||||||
|
stream.seek(offset, whence)
|
||||||
|
fd, ret = tempfile.mkstemp("." + outext)
|
||||||
|
bufsz = rsz = self.args.iobuf
|
||||||
|
maxsz = self.args.th_extr_sz * 1048576
|
||||||
|
outsz = 0
|
||||||
|
with os.fdopen(fd, "wb", rsz) as f:
|
||||||
|
fd = 0
|
||||||
|
while True:
|
||||||
|
if remains < rsz and remains >= 0:
|
||||||
|
bufsz = remains
|
||||||
|
buf = stream.read(bufsz)
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
remains -= len(buf)
|
||||||
|
outsz += len(buf)
|
||||||
|
if outsz >= maxsz:
|
||||||
|
raise Exception("too large")
|
||||||
|
f.write(buf)
|
||||||
|
return ret
|
||||||
|
except Exception as e:
|
||||||
|
if fd:
|
||||||
|
os.close(fd)
|
||||||
|
if ret:
|
||||||
|
wunlink(self.log, ret, vn.flags)
|
||||||
|
t = "failed to extract thumbnail from %r: %s"
|
||||||
|
self.log(t % (abspath, min_ex()))
|
||||||
|
return ""
|
||||||
|
finally:
|
||||||
|
if stream:
|
||||||
|
stream.close()
|
||||||
|
|
||||||
def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image":
|
def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image":
|
||||||
# exif_transpose is expensive (loads full image + unconditional copy)
|
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||||
res = self.getres(vn, fmt)
|
res = self.getres(vn, fmt)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue