wrote base source code
This commit is contained in:
23
README.md
Normal file
23
README.md
Normal file
@@ -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.
|
||||||
193
main.py
Normal file
193
main.py
Normal file
@@ -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()
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
wxPython>=4.2.1
|
||||||
Reference in New Issue
Block a user