diff options
-rw-r--r-- | options.py | 33 | ||||
-rw-r--r-- | output.py | 139 | ||||
-rw-r--r-- | sources.py | 4 | ||||
-rw-r--r-- | vidslice.bat | 1 | ||||
-rw-r--r-- | vidslice.py | 61 |
5 files changed, 214 insertions, 24 deletions
@@ -197,10 +197,10 @@ class OptionsPanel(wx.Panel): 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_range(0, orig_framerate) self.framerate.set_calc_new(orig_framerate) - def update(self, path, info): + def update(self, info): import fractions if info is None: @@ -224,3 +224,32 @@ class OptionsPanel(wx.Panel): self.Enable() self.Layout() self.enforce_constraints() + + def ffmpeg_opts(self): + opts = [] + + if self.start_time.is_edit(): + opts += ['-ss', str(self.start_time.get_final())] + 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 + opts += ['-ss', str(new_start)] + if self.end_time.is_edit(): + opts += ['-to', str(self.end_time.get_final())] + if self.duration.is_edit(): + opts += ['-t', str(self.duration.get_final())] + + if self.width.is_edit() or self.height.is_edit(): + width = str(self.width.get_final()) + height = str(self.height.get_final()) + if not self.width.is_edit(): + width = "-1" + if not self.height.is_edit(): + height = "-1" + opts += ['-vf', 'scale=' + width + ':' + height] + + if self.framerate.is_edit(): + opts += ['-r', str(self.framerate.get_final())] + + return opts diff --git a/output.py b/output.py new file mode 100644 index 0000000..6f46f01 --- /dev/null +++ b/output.py @@ -0,0 +1,139 @@ +import os +import subprocess +import threading + +import wx + + +class OutputPanel(wx.Panel): + def __init__(self, *args, get_ffmpeg_args=lambda: [], **kw): + super(OutputPanel, self).__init__(*args, **kw) + self.update_listeners = [] + self.input_path = None + self.get_ffmpeg_args = get_ffmpeg_args + + root_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, label="Output") + self.SetSizer(root_sizer) + + file_panel = wx.Panel(self) + root_sizer.Add(file_panel, flag=wx.EXPAND, border=5) + file_sizer = wx.BoxSizer(wx.HORIZONTAL) + file_panel.SetSizer(file_sizer) + file_sizer.Add(wx.StaticText(file_panel, label="File"), flag=wx.EXPAND, border=5) + self.file_text = wx.TextCtrl(file_panel) + self.file_text.Bind(wx.EVT_TEXT, self.handle_file_changed) + file_sizer.Add(self.file_text, proportion=1, flag=wx.EXPAND, border=5) + self.file_browse_button = wx.Button(file_panel, label="Browse") + self.file_browse_button.Bind(wx.EVT_BUTTON, self.handle_file_browse_pressed) + file_sizer.Add(self.file_browse_button, flag=wx.EXPAND, border=5) + + options_panel = wx.Panel(self) + root_sizer.Add(options_panel, flag=wx.EXPAND, border=5) + options_sizer = wx.BoxSizer(wx.HORIZONTAL) + options_panel.SetSizer(options_sizer) + self.silence = wx.CheckBox(options_panel, label="Silence") + options_sizer.Add(self.silence, proportion=1, flag=wx.EXPAND, border=5) + self.improve_gif = wx.CheckBox(options_panel, label="Improve GIF") + options_sizer.Add(self.improve_gif, proportion=1, flag=wx.EXPAND, border=5) + self.improve_gif.Disable() + + self.run_panel = wx.Panel(self) + root_sizer.Add(self.run_panel, flag=wx.EXPAND, border=5) + run_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.run_panel.SetSizer(run_sizer) + run_button = wx.Button(self.run_panel, label="Run") + run_button.Bind(wx.EVT_BUTTON, self.handle_run_pressed) + run_sizer.Add(run_button, proportion=1, flag=wx.EXPAND, border=5) + run_preview_button = wx.Button(self.run_panel, label="Run && Preview") + run_preview_button.Bind(wx.EVT_BUTTON, self.handle_run_preview_pressed) + run_sizer.Add(run_preview_button, proportion=1, flag=wx.EXPAND, border=5) + run_quit_button = wx.Button(self.run_panel, label="Run && Quit") + run_quit_button.Bind(wx.EVT_BUTTON, self.handle_run_quit_pressed) + run_sizer.Add(run_quit_button, proportion=1, flag=wx.EXPAND, border=5) + self.run_panel.Disable() + + self.logs = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY) + root_sizer.Add(self.logs, proportion=1, flag=wx.EXPAND, border=5) + + self.Disable() + + def handle_file_changed(self, _): + path = self.file_text.GetValue() + (folder, name) = os.path.split(path) + try: + os.stat(folder) + self.run_panel.Enable() + except FileNotFoundError: + self.run_panel.Disable() + (name, ext) = os.path.splitext(name) + if ext == '.gif': + self.improve_gif.Enable() + else: + self.improve_gif.SetValue(False) + self.improve_gif.Disable() + + def handle_file_browse_pressed(self, _): + dialog = wx.FileDialog(self, style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) + if dialog.ShowModal() == wx.ID_OK: + self.file_text.SetValue(dialog.GetPath()) + + def handle_run_pressed(self, _, callback=lambda _: None): + self.logs.Clear() + self.run_panel.Disable() + real_args = self.get_ffmpeg_args() + if self.silence.GetValue(): + real_args += ['-an'] + if self.improve_gif.GetValue(): + filter_before = '[0:v] ' + filter_after = 'split [a][b];[a] palettegen [p];[b][p] paletteuse' + filter_during = '' + if '-vf' in real_args: + for i in range(len(real_args) - 1): + [a, b] = real_args[i:i + 2] + if a == '-vf': + filter_during = b + ',' + real_args[i:i + 2] = [] + break + real_args += ['-filter_complex', filter_before + filter_during + filter_after] + args = ['ffmpeg', '-hide_banner', '-y', '-i', self.input_path] + real_args + [self.file_text.GetValue()] + print(args) + + def run(): + # noinspection PyArgumentList + proc = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True) + while proc.poll() is None: + out_data = proc.stdout.readline() + if out_data != '': + wx.CallAfter(lambda: self.add_log(out_data)) + wx.CallAfter(lambda: self.run_panel.Enable()) + wx.CallAfter(lambda: callback(proc.returncode)) + + threading.Thread(target=run).start() + + def handle_run_preview_pressed(self, _event): + def preview(code): + if code == 0: + out_file = self.file_text.GetValue() + subprocess.Popen(['ffplay', '-autoexit', out_file]) + + self.handle_run_pressed(_event, callback=preview) + + def handle_run_quit_pressed(self, _event): + def quit(code): + if code == 0: + parent = self.GetTopLevelParent() + parent.Close(True) + + self.handle_run_pressed(_event, callback=quit) + + def set_input_path(self, path, data): + if data is None: + self.input_path = None + self.Disable() + else: + self.input_path = path + self.Enable() + + def add_log(self, data): + self.logs.AppendText(data) @@ -82,11 +82,11 @@ class SourcesPanel(wx.Panel): threading.Thread(target=download).start() def handle_file_browse_pressed(self, _): - dialog = wx.FileDialog(self, message="message") + dialog = wx.FileDialog(self, style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) if dialog.ShowModal() == wx.ID_OK: self.file_text.SetValue(dialog.GetPath()) - def handle_file_changed(self, event): + 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', diff --git a/vidslice.bat b/vidslice.bat new file mode 100644 index 0000000..139fe3b --- /dev/null +++ b/vidslice.bat @@ -0,0 +1 @@ +D:\Melody\Projects\vidslice\venv\Scripts\python.exe D:/Melody/Projects/vidslice/vidslice.py %1 diff --git a/vidslice.py b/vidslice.py index 267c064..6cf157c 100644 --- a/vidslice.py +++ b/vidslice.py @@ -1,37 +1,54 @@ +import sys + import wx +import wx.adv -from sources import SourcesPanel from options import OptionsPanel +from output import OutputPanel +from sources import SourcesPanel -class HelloFrame(wx.Frame): +class VidsliceFrame(wx.Frame): """ - A Frame that says Hello World + A Frame that contains vidslice logic """ def __init__(self, *args, **kw): # ensure the parent's __init__ is called - super(HelloFrame, self).__init__(*args, **kw) + super(VidsliceFrame, self).__init__(*args, **kw) - main_sizer = wx.BoxSizer(wx.VERTICAL) + root_sizer = wx.BoxSizer(wx.VERTICAL) + 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) # set up sources panel - self.sources_panel = SourcesPanel(self) - main_sizer.Add(self.sources_panel, proportion=0, flag=wx.EXPAND, border=5) + self.sources_panel = SourcesPanel(main) + main_sizer.Add(self.sources_panel, wx.GBPosition(0, 0), wx.GBSpan(1, 2), flag=wx.EXPAND) - # create a panel in the frame - 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)) + # set up options panel + self.options_panel = OptionsPanel(main) + main_sizer.Add(self.options_panel, wx.GBPosition(1, 0), flag=wx.EXPAND) + main_sizer.AddGrowableRow(1, proportion=1) + self.sources_panel.on_update(self.options_panel.update) + + # set up output panel + self.output_panel = OutputPanel(main, get_ffmpeg_args=self.options_panel.ffmpeg_opts) + main_sizer.Add(self.output_panel, wx.GBPosition(1, 1), flag=wx.EXPAND) + main_sizer.AddGrowableCol(1, proportion=1) + self.sources_panel.on_update(lambda data: self.output_panel.set_input_path(self.sources_panel.get_file(), data)) # create a menu bar self.make_menu_bar() - # main_sizer.SetSizeHints(self) - self.SetSizer(main_sizer) - size = main_sizer.GetMinSize() + size = root_sizer.GetMinSize() self.SetMinClientSize(size) + if len(sys.argv) > 1: + self.sources_panel.file_text.SetValue(sys.argv[1]) + def make_menu_bar(self): """ A menu bar is composed of menus, which are composed of menu items. @@ -43,8 +60,7 @@ class HelloFrame(wx.Frame): file_menu = wx.Menu() # The "\t..." syntax defines an accelerator key that also triggers # the same event - hello_item = file_menu.Append(-1, "&Hello...\tCtrl-H", - "Help string shown in status bar for this menu item") + hello_item = file_menu.Append(-1, "&Hello...\tCtrl-H") file_menu.AppendSeparator() # When using a stock ID we don't need to specify the menu item's # label @@ -82,15 +98,20 @@ class HelloFrame(wx.Frame): def on_about(self, event): """Display an About Dialog""" - wx.MessageBox("This is a wxPython Hello World sample", - "About Hello World 2", - wx.OK | wx.ICON_INFORMATION) + info = wx.adv.AboutDialogInfo() + info.SetName("vidslice") + info.SetDescription("video manipulator wrapping youtube-dl and ffmpeg") + info.SetDevelopers(["Melody Horn"]) + info.SetWebSite("https://www.boringcactus.com") + + wx.adv.AboutBox(info) if __name__ == '__main__': # When this module is run (not imported) then create the app, the # frame, show it, and start the event loop. app = wx.App() - frm = HelloFrame(None, title='Hello World 2') + frm = VidsliceFrame(None, title='vidslice') + app.SetTopWindow(frm) frm.Show() app.MainLoop() |