diff options
-rw-r--r-- | options.py | 155 | ||||
-rw-r--r-- | output.py | 174 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | screenshot.png | bin | 29314 -> 21013 bytes | |||
-rw-r--r-- | sources.py | 133 | ||||
-rw-r--r-- | vidslice.py | 164 |
6 files changed, 295 insertions, 332 deletions
@@ -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()) @@ -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) diff --git a/requirements.txt b/requirements.txt index 5767b25..c95cfcb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -wxPython cx_Freeze diff --git a/screenshot.png b/screenshot.png Binary files differindex d6d0e15..bb8aeb5 100644 --- a/screenshot.png +++ b/screenshot.png @@ -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() diff --git a/vidslice.py b/vidslice.py index dc37412..632e426 100644 --- a/vidslice.py +++ b/vidslice.py @@ -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() |