PyVideoKit-GUI 0.3.0__py3-none-any.whl → 0.4.0__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyVideoKit-GUI
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: GUI for PyVideoKit — FFmpeg-based video processing
5
5
  Author-email: Evilchuck666 <carlos.martinez.medina1992@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -693,19 +693,21 @@ Dynamic: license-file
693
693
  ![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)
694
694
  ![License](https://img.shields.io/badge/license-GPLv3-green)
695
695
 
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.
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, drag-and-drop input, and batch processing.
697
697
 
698
698
  ---
699
699
 
700
700
  ## ✨ Features
701
701
 
702
- - 📼 **VHS effect** — retro visual noise, color bleed, and audio degradation
702
+ - 🎞️ **Convert to FFV1** — batch-convert multiple videos to lossless FFV1/MKV masters
703
703
  - ✂️ **Trim** — cut a segment by start/end time with stream copy (no re-encoding)
704
- - 🔗 **Concatenate** — join multiple videos with drag-and-drop reordering
704
+ - 🔗 **Concatenate** — join multiple videos with drag-and-drop reordering and multi-selection
705
705
  - 🎬 **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
706
+ - 📼 **VHS effect** — batch-apply retro visual noise, color bleed, and audio degradation
707
+ - 🔊 **Extract audio** — batch-export audio tracks to uncompressed WAV (PCM 16-bit)
708
+ - 📺 **Prepare for YouTube** — batch-encode to ProRes 422 HQ MOV, upscaled to 4K
709
+ - 🖱️ **Drag & drop** — drop files anywhere on any panel; drop multiple files onto batch panels
710
+ - 📁 **Output directory** — optionally redirect all batch output to a chosen folder
709
711
 
710
712
  ---
711
713
 
@@ -716,6 +718,7 @@ Graphical interface for PyVideoKit — FFmpeg-based video processing. Provides t
716
718
  - **FFmpeg** and **FFprobe** available in `PATH`
717
719
  - **SoX** available in `PATH` (required by VHS effect)
718
720
  - **PyVideoKit-Libs** — installed automatically as a dependency
721
+ - **`paplay`**, **`pw-play`**, or **`ffplay`** in `PATH` *(optional — for completion sound)*
719
722
 
720
723
  ---
721
724
 
@@ -779,7 +782,7 @@ Keywords=video;ffmpeg;trim;fade;vhs;youtube;convert;
779
782
  EOF
780
783
  ```
781
784
 
782
- **3. Refresh the icon cache** *(may be required on some desktop environments)*:
785
+ **3. Refresh the icon cache** *(maybe required in some desktop environments)*:
783
786
 
784
787
  ```bash
785
788
  gtk-update-icon-cache ~/.local/share/icons/hicolor/
@@ -797,17 +800,19 @@ pvk-gui
797
800
 
798
801
  The application opens a tabbed window. Each tab corresponds to one operation:
799
802
 
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.
803
+ | Tab | Operation | Batch |
804
+ |---------------------|----------------------------------------------------------------|:-----:|
805
+ | 🎞️ Convert to FFV1 | Convert videos to lossless FFV1/MKV masters | ✅ |
806
+ | ✂️ Trim | Cut a segment by start/end time (no re-encoding) | — |
807
+ | 🔗 Concat | Join multiple videos (drag to reorder, multi-select to remove) | — |
808
+ | 🎬 Fade | Add fade-in and/or fade-out | — |
809
+ | 📼 VHS Effect | Apply retro VHS visual and audio effect | ✅ |
810
+ | 🔊 Extract Audio | Export audio tracks to WAV | ✅ |
811
+ | 📺 YouTube | Encode to ProRes 422 HQ MOV for upload | ✅ |
812
+
813
+ 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.
814
+
815
+ All operations run in a background thread. A system sound plays on completion.
811
816
 
812
817
  ---
813
818
 
@@ -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.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
+ pyvideokit_gui-0.4.0.dist-info/METADATA,sha256=2c--D8t-HQTmX4_ePuOaIUke0s9wz0dZ9KmkMOWOn78,46125
15
+ pyvideokit_gui-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
+ pyvideokit_gui-0.4.0.dist-info/entry_points.txt,sha256=svm4llnRZU8Y0-2ThznB9avSUowxpuDxZc2rBfedrX0,54
17
+ pyvideokit_gui-0.4.0.dist-info/top_level.txt,sha256=SDnrDTKTriy3iVrM4dlbpb2fDHWjnzLK9bZ05nzRl0U,15
18
+ pyvideokit_gui-0.4.0.dist-info/RECORD,,
@@ -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,,