aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMelody Horn <melody@boringcactus.com>2019-02-22 02:01:41 -0700
committerMelody Horn <melody@boringcactus.com>2019-02-22 02:01:41 -0700
commitff5f3f50d42193f6dcdedcd48403f8718a6fa757 (patch)
treed06c0318da6e6c71e45d8e35889b548a1dc0d01f
parent14723ea9bd40e4102dab99adecc060e69bbde018 (diff)
downloadvidslice-ff5f3f50d42193f6dcdedcd48403f8718a6fa757.tar.gz
vidslice-ff5f3f50d42193f6dcdedcd48403f8718a6fa757.zip
edit options
-rw-r--r--options.py226
-rw-r--r--sources.py109
-rw-r--r--vidslice.py131
3 files changed, 344 insertions, 122 deletions
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):
"""