PyVideoKit-GUI 0.2.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 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,20 +8,27 @@ 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
- layout.addWidget(QLabel("FPS"))
13
+ fps_row = QHBoxLayout()
14
+ self._fps_override = QCheckBox("Override FPS")
14
15
  self._fps = QSpinBox()
15
16
  self._fps.setRange(1, 240)
16
17
  self._fps.setValue(60)
17
- layout.addWidget(self._fps)
18
+ self._fps.setEnabled(False)
19
+ self._fps_override.toggled.connect(self._fps.setEnabled)
20
+ fps_row.addWidget(self._fps_override)
21
+ fps_row.addWidget(self._fps)
22
+ fps_row.addStretch()
23
+ layout.addLayout(fps_row)
18
24
 
19
- self._output = self._output_row(layout, "Output MKV (optional)")
25
+ self._output_dir = self._output_dir_row(layout)
20
26
 
21
27
  def _run(self):
22
- path = self._input.text().strip()
23
- if not path:
24
- 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.")
25
31
  return
26
- output = self._output.text().strip() or None
27
- self._start_worker(convert_to_ffv1, Path(path), fps=self._fps.value(), output=output)
32
+ fps = self._fps.value() if self._fps_override.isChecked() else None
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})
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- from PySide6.QtWidgets import QDoubleSpinBox, QHBoxLayout, QLabel, QSpinBox, QVBoxLayout
3
+ from PySide6.QtWidgets import QDoubleSpinBox, QHBoxLayout, QLabel, QVBoxLayout
4
4
 
5
5
  from pyvideokit_gui._base_panel import BasePanel
6
6
  from pyvideokit_libs import fade_video
@@ -30,12 +30,6 @@ class FadePanel(BasePanel):
30
30
  row.addLayout(fo_col)
31
31
  layout.addLayout(row)
32
32
 
33
- layout.addWidget(QLabel("FPS"))
34
- self._fps = QSpinBox()
35
- self._fps.setRange(1, 240)
36
- self._fps.setValue(60)
37
- layout.addWidget(self._fps)
38
-
39
33
  self._output = self._output_row(layout, "Output (optional)")
40
34
 
41
35
  def _run(self):
@@ -54,6 +48,5 @@ class FadePanel(BasePanel):
54
48
  Path(path),
55
49
  fade_in=fi,
56
50
  fade_out=fo,
57
- fps=self._fps.value(),
58
51
  output=output,
59
52
  )
@@ -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.2.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
@@ -687,3 +687,145 @@ License-File: LICENSE
687
687
  Requires-Dist: PySide6>=6.0
688
688
  Requires-Dist: PyVideoKit-Libs
689
689
  Dynamic: license-file
