PyVideoKit-GUI 0.3.0__py3-none-any.whl → 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,6 @@
1
- __version__ = "0.1.0"
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("PyVideoKit-GUI")
5
+ except PackageNotFoundError:
6
+ __version__ = "unknown"
@@ -1,26 +1,84 @@
1
+ import shutil
2
+ import subprocess
1
3
  from pathlib import Path
2
4
 
3
5
  from PySide6.QtCore import Qt
4
6
  from PySide6.QtWidgets import (
7
+ QAbstractItemView,
5
8
  QFileDialog,
6
9
  QHBoxLayout,
7
10
  QLabel,
8
11
  QLineEdit,
12
+ QListWidget,
9
13
  QProgressBar,
10
14
  QPushButton,
11
15
  QVBoxLayout,
12
16
  QWidget,
13
17
  )
14
18
 
15
- from pyvideokit_gui._worker import Worker
19
+ from pyvideokit_gui._worker import BatchWorker, Worker
16
20
 
17
21
  _VIDEO_FILTER = "Video files (*.mkv *.mp4 *.mov *.avi *.webm);;All files (*)"
22
+ _VIDEO_EXTS = {".mkv", ".mp4", ".mov", ".avi", ".webm"}
23
+
24
+ _COMPLETE_SOUND = "/usr/share/sounds/freedesktop/stereo/complete.oga"
25
+ _SOUND_CMD: list[str] | None = None
26
+ for _cmd, _extra in [("paplay", []), ("pw-play", []), ("ffplay", ["-nodisp", "-autoexit", "-loglevel", "quiet"])]:
27
+ if shutil.which(_cmd):
28
+ _SOUND_CMD = [_cmd, *_extra, _COMPLETE_SOUND]
29
+ break
30
+
31
+
32
+ class DropLineEdit(QLineEdit):
33
+ def __init__(self, *args, **kwargs):
34
+ super().__init__(*args, **kwargs)
35
+ self.setAcceptDrops(True)
36
+
37
+ def dragEnterEvent(self, event):
38
+ if event.mimeData().hasUrls():
39
+ event.acceptProposedAction()
40
+
41
+ def dropEvent(self, event):
42
+ urls = event.mimeData().urls()
43
+ if urls:
44
+ self.setText(urls[0].toLocalFile())
45
+
46
+
47
+ class DropListWidget(QListWidget):
48
+ def __init__(self, *args, **kwargs):
49
+ super().__init__(*args, **kwargs)
50
+ self.setAcceptDrops(True)
51
+ self.setDragDropMode(QAbstractItemView.DragDrop)
52
+ self.setDefaultDropAction(Qt.MoveAction)
53
+
54
+ def dragEnterEvent(self, event):
55
+ if event.mimeData().hasUrls():
56
+ event.acceptProposedAction()
57
+ else:
58
+ super().dragEnterEvent(event)
59
+
60
+ def dragMoveEvent(self, event):
61
+ if event.mimeData().hasUrls():
62
+ event.acceptProposedAction()
63
+ else:
64
+ super().dragMoveEvent(event)
65
+
66
+ def dropEvent(self, event):
67
+ if event.mimeData().hasUrls():
68
+ for url in event.mimeData().urls():
69
+ path = url.toLocalFile()
70
+ if Path(path).suffix.lower() in _VIDEO_EXTS:
71
+ self.addItem(path)
72
+ event.acceptProposedAction()
73
+ else:
74
+ super().dropEvent(event)
18
75
 
19
76
 
20
77
  class BasePanel(QWidget):
21
78
  def __init__(self, parent=None):
22
79
  super().__init__(parent)
23
80
  self._worker = None
81
+ self.setAcceptDrops(True)
24
82
  root = QVBoxLayout(self)
25
83
  root.setAlignment(Qt.AlignTop)
26
84
  root.setSpacing(10)
@@ -29,6 +87,22 @@ class BasePanel(QWidget):
29
87
  root.addStretch()
30
88
  self._build_run_section(root)
31
89
 
