commit 5ab9d2e36b43dec993a59a82196f3bec527b9415 Author: korn20123 Date: Wed Feb 11 15:23:53 2026 +0100 wrote base source code diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ee5fd7 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Audio/Video Converter (wxPython + FFmpeg) + +Simple GUI converter that wraps `ffmpeg` for audio/video format conversion. + +## Requirements +- Python 3.9+ +- `ffmpeg` available on PATH + +## Install +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +``` + +## Run +```powershell +python main.py +``` + +## Notes +- The app does not bundle `ffmpeg`. Install it separately and ensure `ffmpeg` is on PATH. +- Conversion runs in a background thread; the UI stays responsive. diff --git a/main.py b/main.py new file mode 100644 index 0000000..a4d706b --- /dev/null +++ b/main.py @@ -0,0 +1,193 @@ +import os +import threading +import shutil +import subprocess +import wx + +AUDIO_FORMATS = ["mp3", "wav", "aac", "flac", "ogg", "m4a"] +VIDEO_FORMATS = ["mp4", "mkv", "mov", "webm"] + + +class ConverterFrame(wx.Frame): + def __init__(self): + super().__init__(parent=None, title="Audio/Video Converter", size=(720, 460)) + panel = wx.Panel(self) + + self.input_picker = wx.FilePickerCtrl(panel, message="Select input file") + self.mode_choice = wx.Choice(panel, choices=["Audio", "Video"]) + self.mode_choice.SetSelection(0) + self.format_choice = wx.Choice(panel, choices=AUDIO_FORMATS) + self.format_choice.SetSelection(0) + self.output_picker = wx.FilePickerCtrl( + panel, + message="Select output file", + style=wx.FLP_SAVE | wx.FLP_OVERWRITE_PROMPT, + ) + self.audio_bitrate = wx.TextCtrl(panel, value="", size=(120, -1)) + self.video_bitrate = wx.TextCtrl(panel, value="", size=(120, -1)) + self.copy_streams = wx.CheckBox(panel, label="Copy streams (fast, no re-encode)") + self.start_btn = wx.Button(panel, label="Convert") + self.log_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY) + self.status = wx.StaticText(panel, label="Ready") + + self.audio_bitrate.SetHint("e.g. 192k") + self.video_bitrate.SetHint("e.g. 2000k") + + form = wx.FlexGridSizer(cols=3, hgap=8, vgap=8) + form.Add(wx.StaticText(panel, label="Input"), 0, wx.ALIGN_CENTER_VERTICAL) + form.Add(self.input_picker, 1, wx.EXPAND) + form.Add((1, 1)) + + form.Add(wx.StaticText(panel, label="Mode"), 0, wx.ALIGN_CENTER_VERTICAL) + form.Add(self.mode_choice, 0) + form.Add((1, 1)) + + form.Add(wx.StaticText(panel, label="Format"), 0, wx.ALIGN_CENTER_VERTICAL) + form.Add(self.format_choice, 0) + form.Add((1, 1)) + + form.Add(wx.StaticText(panel, label="Output"), 0, wx.ALIGN_CENTER_VERTICAL) + form.Add(self.output_picker, 1, wx.EXPAND) + form.Add((1, 1)) + + form.Add(wx.StaticText(panel, label="Audio bitrate"), 0, wx.ALIGN_CENTER_VERTICAL) + form.Add(self.audio_bitrate, 0) + form.Add(wx.StaticText(panel, label="Optional"), 0, wx.ALIGN_CENTER_VERTICAL) + + form.Add(wx.StaticText(panel, label="Video bitrate"), 0, wx.ALIGN_CENTER_VERTICAL) + form.Add(self.video_bitrate, 0) + form.Add(wx.StaticText(panel, label="Optional"), 0, wx.ALIGN_CENTER_VERTICAL) + + form.Add(wx.StaticText(panel, label=""), 0) + form.Add(self.copy_streams, 0) + form.Add((1, 1)) + + form.AddGrowableCol(1, 1) + + main = wx.BoxSizer(wx.VERTICAL) + main.Add(form, 0, wx.EXPAND | wx.ALL, 12) + main.Add(self.start_btn, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 12) + main.Add(self.log_ctrl, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 12) + main.Add(self.status, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 12) + + panel.SetSizer(main) + + self.mode_choice.Bind(wx.EVT_CHOICE, self.on_mode_change) + self.format_choice.Bind(wx.EVT_CHOICE, self.on_format_change) + self.input_picker.Bind(wx.EVT_FILEPICKER_CHANGED, self.on_input_change) + self.start_btn.Bind(wx.EVT_BUTTON, self.on_start) + + self.update_format_choices() + self.Center() + + def on_mode_change(self, _event): + self.update_format_choices() + self.suggest_output_path() + + def on_format_change(self, _event): + self.suggest_output_path() + + def on_input_change(self, _event): + self.suggest_output_path() + + def update_format_choices(self): + mode = self.mode_choice.GetStringSelection() + if mode == "Audio": + choices = AUDIO_FORMATS + else: + choices = VIDEO_FORMATS + self.format_choice.Set(choices) + self.format_choice.SetSelection(0) + + def suggest_output_path(self): + in_path = self.input_picker.GetPath() + if not in_path: + return + out_ext = self.format_choice.GetStringSelection() + base, _ = os.path.splitext(in_path) + suggested = base + "." + out_ext + self.output_picker.SetPath(suggested) + + def on_start(self, _event): + if not shutil.which("ffmpeg"): + wx.MessageBox("ffmpeg not found on PATH.", "Error", wx.ICON_ERROR) + return + + in_path = self.input_picker.GetPath() + out_path = self.output_picker.GetPath() + if not in_path or not out_path: + wx.MessageBox("Please choose input and output files.", "Error", wx.ICON_ERROR) + return + + self.log_ctrl.Clear() + self.status.SetLabel("Running...") + self.start_btn.Disable() + + thread = threading.Thread(target=self.run_ffmpeg, args=(in_path, out_path), daemon=True) + thread.start() + + def run_ffmpeg(self, in_path, out_path): + mode = self.mode_choice.GetStringSelection() + a_bitrate = self.audio_bitrate.GetValue().strip() + v_bitrate = self.video_bitrate.GetValue().strip() + copy = self.copy_streams.GetValue() + + cmd = ["ffmpeg", "-y", "-i", in_path] + + if mode == "Audio": + cmd.append("-vn") + if copy: + cmd += ["-c:a", "copy"] + elif a_bitrate: + cmd += ["-b:a", a_bitrate] + else: + if copy: + cmd += ["-c:v", "copy", "-c:a", "copy"] + else: + if v_bitrate: + cmd += ["-b:v", v_bitrate] + if a_bitrate: + cmd += ["-b:a", a_bitrate] + + cmd.append(out_path) + + self.append_log("Command: " + " ".join(cmd)) + + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + for line in proc.stdout: + self.append_log(line.rstrip()) + proc.wait() + if proc.returncode == 0: + self.finish("Done") + else: + self.finish("Failed") + except Exception as exc: + self.append_log(f"Error: {exc}") + self.finish("Failed") + + def append_log(self, text): + wx.CallAfter(self.log_ctrl.AppendText, text + "\n") + + def finish(self, status_text): + wx.CallAfter(self.status.SetLabel, status_text) + wx.CallAfter(self.start_btn.Enable) + + +class App(wx.App): + def OnInit(self): + frame = ConverterFrame() + frame.Show() + return True + + +if __name__ == "__main__": + app = App() + app.MainLoop() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b804d30 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +wxPython>=4.2.1