~boringcactus/vidslice

3364694f14f8a0b92ce70ca608a73966c4043256 — Melody Horn 1 year, 9 months ago 940e875
switch from wxpython to tkinter
6 files changed, 295 insertions(+), 332 deletions(-)

M options.py
M output.py
M requirements.txt
M screenshot.png
M sources.py
M vidslice.py
M options.py => options.py +71 -84
@@ 1,4 1,6 @@
import wx
import typing
from tkinter import *
from tkinter import ttk

HEADERS_ROW = 0
START_ROW = 1


@@ 20,135 22,113 @@ class FFmpegOptions:


class Property:
    def __init__(self, parent, name, *, label=None, orig=None, edit=None, new_class=None, new=None):
    def __init__(self, parent, name, row, convert: typing.Callable = int):
        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)
        ttk.Label(parent, text=name).grid(column=LABEL_COL, row=row, sticky=(E, W))
        self.orig = StringVar(parent, value="N/A")
        ttk.Label(parent, textvariable=self.orig).grid(column=ORIG_COL, row=row, sticky=(E, W))
        self.edit = BooleanVar(parent)
        self.edit_widget = ttk.Checkbutton(parent, variable=self.edit, command=self.handle_edit)
        self.edit_widget.grid(column=EDIT_BOX_COL, row=row, sticky=(E, W))
        self.new = ttk.Spinbox(parent, command=self.handle_change)
        self.new.state(['disabled'])
        self.new.grid(column=NEW_COL, row=row, sticky=(E, W))
        self.convert = convert
        self.disable()

    def disable(self):
        self.enable(False)

    def enable(self, enabled=True):
        if enabled:
            self.edit.Enable()
            self.edit_widget.state(['!disabled'])
        else:
            self.edit.SetValue(False)
            self.edit.Disable()
            self.new.Disable()
            self.edit.set(False)
            self.edit_widget.state(['disabled'])
            self.new.state(['disabled'])

    def is_enabled(self):
        return self.edit.Enabled
        return self.edit_widget.instate(['!disabled'])

    def is_edit(self):
        return self.edit.GetValue()
        return self.edit.get()

    def set_orig(self, val):
        self.orig.SetLabel(str(val))
        self.orig.set(str(val))

    def get_orig(self):
        return self.orig.GetLabel()
        return self.orig.get()

    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)
            if self.convert(self.new['from']) > val:
                self.new.configure(from_=val)
            if self.convert(self.new['to']) < val:
                self.new.configure(to=val)
            self.new.set(val)

    def set_range(self, min, max):
        self.new.SetRange(min, max)
        self.new.configure(from_=min, to=max)

    def get_final(self):
        if self.edit.GetValue():
            return self.new.GetValue()
        else:
            return self.orig.GetLabel()
        if len(self.new.get()) == 0:
            return self.orig.get()
        return self.new.get()

    def handle_edit(self, _event):
        self.new.Enable(self.edit.GetValue())
    def handle_edit(self, *args):
        if self.edit.get():
            self.new.state(['!disabled'])
        else:
            self.new.state(['disabled'])
        self.handle_change(None)

    def on_change(self, callback):
        self.handlers.append(callback)

    def handle_change(self, _event):
    def handle_change(self, *args):
        for handler in self.handlers:
            handler()