90
+ def dragEnterEvent(self, event):
91
+ if event.mimeData().hasUrls():
92
+ event.acceptProposedAction()
93
+
94
+ def dropEvent(self, event):
95
+ paths = [u.toLocalFile() for u in event.mimeData().urls()]
96
+ if paths:
97
+ self._on_panel_drop(paths)
98
+
99
+ def _on_panel_drop(self, paths: list):
100
+ if hasattr(self, "_inputs"):
101
+ for p in paths:
102
+ self._inputs.addItem(p)
103
+ elif hasattr(self, "_input"):
104
+ self._input.setText(paths[0])
105
+
32
106
  def _build_ui(self, layout: QVBoxLayout):
33
107
  raise NotImplementedError
34
108
 
@@ -40,7 +114,7 @@ class BasePanel(QWidget):
40
114
  def _input_row(self, layout, label="Input file", filter=_VIDEO_FILTER) -> QLineEdit:
41
115
  layout.addWidget(QLabel(label))
42
116
  row = QHBoxLayout()
43
- le = QLineEdit()
117
+ le = DropLineEdit()
44
118
  le.setPlaceholderText("Select a file…")
45
119
  btn = QPushButton("Browse…")
46
120
  row.addWidget(le)
@@ -52,7 +126,7 @@ class BasePanel(QWidget):
52
126
  def _output_row(self, layout, label="Output (optional)") -> QLineEdit:
53
127
  layout.addWidget(QLabel(label))
54
128
  row = QHBoxLayout()
55
- le = QLineEdit()
129
+ le = DropLineEdit()
56
130
  le.setPlaceholderText("Leave empty for default…")
57
131
  btn = QPushButton("Browse…")
58
132
  row.addWidget(le)
@@ -71,6 +145,50 @@ class BasePanel(QWidget):
71
145
  if path:
72
146
  le.setText(path)
73
147
 
148
+ def _output_dir_row(self, layout, label="Output directory (optional)") -> QLineEdit:
149
+ layout.addWidget(QLabel(label))
150
+ row = QHBoxLayout()
151
+ le = DropLineEdit()
152
+ le.setPlaceholderText("Leave empty to save next to each input file…")
153
+ btn = QPushButton("Browse…")
154
+ row.addWidget(le)
155
+ row.addWidget(btn)
156
+ layout.addLayout(row)
157
+ btn.clicked.connect(lambda: self._browse_dir(le))
158
+ return le
159
+
160
+ def _browse_dir(self, le: QLineEdit):
161
+ path = QFileDialog.getExistingDirectory(self, "Select output directory", "")
162
+ if path:
163
+ le.setText(path)
164
+
165
+ # ── batch input list ──────────────────────────────────────────────────
166
+
167
+ def _batch_input_list(self, layout, label="Input files") -> "DropListWidget":
168
+ layout.addWidget(QLabel(label))
169
+ lst = DropListWidget()
170
+ lst.setSelectionMode(QAbstractItemView.ExtendedSelection)
171
+ lst.setMinimumHeight(120)
172
+ layout.addWidget(lst)
173
+ btns = QHBoxLayout()
174
+ add_btn = QPushButton("Add files…")
175
+ rm_btn = QPushButton("Remove selected")
176
+ btns.addWidget(add_btn)
177
+ btns.addWidget(rm_btn)
178
+ layout.addLayout(btns)
179
+ add_btn.clicked.connect(lambda: self._batch_add(lst))
180
+ rm_btn.clicked.connect(lambda: self._batch_remove(lst))
181
+ return lst
182
+
183
+ def _batch_add(self, lst):
184
+ paths, _ = QFileDialog.getOpenFileNames(self, "Add video files", "", _VIDEO_FILTER)
185
+ for p in paths:
186
+ lst.addItem(p)
187
+
188
+ def _batch_remove(self, lst):
189
+ for item in lst.selectedItems():
190
+ lst.takeItem(lst.row(item))
191
+
74
192
  # ── worker ────────────────────────────────────────────────────────────
75
193
 
76
194
  def _start_worker(self, fn, *args, **kwargs):
@@ -83,24 +201,65 @@ class BasePanel(QWidget):
83
201
  self._worker.error.connect(self._on_error)
84
202
  self._worker.start()
85
203
 
