diff --git a/main.py b/main.py index 1a513c5..4a114f2 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import os +import json import threading import shutil import subprocess @@ -34,6 +35,8 @@ class ConverterFrame(wx.Frame): self.activation_bytes = wx.TextCtrl(panel, value="", size=(120, -1)) self.copy_streams = wx.CheckBox(panel, label="Co&py streams (fast, no re-encode)") self.copy_streams.SetForegroundColour(label_color) + self.split_mp3_choice = wx.Choice(panel, choices=["No", "Yes"]) + self.split_mp3_choice.SetSelection(0) 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") @@ -63,6 +66,7 @@ class ConverterFrame(wx.Frame): self.format_choice.SetName("Output format") self.configure_file_picker_accessibility(self.output_picker, "Output file") self.copy_streams.SetName("Copy streams") + self.split_mp3_choice.SetName("Split MP3 into captures") self.start_btn.SetName("Convert") self.play_btn.SetName("Play or pause") self.stop_btn.SetName("Stop playback") @@ -74,6 +78,7 @@ class ConverterFrame(wx.Frame): self.video_bitrate.SetHelpText("Video bitrate") self.activation_bytes.SetHelpText("Activation bytes") self.copy_streams.SetHelpText("Copy streams") + self.split_mp3_choice.SetHelpText("Split MP3 output into captures based on chapters") self.start_btn.SetHelpText("Convert") self.play_btn.SetHelpText("Play or pause") self.stop_btn.SetHelpText("Stop playback") @@ -90,6 +95,7 @@ class ConverterFrame(wx.Frame): self.add_labeled_control(panel, form, "Video b&itrate", self.video_bitrate, "Video bitrate") self.add_labeled_control(panel, form, "A&ctivation bytes", self.activation_bytes, "Activation bytes") form.Add(self.copy_streams, 0, wx.EXPAND | wx.BOTTOM, 6) + self.add_labeled_control(panel, form, "Split MP3 into &captures", self.split_mp3_choice, "Split MP3 into captures") main = wx.BoxSizer(wx.VERTICAL) main.Add(form, 0, wx.EXPAND | wx.ALL, 10) @@ -138,6 +144,7 @@ class ConverterFrame(wx.Frame): choices = VIDEO_FORMATS self.format_choice.Set(choices) self.format_choice.SetSelection(0) + self.update_split_option_state() def suggest_output_path(self): in_path = self.input_picker.GetPath() @@ -147,6 +154,13 @@ class ConverterFrame(wx.Frame): base, _ = os.path.splitext(in_path) suggested = base + "." + out_ext self.output_picker.SetPath(suggested) + self.update_split_option_state() + + def update_split_option_state(self): + is_mp3 = self.format_choice.GetStringSelection().lower() == "mp3" + self.split_mp3_choice.Enable(is_mp3) + if not is_mp3: + self.split_mp3_choice.SetSelection(0) def build_player(self, panel): player = wx.BoxSizer(wx.VERTICAL) @@ -352,6 +366,9 @@ class ConverterFrame(wx.Frame): v_bitrate = self.video_bitrate.GetValue().strip() activation_bytes = self.activation_bytes.GetValue().strip() copy = self.copy_streams.GetValue() + split_mp3 = self.split_mp3_choice.GetStringSelection().lower() == "yes" + output_format = self.format_choice.GetStringSelection().lower() + should_split_mp3 = split_mp3 and output_format == "mp3" cmd = ["ffmpeg", "-y"] if activation_bytes: @@ -373,7 +390,26 @@ class ConverterFrame(wx.Frame): if a_bitrate: cmd += ["-b:a", a_bitrate] - cmd.append(out_path) + if should_split_mp3: + chapter_times = self.get_chapter_end_times(in_path) + if chapter_times: + output_pattern = self.build_capture_output_pattern(out_path) + cmd += [ + "-f", + "segment", + "-segment_times", + ",".join(chapter_times), + "-reset_timestamps", + "1", + "-segment_format", + "mp3", + output_pattern, + ] + else: + self.append_log("No chapter markers found; writing a single MP3 file.") + cmd.append(out_path) + else: + cmd.append(out_path) self.append_log("Command: " + " ".join(cmd)) @@ -404,6 +440,58 @@ class ConverterFrame(wx.Frame): wx.CallAfter(self.status.SetLabel, status_text) wx.CallAfter(self.start_btn.Enable) + def build_capture_output_pattern(self, out_path): + base, ext = os.path.splitext(out_path) + return f"{base}_capture_%03d{ext}" + + def get_chapter_end_times(self, in_path): + if not shutil.which("ffprobe"): + self.append_log("ffprobe not found on PATH; cannot split by chapters.") + return [] + cmd = [ + "ffprobe", + "-v", + "error", + "-print_format", + "json", + "-show_chapters", + "-i", + in_path, + ] + try: + proc = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + if proc.returncode != 0: + self.append_log("ffprobe failed; cannot split by chapters.") + return [] + data = json.loads(proc.stdout or "{}") + except Exception as exc: + self.append_log(f"ffprobe error: {exc}") + return [] + + chapters = data.get("chapters", []) + end_times = [] + for chapter in chapters: + end_time = chapter.get("end_time") + if end_time is None: + continue + try: + end_times.append(float(end_time)) + except (TypeError, ValueError): + continue + if len(end_times) <= 1: + return [] + end_times = end_times[:-1] + formatted = [] + for value in end_times: + formatted.append(f"{value:.3f}".rstrip("0").rstrip(".")) + return formatted + class App(wx.App): def OnInit(self):