690
+
691
+ # 🎬 PyVideoKit-GUI
692
+
693
+ ![Python](https://img.shields.io/badge/python-%3E%3D3.10-blue)
694
+ ![License](https://img.shields.io/badge/license-GPLv3-green)
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, drag-and-drop input, and batch processing.
697
+
698
+ ---
699
+
700
+ ## ✨ Features
701
+
702
+ - 🎞️ **Convert to FFV1** — batch-convert multiple videos to lossless FFV1/MKV masters
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 and multi-selection
705
+ - 🎬 **Fade** — fade-in and/or fade-out on an FFV1 master
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
711
+
712
+ ---
713
+
714
+ ## 📦 Requirements
715
+
716
+ - **Python** ≥ 3.10
717
+ - **PySide6** ≥ 6.0
718
+ - **FFmpeg** and **FFprobe** available in `PATH`
719
+ - **SoX** available in `PATH` (required by VHS effect)
720
+ - **PyVideoKit-Libs** — installed automatically as a dependency
721
+ - **`paplay`**, **`pw-play`**, or **`ffplay`** in `PATH` *(optional — for completion sound)*
722
+
723
+ ---
724
+
725
+ ## 🔧 Installation
726
+
727
+ ### 🏗️ Arch Linux (AUR)
728
+
729
+ ```bash
730
+ yay -S python-pyvideokit-gui
731
+ ```
732
+
733
+ FFmpeg, FFprobe, SoX, PySide6, and PyVideoKit-Libs are installed automatically as pacman dependencies.
734
+
735
+ ### 🐍 Other systems (pip)
736
+
737
+ ```bash
738
+ pip install PyVideoKit-GUI
739
+ ```
740
+
741
+ Make sure **FFmpeg**, **FFprobe**, and **SoX** are available in your `PATH`.
742
+
743
+ ---
744
+
745
+ ## 🖥️ Desktop Integration
746
+
747
+ Arch Linux users get the launcher entry and icon automatically via the AUR package. For all other systems, create them manually:
748
+
749
+ **1. Save the icon:**
750
+
751
+ ```bash
752
+ mkdir -p ~/.local/share/icons/hicolor/scalable/apps
753
+ cat > ~/.local/share/icons/hicolor/scalable/apps/pvk-gui.svg << 'EOF'
754
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
755
+ <rect x="4" y="14" width="40" height="30" rx="3" fill="#2d3561"/>
756
+ <rect x="10" y="19" width="28" height="20" rx="2" fill="#1a2038"/>
757
+ <polygon points="18,21 18,37 33,29" fill="#ff6b6b"/>
758
+ <rect x="4" y="6" width="40" height="8" rx="2" fill="#ff6b6b"/>
759
+ <rect x="11" y="6" width="6" height="8" fill="#ffffff"/>
760
+ <rect x="23" y="6" width="6" height="8" fill="#ffffff"/>
761
+ <rect x="35" y="6" width="5" height="8" fill="#ffffff"/>
762
+ </svg>
763
+ EOF
764
+ ```
765
+
766
+ **2. Create the `.desktop` entry:**
767
+
768
+ ```bash
769
+ mkdir -p ~/.local/share/applications
770
+ cat > ~/.local/share/applications/pvk-gui.desktop << 'EOF'
771
+ [Desktop Entry]
772
+ Version=1.0
773
+ Type=Application
774
+ Name=PyVideoKit
775
+ GenericName=Video Processing Tool
776
+ Comment=FFmpeg-based video processing GUI
777
+ Exec=pvk-gui
778
+ Icon=pvk-gui
779
+ Categories=AudioVideo;Video;AudioVideoEditing;
780
+ Terminal=false
781
+ Keywords=video;ffmpeg;trim;fade;vhs;youtube;convert;
782
+ EOF
783
+ ```
784
+
785
+ **3. Refresh the icon cache** *(maybe required in some desktop environments)*:
786
+
787
+ ```bash
788
+ gtk-update-icon-cache ~/.local/share/icons/hicolor/
789
+ ```
790
+
791
+ PyVideoKit will now appear in your application launcher.
792
+
793
+ ---
794
+
795
+ ## 🚀 Usage
796
+
797
+ ```bash
798
+ pvk-gui
799
+ ```
800
+
801
+ The application opens a tabbed window. Each tab corresponds to one operation:
802
+
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.
816
+
817
+ ---
818
+
819
+ ## 🔄 Typical Workflow
820
+
821
+ ```
822
+ 🎞️ Convert to FFV1 → ✂️ Trim / 🎬 Fade / 📼 VHS Effect → 📺 YouTube
823
+ ```
824
+
825
+ > Operations that re-encode (Fade, VHS Effect, YouTube) expect an FFV1 `.mkv` as input.
826
+
827
+ ---
828
+
829
+ ## ⚖️ License
830
+
831
+ This project is licensed under the GPLv3 License — see the [LICENSE](LICENSE) file for details.
@@ -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=BVs1ltCslHkRtb3MxpUlYGkM4fA11odiEp9nP4mq9hI,880
8
- pyvideokit_gui/extract_audio_panel.py,sha256=NADJT0rkNhO61dKDebYZdQhZUvjmW833OHygCU8746Q,665
9
- pyvideokit_gui/fade_panel.py,sha256=kpGfikGDY_VPJqPPlYEjPD4dAAkcWhzJIGYEg_I1zfY,1853
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.2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
14
- pyvideokit_gui-0.2.0.dist-info/METADATA,sha256=jPLAHBUTDWR_2kIJ-0DCPeWrnZva37-KjPSCAXJm6Bo,41076
15
- pyvideokit_gui-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
16
- pyvideokit_gui-0.2.0.dist-info/entry_points.txt,sha256=svm4llnRZU8Y0-2ThznB9avSUowxpuDxZc2rBfedrX0,54
17
- pyvideokit_gui-0.2.0.dist-info/top_level.txt,sha256=SDnrDTKTriy3iVrM4dlbpb2fDHWjnzLK9bZ05nzRl0U,15
18
- pyvideokit_gui-0.2.0.dist-info/RECORD,,