class OptionsPanel(wx.Panel):
class OptionsPanel(ttk.LabelFrame):
    """
    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)
        super(OptionsPanel, self).__init__(*args, text="Options", **kw)

        def place_header(text, **kwargs):
            ttk.Label(self, text=text, font='TkHeadingFont', justify='center', anchor='center'
                      ).grid(sticky=(E, W), **kwargs)

        place_header("Field", column=LABEL_COL, row=HEADERS_ROW)
        place_header("Original Value", column=ORIG_COL, row=HEADERS_ROW)
        place_header("Edit?", column=EDIT_BOX_COL, row=HEADERS_ROW)
        place_header("New Value", column=NEW_COL, row=HEADERS_ROW)

        self.start_time = Property(self, "Start time (seconds)", START_ROW, float)
        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 = Property(self, "End time (seconds)", END_ROW, float)
        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 = Property(self, "Duration (seconds)", DURATION_ROW, float)
        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 = Property(self, "Width", WIDTH_ROW, int)
        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 = Property(self, "Height", HEIGHT_ROW, int)
        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 = Property(self, "Framerate", FRAMERATE_ROW, float)
        self.framerate.on_change(self.enforce_constraints)

        self.Disable()
        for child in self.winfo_children():
            child.grid_configure(padx=2, pady=2)

        self.state(['disabled'])

    def enforce_constraints(self):
        self.start_time.enable()


@@ 211,11 191,13 @@ class OptionsPanel(wx.Panel):
            self.framerate.set_range(0, orig_framerate)
            self.framerate.set_calc_new(orig_framerate)

    def update(self, info):
    def update_info(self, info):
        import fractions

        if info is None:
            self.Disable()
            self.state(['disabled'])
            for prop in [self.start_time, self.duration, self.end_time, self.width, self.height, self.framerate]:
                prop.disable()
        else:
            start_time = float(info['format']['start_time'])
            self.start_time.set_orig(start_time)


@@ 229,18 211,20 @@ class OptionsPanel(wx.Panel):
                             stream['codec_type'] == 'video' and stream['avg_frame_rate'] != '0/0']
            if len(video_streams) > 0:
                video_stream = video_streams[0]
                self.width.enable()
                self.width.set_orig(video_stream['width'])
                self.height.enable()
                self.height.set_orig(video_stream['height'])

                framerate = round(float(fractions.Fraction(video_stream['avg_frame_rate'])), 3)
                self.framerate.enable()
                self.framerate.set_orig(framerate)
            else:
                self.width.disable()
                self.height.disable()
                self.framerate.disable()

            self.Enable()
            self.Layout()
            self.state(['!disabled'])
            self.enforce_constraints()

    def ffmpeg_opts(self):


@@ 272,3 256,6 @@ class OptionsPanel(wx.Panel):
            output_opts += ['-r', str(self.framerate.get_final())]

        return FFmpegOptions(input_opts, output_opts)

    def frame_count(self):
        return float(self.duration.get_final()) * float(self.framerate.get_final())

M output.py => output.py +95 -79
@@ 1,87 1,100 @@
import os
import subprocess
import threading

import wx
from tkinter import *
from tkinter import filedialog
from tkinter import ttk

from options import FFmpegOptions


class OutputPanel(wx.Panel):
    def __init__(self, *args, get_ffmpeg_args=lambda: FFmpegOptions([], []), **kw):
        super(OutputPanel, self).__init__(*args, **kw)
class OutputPanel(ttk.LabelFrame):
    def __init__(self, *args, get_ffmpeg_args=lambda: FFmpegOptions([], []), get_frame_count=lambda: 0, **kw):
        super(OutputPanel, self).__init__(*args, text='Output', **kw)
        self.update_listeners = []
        self.input_path = None
        self.get_ffmpeg_args = get_ffmpeg_args
        self.get_frame_count = get_frame_count

        ttk.Label(self, text="File").grid(column=0, row=0, sticky=W)
        self.file_text = StringVar(self)
        ttk.Entry(self, textvariable=self.file_text, width=30).grid(column=1, row=0, columnspan=2, sticky=(E, W))
        self.columnconfigure(1, weight=1)
        self.columnconfigure(2, weight=1)
        self.file_text.trace_add("write", self.handle_file_changed)
        ttk.Button(self, text="Browse", command=self.handle_file_browse_pressed).grid(column=3, row=0, sticky=E)

        self.silence = BooleanVar(self)
        ttk.Checkbutton(self, text="Silence", variable=self.silence, onvalue=True, offvalue=False
                        ).grid(column=0, row=1, columnspan=2, sticky=W)
        self.deepfry = BooleanVar(self)
        ttk.Checkbutton(self, text="Compress beyond recognition", variable=self.deepfry, onvalue=True, offvalue=False
                        ).grid(column=2, row=1, columnspan=2, sticky=W)

        run_button = ttk.Button(self, text="Run", command=self.handle_run_pressed)
        run_button.grid(column=0, row=2, sticky=(E, W))
        run_preview_button = ttk.Button(self, text="Run & Preview", command=self.handle_run_preview_pressed)
        run_preview_button.grid(column=1, row=2, columnspan=2, sticky=(E, W))
        run_quit_button = ttk.Button(self, text="Run & Quit", command=self.handle_run_quit_pressed)
        run_quit_button.grid(column=3, row=2, sticky=(E, W))

        self.progress = ttk.Progressbar(self, orient=HORIZONTAL, mode='determinate')
        self.progress.grid(column=0, row=3, columnspan=4, sticky=(N, S, E, W))

        self.logs = StringVar(self)
        logs_widget = ttk.Label(self, textvariable=self.logs, font="TkFixedFont",
                                justify='left')
        logs_widget.grid(column=0, row=4, columnspan=4, sticky=(N, S, E, W))
        self.rowconfigure(4, weight=1)

        for child in self.winfo_children():
            child.grid_configure(padx=2, pady=2)

        self.enable(False)

    def enable(self, enabled, run_enabled=None):
        if run_enabled is None:
            run_enabled = enabled
        state = 'disabled'
        if enabled:
            state = '!' + state
        run_state = 'disabled'
        if run_enabled:
            run_state = '!' + run_state
        self.state([state])
        for child in self.winfo_children():
            if 'text' in child.configure() and child['text'].startswith('Run'):
                child.state([run_state])
            else:
                child.state([state])

        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.deepfry = wx.CheckBox(options_panel, label="Compress beyond recognition")
        options_sizer.Add(self.deepfry, proportion=1, flag=wx.EXPAND, border=5)

        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()
    def handle_file_changed(self, *args):
        path = self.file_text.get()
        (folder, name) = os.path.split(path)
        try:
            os.stat(folder)
            self.run_panel.Enable()
            self.enable(True)
        except FileNotFoundError:
            self.run_panel.Disable()
            self.enable(True, False)

    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_file_browse_pressed(self, *args):
        filename = filedialog.asksaveasfilename(parent=self)
        if filename != '':
            self.file_text.set(filename)

    def handle_run_pressed(self, _, callback=lambda _: None):
        self.logs.Clear()
        self.run_panel.Disable()
    def handle_run_pressed(self, *args, callback=lambda _: None):
        self.logs.set('')
        self.progress['value'] = 0
        self.enable(False)
        real_args = self.get_ffmpeg_args()
        self.progress['maximum'] = float(self.get_frame_count())
        print(self.get_frame_count())
        input_args = real_args.input
        output_args = real_args.output
        output_path = self.file_text.GetValue()
        output_path = self.file_text.get()
        (folder, name) = os.path.split(output_path)
        (name, ext) = os.path.splitext(name)
        if self.silence.GetValue():
        if self.silence.get():
            output_args += ['-an']
        if ext == '.gif':
            filter_before = '[0:v] '


@@ 95,12 108,14 @@ class OutputPanel(wx.Panel):
                    output_args[i:i + 2] = []
                    break
            output_args += ['-filter_complex', filter_before + filter_during + filter_after]
        if self.deepfry.GetValue():
        if self.deepfry.get():
            if ext == '.mp3':
                output_args += ['-q:a', '9']
            else:
                output_args += ['-q:a', '0.1', '-crf', '51']
        args = ['ffmpeg', '-hide_banner', '-y'] + input_args + ['-i', self.input_path] + output_args + [output_path]
        args = ['ffmpeg', '-hide_banner', '-v', 'warning', '-stats', '-y'] + input_args + ['-i',
                                                                                           self.input_path] + output_args + [
                   output_path]
        print(args)

        def run():


@@ 110,35 125,36 @@ class OutputPanel(wx.Panel):
            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))
                    progress_data = re.match(r'^frame=\s*(\d+)', out_data)
                    print(out_data, end='')
                    if progress_data is not None:
                        self.progress['value'] = float(progress_data.group(1))
                    else:
                        self.logs.set(self.logs.get() + out_data)
            self.enable(True)
            callback(proc.returncode)

        threading.Thread(target=run).start()

    def handle_run_preview_pressed(self, _event):
    def handle_run_preview_pressed(self, *args):
        def preview(code):
            if code == 0:
                out_file = self.file_text.GetValue()
                out_file = self.file_text.get()
                subprocess.Popen(['ffplay', '-autoexit', out_file], creationflags=subprocess.CREATE_NO_WINDOW)

        self.handle_run_pressed(_event, callback=preview)
        self.handle_run_pressed(*args, callback=preview)

    def handle_run_quit_pressed(self, _event):
    def handle_run_quit_pressed(self, *args):
        def quit(code):
            if code == 0:
                parent = self.GetTopLevelParent()
                parent.Close(True)
                toplevel = self.winfo_toplevel()
                toplevel.destroy()

        self.handle_run_pressed(_event, callback=quit)
        self.handle_run_pressed(*args, callback=quit)

    def set_input_path(self, path, data):
        self.enable(data is not None)
        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)

M requirements.txt => requirements.txt +0 -1
@@ 1,2 1,1 @@
wxPython
cx_Freeze

M screenshot.png => screenshot.png +0 -0
M sources.py => sources.py +64 -69
@@ 4,8 4,10 @@ import os
import subprocess
import tempfile
import threading

import wx
from tkinter import *
from tkinter import filedialog
from tkinter import messagebox
from tkinter import ttk


def has_ytdl():


@@ 17,7 19,7 @@ def has_ytdl():
        return False


def update_ytdl(parent_win):
def update_ytdl(root):
    try:
        youtube_dl_found = subprocess.run(['where', 'youtube-dl'], stdout=subprocess.PIPE, text=True,
                                          creationflags=subprocess.CREATE_NO_WINDOW)


@@ 25,115 27,108 @@ def update_ytdl(parent_win):
        youtube_dl_found = subprocess.run(['which', 'youtube-dl'], stdout=subprocess.PIPE, text=True,
                                          creationflags=subprocess.CREATE_NO_WINDOW)
    if youtube_dl_found.returncode != 0:
        def poll():
            answer = wx.MessageBox("Could not find youtube-dl. Open vidslice README?", "Error", wx.YES_NO, parent_win)
            if answer == wx.YES:
                import webbrowser
                webbrowser.open("https://github.com/boringcactus/vidslice/blob/master/README.md")
            return

        wx.CallAfter(poll)
        answer = messagebox.askyesno(message="Could not find youtube-dl. Open vidslice README?", title="Error",
                                     icon='error', parent=root)
        if answer:
            import webbrowser
            webbrowser.open("https://github.com/boringcactus/vidslice/blob/master/README.md")
    youtube_dl_path = youtube_dl_found.stdout.split("\n")[0]
    old_mtime = os.stat(youtube_dl_path).st_mtime
    proc = subprocess.run(["youtube-dl", "-U"], stdout=subprocess.PIPE, text=True,
    proc = subprocess.run(["youtube-dl", "-U"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
                          creationflags=subprocess.CREATE_NO_WINDOW)
    if not proc.stdout.startswith("youtube-dl is up-to-date"):
    if not proc.stdout.startswith("youtube-dl is up-to-date") and not proc.stdout.startswith("ERROR"):
        while os.stat(youtube_dl_path).st_mtime == old_mtime:
            from time import sleep
            sleep(0.25)
    wx.CallAfter(lambda: wx.MessageBox("Updated youtube-dl successfully", "Complete", wx.OK, parent_win))
    messagebox.showinfo(message="Updated youtube-dl successfully", title="Complete", parent=root)


class SourcesPanel(wx.Panel):
class SourcesPanel(ttk.LabelFrame):
    """
    A Panel representing source info
    """

    def __init__(self, *args, **kw):
        super(SourcesPanel, self).__init__(*args, **kw)
        super(SourcesPanel, self).__init__(*args, text='Sources', **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)
            ttk.Label(self, text="URL").grid(column=0, row=0, sticky=(E, W))
            self.url_text = StringVar(self)
            ttk.Entry(self, textvariable=self.url_text).grid(column=1, row=0, sticky=(E, W))
            ttk.Button(self, text="Download", command=self.handle_url_download_pressed
                       ).grid(column=2, row=0, sticky=(E, W))
        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)
            ttk.Label(self, text="Could not find youtube-dl, can't download videos automatically"
                      ).grid(column=0, row=0, columnspan=3, sticky=(E, W))
            self.url_text = None

        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)
        ttk.Label(self, text="File").grid(column=0, row=1, sticky=(E, W))
        self.file_text = StringVar(self)
        self.file_text.trace_add("write", self.handle_file_changed)
        ttk.Entry(self, textvariable=self.file_text).grid(column=1, row=1, sticky=(E, W))
        self.columnconfigure(1, weight=1)
        ttk.Button(self, text="Browse", command=self.handle_file_browse_pressed).grid(column=2, row=1, sticky=(E, W))

        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))
        self.status_label = StringVar(self, "Status: Select a file")
        ttk.Label(self, textvariable=self.status_label).grid(column=0, row=2, columnspan=3, sticky=(E, W))

        main_sizer.AddGrowableCol(1, proportion=1)
        for child in self.winfo_children():
            child.grid_configure(padx=2, pady=2)

    def set_status(self, text):
        self.status_label.SetLabel("Status: " + text)
        self.status_label.set("Status: " + text)

    def handle_url_download_pressed(self, _):
    def handle_url_download_pressed(self, *args):
        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()
                'youtube-dl', '-o', file.name + '.%(ext)s', self.url_text.get()
            ], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
                creationflags=subprocess.CREATE_NO_WINDOW)
            while proc.poll() is None:
                out_data = proc.stdout.readline()
                if out_data != '':
                    wx.CallAfter(lambda: self.set_status("Downloading: " + out_data.strip()))
                    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))
                self.set_status("Downloaded!")
                self.file_text.set(output_file)
            else:
                error = ''.join(proc.stderr.readlines()).strip()
                wx.CallAfter(lambda: self.set_status("Couldn't download: " + error))
                self.set_status("Couldn't download: " + error)

        threading.Thread(target=download).start()

    def handle_file_browse_pressed(self, _):
        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):
        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, creationflags=subprocess.CREATE_NO_WINDOW)
        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 handle_file_browse_pressed(self, *args):
        filename = filedialog.askopenfilename(parent=self)
        if filename != '':
            self.file_text.set(filename)

    def handle_file_changed(self, *args):
        def probe():
            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.get()
            ], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
            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)

        threading.Thread(target=probe).start()

    def on_update(self, callback):
        self.update_listeners.append(callback)

    def get_file(self):
        return self.file_text.GetValue()
        return self.file_text.get()

M vidslice.py => vidslice.py +65 -99
@@ 1,10 1,9 @@
import json
import subprocess
import sys
import urllib.request

import wx
import wx.adv
from tkinter import *
from tkinter import messagebox
from tkinter import ttk

from options import OptionsPanel
from output import OutputPanel


@@ 19,8 18,9 @@ def check_update(parent):
        latest_release_obj = json.load(latest_release_response)
    newest_version = latest_release_obj['tag_name'].lstrip('v')
    if VERSION != newest_version:
        answer = wx.MessageBox("vidslice update available. download?", "Update", wx.YES_NO, parent)
        if answer == wx.YES:
        open_update = messagebox.askyesno(message="vidslice update available. download?", title="Update",
                                          parent=parent)
        if open_update:
            import webbrowser

            webbrowser.open("https://github.com/boringcactus/vidslice/releases/latest")


@@ 35,118 35,84 @@ def has_ffmpeg():
        return False


class VidsliceFrame(wx.Frame):
    """
    A Frame that contains vidslice logic
    """

    def __init__(self, *args, **kw):
        # ensure the parent's __init__ is called
        super(VidsliceFrame, self).__init__(*args, **kw)
class VidsliceFrame:
    def __init__(self, root: Tk):
        self.root = root
        root.title('vidslice')

        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)
        mainframe = ttk.Frame(root, padding="3 3 12 12")
        mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
        root.columnconfigure(0, weight=1)
        root.rowconfigure(0, weight=1)

        # set up sources panel
        self.sources_panel = SourcesPanel(main)
        main_sizer.Add(self.sources_panel, wx.GBPosition(0, 0), wx.GBSpan(1, 2), flag=wx.EXPAND)
        self.sources_panel = SourcesPanel(mainframe)
        self.sources_panel.grid(column=0, row=0, columnspan=2, sticky=(W, E), padx=5, pady=5)

        # 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)
        self.options_panel = OptionsPanel(mainframe)
        self.options_panel.grid(column=0, row=1, sticky=(W, E, N), padx=5, pady=5)
        mainframe.rowconfigure(1, weight=1)
        self.sources_panel.on_update(self.options_panel.update_info)

        # 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.output_panel = OutputPanel(mainframe, get_ffmpeg_args=self.options_panel.ffmpeg_opts,
                                        get_frame_count=self.options_panel.frame_count)
        self.output_panel.grid(column=1, row=1, sticky=(W, E, N, S), padx=5, pady=5)
        mainframe.columnconfigure(1, weight=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()

        size = root_sizer.GetMinSize()
        self.SetMinClientSize(size)
        self.make_menu_bar(root)

        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.
        This method builds a set of menus and binds handlers to be called
        when the menu item is selected.
        """

        # Make a file menu with Hello and Exit items
        file_menu = wx.Menu()
        # The "\t..." syntax defines an accelerator key that also triggers
        # the same event
        update_item = file_menu.Append(-1, "Update youtube-dl")
        file_menu.AppendSeparator()
        # When using a stock ID we don't need to specify the menu item's
        # label
        exit_item = file_menu.Append(wx.ID_EXIT)

        # Now a help menu for the about item
        help_menu = wx.Menu()
        about_item = help_menu.Append(wx.ID_ABOUT)

        # Make the menu bar and add the two menus to it. The '&' defines
        # that the next letter is the "mnemonic" for the menu item. On the
        # platforms that support it those letters are underlined and can be
        # triggered from the keyboard.
        menu_bar = wx.MenuBar()
        menu_bar.Append(file_menu, "&File")
        menu_bar.Append(help_menu, "&Help")

        # Give the menu bar to the frame
        self.SetMenuBar(menu_bar)

        # Finally, associate a handler function with the EVT_MENU event for
        # each of the menu items. That means that when that menu item is
        # activated then the associated handler function will be called.
        self.Bind(wx.EVT_MENU, self.on_update, update_item)
        self.Bind(wx.EVT_MENU, self.on_exit, exit_item)
        self.Bind(wx.EVT_MENU, self.on_about, about_item)

    def on_exit(self, event):
        """Close the frame, terminating the application."""
        self.Close(True)

    def on_update(self, event):
        import threading
        threading.Thread(target=update_ytdl, args=(self,)).start()
    def make_menu_bar(self, root):
        root.option_add('*tearOff', FALSE)

    def on_about(self, event):
        """Display an About Dialog"""
        info = wx.adv.AboutDialogInfo()
        info.SetName("vidslice")
        info.SetVersion(VERSION)
        info.SetDescription("video manipulator wrapping youtube-dl and ffmpeg")
        info.SetWebSite("https://github.com/boringcactus/vidslice")
        menubar = Menu(root)
        root['menu'] = menubar

        wx.adv.AboutBox(info)
        file_menu = Menu(menubar)
        menubar.add_cascade(menu=file_menu, label='File', underline=0)
        file_menu.add_command(label="Update youtube-dl", command=self.on_update, underline=0)
        file_menu.add_separator()
        file_menu.add_command(label='Exit', command=self.on_exit, underline=1)

        help_menu = Menu(menubar)
        menubar.add_cascade(menu=help_menu, label='Help', underline=0)
        help_menu.add_command(label='About', command=self.on_about, underline=0)

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()
    def on_exit(self, *args):
        self.root.destroy()

    def on_update(self, *args):
        import threading
        threading.Thread(target=update_ytdl, args=(self.root,)).start()

    def on_about(self, *args):
        messagebox.showinfo(message=f"vidslice {VERSION}")


def check_ffmpeg(root: Tk):
    if not has_ffmpeg():
        answer = wx.MessageBox("Could not find ffmpeg. Open vidslice README?", "Error", wx.YES_NO, None)
        if answer == wx.YES:
        open_readme = messagebox.askyesno(message="Could not find ffmpeg. Open vidslice README?", title="Error",
                                          icon='error', parent=root)
        if open_readme:
            import webbrowser

            webbrowser.open("https://github.com/boringcactus/vidslice/blob/master/README.md")
    else:
        frm = VidsliceFrame(None, title='vidslice')
        app.SetTopWindow(frm)
        frm.Show()
        check_update(frm)
    app.MainLoop()
            root.after(1000, root.destroy)


def main():
    root = Tk()
    frame = VidsliceFrame(root)
    root.after_idle(check_ffmpeg, root)
    root.after(1000, check_update, root)
    root.mainloop()


if __name__ == '__main__':
    main()