204
+ def _start_batch(self, fn, paths, make_kwargs):
205
+ self._progress.setValue(0)
206
+ self._progress_item.setValue(0)
207
+ self._status.setText(f"Starting batch ({len(paths)} files)…")
208
+ self._run_btn.setEnabled(False)
209
+ self._worker = BatchWorker(fn, paths, make_kwargs)
210
+ self._worker.progress.connect(lambda pct: self._progress.setValue(int(pct)))
211
+ self._worker.item_progress.connect(lambda pct: self._progress_item.setValue(int(pct)))
212
+ self._worker.item_status.connect(self._status.setText)
213
+ self._worker.finished.connect(self._on_batch_finished)
214
+ self._worker.start()
215
+
216
+ def _on_batch_finished(self, errors):
217
+ n = len(self._worker._paths)
218
+ self._run_btn.setEnabled(True)
219
+ if errors:
220
+ self._progress.setValue(0)
221
+ self._progress_item.setValue(0)
222
+ self._status.setText(f"❌ {len(errors)} error(s) — {errors[0]}")
223
+ else:
224
+ self._progress.setValue(100)
225
+ self._progress_item.setValue(100)
226
+ self._status.setText(f"✅ Batch complete ({n} files)")
227
+ self._play_complete_sound()
228
+
86
229
  # ── run section ───────────────────────────────────────────────────────
87
230
 
88
231
  def _build_run_section(self, layout):
89
232
  self._run_btn = QPushButton("▶ Run")
90
233
  self._run_btn.setFixedHeight(36)
91
234
  self._run_btn.clicked.connect(self._run)
235
+ layout.addWidget(self._run_btn)
236
+
237
+ if hasattr(self, "_inputs"):
238
+ self._progress_item = QProgressBar()
239
+ self._progress_item.setRange(0, 100)
240
+ self._progress_item.setValue(0)
241
+ self._progress_item.setFormat("Current: %p%")
242
+ layout.addWidget(self._progress_item)
243
+
92
244
  self._progress = QProgressBar()
93
245
  self._progress.setRange(0, 100)
94
246
  self._progress.setValue(0)
95
- self._status = QLabel("")
96
- layout.addWidget(self._run_btn)
247
+ if hasattr(self, "_inputs"):
248
+ self._progress.setFormat("Overall: %p%")
97
249
  layout.addWidget(self._progress)
250
+
251
+ self._status = QLabel("")
98
252
  layout.addWidget(self._status)
99
253
 
254
+ def _play_complete_sound(self):
255
+ if _SOUND_CMD:
256
+ subprocess.Popen(_SOUND_CMD, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
257
+
100
258
  def _on_finished(self, result):
101
259
  self._progress.setValue(100)
102
260
  self._status.setText(f"✅ Done → {Path(str(result)).name}")
103
261
  self._run_btn.setEnabled(True)
262
+ self._play_complete_sound()
104
263
 
105
264
  def _on_error(self, msg):
106
265
  self._progress.setValue(0)
@@ -13,7 +13,7 @@ class MainWindow(QMainWindow):
13
13
  def __init__(self):
14
14
  super().__init__()
15
15
  self.setWindowTitle("PyVideoKit")
16
- self.setMinimumSize(660, 460)
16
+ self.setMinimumSize(670, 460)
17
17
 
18
18
  tabs = QTabWidget()
19
19
  tabs.addTab(ConvertPanel(), "🎞️ Convert to FFV1")
pyvideokit_gui/_worker.py CHANGED
@@ -19,3 +19,37 @@ class Worker(QThread):
19
19
  self.finished.emit(result)
20
20
  except Exception as e:
21
21
  self.error.emit(str(e))
22
+
23
+
24
+ class BatchWorker(QThread):
25
+ progress = Signal(float)
26
+ item_progress = Signal(float)
27
+ item_status = Signal(str)
28
+ finished = Signal(list)
29
+
30
+ def __init__(self, fn, paths, make_kwargs):
31
+ super().__init__()
32
+ self._fn = fn
33
+ self._paths = paths
34
+ self._make_kwargs = make_kwargs
35
+
36
+ def run(self):
37
+ errors = []
38
+ total = len(self._paths)
39
+ for i, path in enumerate(self._paths):
40
+ self.item_status.emit(f"Processing {i + 1}/{total}: {path.name}…")
41
+ self.item_progress.emit(0)
42
+ base = i / total * 100
43
+
44
+ def on_progress(pct, _base=base):
45
+ self.item_progress.emit(pct)
46
+ self.progress.emit(_base + pct / total)
47
+
48
+ try:
49
+ kwargs = self._make_kwargs(path)
50
+ kwargs["on_progress"] = on_progress
51
+ self._fn(path, **kwargs)
52
+ except Exception as e:
53
+ errors.append(f"{path.name}: {e}")
54
+ self.progress.emit((i + 1) / total * 100)
55
+ self.finished.emit(errors)
@@ -5,12 +5,11 @@ from PySide6.QtWidgets import (
5
5
  QFileDialog,
6
6
  QHBoxLayout,
7
7
  QLabel,
8
- QListWidget,
9
8
  QPushButton,
10
9
  QVBoxLayout,
11
10
  )
12
11
 
13
- from pyvideokit_gui._base_panel import BasePanel
12
+ from pyvideokit_gui._base_panel import BasePanel, DropListWidget, _VIDEO_EXTS
14
13
  from pyvideokit_libs import join_videos
15
14
 
16
15
 
@@ -18,8 +17,8 @@ class ConcatPanel(BasePanel):
18
17
  def _build_ui(self, layout: QVBoxLayout):
19
18
  layout.addWidget(QLabel("Video files (add at least 2, drag to reorder)"))
20
19
 
21
- self._list = QListWidget()
22
- self._list.setDragDropMode(QAbstractItemView.InternalMove)
20
+ self._list = DropListWidget()
21
+ self._list.setSelectionMode(QAbstractItemView.ExtendedSelection)
23
22
  self._list.setMinimumHeight(120)
24
23
  layout.addWidget(self._list)
25
24
 
@@ -47,6 +46,11 @@ class ConcatPanel(BasePanel):
47
46
  for item in self._list.selectedItems():
48
47
  self._list.takeItem(self._list.row(item))
49
48
 
49
+ def _on_panel_drop(self, paths: list):
50
+ for p in paths:
51
+ if Path(p).suffix.lower() in _VIDEO_EXTS:
52
+ self._list.addItem(p)
53
+
50
54
  def _run(self):
51
55
  paths = [Path(self._list.item(i).text()) for i in range(self._list.count())]
52
56
  if len(paths) < 2:
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QLabel, QSpinBox, QVBoxLayout
3
+ from PySide6.QtWidgets import QCheckBox, QHBoxLayout, QSpinBox, QVBoxLayout
4
4
 
5
5
  from pyvideokit_gui._base_panel import BasePanel
6
6
  from pyvideokit_libs import convert_to_ffv1
@@ -8,7 +8,7 @@ from pyvideokit_libs import convert_to_ffv1
8
8
 
9
9
  class ConvertPanel(BasePanel):
10
10
  def _build_ui(self, layout: QVBoxLayout):
11
- self._input = self._input_row(layout, "Input file")
11
+ self._inputs = self._batch_input_list(layout, "Input files")
12
12
 
13
13
  fps_row = QHBoxLayout()
14
14
  self._fps_override = QCheckBox("Override FPS")
@@ -22,13 +22,13 @@ class ConvertPanel(BasePanel):
22
22
  fps_row.addStretch()
23
23
  layout.addLayout(fps_row)
24
24
 
25
- self._output = self._output_row(layout, "Output MKV (optional)")
25
+ self._output_dir = self._output_dir_row(layout)
26
26
 
27
27
  def _run(self):
28
- path = self._input.text().strip()
29
- if not path:
30
- self._status.setText("❌ Select an input file.")
28
+ paths = [Path(self._inputs.item(i).text()) for i in range(self._inputs.count())]
29
+ if not paths:
30
+ self._status.setText("❌ Add at least one file.")
31
31
  return
32
- output = self._output.text().strip() or None
33
32
  fps = self._fps.value() if self._fps_override.isChecked() else None
34
- self._start_worker(convert_to_ffv1, Path(path), fps=fps, output=output)
33
+ out_dir = self._output_dir.text().strip() or None
34
+ self._start_batch(convert_to_ffv1, paths, lambda p: {"fps": fps, "output": out_dir})
@@ -8,13 +8,13 @@ from pyvideokit_libs import extract_audio
8
8
 
9
9
  class ExtractAudioPanel(BasePanel):
10
10
  def _build_ui(self, layout: QVBoxLayout):
11
- self._input = self._input_row(layout, "Input file")
12
- self._output = self._output_row(layout, "Output WAV (optional)")
11
+ self._inputs = self._batch_input_list(layout, "Input files")
12
+ self._output_dir = self._output_dir_row(layout)
13
13
 
14
14
  def _run(self):
15
- path = self._input.text().strip()
16
- if not path:
17
- self._status.setText("❌ Select an input file.")
15
+ paths = [Path(self._inputs.item(i).text()) for i in range(self._inputs.count())]
16
+ if not paths:
17
+ self._status.setText("❌ Add at least one file.")
18
18
  return
19
- output = self._output.text().strip() or None
20
- self._start_worker(extract_audio, Path(path), output=output)
19
+ out_dir = self._output_dir.text().strip() or None
20
+ self._start_batch(extract_audio, paths, lambda p: {"output": out_dir})
@@ -8,13 +8,13 @@ from pyvideokit_libs import apply_vhs_effect
8
8
 
9
9
  class VhsPanel(BasePanel):
10
10
  def _build_ui(self, layout: QVBoxLayout):
11
- self._input = self._input_row(layout, "Input file (must have audio)")
12
- self._output = self._output_row(layout, "Output MKV (optional)")
11
+ self._inputs = self._batch_input_list(layout, "Input files (must have audio)")
12
+ self._output_dir = self._output_dir_row(layout)
13
13
 
14
14
  def _run(self):
15
- path = self._input.text().strip()
16
- if not path:
17
- self._status.setText("❌ Select an input file.")
15
+ paths = [Path(self._inputs.item(i).text()) for i in range(self._inputs.count())]
16
+ if not paths:
17
+ self._status.setText("❌ Add at least one file.")
18
18
  return
19
- output = self._output.text().strip() or None
20
- self._start_worker(apply_vhs_effect, Path(path), output=output)
19
+ out_dir = self._output_dir.text().strip() or None
20
+ self._start_batch(apply_vhs_effect, paths, lambda p: {"output": out_dir})
@@ -8,13 +8,13 @@ from pyvideokit_libs import prepare_youtube
8
8
 
9
9
  class YouTubePanel(BasePanel):
10
10
  def _build_ui(self, layout: QVBoxLayout):
11
- self._input = self._input_row(layout, "Input file (FFV1)")
12
- self._output = self._output_row(layout, "Output MOV (optional)")
11
+ self._inputs = self._batch_input_list(layout, "Input files (FFV1)")
12
+ self._output_dir = self._output_dir_row(layout)
13
13
 
14
14
  def _run(self):
15
- path = self._input.text().strip()
16
- if not path:
17
- self._status.setText("❌ Select an input file.")
15
+ paths = [Path(self._inputs.item(i).text()) for i in range(self._inputs.count())]
16
+ if not paths:
17
+ self._status.setText("❌ Add at least one file.")
18
18
  return
19
- output = self._output.text().strip() or None
20
- self._start_worker(prepare_youtube, Path(path), output=output)
19
+ out_dir = self._output_dir.text().strip() or None
20
+ self._start_batch(prepare_youtube, paths, lambda p: {"output": out_dir})
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyVideoKit-GUI
3
- Version: 0.3.0
4
- Summary: GUI for PyVideoKit — FFmpeg-based video processing
3
+ Version: 0.4.2
4
+ Summary: PySide6 desktop app for PyVideoKit: trim, concatenate, fade, VHS effect, WAV extraction, lossless FFV1 masters and 4K ProRes exports for YouTube, with batch processing
5
5
  Author-email: Evilchuck666 <carlos.martinez.medina1992@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
7
7
  Version 3, 29 June 2007
@@ -678,14 +678,20 @@ License: GNU GENERAL PUBLIC LICENSE
678
678
  Public License instead of this License. But first, please read
679
679
  <https://www.gnu.org/licenses/why-not-lgpl.html>.
680
680
 
681
+ Project-URL: Homepage, https://github.com/Evilchuck666/PyVideoKit-GUI
682
+ Project-URL: Source, https://github.com/Evilchuck666/PyVideoKit-GUI
683
+ Project-URL: Issues, https://github.com/Evilchuck666/PyVideoKit-GUI/issues
684
+ Project-URL: Releases, https://github.com/Evilchuck666/PyVideoKit-GUI/releases
685
+ Keywords: ffmpeg,video,gui,pyside6,qt6,video-processing,ffv1,prores,vhs,youtube
681
686
  Classifier: Programming Language :: Python :: 3
682
687
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
683
688
  Classifier: Operating System :: OS Independent
684
689
  Requires-Python: >=3.10
685
690
  Description-Content-Type: text/markdown
686
691
  License-File: LICENSE
687
- Requires-Dist: PySide6>=6.0
688
- Requires-Dist: PyVideoKit-Libs
692
+ Requires-Dist: PySide6!=6.12.0,>=6.0; python_version < "3.12"
693
+ Requires-Dist: PySide6>=6.0; python_version >= "3.12"
694
+ Requires-Dist: PyVideoKit-Libs>=0.4.2
689
695
  Dynamic: license-file
690
696
 
691
697
  # 🎬 PyVideoKit-GUI
@@ -693,19 +699,21 @@ Dynamic: license-file
693
699
  ![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)
694
700
  ![License](https://img.shields.io/badge/license-GPLv3-green)
695
701
 
696
- Graphical interface for PyVideoKit — FFmpeg-based video processing. Provides the same operations as [PyVideoKit-CLI](../PyVideoKit-CLI) through a PySide6 desktop application with real-time progress tracking.
702
+ Graphical interface for PyVideoKit — FFmpeg-based video processing. Provides the same operations as [PyVideoKit-CLI](../PyVideoKit-CLI) through a PySide6 desktop application with real-time progress tracking, drag-and-drop input, and batch processing.
697
703
 
698
704
  ---
699
705
 
700
706
  ## ✨ Features
701
707
 
702
- - 📼 **VHS effect** — retro visual noise, color bleed, and audio degradation
708
+ - 🎞️ **Convert to FFV1** — batch-convert multiple videos to lossless FFV1/MKV masters
703
709
  - ✂️ **Trim** — cut a segment by start/end time with stream copy (no re-encoding)
704
- - 🔗 **Concatenate** — join multiple videos with drag-and-drop reordering
710
+ - 🔗 **Concatenate** — join multiple videos with drag-and-drop reordering and multi-selection
705
711
  - 🎬 **Fade** — fade-in and/or fade-out on an FFV1 master
706
- - 🔊 **Extract audio** — dump the audio track to uncompressed WAV (PCM 16-bit)
707
- - 🎞️ **Convert to FFV1** — create a lossless MKV master for editing
708
- - 📺 **Prepare for YouTube** — encode to ProRes 422 HQ MOV, upscaled to 4K
712
+ - 📼 **VHS effect** — batch-apply retro visual noise, color bleed, and audio degradation
713
+ - 🔊 **Extract audio** — batch-export audio tracks to uncompressed WAV (PCM 16-bit)
714
+ - 📺 **Prepare for YouTube** — batch-encode to ProRes 422 HQ MOV, upscaled to 4K
715
+ - 🖱️ **Drag & drop** — drop files anywhere on any panel; drop multiple files onto batch panels
716
+ - 📁 **Output directory** — optionally redirect all batch output to a chosen folder
709
717
 
710
718
  ---
711
719
 
@@ -716,6 +724,7 @@ Graphical interface for PyVideoKit — FFmpeg-based video processing. Provides t
716
724
  - **FFmpeg** and **FFprobe** available in `PATH`
717
725
  - **SoX** available in `PATH` (required by VHS effect)
718
726
  - **PyVideoKit-Libs** — installed automatically as a dependency
727
+ - **`paplay`**, **`pw-play`**, or **`ffplay`** in `PATH` *(optional — for completion sound)*
719
728
 
720
729
  ---
721
730
 
@@ -779,7 +788,7 @@ Keywords=video;ffmpeg;trim;fade;vhs;youtube;convert;
779
788
  EOF
780
789
  ```
781
790
 
782
- **3. Refresh the icon cache** *(may be required on some desktop environments)*:
791
+ **3. Refresh the icon cache** *(maybe required in some desktop environments)*:
783
792
 
784
793
  ```bash
785
794
  gtk-update-icon-cache ~/.local/share/icons/hicolor/
@@ -797,17 +806,19 @@ pvk-gui
797
806
 
798
807
  The application opens a tabbed window. Each tab corresponds to one operation:
799
808
 
800
- | Tab | Operation |
801
- |---|---|
802
- | 🎞️ Convert to FFV1 | Convert any video to a lossless FFV1/MKV master |
803
- | ✂️ Trim | Cut a segment by start/end time (no re-encoding) |
804
- | 🔗 Concat | Join multiple videos (drag to reorder) |
805
- | 🎬 Fade | Add fade-in and/or fade-out |
806
- | 📼 VHS Effect | Apply retro VHS visual and audio effect |
807
- | 🔊 Extract Audio | Export audio track to WAV |
808
- | 📺 YouTube | Encode to ProRes 422 HQ MOV for upload |
809
-
810
- All operations run in a background thread and display a real-time progress bar.
809
+ | Tab | Operation | Batch |
810
+ |---------------------|----------------------------------------------------------------|:-----:|
811
+ | 🎞️ Convert to FFV1 | Convert videos to lossless FFV1/MKV masters | ✅ |
812
+ | ✂️ Trim | Cut a segment by start/end time (no re-encoding) | — |
813
+ | 🔗 Concat | Join multiple videos (drag to reorder, multi-select to remove) | — |
814
+ | 🎬 Fade | Add fade-in and/or fade-out | — |
815
+ | 📼 VHS Effect | Apply retro VHS visual and audio effect | ✅ |
816
+ | 🔊 Extract Audio | Export audio tracks to WAV | ✅ |
817
+ | 📺 YouTube | Encode to ProRes 422 HQ MOV for upload | ✅ |
818
+
819
+ Batch panels accept multiple input files via drag & drop or the "Add files…" button, and optionally redirect output to a chosen directory. They show two progress bars: one for the current file and one for the overall batch.
820
+
821
+ All operations run in a background thread. A system sound plays on completion.
811
822
 
812
823
  ---
813
824
 
@@ -0,0 +1,18 @@
1
+ pyvideokit_gui/__init__.py,sha256=jWZkgAAsHarO-6c_tvpY0Tcx5OJDmg80wiP80_oBGWo,168
2
+ pyvideokit_gui/_base_panel.py,sha256=honS4LLd3yb_4esULGxd6aMSUEqY4fbH6mLqO-36bPw,9707
3
+ pyvideokit_gui/_main.py,sha256=-iyypcAoThr7Ysa4rvrJ7hYq-cd8h8RqYxL77_lkDqM,331
4
+ pyvideokit_gui/_main_window.py,sha256=tFwucsSbMIMbTwEvHBQBhnW6jSu6mghyExSP8TC4AlQ,1032
5
+ pyvideokit_gui/_worker.py,sha256=2dQgce9XqgvD16L33pg6saBu5irNyJO1iFW9OZEGc0s,1649
6
+ pyvideokit_gui/concat_panel.py,sha256=03uGRSVBaIh0FeloiKTrrW-UfECe0Jg5Ms0IbZrDtII,1915
7
+ pyvideokit_gui/convert_panel.py,sha256=S0Ytf3ZG2Qu2fQznDwI3fV47bNWw61G0hbph0aE2eAg,1275
8
+ pyvideokit_gui/extract_audio_panel.py,sha256=M74SxUnlpRjNGktdZh5BP7CzYr6wk3eEhSGgthzwFj0,721
9
+ pyvideokit_gui/fade_panel.py,sha256=8h6T4O-56qs8BMA3SH0tOfbL_2ym3LZ95NzLFUi9vrw,1634
10
+ pyvideokit_gui/trim_panel.py,sha256=yjdVhwxmfaSp2aflOywWlmW4WwFgtVKfGzqhUxac0UI,1425
11
+ pyvideokit_gui/vhs_panel.py,sha256=zGpwmhgLECn_HSqtBTe9I8-XlCfaq0GhBZgtrM_CcU0,736
12
+ pyvideokit_gui/youtube_panel.py,sha256=BLPfOY2TReGBIJiRG4qZCW88CULFF3CIdluQLTJhE4M,727
13
+ pyvideokit_gui-0.4.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
+ pyvideokit_gui-0.4.2.dist-info/METADATA,sha256=ge3yjrlyyluLjTJuMMLEFD6CW_MMs40ubGCRzO_rp34,46707
15
+ pyvideokit_gui-0.4.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
16
+ pyvideokit_gui-0.4.2.dist-info/entry_points.txt,sha256=svm4llnRZU8Y0-2ThznB9avSUowxpuDxZc2rBfedrX0,54
17
+ pyvideokit_gui-0.4.2.dist-info/top_level.txt,sha256=SDnrDTKTriy3iVrM4dlbpb2fDHWjnzLK9bZ05nzRl0U,15
18
+ pyvideokit_gui-0.4.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.2.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,18 +0,0 @@
1
- pyvideokit_gui/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
- pyvideokit_gui/_base_panel.py,sha256=f2OVYRlZSV5DwZ_EdcozkdDyY1OnBwnIhZV5k5Zq5Hk,3867
3
- pyvideokit_gui/_main.py,sha256=-iyypcAoThr7Ysa4rvrJ7hYq-cd8h8RqYxL77_lkDqM,331
4
- pyvideokit_gui/_main_window.py,sha256=Pls03EJ9CoREhV1eiYavVg9oK0Z8JmXyAUFTGHc0pAU,1032
5
- pyvideokit_gui/_worker.py,sha256=1f151Q0F2JzpHFxczl4ex0mP6Vf1h-c2ZQuJL5CGv9o,574
6
- pyvideokit_gui/concat_panel.py,sha256=FSg0-FQpALW4fD9rBIxX89DlkVq56h2VJDJnN3kEg7A,1734
7
- pyvideokit_gui/convert_panel.py,sha256=7HaDpuhatgOw1TvTvYNXPWcwwNb4QQwWwYXmMzQYYx4,1224
8
- pyvideokit_gui/extract_audio_panel.py,sha256=NADJT0rkNhO61dKDebYZdQhZUvjmW833OHygCU8746Q,665
9
- pyvideokit_gui/fade_panel.py,sha256=8h6T4O-56qs8BMA3SH0tOfbL_2ym3LZ95NzLFUi9vrw,1634
10
- pyvideokit_gui/trim_panel.py,sha256=yjdVhwxmfaSp2aflOywWlmW4WwFgtVKfGzqhUxac0UI,1425
11
- pyvideokit_gui/vhs_panel.py,sha256=DMCcXIB1_bZD8RvmXujJwb27ODOlF_e8NOL-QEv1wZs,680
12
- pyvideokit_gui/youtube_panel.py,sha256=4LogFd2Cc4Fj2xRM5RQsYBW4M9aW_pb-BJrGlBM4Hc0,671
13
- pyvideokit_gui-0.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
- pyvideokit_gui-0.3.0.dist-info/METADATA,sha256=8rpdzn1ag15rh0UD-nTwycx0qx_K9ovkzqJASe3IZV4,45053
15
- pyvideokit_gui-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
- pyvideokit_gui-0.3.0.dist-info/entry_points.txt,sha256=svm4llnRZU8Y0-2ThznB9avSUowxpuDxZc2rBfedrX0,54
17
- pyvideokit_gui-0.3.0.dist-info/top_level.txt,sha256=SDnrDTKTriy3iVrM4dlbpb2fDHWjnzLK9bZ05nzRl0U,15
18
- pyvideokit_gui-0.3.0.dist-info/RECORD,,