From ff5f3f50d42193f6dcdedcd48403f8718a6fa757 Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Fri, 22 Feb 2019 02:01:41 -0700 Subject: edit options --- options.py | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ sources.py | 109 +++++++++++++++++++++++++++++ vidslice.py | 131 +++-------------------------------- 3 files changed, 344 insertions(+), 122 deletions(-) create mode 100644 options.py create mode 100644 sources.py diff --git a/options.py b/options.py new file mode 100644 index 0000000..e93826f --- /dev/null +++ b/options.py @@ -0,0 +1,226 @@ +import wx + +HEADERS_ROW = 0 +START_ROW = 1 +END_ROW = 2 +DURATION_ROW = 3 +WIDTH_ROW = 4 +HEIGHT_ROW = 5 +FRAMERATE_ROW = 6 +LABEL_COL = 0 +ORIG_COL = 1 +EDIT_BOX_COL = 2 +NEW_COL = 3 + + +class Property: + def __init__(self, parent, name, *, label=None, orig=None, edit=None, new_class=None, new=None): + self.handlers = [] + if label is None: + label = wx.StaticText(parent, label=name) + self.label = label + if orig is None: + orig = wx.StaticText(parent, label="N/A") + self.orig = orig + if edit is None: + edit = wx.CheckBox(parent) + edit.Bind(wx.EVT_CHECKBOX, self.handle_edit) + self.edit = edit + if new is None: + if new_class is None: + new_class = wx.TextCtrl + new = new_class(parent) + new.Bind(wx.EVT_SPINCTRLDOUBLE, self.handle_change) + new.Bind(wx.EVT_SPINCTRL, self.handle_change) + new.Disable() + self.new = new + + def add_to(self, sizer, row): + sizer.Add(self.label, wx.GBPosition(row, LABEL_COL)) + sizer.Add(self.orig, wx.GBPosition(row, ORIG_COL)) + sizer.Add(self.edit, wx.GBPosition(row, EDIT_BOX_COL)) + sizer.Add(self.new, wx.GBPosition(row, NEW_COL), flag=wx.EXPAND) + + def disable(self): + self.enable(False) + + def enable(self, enabled=True): + if enabled: + self.edit.Enable() + else: + self.edit.SetValue(False) + self.edit.Disable() + self.new.Disable() + + def is_edit(self): + return self.edit.GetValue() + + def set_orig(self, val): + self.orig.SetLabel(str(val)) + + def get_orig(self): + return self.orig.GetLabel() + + def set_calc_new(self, val): + if not self.is_edit(): + if self.new.GetMin() > val: + self.new.SetMin(val) + if self.new.GetMax() < val: + self.new.SetMax(val) + self.new.SetValue(val) + + def set_range(self, min, max): + self.new.SetRange(min, max) + + def get_final(self): + if self.edit.GetValue(): + return self.new.GetValue() + else: + return self.orig.GetLabel() + + def handle_edit(self, _event): + self.new.Enable(self.edit.GetValue()) + self.handle_change(None) + + def on_change(self, callback): + self.handlers.append(callback) + + def handle_change(self, _event): + for handler in self.handlers: + handler() + + +class OptionsPanel(wx.Panel): + """ + A Panel displaying ffmpeg options + """ + + def __init__(self, *args, **kw): + super(OptionsPanel, self).__init__(*args, **kw) + + root_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label="Options") + self.SetSizer(root_sizer) + main = wx.Panel(self) + root_sizer.Add(main, proportion=1, flag=wx.EXPAND, border=5) + main_sizer = wx.GridBagSizer(5, 5) + main.SetSizer(main_sizer) + + def make_header(text): + st = wx.StaticText(main, label=text, style=wx.ALIGN_CENTER_HORIZONTAL) + st.SetFont(st.GetFont().Bold()) + return st + + main_sizer.Add(make_header("Field"), wx.GBPosition(HEADERS_ROW, LABEL_COL), flag=wx.EXPAND) + main_sizer.Add(make_header("Original Value"), wx.GBPosition(HEADERS_ROW, ORIG_COL), flag=wx.EXPAND) + main_sizer.Add(make_header("Edit?"), wx.GBPosition(HEADERS_ROW, EDIT_BOX_COL), flag=wx.EXPAND) + main_sizer.Add(make_header("New Value"), wx.GBPosition(HEADERS_ROW, NEW_COL), flag=wx.EXPAND) + + self.start_time = Property(main, "Start time (seconds)", new_class=wx.SpinCtrlDouble) + self.start_time.add_to(main_sizer, START_ROW) + self.start_time.on_change(self.enforce_constraints) + + self.end_time = Property(main, "End time (seconds)", new_class=wx.SpinCtrlDouble) + self.end_time.add_to(main_sizer, END_ROW) + self.end_time.on_change(self.enforce_constraints) + + self.duration = Property(main, "Duration (seconds)", new_class=wx.SpinCtrlDouble) + self.duration.add_to(main_sizer, DURATION_ROW) + self.duration.on_change(self.enforce_constraints) + + self.width = Property(main, "Width", new_class=wx.SpinCtrl) + self.width.add_to(main_sizer, WIDTH_ROW) + self.width.on_change(self.enforce_constraints) + + self.height = Property(main, "Height", new_class=wx.SpinCtrl) + self.height.add_to(main_sizer, HEIGHT_ROW) + self.height.on_change(self.enforce_constraints) + + self.framerate = Property(main, "Framerate", new_class=wx.SpinCtrlDouble) + self.framerate.add_to(main_sizer, FRAMERATE_ROW) + self.framerate.on_change(self.enforce_constraints) + + self.Disable() + + def enforce_constraints(self): + self.start_time.enable() + self.end_time.enable() + self.duration.enable() + orig_start = float(self.start_time.get_orig()) + orig_end = float(self.end_time.get_orig()) + orig_duration = float(self.duration.get_orig()) + if self.start_time.is_edit() and self.end_time.is_edit(): + new_start = float(self.start_time.get_final()) + new_end = float(self.end_time.get_final()) + new_duration = new_end - new_start + self.start_time.set_range(orig_start, new_end) + self.end_time.set_range(new_start, orig_end) + self.duration.disable() + self.duration.set_calc_new(new_duration) + elif self.start_time.is_edit() and self.duration.is_edit(): + new_start = float(self.start_time.get_final()) + new_duration = float(self.duration.get_final()) + new_end = new_start + new_duration + self.start_time.set_range(orig_start, orig_end - new_duration) + self.duration.set_range(0, orig_end - new_start) + self.end_time.disable() + self.end_time.set_calc_new(new_end) + elif self.end_time.is_edit() and self.duration.is_edit(): + new_end = float(self.end_time.get_final()) + new_duration = float(self.duration.get_final()) + new_start = new_end - new_duration + self.end_time.set_range(orig_start + new_duration, orig_end) + self.duration.set_range(0, new_end - orig_start) + self.start_time.disable() + self.start_time.set_calc_new(new_start) + else: + new_start = float(self.start_time.get_final()) + new_end = float(self.end_time.get_final()) + new_duration = new_end - new_start + if self.duration.is_edit(): + new_duration = float(self.duration.get_final()) + new_end = new_start + new_duration + self.start_time.set_range(orig_start, orig_end) + self.end_time.set_range(orig_start, orig_end) + self.duration.set_range(0, orig_duration) + self.start_time.set_calc_new(new_start) + self.end_time.set_calc_new(new_end) + self.duration.set_calc_new(new_duration) + + orig_width = int(self.width.get_orig()) + orig_height = int(self.height.get_orig()) + new_width = int(self.width.get_final()) + new_height = int(self.height.get_final()) + self.width.set_range(1, 10 * orig_width) + self.height.set_range(1, 10 * orig_height) + + self.width.set_calc_new(round(orig_width / orig_height * new_height)) + self.height.set_calc_new(round(orig_height / orig_width * new_width)) + + orig_framerate = float(self.framerate.get_orig()) + self.framerate.set_range(0, 10 * orig_framerate) + self.framerate.set_calc_new(orig_framerate) + + def update(self, path, info): + import fractions + + if info is None: + self.Disable() + else: + start_time = float(info['format']['start_time']) + self.start_time.set_orig(start_time) + + duration = float(info['format']['duration']) + self.duration.set_orig(duration) + + self.end_time.set_orig(start_time + duration) + + video_stream = [stream for stream in info['streams'] if stream['codec_type'] == 'video'][0] + self.width.set_orig(video_stream['width']) + self.height.set_orig(video_stream['height']) + + framerate = round(float(fractions.Fraction(video_stream['avg_frame_rate'])), 3) + self.framerate.set_orig(framerate) + + self.Enable() + self.Layout() + self.enforce_constraints() diff --git a/sources.py b/sources.py new file mode 100644 index 0000000..b9c8e42 --- /dev/null +++ b/sources.py @@ -0,0 +1,109 @@ +import glob +import json +import subprocess +import tempfile +import threading + +import wx + + +def has_ytdl(): + try: + subprocess.run(["youtube-dl", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return True + except FileNotFoundError: + return False + + +class SourcesPanel(wx.Panel): + """ + A Panel representing source info + """ + + def __init__(self, *args, **kw): + super(SourcesPanel, self).__init__(*args, **kw) + self.update_listeners = [] + + root_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label="Sources") + self.SetSizer(root_sizer) + main = wx.Panel(self) + root_sizer.Add(main, proportion=1, flag=wx.EXPAND, border=5) + main_sizer = wx.GridBagSizer(5, 5) + main.SetSizer(main_sizer) + + if has_ytdl(): + main_sizer.Add(wx.StaticText(main, label="URL"), wx.GBPosition(0, 0), flag=wx.EXPAND) + self.url_text = wx.TextCtrl(main) + main_sizer.Add(self.url_text, wx.GBPosition(0, 1), flag=wx.EXPAND) + self.url_download_button = wx.Button(main, label="Download") + self.url_download_button.Bind(wx.EVT_BUTTON, self.handle_url_download_pressed) + main_sizer.Add(self.url_download_button, wx.GBPosition(0, 2), flag=wx.EXPAND) + else: + no_ytdl_label = wx.StaticText(main, label="Could not find youtube-dl, can't download videos automatically") + main_sizer.Add(no_ytdl_label, wx.GBPosition(0, 0), wx.GBSpan(1, 3), flag=wx.EXPAND) + + main_sizer.Add(wx.StaticText(main, label="File"), wx.GBPosition(1, 0), flag=wx.EXPAND) + self.file_text = wx.TextCtrl(main) + self.file_text.Bind(wx.EVT_TEXT, self.handle_file_changed) + main_sizer.Add(self.file_text, wx.GBPosition(1, 1), flag=wx.EXPAND) + self.file_browse_button = wx.Button(main, label="Browse") + self.file_browse_button.Bind(wx.EVT_BUTTON, self.handle_file_browse_pressed) + main_sizer.Add(self.file_browse_button, wx.GBPosition(1, 2), flag=wx.EXPAND) + + self.status_label = wx.StaticText(main, label="Status: Select a file") + main_sizer.Add(self.status_label, wx.GBPosition(2, 0), wx.GBSpan(1, 3)) + + main_sizer.AddGrowableCol(1, proportion=1) + + def set_status(self, text): + self.status_label.SetLabel("Status: " + text) + + def handle_url_download_pressed(self, _): + self.set_status("Downloading...") + + def download(): + file = tempfile.NamedTemporaryFile(delete=False) + # noinspection PyArgumentList + proc = subprocess.Popen([ + 'youtube-dl', '-o', file.name + '.%(ext)s', self.url_text.GetValue() + ], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + while proc.poll() is None: + out_data = proc.stdout.readline() + if out_data != '': + wx.CallAfter(lambda: self.set_status("Downloading: " + out_data.strip())) + if proc.returncode == 0: + output_file = glob.glob(glob.escape(file.name) + '.*')[0] + wx.CallAfter(lambda: self.set_status("Downloaded!")) + wx.CallAfter(lambda: self.file_text.SetValue(output_file)) + else: + error = ''.join(proc.stderr.readlines()).strip() + wx.CallAfter(lambda: self.set_status("Couldn't download: " + error)) + + threading.Thread(target=download).start() + + def handle_file_browse_pressed(self, _): + dialog = wx.FileDialog(self, message="message") + if dialog.ShowModal() == wx.ID_OK: + self.file_text.SetValue(dialog.GetPath()) + + def handle_file_changed(self, event): + result = subprocess.run([ + 'ffprobe', '-v', 'error', '-of', 'json', + '-show_entries', 'format=start_time,duration:stream=index,codec_type,avg_frame_rate,width,height', + self.file_text.GetValue() + ], capture_output=True, text=True) + if result.returncode == 0: + ffprobe_data = json.loads(result.stdout) + self.set_status("Successfully loaded media info") + for listener in self.update_listeners: + listener(ffprobe_data) + else: + self.set_status("Failed to load media info: " + result.stderr) + for listener in self.update_listeners: + listener(None) + + def on_update(self, callback): + self.update_listeners.append(callback) + + def get_file(self): + return self.file_text.GetValue() diff --git a/vidslice.py b/vidslice.py index 08673cf..267c064 100644 --- a/vidslice.py +++ b/vidslice.py @@ -1,112 +1,7 @@ -# First things, first. Import the wxPython package. -import glob -import json -import subprocess -import tempfile -import threading - import wx - -def has_ytdl(): - try: - subprocess.run(["youtube-dl", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - return True - except FileNotFoundError: - return False - - -class SourcesPanel(wx.Panel): - """ - A Panel representing source info - """ - - def __init__(self, *args, **kw): - super(SourcesPanel, self).__init__(*args, **kw) - self.update_listeners = [] - - root_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label="Sources") - self.SetSizer(root_sizer) - main = wx.Panel(self) - root_sizer.Add(main, proportion=1, flag=wx.EXPAND, border=3) - main_sizer = wx.GridBagSizer() - main.SetSizer(main_sizer) - - url_label = wx.StaticText(main, label="URL") - main_sizer.Add(url_label, wx.GBPosition(0, 0), flag=wx.EXPAND) - self.url_text = wx.TextCtrl(main) - main_sizer.Add(self.url_text, wx.GBPosition(0, 1), flag=wx.EXPAND) - self.url_download_button = wx.Button(main, label="Download") - self.url_download_button.Bind(wx.EVT_BUTTON, self.handle_url_download_pressed) - main_sizer.Add(self.url_download_button, wx.GBPosition(0, 2), flag=wx.EXPAND) - if not has_ytdl(): - tooltip = "Could not find youtube-dl" - url_label.Disable() - self.url_text.Disable() - self.url_text.SetToolTip(tooltip) - self.url_download_button.Disable() - self.url_download_button.SetToolTip(tooltip) - - main_sizer.Add(wx.StaticText(main, label="File"), wx.GBPosition(1, 0), flag=wx.EXPAND) - self.file_text = wx.TextCtrl(main) - self.file_text.Bind(wx.EVT_TEXT, self.handle_file_changed) - main_sizer.Add(self.file_text, wx.GBPosition(1, 1), flag=wx.EXPAND) - self.file_browse_button = wx.Button(main, label="Browse") - self.file_browse_button.Bind(wx.EVT_BUTTON, self.handle_file_browse_pressed) - main_sizer.Add(self.file_browse_button, wx.GBPosition(1, 2), flag=wx.EXPAND) - - self.status_label = wx.StaticText(main, label="Status: Select a file") - main_sizer.Add(self.status_label, wx.GBPosition(2, 0), wx.GBSpan(1, 3)) - - main_sizer.AddGrowableCol(1, proportion=1) - - def set_status(self, text): - self.status_label.SetLabel("Status: " + text) - - def handle_url_download_pressed(self, _): - self.set_status("Downloading...") - - def download(): - file = tempfile.NamedTemporaryFile(delete=False) - # noinspection PyArgumentList - proc = subprocess.Popen([ - 'youtube-dl', '-o', file.name + '.%(ext)s', self.url_text.GetValue() - ], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - while proc.poll() is None: - out_data = proc.stdout.readline() - if out_data != '': - wx.CallAfter(lambda: self.set_status("Downloading: " + out_data.strip())) - output_file = glob.glob(glob.escape(file.name) + '.*')[0] - wx.CallAfter(lambda: self.set_status("Downloaded!")) - wx.CallAfter(lambda: self.file_text.SetValue(output_file)) - - threading.Thread(target=download).start() - - def handle_file_browse_pressed(self, _): - dialog = wx.FileDialog(self, message="message") - if dialog.ShowModal() == wx.ID_OK: - self.file_text.SetValue(dialog.GetPath()) - - def handle_file_changed(self, event): - result = subprocess.run([ - 'ffprobe', '-v', 'error', '-of', 'json', - '-show_entries', 'format=start_time,duration:stream=index,codec_type,avg_frame_rate,width,height', - self.file_text.GetValue() - ], capture_output=True, text=True) - if result.stderr != "": - print(result.stderr) - ffprobe_data = json.loads(result.stdout) - del ffprobe_data['programs'] - print(ffprobe_data) - if result.returncode == 0: - self.set_status("Successfully loaded media info") - for listener in self.update_listeners: - listener(ffprobe_data) - else: - self.set_status("Failed to load media info: " + result.stderr) - - def on_update(self, callback): - self.update_listeners.append(callback) +from sources import SourcesPanel +from options import OptionsPanel class HelloFrame(wx.Frame): @@ -121,29 +16,21 @@ class HelloFrame(wx.Frame): main_sizer = wx.BoxSizer(wx.VERTICAL) # set up sources panel - sources_panel = SourcesPanel(self) - main_sizer.Add(sources_panel, proportion=0, flag=wx.EXPAND) + self.sources_panel = SourcesPanel(self) + main_sizer.Add(self.sources_panel, proportion=0, flag=wx.EXPAND, border=5) # create a panel in the frame - options_panel = wx.Panel(self) - main_sizer.Add(options_panel, proportion=1, flag=wx.EXPAND) - - # and put some text with a larger bold font on it - st = wx.StaticText(options_panel, label="Hello World!") - font = st.GetFont() - font.PointSize += 10 - font = font.Bold() - st.SetFont(font) + self.options_panel = OptionsPanel(self) + main_sizer.Add(self.options_panel, proportion=1, flag=wx.EXPAND, border=5) + self.sources_panel.on_update(lambda data: self.options_panel.update(self.sources_panel.get_file(), data)) # create a menu bar self.make_menu_bar() - # and a status bar - self.CreateStatusBar() - self.SetStatusText("Welcome to wxPython!") - # main_sizer.SetSizeHints(self) self.SetSizer(main_sizer) + size = main_sizer.GetMinSize() + self.SetMinClientSize(size) def make_menu_bar(self): """ -- cgit v1.2.3