Skip to content

Instantly share code, notes, and snippets.

@hhsprings
Last active April 6, 2022 21:12
Show Gist options
  • Save hhsprings/b10dff2a3d1005ce705d5d9fbb774da6 to your computer and use it in GitHub Desktop.
Save hhsprings/b10dff2a3d1005ce705d5d9fbb774da6 to your computer and use it in GitHub Desktop.
to preview ffmpeg's trim
#! py -3
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import io
import sys
import subprocess
import pipes
import os
import re
import logging
import json
import threading
import tempfile
import functools
import itertools
import math
from glob import glob
from PIL import Image, ImageTk # require pillow. if you dont have it, do "pip install pillow".
from mako.template import Template # require mako. if you dont have it, do "pip install mako".
from mako.exceptions import MakoException
__MYNAME__, _ = os.path.splitext(
os.path.basename(sys.modules[__name__].__file__))
#_log = logging.getLogger(
# os.path.basename(sys.modules[__name__].__file__))
_log = logging.getLogger()
if hasattr("", "decode"):
_encode = lambda s: s.encode(sys.getfilesystemencoding())
else:
_encode = lambda s: s
def _abbreviate(s, left=8, right=8):
if len(s) > left + right:
rgx = re.compile(r"^(.{%d}).*(.{%d})$" % (left, right))
return rgx.sub(r"\g<1>...\g<2>", s)
return s
def parse_time(s):
"""
>>> print("%.3f" % parse_time(3.2))
3.200
>>> print("%.3f" % parse_time(3))
3.000
>>> print("%.3f" % parse_time("00:00:01"))
1.000
>>> print("%.3f" % parse_time("00:00:01.3"))
1.300
>>> print("%.3f" % parse_time("00:00:01.34"))
1.340
>>> print("%.3f" % parse_time("00:00:01.034"))
1.034
>>> print("%.3f" % parse_time("00:00:01.345"))
1.345
>>> print("%.3f" % parse_time("00:01:01.345"))
61.345
>>> print("%.3f" % parse_time("02:01:01.345"))
7261.345
>>> print("%.3f" % parse_time("01:01.345"))
61.345
"""
try:
return float(s)
except ValueError:
if "." in s:
n, _, ss = s.rpartition(".")
else:
n, ss = s, "0"
n = n.split(":")
if len(n) > 3:
raise ValueError("'{}' is not valid time.".format(s))
result = sum([
p * 60**(len(n) - 1 - i)
for i, p in enumerate(list(map(int, n)))])
result += int(ss) / float((10**len(ss)))
return result
def _ts_to_tss(ts, frac=3):
d, _, f = (("%%.%df" % frac) % ts).partition(".")
d = abs(int(d))
ss_h = int(d / 3600)
d -= ss_h * 3600
ss_m = int(d / 60)
d -= ss_m * 60
ss_s = int(d)
return "%s%02d:%02d:%02d.%s" % (
"" if ts >= 0 else "-",
ss_h, ss_m, ss_s, f)
def _getslice_from_arg(arg, isjson):
try:
if isjson:
_sl = json.loads(arg)
else:
_sl = arg
_times = 5
_t = 2.0
if isinstance(_sl, (dict,)):
_ss, _t = _sl.get("ss", 0.0), _sl.get("step", _t)
_ss = parse_time(_ss)
_to = _sl.get("to", _ss + _t * _times)
elif not isinstance(_sl, (list,)) or not len(_sl):
raise ValueError("--from_slice: invalid")
elif len(_sl) == 1:
_ss = parse_time(_sl[0])
_to = _ss + _t * _times
elif len(_sl) == 2:
_ss, _to = _sl
elif len(_sl) >= 2:
_ss, _to, _t = _sl[:3]
_t = float(_t)
_ss, _to = parse_time(_ss), parse_time(_to)
except Exception as exc:
raise ValueError("cannot generate ranges:\n {!r} ({})".format(arg, exc))
if _ss >= _to or _t <= 0:
raise ValueError("cannot generate ranges:\n {!r}".format(arg))
_t = float(_t)
return _ss, _to, _t
def _getrangelist_from_arg(arg, isjson):
_mar = 1.5
invalid = True
try:
if isjson:
_aslst = json.loads(arg)
else:
_aslst = arg
if isinstance(_aslst, (list,)) and len(_aslst) == 1:
_aslst = _aslst[0]
if isinstance(_aslst, (list,)):
_aslst = list(map(parse_time, _aslst))
invalid = False
elif isinstance(_aslst, (str, float, int)):
t = parse_time(_aslst)
if t > _mar:
_aslst = [t - _mar, t, t + _mar]
else:
_aslst = [t, t + _mar]
invalid = False
except (json.decoder.JSONDecodeError, ValueError):
pass
if invalid:
raise ValueError("invalid ranges:\n {!r}".format(arg))
return _aslst
def _yr_g(ss, to, t):
while ss < to:
_t = min(min(ss + t, to) - ss, t)
if _t < (1./120):
_log.warning(
'omit {"ss": %s, "t": %g}, because it\'s too small.',
_ts_to_tss(ss), _t)
else:
yield ss, _t
ss += t
def _yr_r(aslst):
for _ss, _to in zip(aslst[:-1], aslst[1:]):
_t = _to - _ss
if _t <= 0:
ap.error("invalid ranges:\n {!r}".format(
[_ts_to_tss(_ss), _ts_to_tss(_ss + _t)]))
yield _ss, _t
def _yr(args):
if args.from_slice:
_ss, _to, _t = _getslice_from_arg(args.from_slice, True)
return list(_yr_g(_ss, _to, _t))
elif args.from_splitpoints:
_aslst = _getrangelist_from_arg(args.from_splitpoints, True)
return list(_yr_r(_aslst))
else:
return []
def _remove_silently(fn):
if os.path.exists(fn):
try:
os.remove(fn)
except OSError as ex:
_log.warning(ex)
def _get_streamsinfo(args):
cmdl = [
"ffprobe",
"-hide_banner",
"-show_streams",
args.video,
]
tfn = tempfile.mktemp()
try:
with io.open(tfn, "wb") as err:
#raw = subprocess.check_output(
# list(map(_encode, filter(None, cmdl))), stderr=subprocess.DEVNULL)
raw = subprocess.check_output(list(map(_encode, filter(None, cmdl))), stderr=err)
stderrout = io.open(tfn, "rb").read()
dur = re.search(br" Duration: ([\d:.]+),", stderrout).group(1).decode()
finally:
if os.path.exists(tfn):
os.remove(tfn)
result = []
for line in re.split(r"\r?\n", raw.decode("utf-8").strip()):
if line == "[STREAM]":
result.append({})
elif line == "[/STREAM]":
pass
else:
k, _, v = line.partition("=")
result[-1][k] = v
for s in result:
if s.get("TAG:DURATION", "N/A") == "N/A" and \
s.get("duration", "N/A") == "N/A":
s["duration"] = dur
return result
def _update_argmediainfo(args):
mi = _get_streamsinfo(args)
hasv, hasa = False, False
channel_layout = "stereo"
for it in mi:
#_log.info("\n%s", json.dumps(it, indent=2))
ct = it.get("codec_type", "")
if ct == "video":
hasv = True
elif ct == "audio":
hasa = True
if "channel_layout" in it:
channel_layout = it["channel_layout"]
durs = [
s.get("TAG:DURATION", "N/A") for s in mi] + [
s.get("duration", "N/A") for s in mi]
#_log.info("%s", durs)
setattr(args, "actually_hasvideo", hasv)
setattr(args, "actually_hasaudio", hasa)
#
duration = min([parse_time(d) for d in durs if d != "N/A"])
setattr(args, "duration", duration)
if not hasv and not args.no_input_vstream:
args.no_input_vstream = True
if not hasa and not args.no_input_astream:
args.no_input_astream = True
setattr(args, "channel_layout", channel_layout)
def _build_ffmpeg_cmdline(args, _ss, _t, scale, volume, outfn):
isstill = not outfn.endswith(".mkv")
#
af = ""
vf = ["[0:v]"]
if not isstill:
vf.append("subtitles='{}'".format(
outfn.replace("\\", "/").replace(":", "\\:").replace(".mkv", ".srt")))
hasv = not args.no_input_vstream
hasa = not args.no_input_astream
audobsdur = min(_ss, args.audio_visualize_duration_if_no_input_vstream)
if isstill:
rngctlargs = [
["-ss", "{}".format(_ss)], # input's -ss (first occurence)
["-r", "1/1"],
0, # trim={}
["-t", "1"],
]
_t = 1
else:
rngctlargs = [
["-ss", "{}".format(_ss)], # input's -ss (first occurence)
[None, None],
0, # trim={}
["-t", "{}".format(_t)],
]
#
if hasa:
if args.no_input_vstream and args.audio_visualize_if_no_input_vstream:
exf = args.audio_visualize_if_no_input_vstream
if isstill:
vf[0] = """[0:a]{},""".format(exf)
else:
af = """[0:a]asplit[a1][a2];[a2]volume={:g}""".format(volume)
vf[0] = """[a1]{},""".format(exf)
hasv = True
_ss -= audobsdur
if isstill:
_t = audobsdur
rngctlargs[0][1] = "{}".format(_ss) # -ss
rngctlargs[1][1] = "1/{}".format(_t) # -r
rngctlargs[2] = _t # trim={}
rngctlargs[3][1] = "{}".format(_t) # -t
else:
rngctlargs[0][1] = "{}".format(_ss) # -ss
rngctlargs[2] = audobsdur # trim={}
rngctlargs[3][1] = "{}".format(_t) # -t
elif not isstill:
af = """[0:a]volume={:g}""".format(volume)
if not hasv:
vf[0] = "color=black:size=1280x720,loop=-1:size=2,trim=0:{},".format(_t)
vf.append("trim={},setpts=PTS-STARTPTS".format(rngctlargs[2]))
if af:
af = af + ",aformat=channel_layouts={},atrim={},asetpts=PTS-STARTPTS".format(
args.channel_layout,
rngctlargs[2])
if not args.no_drawpts and not isstill:
vf.append(
"drawtext='fontsize=90:fontcolor=gray:text=%{{pts\:hms}}':x=80:y=80".format())
vf.extend([
"scale={}".format(scale),
"setsar=1",
])
#
#
cf = ";".join(filter(None, [
af,
vf[0] + ",".join(vf[1:])]))
#
cmdl = [args.ffmpeg, "-y", "-hide_banner"]
cmdl.extend(rngctlargs[0]) # -ss
cmdl.extend([
"-i", args.video,
"-filter_complex", cf,])
if isstill:
cmdl.extend(rngctlargs[1]) # -r
cmdl.extend(rngctlargs[3]) # -t
cmdl.append(outfn)
return cmdl
def _execute_preview_one(args, player_command, _ss, _t, scale, volume, cb=lambda msg: _log.info(msg)):
_ss1_s, _to1_s = _ts_to_tss(_ss), _ts_to_tss(_ss + _t)
_ss2_s, _to2_s = _ts_to_tss(0), _ts_to_tss(_t)
if _ss >= args.duration:
_log.warning("start time %s is out of range. (must be < %s)", _ss1_s, _ts_to_tss(args.duration))
return
elif _t <= 0:
_log.warning("invalid duration: t=%g", _t)
return
#
r = _Renderer(
args,
args.logview_initial_template)
result, error = r.render(ss=_ss, to=_ss + _t, selected_ranges=[(_ss, _ss + _t)])
if not error:
# TODO?: customizable output stream...?
sys.stdout.write(result + "\n")
sys.stdout.flush()
else:
_log.warning("%s", error)
#
tmpoutb = tempfile.mktemp()
tmpout, tmpoutsrt = tmpoutb + ".mkv", (tmpoutb + ".srt").replace("\\", "/")
atexit.register(_remove_silently, tmpout)
atexit.register(_remove_silently, tmpoutsrt)
#
srttxt = """\
1
{} --> {}
{} <font color=0x7F7FFF3F>~</font> {}
""".format(
_ss2_s.replace(".", ","),
_to2_s.replace(".", ","),
re.sub(r"([\d:]+\.)(\d+)", r"<font size=30>\1</font><font size=20>\2</font>", _ss1_s),
re.sub(r"([\d:]+\.)(\d+)", r"<font size=30>\1</font><font size=20>\2</font>", _to1_s))
with io.open(tmpoutsrt, "w") as fosrt:
fosrt.write(srttxt)
#
cmdl = _build_ffmpeg_cmdline(args, _ss, _t, scale, volume, tmpout)
kwam, kwap = {}, {}
if "ffmpeg_stdout" not in args.verbose:
kwam["stdout"] = subprocess.DEVNULL
if "ffmpeg_stderr" not in args.verbose:
kwam["stderr"] = subprocess.DEVNULL
if "player_stdout" not in args.verbose:
kwap["stdout"] = subprocess.DEVNULL
if "player_stderr" not in args.verbose:
kwap["stderr"] = subprocess.DEVNULL
#_log.debug("%r", cmdl)
cmdl = list(map(_encode, filter(None, cmdl)))
cb('"{}" -ss "{}" -i "{}" -ss 0 -t {} ...'.format(
cmdl[0], _ts_to_tss(_ss),
_abbreviate(os.path.basename(args.video), left=12, right=16),
_t))
subprocess.check_call(cmdl, **kwam)
cb("")
#
cmdl = json.loads(player_command)
cmdl.append(tmpout)
cb('"{}" "{}"'.format(cmdl[0], tmpout))
subprocess.check_call(list(map(_encode, cmdl)), **kwap)
cb("")
def _create_thumbnailimage(args, ss, ressize=(320, 180)):
thmsize = args.guimain_thumbnail_size
thmlsize = args.guimain_thumbnail_size_large
ffmscale = "{}:{}".format(*thmlsize)
#_log.info("%s, %s, %s, %s", args.video, ss, args.duration, ss >= args.duration)
if ss >= args.duration:
_log.warning("start time %s is out of range. (must be < %s)", _ts_to_tss(ss), _ts_to_tss(args.duration))
im_s = ImageTk.PhotoImage(Image.new("RGB", thmsize))
im_l = ImageTk.PhotoImage(Image.new("RGB", thmlsize))
return im_s, im_l
#
tmpout = os.path.join(
tempfile.gettempdir(),
re.sub(r"[\s:/\\.]", r"_", "{},{},{},{},{},{}".format(
args.video,
ss,
args.no_input_vstream,
args.no_input_astream,
args.audio_visualize_if_no_input_vstream,
args.audio_visualize_duration_if_no_input_vstream)) + ".png")
#
cmdl = _build_ffmpeg_cmdline(args, ss, None, ffmscale, 1.0, tmpout)
if not os.path.exists(tmpout):
atexit.register(_remove_silently, tmpout)
kwam = {}
if "ffmpeg_stdout" not in args.verbose:
kwam["stdout"] = subprocess.DEVNULL
if "ffmpeg_stderr" not in args.verbose:
kwam["stderr"] = subprocess.DEVNULL
subprocess.check_call(list(map(_encode, filter(None, cmdl))), **kwam)
#
image = Image.open(tmpout)
image_s = image.resize(ressize)
img = ImageTk.PhotoImage(image_s)
img_l = ImageTk.PhotoImage(image)
return img, img_l
try:
import Tkinter as tkinter # python 2.x, but sorry, I'm not testing it.
import ttk
from Tkinter import E, W, N, S # for sticky
def _askopenfilename(master, *args, **kwargs):
import tkFileDialog
return tkFileDialog.askopenfilename(*args, **kwargs)
from tkSimpleDialog import Dialog
from ScrolledText import ScrolledText
except ImportError:
import tkinter
from tkinter import ttk
from tkinter import E, W, N, S # for sticky
def _askopenfilename(master, *args, **kwargs):
from tkinter.filedialog import askopenfilenames
return askopenfilenames(*args, **kwargs)
from tkinter.simpledialog import Dialog
from tkinter.scrolledtext import ScrolledText
try:
import idlelib # for ToolTip, etc.
try:
from idlelib.ToolTip import ToolTip as Hovertip
except ImportError:
from idlelib.tooltip import Hovertip
except ImportError:
def Hovertip(*args): pass
class CmdwrapperForDynmenu(object):
"""
This is a workaround for 'No more menus can be allocated.'
when we are using tkinter.Menu dynamically.
"""
def __init__(self, menu, cmd):
self._menu = menu
self._cmd = cmd
def __call__(self, *args, **kwargs):
try:
self._cmd(args, kwargs)
finally:
#self._menu.delete(0, "end")
self._menu.destroy()
class AskOpenFilenameDialog(Dialog):
"""
A pair of a text entry for path and a browse button.
"""
def __init__(self,
parent, title,
entry_width,
initialpath,
*args, **kwargs):
self._parent = parent
self._entry_width = entry_width
self._result = self._initialpath = initialpath
Dialog.__init__(self, parent, title)
def body(self, master):
self._pathentvar = tkinter.StringVar()
self._pathentvar.set(self._initialpath)
pathent = tkinter.Entry(
master, textvariable=self._pathentvar, width=self._entry_width)
pathent.pack(side=tkinter.LEFT)
_HISTMNG.register_control("openvideo", pathent)
browsebtn = tkinter.Button(master, text="...", command=self._browse)
browsebtn.pack()
return pathent
def _browse(self, *args):
initial = self._pathentvar.get()
initialdir = None
if initial:
if os.path.isdir(initial):
initialdir = os.path.abspath(initial)
else:
initialdir = os.path.abspath(os.path.dirname(initial))
f = _askopenfilename(self._parent, multiple=0, initialdir=initialdir)
if f:
self._pathentvar.set(f[0])
def apply(self):
self._result = self._pathentvar.get()
@property
def path(self):
return self._result
def _my_askopenfilename(master, inidir=""):
dlg = AskOpenFilenameDialog(
master, "select video", 120, inidir)
return dlg.path
def _setup_listboxframe(master, *args, **kwargs):
"""warn: this function is not for general purposes but only for my script."""
fr = tkinter.Frame(master)
lb = tkinter.Listbox(fr, *args, **kwargs)
#
if kwargs.get("selectmode") == tkinter.MULTIPLE:
# we want get features of both MULTIPLE and EXTENDED...
lb.configure(selectmode=tkinter.MULTIPLE)
class _ExtImpl(object):
def __init__(self, lb_):
self._lb = lb_
self._lb.bind("<<ListboxSelect>>", self._track_lastselection, add=1)
self._lastsels = self._lb.curselection()
self._lastsel = None
def _track_lastselection(self, *args):
newsels = self._lb.curselection()
es = list(set(self._lastsels) ^ set(newsels))
if es:
self._lastsel = es[0]
else:
self._lastsel = None
self._lastsels = newsels
def extend_selection(self, sign, *args):
if self._lastsel is None:
return
self._lastsel = min(max(0, self._lastsel + sign), self._lb.size())
self._lb.activate(self._lastsel)
self._lb.selection_set(self._lastsel)
eimpl = _ExtImpl(lb)
lb.bind("<Shift-Up>", functools.partial(eimpl.extend_selection, -1))
lb.bind("<Shift-Down>", functools.partial(eimpl.extend_selection, 1))
# exportselection=False to keep selections highlighted
lb.configure(exportselection=False)
#
lb.pack(side=tkinter.LEFT, fill=tkinter.Y)
sc = tkinter.Scrollbar(fr)
sc.pack(side=tkinter.RIGHT, fill=tkinter.Y)
lb.config(yscrollcommand=sc.set)
sc.config(command=lb.yview, takefocus=1)
sc.bind("<Up>", lambda *args: lb.yview_scroll(-1, "unit"))
sc.bind("<Prior>", lambda *args: lb.yview_scroll(-1, "page"))
sc.bind("<Down>", lambda *args: lb.yview_scroll(1, "unit"))
sc.bind("<Next>", lambda *args: lb.yview_scroll(1, "page"))
return fr, sc, lb
class _MyEntry(tkinter.Entry):
def __init__(
self,
root,
validcharsfun,
clipboard_filter_fun,
*args, **kwargs):
#
tkinter.Entry.__init__(self, root, *args, **kwargs)
self._last_valid = ""
#
def _validator(*args):
P, d, s = args
if d == "0": # delete
if validcharsfun(s):
self._last_valid = s
elif d == "1": # insert
if validcharsfun(P):
self._last_valid = P
else:
class _revert():
__name__ = "_MyEntry.__init__._validator._revert"
def __init__(self, e, rv):
self._e = e
self._rv = rv
def __call__(self):
self._e.delete(0, "end")
self._e.insert(0, self._rv)
self.after(10, _revert(self, self._last_valid))
return True
#
# %d = Type of action (1=insert, 0=delete, -1 for others)
# %i = index of char string to be inserted/deleted, or -1
# %P = value of the entry if the edit is allowed
# %s = value of entry prior to editing
# %S = the text string being inserted or deleted, if any
# %v = the type of validation that is currently set
# %V = the type of validation that triggered the callback
# (key, focusin, focusout, forced)
# %W = the tk name of the widget
self.config(validate="key")
self.config(validatecommand=(root.register(_validator), '%P', '%d', '%s'))
def _clipboard_filter(*args):
nc = clipboard_filter_fun(self.clipboard_get())
self.clipboard_clear()
self.clipboard_append(nc)
self.bind("<Control-v>", _clipboard_filter)
class _MyNumEntry(_MyEntry):
def __init__(
self, root,
validcharsfun,
minval,
convfuns,
incrvals, # (Up, Control-Up, Shift-Up, Prior)
*args, **kwargs):
_MyEntry.__init__(
self, root,
validcharsfun,
lambda txt: re.sub(r"[^0-9.:,eE+-]", "", txt), # for float, time.
*args, **kwargs)
self._minval = minval
self._fromstrfun, self._tostrfun = convfuns
self._incrvals = incrvals
self.bind("<Up>", lambda *args: self._up(0, *args))
self.bind("<Control-Up>", lambda *args: self._up(1, *args))
self.bind("<Shift-Up>", lambda *args: self._up(2, *args))
self.bind("<Prior>", lambda *args: self._up(3, *args))
self.bind("<Down>", lambda *args: self._dn(0, *args))
self.bind("<Control-Down>", lambda *args: self._dn(1, *args))
self.bind("<Shift-Down>", lambda *args: self._dn(2, *args))
self.bind("<Next>", lambda *args: self._dn(3, *args))
def _up(self, m, *args):
delta = self._incrvals[m]
try:
cur = self.get()
rndi = len(("%g" % delta).partition(".")[-1])
if rndi:
cur = round(self._fromstrfun(cur), rndi)
else:
# FIXME: we should take whether if you want to round or not explicitly.
cur = self._fromstrfun(cur)
cur += delta
cur = max(self._minval, cur)
# FIXME: idealy, i want to set via textvariable...
self.delete(0, "end")
self.insert(0, self._tostrfun(cur))
except ValueError as exc:
pass
def _dn(self, m, *args):
delta = self._incrvals[m]
try:
cur = self.get()
rndi = len(("%g" % delta).partition(".")[-1])
if rndi:
cur = round(self._fromstrfun(cur), rndi)
else:
# FIXME: we should take whether if you want to round or not explicitly.
cur = self._fromstrfun(cur)
cur -= delta
cur = max(self._minval, cur)
# FIXME: idealy, i want to set via textvariable...
self.delete(0, "end")
self.insert(0, self._tostrfun(cur))
except ValueError:
pass
class _MyTSEntry(_MyNumEntry):
@staticmethod
def _vch(s):
return (re.match(r"^[\d:.]+$", s) is not None and
not re.search(r"([:.]{2,}|[:.][:.])", s) and
not re.search(r":\d{3,}", s) and
not re.search(r":[6-9]\d", s) and
s.count(":") <= 2 and s.count(".") <= 1 and
not re.search(r"\..*:", s))
def __init__(self, root, *args, **kwargs):
_MyNumEntry.__init__(
self, root,
_MyTSEntry._vch,
0,
(parse_time, _ts_to_tss),
(0.1, 1, 0.01, 10),
*args, **kwargs)
self.config(justify="center")
class _MyTSSsToEntry(_MyTSEntry):
def __init__(
self, root,
history, # _last_previewed_hist
*args, **kwargs):
self._font = kwargs.get("font")
_MyTSEntry.__init__(
self, root, *args, **kwargs)
self._history = history
def _replacetxt(newtxt, *args):
self.delete(0, "end")
self.insert(0, newtxt)
def _complpop(*args):
menu = tkinter.Menu(self, tearoff=0, font=self._font)
for ss, t in reversed(self._history):
ss_s, to_s = _ts_to_tss(ss), _ts_to_tss(ss + t)
spc = " " * (len(ss_s))
menu.add_command(
label=ss_s + spc, command=CmdwrapperForDynmenu(
menu, functools.partial(_replacetxt, ss_s)))
menu.add_command(
label=spc + to_s, command=CmdwrapperForDynmenu(
menu, functools.partial(_replacetxt, to_s)))
ppos_x = self.winfo_rootx() + 10
ppos_y = self.winfo_rooty() - 20
try:
menu.tk_popup(ppos_x, ppos_y)
finally:
menu.grab_release()
self.bind("<Control-space>", _complpop, add=1)
class _MyFloatEntry(_MyNumEntry):
@staticmethod
def _vch(s):
# should we allow exponential form and negative values?
# i dont think so, in this application.
return (re.match(r"^[\d.]+$", s) is not None and
s.count(".") <= 1)
def __init__(
self,
root,
minval,
fmt,
incrvals,
*args, **kwargs):
_MyNumEntry.__init__(
self, root,
_MyFloatEntry._vch,
minval,
(float, fmt.format),
incrvals,
*args, **kwargs)
class _MyIntEntry(_MyNumEntry):
@staticmethod
def _vch(s):
return (re.match(r"^[\d]+$", s) is not None)
def __init__(
self,
root,
minval,
incrvals,
*args, **kwargs):
_MyNumEntry.__init__(
self, root,
_MyIntEntry._vch,
minval,
(int, "{:d}".format),
incrvals,
*args, **kwargs)
class _MyStringVar(tkinter.StringVar):
_WRITING_TIMEOUT = 500
def __init__(self, observer, paused_writing_callback, master=None, value=None, name=None):
tkinter.StringVar.__init__(self, master, value, name)
if not hasattr(self, "trace_add"):
def _trace(mode, callback):
mode = {"write": "w", "read": "r", "undefine": "u"}[mode]
self.trace(mode, callback)
self.trace_add = _trace
self.trace_add("write", functools.partial(self._track_writing, "trace"))
self._observer = observer # the target for invoking "after"
self._paused_writing_callback = paused_writing_callback
self._now_writing = None
def _track_writing(self, entrypoint, *args):
f = functools.partial(self._track_writing, "after")
setattr(f, "__name__", "_MyStringVar_track_writing_after")
if entrypoint == "trace": # now writing
if self._now_writing:
self._observer.after_cancel(self._now_writing)
self._now_writing = self._observer.after(self._WRITING_TIMEOUT, f)
else:
self._paused_writing_callback("paused writing")
self._now_writing = None
class HistCompletionPopupMng(object):
_MAXHIST = 15
def __init__(self, savename):
self._savepath = os.path.join(
os.environ.get("HOME", os.environ.get("USERPROFILE")), savename)
# key: ident, value: a list of history items
self.load_history()
# key: ident, value: instance of control
self._controls = {}
def _append_history(self, ident, *args):
v = self._controls[ident].get().strip()
if not v:
return
if ident not in self._histories:
self._histories[ident] = []
if v not in self._histories[ident]:
self._histories[ident].append(v)
while len(self._histories[ident]) > self._MAXHIST:
self._histories[ident].pop(0)
def _popup_history_selection(self, ident, *args):
entry = self._controls[ident]
hist = self._histories.get(ident, [])
if not hist:
return
def _replacetxt(ent, newtxt, *args):
ent.delete(0, "end")
ent.insert(0, newtxt)
menu = tkinter.Menu(entry, tearoff=0)
for item in hist:
menu.add_command(
label=item,
command=CmdwrapperForDynmenu(
menu, functools.partial(_replacetxt, entry, item)))
ppos_x = entry.winfo_rootx()
ppos_y = entry.winfo_rooty()
try:
menu.tk_popup(ppos_x, ppos_y)
finally:
menu.grab_release()
def register_control(
self,
ident,
entry # the instance of tkinter.Entry (etc.)
):
self._controls[ident] = entry
entry.bind(
"<FocusOut>", functools.partial(self._append_history, ident), add=1)
entry.bind(
"<Control-space>", functools.partial(self._popup_history_selection, ident), add=1)
def save_history(self):
json.dump(self._histories, io.open(self._savepath, "w", encoding="utf-8"))
def load_history(self):
# key: ident, value: a list of history items
self._histories = {}
if os.path.exists(self._savepath):
self._histories = json.load(io.open(self._savepath, encoding="utf-8"))
_HISTMNG = HistCompletionPopupMng(".{}.uihist".format(__MYNAME__))
class _FCPSettings(Dialog):
def __init__(self, parent, settings):
self._settings = settings
Dialog.__init__(self, parent, "settings")
def body(self, master):
font = self._settings.font_main
font_s = self._settings.font_small
# ##
gr1 = tkinter.LabelFrame(
master,
text="[app_defaults]",
font=font, padx=10, pady=5)
gr1.pack()
# ##
lbl_sc = tkinter.Label(gr1, text="scale:", font=font_s)
lbl_sc.grid(row=0, column=0, sticky=E)
self._pdsc = ttk.Combobox(gr1, width=12, font=font)
sccands = [
"640:-1", "-1:360", "-1:270", "-1:180", "-1:90",
"480:270", "640:360", "800:450", "960:540", "1280:720",
"1440:810", "1600:900", "1920:1080", "3840:2040"
]
if self._settings.scale not in sccands:
sccands.append(self._settings.scale)
self._pdsc["values"] = sccands
for i, c in enumerate(self._pdsc["values"]):
if c == self._settings.scale:
self._pdsc.current(i)
break
self._pdsc.grid(row=0, column=1, sticky=W)
lbl_vol = tkinter.Label(gr1, text="volume:", font=font_s)
lbl_vol.grid(row=1, column=0, sticky=E)
self._entvol = _MyFloatEntry(
gr1,
0, "{:.5f}", (0.01, 0.1, 0.001, 0.5),
width=10, font=font)
self._entvol_var = tkinter.StringVar()
self._entvol.config(textvariable=self._entvol_var)
self._entvol_var.set("%.5f" % float(self._settings.volume))
self._entvol.grid(row=1, column=1, sticky=W)
#
lbl_fba2vfilter = tkinter.Label(
gr1, text="audio visualizing filter\n(if media has no video stream):", font=font_s)
lbl_fba2vfilter.grid(row=2, column=0, sticky=E)
self._entfba2vfilter = tkinter.Entry(
gr1, width=120, font=font)
_HISTMNG.register_control("audio visualizing filter", self._entfba2vfilter)
self._entfba2vfilter_var = tkinter.StringVar()
self._entfba2vfilter.config(textvariable=self._entfba2vfilter_var)
self._entfba2vfilter_var.set(
self._settings.audio_visualize_if_no_input_vstream)
self._entfba2vfilter.grid(row=2, column=1, sticky=W)
##
lbl_fba2vfilterdur = tkinter.Label(
gr1, text="audio visualizing filter buffer (sec)\n(if media has no video stream):", font=font_s)
lbl_fba2vfilterdur.grid(row=3, column=0, sticky=E)
self._entfba2vfilterdur = _MyIntEntry(
gr1,
1,
(1, 2, 1, 5),
width=120, font=font)
self._entfba2vfilterdur_var = tkinter.StringVar()
self._entfba2vfilterdur.config(textvariable=self._entfba2vfilterdur_var)
self._entfba2vfilterdur_var.set(
self._settings.audio_visualize_duration_if_no_input_vstream)
self._entfba2vfilterdur.grid(row=3, column=1, sticky=W)
# ##
gr2 = tkinter.LabelFrame(
master,
text="[env]",
font=font, padx=10, pady=5)
gr2.pack(fill=tkinter.BOTH)
#
lbl_ffmpegexe = tkinter.Label(gr2, text="ffmpeg:", font=font_s)
lbl_ffmpegexe.grid(row=0, column=0, sticky=E)
self._entffmpegexe = tkinter.Entry(
gr2,
width=120, font=font)
self._entffmpegexe_var = tkinter.StringVar()
self._entffmpegexe.config(textvariable=self._entffmpegexe_var)
self._entffmpegexe_var.set(self._settings.ffmpeg)
self._entffmpegexe.grid(row=0, column=1, sticky=W)
btreffm = tkinter.Button(
gr2,
text="...",
command=lambda *arg: self._entffmpegexe_var.set(_my_askopenfilename(gr2)))
btreffm.grid(row=0, column=2, sticky=E)
# ---------
#
self._player_sel = tkinter.IntVar()
self._player_sel.set(self._settings.player)
def _playervalbrowse(var, *args):
n = _my_askopenfilename(gr2)
if n:
cmdl = json.loads(var.get().strip())
cmdl[0] = n
var.set(json.dumps(cmdl, ensure_ascii=False))
lbl_player = tkinter.Radiobutton(
gr2, text="player:", value=1, variable=self._player_sel, font=font_s)
lbl_player.grid(row=1, column=0, sticky=E)
self._entplayer = tkinter.Entry(
gr2,
width=120, font=font)
_HISTMNG.register_control("player", self._entplayer)
self._entplayer_var = tkinter.StringVar()
self._entplayer.config(textvariable=self._entplayer_var)
self._entplayer_var.set(self._settings.player_command)
self._entplayer.grid(row=1, column=1, sticky=W)
btrefpl = tkinter.Button(
gr2,
text="...",
command=functools.partial(_playervalbrowse, self._entplayer_var))
btrefpl.grid(row=1, column=2, sticky=E)
#
#
lbl_player2 = tkinter.Radiobutton(
gr2, text="alternative player:", value=2, variable=self._player_sel, font=font_s)
lbl_player2.grid(row=2, column=0, sticky=E)
self._entplayer2 = tkinter.Entry(
gr2,
width=120, font=font)
_HISTMNG.register_control("player2", self._entplayer2)
self._entplayer2_var = tkinter.StringVar()
self._entplayer2.config(textvariable=self._entplayer2_var)
self._entplayer2_var.set(self._settings.player2_command)
self._entplayer2.grid(row=2, column=1, sticky=W)
btrefpl2 = tkinter.Button(
gr2,
text="...",
command=functools.partial(_playervalbrowse, self._entplayer2_var))
btrefpl2.grid(row=2, column=2, sticky=E)
# ---------
#
#
return self._pdsc
def apply(self):
self._settings.scale = self._pdsc.get().strip()
self._settings.volume = float(self._entvol_var.get().strip())
self._settings.audio_visualize_if_no_input_vstream = self._entfba2vfilter_var.get().strip()
self._settings.audio_visualize_duration_if_no_input_vstream = int(self._entfba2vfilterdur_var.get().strip())
#
self._settings.ffmpeg = self._entffmpegexe_var.get().strip()
self._settings.player_command = self._entplayer_var.get().strip()
self._settings.player2_command = self._entplayer2_var.get().strip()
self._settings.player = self._player_sel.get()
class _FCPEditTemplateDialog(Dialog):
def __init__(self, parent, settings):
self._settings = settings
self._tdir = self._settings.logview_template_dir
Dialog.__init__(self, parent, "template")
def body(self, master):
# TODO: edittable "logview_template_dir"
font = self._settings.font_main
font_s = self._settings.font_small
frr = tkinter.Frame(master)
self._lbitemsvar = tkinter.StringVar()
lbfr, lbscr, self._lb = _setup_listboxframe(
frr,
listvariable=self._lbitemsvar,
width=34,
font=font,
exportselection=False, selectmode=tkinter.BROWSE)
lbfr.pack(side="top")
self._lb.bind("<<ListboxSelect>>", self._lbselchanged, add=1)
btdelete = tkinter.Button(frr, text="delete", font=font, command=self._delete)
btdelete.pack(side="left")
#
rfr = tkinter.Frame(master)
rtfr = tkinter.Frame(rfr)
tmplnamelbl = tkinter.Label(rtfr, text="name:", font=font_s)
tmplnamelbl.pack(side="left")
self._tmplnameentvar = tkinter.StringVar()
self._tmplnameentvar.set("default")
tmplnameent = tkinter.Entry(
rtfr, textvariable=self._tmplnameentvar, width=34, font=font)
tmplnameent.pack(side="left")
btsave = tkinter.Button(rtfr, text="save", font=font, command=self._save)
btsave.pack(side="left")
rtfr.pack(side="top", anchor="w")
#
self._entrestmpl = ScrolledText(rfr, width=120, height=20, font=font)
self._entrestmpl.insert(
"end", self._settings.logview_initial_template)
self._entrestmpl.pack(side="top", fill="both")
#
rfr.pack(side="left", fill="both")
frr.pack(side="left", anchor="n")
self._refresh()
return self._entrestmpl
def apply(self):
self._settings.logview_initial_template = self._entrestmpl.get("1.0", "end")
def _enum_templates(self, pat):
return (
os.path.basename(fn) for fn in glob(os.path.join(self._tdir, pat))
if os.path.isfile(fn))
def _lbselchanged(self, *args):
cs = self._lb.curselection()
if not cs:
return
item = eval(self._lbitemsvar.get())[cs[0]]
self._tmplnameentvar.set(item)
tmplfn = os.path.join(self._tdir, item)
with io.open(tmplfn, encoding="utf-8") as fi:
self._entrestmpl.delete("1.0", "end")
self._entrestmpl.insert("end", fi.read())
def _save(self, *args):
tmpl = self._entrestmpl.get("1.0", "end")
tfn = self._tmplnameentvar.get().strip()
tmplfn = os.path.join(self._tdir, tfn)
if not os.path.exists(self._tdir):
os.makedirs(self._tdir)
with io.open(tmplfn, "w", encoding="utf-8") as fo:
fo.write(tmpl)
self._refresh()
def _delete(self, *args):
cs = self._lb.curselection()
if not cs:
return
item = eval(self._lbitemsvar.get())[cs[0]]
tmplfn = os.path.join(self._tdir, item)
os.remove(tmplfn)
self._refresh()
def _refresh(self, *args):
cur = self._entrestmpl.get("1.0", "end")
tmpls = list(sorted(self._enum_templates("*")))
self._lbitemsvar.set(tmpls)
if cur in tmpls:
tmpls.index(cur)
self._lb.selection_set(cur)
class _MyStatusBar(tkinter.Frame):
def __init__(self, master):
tkinter.Frame.__init__(self, master)
self.variable = tkinter.StringVar()
self.label = tkinter.Label(
self, bd=1, relief=tkinter.SUNKEN, anchor=tkinter.W,
textvariable=self.variable,
font=('courier', 10, 'normal'))
self.variable.set("")
self.label.pack(fill=tkinter.X)
self.pack()
class _FCPMainGui(object):
def _update_mediainfo(self):
_update_argmediainfo(self._args)
t = "{}{}[DURATION: {}]".format(
self._args.video,
" " * 10,
_ts_to_tss(self._args.duration))
self._root.title(t)
def __init__(self, root, args, values):
font = args.font_main
font_s = args.font_small
self._args = args
self._root = root
# ----------------
self._thmupd_invo = None # for updating thumbnail
# ----------------
self._last_previewed_hist = [] # ss, t
# ----------------
self._update_mediainfo()
# ----------------
menubar = tkinter.Menu(self._root)
filemenu = tkinter.Menu(menubar, tearoff=0)
def _opennewmedia(*args):
nn = _my_askopenfilename(self._root, self._args.video)
if nn:
self._args.video = nn
self._update_mediainfo()
self._ss_changed()
self._to_changed()
filemenu.add_command(label="Open (C-o)", command=_opennewmedia)
self._no_input_vstream = tkinter.BooleanVar()
self._no_input_vstream.set(args.no_input_vstream)
self._no_input_astream = tkinter.BooleanVar()
self._no_input_astream.set(args.no_input_astream)
def _vn_convback(*args):
if not self._args.actually_hasvideo:
self._no_input_vstream.set(True) # ignore user request
else:
self._args.no_input_vstream = self._no_input_vstream.get()
self._ss_changed()
self._to_changed()
def _an_convback(*args):
if not self._args.actually_hasaudio:
self._no_input_astream.set(True) # ignore user request
else:
self._args.no_input_astream = self._no_input_astream.get()
self._ss_changed()
self._to_changed()
filemenu.add_checkbutton(
label="input has no video stream (C-n)",
variable=self._no_input_vstream, command=_vn_convback)
def _toggle_vn(*args):
c = self._no_input_vstream.get()
self._no_input_vstream.set(not c)
_vn_convback(*args)
self._root.bind("<Control-n>", _toggle_vn)
filemenu.add_checkbutton(
label="input has no audio stream",
variable=self._no_input_astream, command=_an_convback)
filemenu.add_command(label="Quit (C-q)", command=self._root.quit)
editmenu = tkinter.Menu(menubar, tearoff=0)
def _querysettings():
dlg = _FCPSettings(self._root, self._args)
self._ss_changed()
self._to_changed()
editmenu.add_command(label="Settings...", command=_querysettings)
menubar.add_cascade(label="File", menu=filemenu)
menubar.add_cascade(label="Edit", menu=editmenu)
self._root.config(menu=menubar)
# ----------------
#
# ##
#
gfr2 = tkinter.Frame(self._root)
grlb = tkinter.LabelFrame(
gfr2,
text="trimming ranges (&R)",
font=font, padx=2, pady=5)
#
class _LBPopupMenu(object):
def __init__(self, lb, menuitems, font):
self._lb = lb
self._menu = tkinter.Menu(lb, tearoff=0, font=font)
for lab, cmd in menuitems:
self._menu.add_command(label=lab, command=cmd)
lb.bind("<Button-3>", self._do_popup)
try:
lb.bind("<App>", self._do_popup)
except Exception:
pass # bad event type
def _do_popup(self, ev, *args):
sels = self._lb.curselection()
if not sels:
return
ccx, ccy, itw, ith = self._lb.bbox(sels[0])
ppos_x = self._lb.winfo_rootx() + ccx + int(itw / 5 * 2)
ppos_y = self._lb.winfo_rooty() + ccy - ith
try:
self._menu.tk_popup(ppos_x, ppos_y)
#self._menu.tk_popup(ev.x_root, ev.y_root)
finally:
self._menu.grab_release()
#
self._lbitems = [
self._sst2lbitem(_ss, _t) for _ss, _t in values]
self._lbitemsvar = tkinter.StringVar(value=self._lbitems)
lbfr, lbsc, self._lb = _setup_listboxframe(
grlb, width=30, height=18, font=font,
listvariable=self._lbitemsvar,
selectmode=tkinter.MULTIPLE,
# exportselection=False to keep selections highlighted
exportselection=False)
self._lbpopupmenu = _LBPopupMenu(
self._lb, [
("concatinate selected (&C)", self._concat_selected_ranges),
("delete selected (&D)", self._delete_selected_ranges),
], font=self._args.font_main)
self._lb.selection_set(0)
lbfr.pack()
#
grra = tkinter.Frame(gfr2)
#
grsted = tkinter.Frame(grra)
lbl_rass = tkinter.Label(grsted, text="start:", font=font)
lbl_rass.grid(row=0, column=0, sticky=E + W)
self._ent_rass = _MyTSSsToEntry(
grsted, self._last_previewed_hist, width=15, font=font)
self._ent_rass_var = tkinter.StringVar()
self._ent_rass.config(textvariable=self._ent_rass_var)
self._ent_rass.grid(row=0, column=1)
lbl_rato = tkinter.Label(grsted, text="stop:", font=font)
lbl_rato.grid(row=0, column=2, sticky=E + W)
self._ent_rato = _MyTSSsToEntry(
grsted, self._last_previewed_hist, width=15, font=font)
self._ent_rato_var = tkinter.StringVar()
self._ent_rato.config(textvariable=self._ent_rato_var)
self._ent_rato.grid(row=0, column=3)
grsted.grid(row=0, column=0, sticky=W)
#
grstep = tkinter.Frame(grra)
lbl_rasp = tkinter.Label(grstep, text=" -> split this range by step:", font=font)
lbl_rasp.grid(row=0, column=0, sticky=E + W)
self._ent_rasp = _MyFloatEntry(
grstep,
1./120, "{:.2f}", (0.1, 1, 0.01, 10),
width=8, font=font)
self._ent_rasp_var = tkinter.StringVar()
self._ent_rasp.config(textvariable=self._ent_rasp_var)
self._ent_rasp.grid(row=0, column=1)
#
grstepbtns = tkinter.Frame(grstep)
btrg = tkinter.Button(
grstepbtns, text="*",
command=self._refresh_rangeslist, font=font)
btad = tkinter.Button(
grstepbtns, text="+",
command=self._extend_rangeslist, font=font)
btrg.pack(side=tkinter.LEFT)
btad.pack()
grstepbtns.grid(row=0, column=2)
#
grstep.grid(row=0, column=1, sticky=E)
#
_ss, _to, _t = 0, args.duration, "2"
if args.from_slice:
_ss, _to, _t = _getslice_from_arg(args.from_slice, True)
elif args.from_splitpoints:
_aslist = list(_yr_r(_getrangelist_from_arg(args.from_splitpoints, True)))
if _aslist:
_ss, _ = _aslist[0]
_sslast, _tlast = _aslist[-1]
_to = _sslast + _tlast
self._ent_rass_var.set(_ts_to_tss(_ss))
self._ent_rato_var.set(_ts_to_tss(_to))
self._ent_rasp_var.set("%.2f" % float(_t))
#
grlb.grid(row=0, column=0, sticky=W + N, rowspan=2)
grra.grid(row=0, column=1, sticky=W + N)
#
grex = tkinter.LabelFrame(
gfr2,
font=font, padx=2, pady=5)
# ----------------------------------------------------------------
lbl_ss = tkinter.Label(grex, text="ss (&S):", font=font)
lbl_ss.grid(row=0, column=0, sticky=E + W)
self._ent_ss = _MyTSSsToEntry(
grex, self._last_previewed_hist, width=15, font=font)
self._ent_ss_var = _MyStringVar(grex, self._ss_changed)
self._ent_ss.config(textvariable=self._ent_ss_var)
self._ent_ss.grid(row=0, column=1)
# #
self._lbl_mp = tkinter.Entry(grex, font=font, state="readonly", justify="center", takefocus=0)
self._lbl_mp_var = tkinter.StringVar()
self._lbl_mp.config(textvariable=self._lbl_mp_var)
self._lbl_mp.grid(row=0, column=2, columnspan=2)
# #
lbl_to = tkinter.Label(grex, text=" to (&T):", font=font)
lbl_to.grid(row=0, column=4, sticky=E + W)
self._ent_to = _MyTSSsToEntry(
grex, self._last_previewed_hist, width=15, font=font)
self._ent_to_var = _MyStringVar(grex, self._to_changed)
self._ent_to.config(textvariable=self._ent_to_var)
self._ent_to.grid(row=0, column=5)
# ----------------------------------------------------------------
thmsize = self._args.guimain_thumbnail_size
thmlsize = self._args.guimain_thumbnail_size_large
thmsize_mp = self._args.guimain_thumbnail_size_small
# ===
# -ss img
self._thn_ss_img = ImageTk.PhotoImage(Image.new("RGB", thmsize))
self._thn_ss_img_l = ImageTk.PhotoImage(Image.new("RGB", thmlsize))
self._thn_ss = tkinter.Label(grex, image=self._thn_ss_img)
self._thn_ss.config(
borderwidth=4,
relief="ridge" # "flat", "raised", "sunken", "ridge", "solid", "groove"
)
self._ent_ss.bind("<FocusOut>", self._ss_changed)
#self._ent_ss.bind("<Control-u>", self._ss_changed)
self._thn_ss.grid(row=1, column=0, columnspan=2)
# ===
# midpoint between [ss, to]
self._thn_mp_img = ImageTk.PhotoImage(Image.new("RGB", thmsize_mp))
self._thn_mp_img_l = ImageTk.PhotoImage(Image.new("RGB", thmsize_mp))
self._thn_mp = tkinter.Label(grex, image=self._thn_mp_img)
self._thn_mp.config(
borderwidth=4,
relief="ridge" # "flat", "raised", "sunken", "ridge", "solid", "groove"
)
self._thn_mp.grid(row=1, column=2, columnspan=2)
# ===
# -to img
self._thn_to_img = ImageTk.PhotoImage(Image.new("RGB", thmsize))
self._thn_to_img_l = ImageTk.PhotoImage(Image.new("RGB", thmlsize))
self._thn_to = tkinter.Label(grex, image=self._thn_to_img)
self._thn_to.config(
borderwidth=4,
relief="ridge" # "flat", "raised", "sunken", "ridge", "solid", "groove"
)
self._ent_to.bind("<FocusOut>", self._to_changed)
#self._ent_to.bind("<Control-u>", self._to_changed)
self._thn_to.grid(row=1, column=4, columnspan=2)
# ----------------------------------------------------------------
#
#
paubtns = tkinter.Frame(grex)
btdo = tkinter.Button(paubtns, text=" P ",
command=self._execute_viewer, font=font)
btdo.pack(side=tkinter.LEFT)
Hovertip(btdo, "Preview (&P)")
#
btap = tkinter.Button(paubtns, text=" A ",
command=self._append_this_range, font=font)
btap.pack(side=tkinter.LEFT)
Hovertip(btap, "Append this range (&A)")
btup = tkinter.Button(paubtns, text=" U ",
command=self._update_with_this_range, font=font)
btup.pack(side=tkinter.LEFT)
Hovertip(btup, "Update with this range (&U)")
paubtns.grid(row=2, column=0, sticky=W)
grex.grid(row=1, column=1, sticky=E + W)
gfr2.pack()
# ----------------
self._logview_collapsed = True
def _toggle_logview(*args):
if self._logview_collapsed:
self._gfr4.pack(fill=tkinter.BOTH, expand=1)
else:
self._gfr4.forget()
self._logview_collapsed = not self._logview_collapsed
gfr3 = tkinter.Frame(self._root)
bttlv = tkinter.Button(
gfr3, text="... (&L)",
command=_toggle_logview, font=font)
bttlv.pack(fill=tkinter.BOTH)
gfr3.pack(fill=tkinter.BOTH, expand=1)
#####
self._gfr4 = tkinter.Frame(self._root)
btdorender = tkinter.Button(
self._gfr4, text="Print (&N)",
command=self._execute_render, font=font)
btdorender.pack(fill=tkinter.BOTH)
#
frentres = tkinter.Frame(self._gfr4)
#
def _tmpleditdialig(*args):
_FCPEditTemplateDialog(self._root, self._args)
bttmpledt = tkinter.Button(
frentres, text="...",
command=_tmpleditdialig, font=font_s)
bttmpledt.pack(side=tkinter.LEFT, anchor=tkinter.N)
#
self._entres = ScrolledText(frentres, width=150, height=12, font=font)
self._entres.pack(side=tkinter.LEFT, fill=tkinter.BOTH)
frentres.pack(fill=tkinter.BOTH)
#
self._gfr4.pack(fill=tkinter.BOTH, expand=1)
self._gfr4.forget()
###
self._root.bind("<Alt-l>", _toggle_logview)
#####
# ----------------
self._statusbar = _MyStatusBar(self._root)
self._statusbar.pack(fill=tkinter.BOTH)
# ----------------
#
if self._args.guimain_exec_on_init:
self._execute_viewer()
#
self._root.bind("<Control-q>", lambda *args: self._root.destroy())
self._root.bind("<Control-o>", _opennewmedia)
self._root.bind("<Alt-r>", lambda *args: self._lb.focus_set())
self._root.bind("<Alt-s>", lambda *args: self._ent_ss.focus_set())
self._root.bind("<Alt-t>", lambda *args: self._ent_to.focus_set())
#
self._root.bind("<Alt-a>", self._append_this_range)
self._root.bind("<Alt-u>", self._update_with_this_range)
self._root.bind("<Alt-d>", self._delete_selected_ranges)
self._root.bind("<Alt-c>", self._concat_selected_ranges)
self._root.bind("<Alt-p>", self._execute_viewer)
self._root.bind("<Alt-n>", self._execute_render)
self._lb.bind("<<ListboxSelect>>", self._lbselchanged, add=1)
# ---
class _ImgShowPopup(Dialog):
def __init__(self,
parent, args,
whrange, iniimg, t, step, tickpos,
on_apply,
preview_command, inherit_commands):
self._args = args
self._img = iniimg
self._t = t
self._ss, self._to = whrange
#
self._step = max(1/60., step)
#
self._on_apply = on_apply
self._preview_command = preview_command
self._inherit_commands = inherit_commands
self._tickpos = tickpos
title = "{} -> {}".format(_ts_to_tss(self._ss), _ts_to_tss(self._to))
#
Dialog.__init__(self, parent, title)
def _build_mini_preview_ranges(self):
_fps = 30.0 # NOTE: is not actual framerate
#facs = (1, 2, 5, 10, 15, 20, 25, 30, 37.5, 45, 60, 75, 90, 120)
facs = (1, 2, 5, 10, 15, 20, 25, 30, 37.5, 45, 60, 75)
#facs = (1, 2, 5, 10, 15, 20, 25, 30, 37.5, 45, 60)
def get0_(f): return 1 / _fps * f
def get1_(f): return self._t + get0_(f)
tlst = []
tlst.extend([(get1_(-f), "{:+.3f}".format(get0_(-f))) for f in facs])
tlst.append((get1_(0), 'here'))
tlst.extend([(get1_(f), "{:+.3f}".format(get0_(f))) for f in facs])
tlst.append((self._ss, '"-ss"'))
tlst.append((self._to, '"-to"'))
tlst.append((self._t - self._step, 'prev'))
tlst.append((self._t + self._step, 'next'))
rngs = list(itertools.combinations(tlst, 2))
return list(sorted([
((p[0] if p[0][0] < p[1][0] else p[1]), (p[1] if p[0][0] < p[1][0] else p[0]))
for p in rngs]))
def _updtstxt(self):
#
whr = self._to - self._ss
prgr = 0.0
if whr:
prgr = (self._t - self._ss) / whr * 100.
prgr = "{:+5.1f}%".format(prgr)
#
t1 = '{}'.format(_ts_to_tss(self._t))
t2 = '(ss{:+.3f}, to{:+.3f}, {}) '.format(
self._t - self._ss, self._t - self._to, prgr)
self._tstxt1.config(text=t1)
self._tstxt2.config(text=re.sub(r"([+-])", r" \1 ", t2))
fgc = "black"
relief = "flat"
if self._t < self._ss or self._to < self._t:
fgc = "red"
elif math.isclose(self._t, self._ss) or math.isclose(self._to, self._t):
relief = "groove"
self._tstxt1.config(foreground=fgc, relief=relief, borderwidth=2)
def _do_previewpopup(self, *args):
rngs = self._build_mini_preview_ranges()
menu = tkinter.Menu(self._btprv, tearoff=0, font=self._args.font_main)
#
if not hasattr(self, "_lastselpreview"):
self._lastselpreview = (None, None)
def _menu_previewcmd(ss, to, *args):
self._lastselpreview = (ss, to)
cmd = CmdwrapperForDynmenu(
menu, functools.partial(self._do_preview, ss, to))
cmd(*args)
#
submenus = [
tkinter.Menu(menu, tearoff=0, font=self._args.font_main), # for "from negative"
tkinter.Menu(menu, tearoff=0, font=self._args.font_main), # for "from positive"
]
menu.add_cascade(label="- ~ ", menu=submenus[0])
for ssf, items in itertools.groupby(rngs, lambda it: it[0]):
if "here" not in ssf[1]:
submenu = submenus[0] if ssf[0] < self._t else submenus[1]
else:
submenu = menu
subsubmenu = tkinter.Menu(submenu, tearoff=0, font=self._args.font_main)
sepadded = False
for i, item in enumerate(items):
if not sepadded and item[1][1][0] not in "+-":
subsubmenu.add_separator()
ss_, to_ = ssf[0], item[1][0]
fg = "red" if str(self._lastselpreview) == str((ss_, to_)) else "black"
subsubmenu.add_command(
label=" ~ " + item[1][1],
foreground=fg,
command=functools.partial(_menu_previewcmd, ss_, to_))
if item[1][1][0] not in "+-":
subsubmenu.add_separator()
sepadded = True
else:
sepadded = False
submenu.add_cascade(label=ssf[1] + " ~ ", menu=subsubmenu)
menu.add_cascade(label="+ ~ ", menu=submenus[1])
#
ppos_x = self._btprv.winfo_rootx() + 10
ppos_y = self._btprv.winfo_rooty() + 20
try:
menu.tk_popup(ppos_x, ppos_y)
finally:
menu.grab_release()
def _nav(self, sign, scale, *args):
self._t += sign * self._step * scale
self._updtstxt()
_, self._img = _create_thumbnailimage(
self._args, self._t, ressize=self._args.guimain_thumbnail_size)
self._im.config(image=self._img)
def _do_preview(self, prvss, prvto, *args):
self._lastpreviewed_ss.set(_ts_to_tss(prvss))
self._lastpreviewed_to.set(_ts_to_tss(prvto))
self._preview_command(prvss, prvto - prvss)
def body(self, master):
frt = tkinter.Frame(master)
frt.pack()
#
frt1 = tkinter.Frame(frt)
frt2 = tkinter.Frame(frt)
frt1.grid(row=0, column=1)
tebt = [" . ", " x "]
btvfrt1 = tkinter.Button(frt, text=tebt[0])
Hovertip(btvfrt1, 'toggle view/hide "nav step" control.')
btvfrt1.grid(row=0, column=2)
frt2.grid(row=0, column=0)
frt1tv = tkinter.Frame(frt1)
frt1tv.pack(expand=1)
def _toggleviewfrt1(*args):
curr = btvfrt1.cget("text")
if curr == tebt[0]:
frt1tv.pack(expand=1)
else:
frt1tv.forget()
btvfrt1.config(text=tebt[1] if curr == tebt[0] else tebt[0])
btvfrt1.config(command=_toggleviewfrt1)
btvfrt1.after(100, _toggleviewfrt1)
btvfrt1.after(200, _toggleviewfrt1)
#
steplb1 = tkinter.Label(
frt1tv, text="nav step:", font=self._args.font_small)
steplb1.grid(row=2, column=0, sticky=E)
self._entstep_var = tkinter.StringVar()
self._entstep = _MyFloatEntry(
frt1tv,
1/60., "{:.3f}", (0.1, 1, 0.01, 5),
width=10, font=self._args.font_main, textvariable=self._entstep_var)
self._entstep_var.set("{:.3f}".format(self._step))
self._entstep.grid(row=2, column=1)
def _updstep(*args):
self._step = float(self._entstep_var.get())
self._navlbtnlbl.set("{:+.3f}".format(-self._step))
self._navrbtnlbl.set("{:+.3f}".format(self._step))
self._entstep.bind("<FocusOut>", _updstep)
#
tslbl = tkinter.Label(
frt2, text="timestamp of this\nsnapshot image:", font=self._args.font_small)
tslbl.grid(row=0, column=0, sticky=E)
frt2tc = tkinter.Frame(frt2)
self._tstxt1 = tkinter.Label(
frt2tc,
font=self._args.font_main)
self._tstxt1.pack(side="left")
frt2tc.grid(row=0, column=1, sticky=W)
self._tstxt2 = tkinter.Label(
frt2tc,
font=self._args.font_small)
self._tstxt2.pack(side="left")
tkinter.Label(frt2tc, text=" "*3).pack(side="left")
frt2tc.grid(row=0, column=1, sticky=W)
self._updtstxt()
#
# tracking user's last preview operation
lstplb1 = tkinter.Label(
frt2, text="last\npreviewed:", font=self._args.font_small)
lstplb1.grid(row=0, column=2, sticky=E)
frt2pc = tkinter.Frame(frt2)
frt2pc.grid(row=0, column=3, sticky=W)
self._lastpreviewed_ss = tkinter.StringVar()
lstplbss = tkinter.Entry(
frt2pc, textvariable=self._lastpreviewed_ss,
width=15, state="readonly", justify="center", takefocus=0,
font=self._args.font_main)
lstplbss.pack(side="left")
lstplb2 = tkinter.Label(
frt2pc, text="->", font=self._args.font_small)
lstplb2.pack(side="left", padx=2)
self._lastpreviewed_to = tkinter.StringVar()
lstplbto = tkinter.Entry(
frt2pc, textvariable=self._lastpreviewed_to,
width=15, state="readonly", justify="center", takefocus=0,
font=self._args.font_main)
lstplbto.pack(side="left")
tkinter.Label(frt2pc, text=" "*3).pack(side="left")
#
frb = tkinter.Frame(master)
frb.pack()
fri = tkinter.Frame(frb)
#
self._navlbtnlbl = tkinter.StringVar()
self._navrbtnlbl = tkinter.StringVar()
self._navlbtnlbl.set("{:+.3f}".format(-self._step))
self._navrbtnlbl.set("{:+.3f}".format(self._step))
btp = tkinter.Button(
fri, textvariable=self._navlbtnlbl,
command=functools.partial(self._nav, -1, 1),
font=self._args.font_main)
btp.grid(row=0, column=0, sticky=N + S)
self._im = tkinter.Label(fri, image=self._img, relief="ridge")
self._im.grid(row=0, column=1)
btn = tkinter.Button(
fri, textvariable=self._navrbtnlbl,
command=functools.partial(self._nav, 1, 1),
font=self._args.font_main)
btn.grid(row=0, column=2, sticky=N + S)
fri.grid(row=2, column=0)
#
self._btprv = tkinter.Button(
fri, text="preview",
command=self._do_previewpopup,
font=self._args.font_main)
self._btprv.grid(row=0, column=3, sticky=N + S)
#
self._cmb_onappliedact = ttk.Combobox(
master, width=70, state="readonly",
font=self._args.font_main)
self._cmb_onappliedact["values"] = [
'update "-ss" with this timestamp',
'update "-to" with this timestamp',
'update "-ss" with this previewed "-ss"',
'update "-to" with this previewed "-to"',
'update both "-ss" and "-to" with this previewed "-ss" and "-to"',
'update "-ss" with this previewed "-to"',
'update "-to" with this previewed "-ss"',
]
self._cmb_onappliedact.current(0 if self._tickpos < 10 else 1)
self._cmb_onappliedact.pack(fill=tkinter.X)
#
def _upd(*args):
threading.Thread(target=self._nav, args=(0, 1)).start()
for w in (btp, btn, self._btprv, self._cmb_onappliedact):
w.bind("<Left>", functools.partial(self._nav, -1, 1))
w.bind("<less>", functools.partial(self._nav, -1, 1))
w.bind("<greater>", functools.partial(self._nav, 1, 1))
w.bind("<Right>", functools.partial(self._nav, 1, 1))
#
w.bind("<Control-Left>", functools.partial(self._nav, -1, 2))
w.bind("<Control-less>", functools.partial(self._nav, -1, 2))
w.bind("<Control-greater>", functools.partial(self._nav, 1, 2))
w.bind("<Control-Right>", functools.partial(self._nav, 1, 2))
#
w.bind("<Shift-Left>", functools.partial(self._nav, -1, 1./2))
w.bind("<Shift-less>", functools.partial(self._nav, -1, 1./2))
w.bind("<Shift-greater>", functools.partial(self._nav, 1, 1./2))
w.bind("<Shift-Right>", functools.partial(self._nav, 1, 1./2))
#
for desc, ksym, cmd in self._inherit_commands:
w.bind(ksym, lambda *args: (cmd(args), _upd(args)))
w.bind("<Alt-p>", self._do_previewpopup)
w.bind("<Button-3>", self._do_previewpopup)
w.bind("<App>", self._do_previewpopup)
w.bind("<Alt-s>", lambda *args: rbss.focus_set())
w.bind("<Alt-t>", lambda *args: rbto.focus_set())
#
return btp
def apply(self):
updmodesel = self._cmb_onappliedact.get()
updmode = self._cmb_onappliedact["values"].index(updmodesel)
prss = self._lastpreviewed_ss.get()
prto = self._lastpreviewed_to.get()
if prss:
prss, prto = parse_time(prss), parse_time(prto)
elif updmode >= 2:
return
self._on_apply(updmode, self._t, prss, prto)
#
def _thumbpop(tick, div, *args): # tick=0, 1, ..div
im, sst = None, 0
(ss, to), _ = self._get_cur_ssto()
step = (to - ss) / div
if tick == 0:
im, sst = self._thn_ss_img_l, ss
elif tick == div:
im, sst = self._thn_to_img_l, to
else:
sst = ss + step * tick
_, im = _create_thumbnailimage(
self._args, sst, ressize=self._args.guimain_thumbnail_size)
def _on_apply(ss_or_to, t, prss, prto):
if ss_or_to == 0:
self._ent_ss_var.set(_ts_to_tss(t))
self._ss_changed()
elif ss_or_to == 1:
self._ent_to_var.set(_ts_to_tss(t))
self._to_changed()
elif ss_or_to == 2:
self._ent_ss_var.set(_ts_to_tss(prss))
self._ss_changed()
elif ss_or_to == 3:
self._ent_to_var.set(_ts_to_tss(prto))
self._to_changed()
elif ss_or_to == 4:
self._ent_ss_var.set(_ts_to_tss(prss))
self._ss_changed()
self._ent_to_var.set(_ts_to_tss(prto))
self._to_changed()
elif ss_or_to == 5:
self._ent_ss_var.set(_ts_to_tss(prto))
self._ss_changed()
elif ss_or_to == 6:
self._ent_to_var.set(_ts_to_tss(prss))
self._to_changed()
_ImgShowPopup(
self._root, self._args, (ss, to),
im, sst, step, tick, _on_apply,
lambda ss_, t_: self._execute_viewer_0(ss_, t_),
[("input has no video stream (C-n)", "<Control-n>", _toggle_vn)])
#
def _do_thumbpop_selectionmenu(inidiv, *args):
ev = args[0]
menu = tkinter.Menu(self._root, tearoff=0, font=self._args.font_main)
#
if not hasattr(self, "_lastselpreview"):
self._lastselpreview = (None, None)
def _menu_previewcmd(ss, t, *args):
self._lastselpreview = (ss, t)
cmd = CmdwrapperForDynmenu(
menu, functools.partial(self._execute_viewer_0, ss, t))
cmd(*args)
#
minstep = 1/60.
(ss, to), _ = self._get_cur_ssto()
div = inidiv
while (to - ss) / div < minstep and 2 < div:
div = int(div / 2)
if (to - ss) / div < minstep:
ticks = [0]
else:
ticks = list(range(-5, (inidiv + 5), int(inidiv / div)))
for i in ticks:
submenu = tkinter.Menu(menu, tearoff=0, font=self._args.font_main)
if i == 0:
menu.add_separator()
menu.add_cascade(label="@{:2d}/{:2d}".format(i, div), menu=submenu)
if i == div:
menu.add_separator()
submenu.add_command(
label="snapshot image",
command=CmdwrapperForDynmenu(
menu, functools.partial(_thumbpop, i, div)))
#
subsubmenu = tkinter.Menu(submenu, tearoff=0, font=self._args.font_main)
submenu.add_cascade(label="mini preview", menu=subsubmenu)
step = (to - ss) / div
mp_ss = ss + i * step
for j in range(i + 1, len(ticks) + 1, 1):
dur = step * (j - i)
if j == 0:
subsubmenu.add_separator()
fg = "red" if str(self._lastselpreview) == str((mp_ss, dur)) else "black"
subsubmenu.add_command(
label="~ @{:2d}/{:2d} [{:.3f} secs]".format(j, div, dur),
foreground=fg,
command=functools.partial(_menu_previewcmd, mp_ss, dur))
if j == div:
subsubmenu.add_separator()
subsubmenu.add_separator()
subsubsubmenus = [
tkinter.Menu(subsubmenu, tearoff=0, font=self._args.font_main),
tkinter.Menu(subsubmenu, tearoff=0, font=self._args.font_main),
tkinter.Menu(subsubmenu, tearoff=0, font=self._args.font_main),
]
subsubmenu.add_cascade(label="- > 0", menu=subsubsubmenus[0])
subsubmenu.add_cascade(label="- > +", menu=subsubsubmenus[1])
subsubmenu.add_cascade(label="0 > +", menu=subsubsubmenus[2])
for f in (1, 2, 5, 10, 15, 20, 25, 30, 37.5, 45, 60, 75):
s = 1/30.*f
for sssmi, (d1, d2) in enumerate((
(-s, 0),
(-s, s),
(0, s),
)):
ss_, t_ = mp_ss + d1, (d2 - d1)
fg = "red" if str(self._lastselpreview) == str((ss_, t_)) else "black"
subsubsubmenus[sssmi].add_command(
label="{:+.3f} ~ {:+.3f}".format(d1, d2),
foreground=fg,
command=functools.partial(_menu_previewcmd, ss_, t_))
try:
menu.tk_popup(ev.x_root + 20, ev.y_root)
finally:
menu.grab_release()
#
self._thn_ss.bind("<Button-1>", functools.partial(_thumbpop, 0, 10))
self._thn_mp.bind("<Button-1>", functools.partial(_thumbpop, 5, 10))
self._thn_to.bind("<Button-1>", functools.partial(_thumbpop, 10, 10))
self._root.bind("<Control-0>", functools.partial(_do_thumbpop_selectionmenu, 10))
self._root.bind("<Control-colon>", functools.partial(_do_thumbpop_selectionmenu, 10))
self._root.bind("<Control-asterisk>", functools.partial(_do_thumbpop_selectionmenu, 20))
self._root.bind("<Control-semicolon>", functools.partial(_do_thumbpop_selectionmenu, 30))
self._root.bind("<Control-plus>", functools.partial(_do_thumbpop_selectionmenu, 40))
self._thn_ss.bind("<Button-3>", functools.partial(_do_thumbpop_selectionmenu, 10))
self._thn_mp.bind("<Button-3>", functools.partial(_do_thumbpop_selectionmenu, 10))
self._thn_to.bind("<Button-3>", functools.partial(_do_thumbpop_selectionmenu, 10))
self._thn_ss.bind("<Shift-Button-3>", functools.partial(_do_thumbpop_selectionmenu, 20))
self._thn_mp.bind("<Shift-Button-3>", functools.partial(_do_thumbpop_selectionmenu, 20))
self._thn_to.bind("<Shift-Button-3>", functools.partial(_do_thumbpop_selectionmenu, 20))
# ---
self._lbselchanged()
def _get_cur_ss(self):
ss, ss_str = None, None
try:
ss_str = self._ent_ss_var.get().strip()
ss = parse_time(ss_str)
except ValueError:
pass
return ss, ss_str
def _get_cur_to(self):
to, to_str = None, None
try:
to_str = self._ent_to_var.get().strip()
to = parse_time(to_str)
except ValueError:
pass
return to, to_str
def _get_cur_ssto(self):
try:
ss_str = self._ent_ss_var.get().strip()
ss = parse_time(ss_str)
to_str = self._ent_to_var.get().strip()
to = parse_time(to_str)
return (ss, to), (ss_str, to_str)
except ValueError:
pass
def _ss_changed(self, *args):
cur = self._get_cur_ssto()
if not cur:
return
(ss, to), (ss_str, to_str) = cur
self._statusbar.variable.set(
'"{}" -> "{}" ("{}" sec)'.format(
ss_str, to_str, _ts_to_tss(to - ss)))
self._updatethumb_ss(ss)
def _to_changed(self, *args):
cur = self._get_cur_ssto()
if not cur:
return
(ss, to), (ss_str, to_str) = cur
self._statusbar.variable.set(
'"{}" -> "{}" ("{}" sec)'.format(
ss_str, to_str, _ts_to_tss(to - ss)))
self._updatethumb_to(to)
def _updatethumb_ss(self, t):
self._thn_ss_img, self._thn_ss_img_l = _create_thumbnailimage(
self._args, t, ressize=self._args.guimain_thumbnail_size)
self._thn_ss.config(image=self._thn_ss_img)
#
to, _ = self._get_cur_to()
mp = t + (to - t) / 2.0
self._lbl_mp_var.set(_ts_to_tss(mp))
self._thn_mp_img, self._thn_mp_img_l = _create_thumbnailimage(
self._args, mp, ressize=self._args.guimain_thumbnail_size_small)
self._thn_mp.config(image=self._thn_mp_img)
def _updatethumb_to(self, t):
self._thn_to_img, self._thn_to_img_l = _create_thumbnailimage(
self._args, t, ressize=self._args.guimain_thumbnail_size)
self._thn_to.config(image=self._thn_to_img)
#
ss, _ = self._get_cur_ss()
mp = ss + (t - ss) / 2.0
self._lbl_mp_var.set(_ts_to_tss(mp))
self._thn_mp_img, self._thn_mp_img_l = _create_thumbnailimage(
self._args, mp, ressize=self._args.guimain_thumbnail_size_small)
self._thn_mp.config(image=self._thn_mp_img)
def _lbselchanged(self, *args):
cs = self._lb.curselection()
if not cs:
return
ci = cs[0]
item = eval(self._lbitemsvar.get())[ci]
(_ss, _to), (_ss_str, _to_str) = self._lbitem2sst(item)
self._ent_ss_var.set(_ss_str)
self._ent_to_var.set(_to_str)
self._lbl_mp_var.set(_ts_to_tss(_ss + (_to - _ss) / 2.0))
if self._thmupd_invo:
self._root.after_cancel(self._thmupd_invo)
self._thmupd_invo = self._root.after(500, self._updatethumb_cb)
def _updatethumb_cb(self, *args, **kwargs):
self._ss_changed()
self._to_changed()
def _build_newranges(self, *args):
try:
_ss = parse_time(self._ent_rass_var.get().strip())
_to = parse_time(self._ent_rato_var.get().strip())
_t = float(self._ent_rasp_var.get().strip())
return _yr_g(_ss, _to, _t)
except ValueError as exc:
_log.warning(exc)
def _updatelist_and_reselect_new(self, newlist, oldv=None):
def _in_range(t, s, e):
return (s <= t and t <= e)
newlist.sort()
newsel = 0
if oldv:
newsel = newlist.index(oldv)
elif self._lbitems:
selidxes = list(self._lb.curselection())
if selidxes:
oldsel = selidxes[0]
else:
oldsel = 0
olds, olde = tuple(map(parse_time, re.split(r"\s*->\s*", self._lbitems[oldsel])))
for i, item in enumerate(newlist):
s, e = tuple(map(parse_time, re.split(r"\s*->\s*", item)))
if _in_range(s, olds, olde) or _in_range(e, olds, olde):
newsel = i
break
self._lbitems = newlist
self._lbitemsvar.set(self._lbitems)
for i in self._lb.curselection():
self._lb.selection_clear(i)
self._lb.selection_set(newsel)
self._lbselchanged()
def _refresh_rangeslist(self, *args):
nl = self._build_newranges()
if not nl:
return
self._updatelist_and_reselect_new([
self._sst2lbitem(_ss, _t) for _ss, _t in nl])
def _extend_rangeslist(self, *args):
nl = self._build_newranges()
if not nl:
return
newlist = list(self._lbitems)
newlist.extend([
self._sst2lbitem(_ss, _t) for _ss, _t in nl])
self._updatelist_and_reselect_new(newlist)
def _append_this_range(self, *args):
v = self._getlbitem_from_sstoent()
if v and v not in self._lbitems:
newlist = list(self._lbitems)
newlist.append(v)
self._updatelist_and_reselect_new(newlist, v)
def _update_with_this_range(self, *args):
selidxes = list(self._lb.curselection())
if len(selidxes) != 1:
return
sel = selidxes[0]
v = self._getlbitem_from_sstoent()
if v:
newlist = list(self._lbitems)
newlist[sel] = v
self._updatelist_and_reselect_new(newlist, v)
def _delete_selected_ranges(self, *args):
selidxes = list(self._lb.curselection())
newlist = [
item for i, item in enumerate(self._lbitems)
if i not in selidxes]
self._updatelist_and_reselect_new(newlist)
def _concat_selected_ranges(self, *args):
selidxes = list(self._lb.curselection())
newlist = [
item for i, item in enumerate(self._lbitems)
if i not in selidxes] # unmodified
minss, maxto = float("inf"), -float("inf")
for item in [item for i, item in enumerate(self._lbitems) if i in selidxes]:
ss, to = tuple(map(parse_time, re.split(r"\s*->\s*", item)))
minss, maxto = min(ss, minss), max(to, maxto)
newlist.append(self._sst2lbitem(minss, maxto - minss))
self._updatelist_and_reselect_new(newlist)
def _getlbitem_from_sstoent(self):
try:
_ss_str = self._ent_ss_var.get().strip()
_ss = parse_time(_ss_str)
_to_str = self._ent_to_var.get().strip()
_to = parse_time(_to_str)
return self._sst2lbitem(_ss, _to - _ss)
except ValueError as exc:
_log.warning(exc)
def _sst2lbitem(self, ss, t):
return "{} -> {}".format(
_ts_to_tss(ss), _ts_to_tss(ss + t))
def _lbitem2sst(self, item):
_ss_str, _to_str = map(lambda s: s.strip(), item.split("->"))
_ss = parse_time(_ss_str.strip())
_t = parse_time(_to_str.strip()) - _ss
return (_ss, _t), (_ss_str, _to_str)
def _execute_viewer(self, *args):
cur = self._get_cur_ssto()
if not cur:
return
(_ss, _to), (_ss_str, _to_str) = cur
_t = _to - _ss
self._execute_viewer_0(_ss, _t)
def _execute_viewer_0(self, ss, t, *args):
if (ss, t) in self._last_previewed_hist:
self._last_previewed_hist.remove((ss, t))
self._last_previewed_hist.append((ss, t))
while len(self._last_previewed_hist) > 5:
self._last_previewed_hist.pop(0)
if self._args.player == 1:
player_command = self._args.player_command
else:
player_command = self._args.player2_command
self._statusbar.variable.set(
'"{}" -> "{}"'.format(
_ts_to_tss(ss), _ts_to_tss(ss + t))) # normally, you cannot see this.
t = threading.Thread(target=self._execute_viewer_run, args=(player_command, ss, t))
t.start()
def _execute_viewer_run(self, player_command, ss, t):
scale = self._args.scale
volume = self._args.volume
_execute_preview_one(
self._args, player_command, ss, t, scale, volume,
lambda msg: self._statusbar.variable.set(msg))
def _execute_render(self, *args):
cur = self._get_cur_ssto()
if not cur:
return
(_ss, _to), (_ss_str, _to_str) = cur
_t = _to - _ss
sr = []
for sel in self._lb.curselection():
s, e = re.split(r"\s*->\s*", self._lbitems[sel])
s = parse_time(s)
e = parse_time(e)
sr.append((s, e))
#
r = _Renderer(
self._args,
self._args.logview_initial_template)
result, error = r.render(ss=_ss, to=_to, selected_ranges=sr)
txt = result if not error else "{}\n".format(error)
self._entres.insert("end", txt)
class _Renderer(object):
def __init__(self, args, bodytempl=""):
self._args = args
self._bodytempl = bodytempl
self._tmpl = None
def render(self, ss, to, selected_ranges):
import random
result, error = "", ""
try:
if not self._tmpl:
self._tmpl = Template(self._bodytempl)
result = self._tmpl.render(
# data
args=self._args,
ss=ss,
to=to,
ranges=selected_ranges,
# modules
os=os,
sys=sys,
pipes=pipes,
math=math,
random=random,
json=json,
# local utilities
parse_time=parse_time,
to_tss=_ts_to_tss,
fmtsec=lambda d: ("%.3f" % d))
except Exception as exc:
error = "Your template is broken: {}: {}".format(
str(type(exc)).split("'")[1],
str(exc))
return result, error
if __name__ == '__main__':
import argparse
import atexit
try:
from configparser import ConfigParser # Python 3.x
except ImportError:
from ConfigParser import ConfigParser # Python 2.x
_LOGFMT = "%(asctime)s:%(levelname)5s:%(module)s(%(lineno)d):%(thread)x| %(message)s"
logging.basicConfig(
level=logging.INFO,
stream=sys.stderr,
format=_LOGFMT)
#
# search conf order: user's home -> current dir
#
# .ffchopreview.conf example:
# ---------------------------------------------------------------------
# [env]
# ffmpeg="c:/Program Files/ffmpeg-4.2.1-win64-shared/bin/ffmpeg"
# player_command=["ffplay", "-hide_banner"]
# player2_command=["c:/Program Files/MPC-HC/mpc-hc64.exe"]
#
# [app_defaults]
# ; if you want to use player2, set 2 to "player" else 1
# player=1
# volume=0.4
# ;scale=960:540
# audio_visualize_if_no_input_vstream=showcqt=size=1280x720
# ;audio_visualize_if_no_input_vstream=showfreqs=size=1280x720
# ;audio_visualize_if_no_input_vstream=showwaves=size=1280x720
# ;audio_visualize_if_no_input_vstream=volume=3.0,showcqt=size=1920x1080
# ;audio_visualize_if_no_input_vstream=volume=2.5,showwaves=size=960x540:split_channels=1
# ;audio_visualize_if_no_input_vstream=ahistogram=size=1280x720:dmode=separate
# audio_visualize_duration_if_no_input_vstream=3
# verbose=ffmpeg_stderr
#
# [gui]
# font_main=["courier", 12, "normal"]
# font_small=["courier", 10, "normal"]
# thumbnail_size_large=[1280, 720]
# thumbnail_size=[540, 270]
# thumbnail_size_small=[320, 180]
confname = ".{}.conf".format(__MYNAME__)
cp = ConfigParser()
cp.read([
os.path.join(d, confname)
for d in (os.environ.get("HOME", os.environ.get("USERPROFILE")), ".")
if d and os.path.exists(os.path.join(d, confname))])
defaults = dict(
ffmpeg=cp.get("env", "ffmpeg", fallback="ffmpeg"),
player=cp.getint("app_defaults", "player", fallback=1),
player_command=cp.get("env", "player_command", fallback='["ffplay", "-hide_banner"]'),
player2_command=cp.get("env", "player2_command", fallback='["vlc"]'),
volume=cp.getfloat("app_defaults", "volume", fallback=0.4),
scale=cp.get("app_defaults", "scale", fallback="640:-1"),
audio_visualize_if_no_input_vstream=cp.get(
"app_defaults",
"audio_visualize_if_no_input_vstream",
fallback="showcqt=size=1280x720"),
audio_visualize_duration_if_no_input_vstream=cp.getint(
"app_defaults",
"audio_visualize_duration_if_no_input_vstream",
fallback=3),
logview_initial_template=cp.get(
"app_defaults",
"logview_initial_template",
fallback=os.path.join(
os.environ.get("HOME", os.environ.get("USERPROFILE")),
".{}.tmpl".format(__MYNAME__))),
logview_template_dir=cp.get(
"app_defaults",
"logview_template_dir",
fallback=os.path.join(
os.environ.get("HOME", os.environ.get("USERPROFILE")),
".{}.d".format(__MYNAME__), "templates")),
verbose=cp.get("app_defaults", "verbose", fallback=""),
font_main=cp.get("gui", "font_main", fallback='["courier", 12, "normal"]'),
font_small=cp.get("gui", "font_small", fallback='["courier", 10, "normal"]'),
guimain_thumbnail_size=cp.get("gui", "thumbnail_size", fallback='[540, 270]'),
guimain_thumbnail_size_small=cp.get("gui", "thumbnail_size_small", fallback='[320, 180]'),
guimain_thumbnail_size_large=cp.get("gui", "thumbnail_size_large", fallback='[960, 540]'),
)
ap = argparse.ArgumentParser()
ap.add_argument("video", nargs="?")
ap.add_argument("--no_tk", action="store_true")
ap.add_argument("--guimain_exec_on_init", action="store_true")
ap.add_argument("--from_splitpoints") # as json's list
ap.add_argument("--from_slice") # [start, end, step]
ap.add_argument("--ffmpeg", default=defaults["ffmpeg"])
ap.add_argument("--player", default=defaults["player"], choices=[1, 2], type=int)
ap.add_argument("--player_command", default=defaults["player_command"])
ap.add_argument("--player2_command", default=defaults["player2_command"])
ap.add_argument("--volume", type=float, default=defaults["volume"])
ap.add_argument("--scale", default=defaults["scale"])
ap.add_argument("--no_input_vstream", action="store_true")
ap.add_argument("--no_input_astream", action="store_true")
ap.add_argument("--audio_visualize_if_no_input_vstream",
default=defaults["audio_visualize_if_no_input_vstream"])
ap.add_argument("--audio_visualize_duration_if_no_input_vstream",
default=defaults["audio_visualize_duration_if_no_input_vstream"],
type=int)
ap.add_argument("--logview_initial_template",
default=defaults["logview_initial_template"])
ap.add_argument("--logview_template_dir",
default=defaults["logview_template_dir"])
ap.add_argument("--no_drawpts", action="store_true",
help="if you use ffmpeg 3.x, you will have to use this option.")
ap.add_argument("--guimain_thumbnail_size", default=defaults["guimain_thumbnail_size"])
ap.add_argument("--guimain_thumbnail_size_large", default=defaults["guimain_thumbnail_size_large"])
ap.add_argument("--guimain_thumbnail_size_small", default=defaults["guimain_thumbnail_size_small"])
# ffmpeg_stdout, player_stdout, ffmpeg_stderr, player_stderr
ap.add_argument("--verbose", default=defaults["verbose"])
args = ap.parse_args()
setattr(args, "font_main", tuple(json.loads(defaults["font_main"])))
setattr(args, "font_small", tuple(json.loads(defaults["font_small"])))
args.guimain_thumbnail_size = tuple(json.loads(args.guimain_thumbnail_size))
args.guimain_thumbnail_size_large = tuple(json.loads(args.guimain_thumbnail_size_large))
args.guimain_thumbnail_size_small = tuple(json.loads(args.guimain_thumbnail_size_small))
if args.logview_initial_template and os.path.exists(args.logview_initial_template):
tn = args.logview_initial_template
tc = io.open(tn, encoding="utf-8").read().strip()
args.logview_initial_template = tc
else:
args.logview_initial_template = """\
ffmpeg -y -ss "${to_tss(ss)}" -i ${pipes.quote(args.video)} -ss 0 -t ${to - ss} out.mkv"""
### other example:
### --------------------------------------
### % for ss, to in ranges:
### ["${to_tss(ss)}", "${to_tss(to)}", [1, "", "volume=0.0025"]],
### % endfor
### --------------------------------------
#
tkroot = None
if not args.no_tk:
tkroot = tkinter.Tk()
while not args.video or not os.path.exists(args.video):
r = _my_askopenfilename(tkroot)
if r:
args.video = r
else:
# cancel?
sys.exit(0)
#
if args.no_tk:
_update_argmediainfo(args)
for _ss, _t in _yr(args):
_execute_preview_one(
args, _ss, _t, args.scale, args.volume)
else:
lb = _FCPMainGui(tkroot, args, list(_yr(args)))
tkroot.mainloop()
_HISTMNG.save_history()
@hhsprings
Copy link
Author

@hhsprings
Copy link
Author

example of .ffchopreview.conf (added on Rev44):

[env]
ffmpeg=c:/Program Files/ffmpeg-4.2.1-win64-shared/bin/ffmpeg
;player_command=["c:/Program Files/ffmpeg-4.2.1-win64-shared/bin/ffplay", "-hide_banner"]
player_command=["c:/Program Files/MPC-HC/mpc-hc64.exe"]

[app_defaults]
volume=0.3
;scale=640:-1
scale=960:540
;verbose=ffmpeg_stderr

@hhsprings
Copy link
Author

new dependancy: pillow

please "pip install pillow"

@hhsprings
Copy link
Author

new dependancy: mako

please "pip install mako"

@hhsprings
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment