diff --git a/main.py b/main.py index f8c2dc9..2c64ac2 100755 --- a/main.py +++ b/main.py @@ -5170,6 +5170,10 @@ class MainWindow(QMainWindow): self._btn_edit_clip.setToolTip( "Open the current audio area in the destructive editor") self._btn_edit_clip.clicked.connect(self._on_edit_clip) + self._btn_add_library = QPushButton("+ To library") + self._btn_add_library.setToolTip( + "Add the current audio area (with edits) to the clip library") + self._btn_add_library.clicked.connect(self._on_add_selection_to_library) self._wave.selection_changed.connect(self._on_wave_selection_changed) self._wave.view_changed.connect(self._on_wave_view_changed) self._wave_pending_view = None @@ -5546,7 +5550,8 @@ class MainWindow(QMainWindow): g.addWidget(self._wave, 4, 0, 1, 4) g.addWidget(self._btn_wave_refresh, 5, 0, 1, 2) g.addWidget(self._btn_audio_play, 5, 2, 1, 2) - g.addWidget(self._btn_edit_clip, 6, 0, 1, 4) # own row: destructive editor entry + g.addWidget(self._btn_edit_clip, 6, 0, 1, 2) # destructive editor entry + g.addWidget(self._btn_add_library, 6, 2, 1, 2) # straight into the clip library g.addWidget(self._btn_extract_audio, 7, 0, 1, 4) g.setColumnStretch(4, 1) # spare gutter column clusters label/field pairs to the left g.setRowStretch(8, 1) # anchor content to the top @@ -7388,6 +7393,48 @@ class MainWindow(QMainWindow): self._merge_temps.add(tmp) # reuse the close-time temp sweep self._open_audio_editor(tmp) + def _on_add_selection_to_library(self) -> None: + """Render the current audio area (with edits) into a managed library + folder and add it to the clip library — no save dialog.""" + if not self._file_path: + self._show_status("Load a video first", 3000) + return + start = self._cursor + dur = self._spn_audio_len.value() + base = (self._settings.value("audio_extract_dir", "") + or self._tab_export_folder() + or os.path.dirname(self._file_path)) + lib_dir = (self._settings.value("audio_library_dir", "") + or os.path.join(base, "audio_library")) + self._settings.setValue("audio_library_dir", lib_dir) + try: + os.makedirs(lib_dir, exist_ok=True) + except OSError: + self._show_status("Could not create the library folder", 4000) + return + stem = os.path.splitext(os.path.basename(self._file_path))[0] + out = os.path.join(lib_dir, f"{stem}_{start:.2f}-{start + dur:.2f}s.wav") + cmd = build_audio_clip_command(self._file_path, start, dur, out, + filters=self._current_edit_filters() or None) + self._show_status("Adding to library…") + QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) + try: + proc = subprocess.run(cmd, capture_output=True, timeout=120) + except Exception: + proc = None + finally: + QApplication.restoreOverrideCursor() + if proc is not None and proc.returncode == 0 and os.path.exists(out): + self._scan_panel._library.add_clip(out) + self._show_status(f"Added to library: {os.path.basename(out)}", 4000) + else: + try: + if os.path.exists(out): + os.remove(out) + except OSError: + pass + self._show_status("Could not add the clip to the library", 3000) + def _open_audio_editor(self, path: str) -> None: """Open the destructive editor on *path*; if the user Saves-as, add the result to the clip library.""" diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index 9a0cd5e..00b9636 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -448,6 +448,38 @@ def test_extract_no_edits_passes_no_filters(win, monkeypatch, tmp_path): assert seen["filters"] is None +def test_add_selection_to_library(win, tmp_path, monkeypatch): + import main as m + win._file_path = "/vids/Chloe.mp4" + win._cursor = 7.0 + win._spn_audio_len.setValue(3.0) + win._settings.setValue("audio_extract_dir", str(tmp_path)) + win._settings.setValue("audio_library_dir", "") # force derive + seen = {} + def fake_cmd(inp, start, dur, out, filters=None): + seen["out"] = out; seen["start"] = start; seen["dur"] = dur; return ["true"] + monkeypatch.setattr(m, "build_audio_clip_command", fake_cmd) + monkeypatch.setattr(m.subprocess, "run", + lambda *a, **k: type("P", (), {"returncode": 0})()) + monkeypatch.setattr(m.os.path, "exists", lambda p: True) + monkeypatch.setattr(m, "probe_duration", lambda p: 3.0) + lib = win._scan_panel._library + lib._key = "audio_library_addsel_test" + win._settings.setValue(lib._key, []) + lib._list.clear() + win._on_add_selection_to_library() + # rendered the current area with the managed-folder auto-name + assert seen["start"] == 7.0 and seen["dur"] == 3.0 + assert os.path.join("audio_library", "Chloe_7.00-10.00s.wav") in seen["out"] + # and it was added to the library + assert seen["out"] in lib.clips() + + +def test_add_selection_to_library_no_file_safe(win): + win._file_path = "" + win._on_add_selection_to_library() # must not raise + + def test_waveform_strip_present_and_safe(win): from PyQt6.QtWidgets import QPushButton assert win._wave is not None