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.
- pyvideokit_gui/__init__.py +6 -1
- pyvideokit_gui/_base_panel.py +164 -5
- pyvideokit_gui/_main_window.py +1 -1
- pyvideokit_gui/_worker.py +34 -0
- pyvideokit_gui/concat_panel.py +8 -4
- pyvideokit_gui/convert_panel.py +17 -10
- pyvideokit_gui/extract_audio_panel.py +7 -7
- pyvideokit_gui/fade_panel.py +1 -8
- pyvideokit_gui/vhs_panel.py +7 -7
- pyvideokit_gui/youtube_panel.py +7 -7
- {pyvideokit_gui-0.2.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/METADATA +143 -1
- pyvideokit_gui-0.4.0.dist-info/RECORD +18 -0
- pyvideokit_gui-0.2.0.dist-info/RECORD +0 -18
- {pyvideokit_gui-0.2.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/WHEEL +0 -0
- {pyvideokit_gui-0.2.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/entry_points.txt +0 -0
- {pyvideokit_gui-0.2.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {pyvideokit_gui-0.2.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/top_level.txt +0 -0
pyvideokit_gui/__init__.py
CHANGED
pyvideokit_gui/_base_panel.py
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
96
|
-
|
|
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)
|
pyvideokit_gui/_main_window.py
CHANGED
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)
|
pyvideokit_gui/concat_panel.py
CHANGED
|
@@ -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 =
|
|
22
|
-
self._list.
|
|
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:
|
pyvideokit_gui/convert_panel.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from PySide6.QtWidgets import
|
|
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.
|
|
11
|
+
self._inputs = self._batch_input_list(layout, "Input files")
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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.
|
|
25
|
+
self._output_dir = self._output_dir_row(layout)
|
|
20
26
|
|
|
21
27
|
def _run(self):
|
|
22
|
-
|
|
23
|
-
if not
|
|
24
|
-
self._status.setText("❌
|
|
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
|
-
|
|
27
|
-
|
|
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.
|
|
12
|
-
self.
|
|
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
|
-
|
|
16
|
-
if not
|
|
17
|
-
self._status.setText("❌
|
|
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
|
-
|
|
20
|
-
self.
|
|
19
|
+
out_dir = self._output_dir.text().strip() or None
|
|
20
|
+
self._start_batch(extract_audio, paths, lambda p: {"output": out_dir})
|
pyvideokit_gui/fade_panel.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
from PySide6.QtWidgets import QDoubleSpinBox, QHBoxLayout, QLabel,
|
|
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
|
)
|
pyvideokit_gui/vhs_panel.py
CHANGED
|
@@ -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.
|
|
12
|
-
self.
|
|
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
|
-
|
|
16
|
-
if not
|
|
17
|
-
self._status.setText("❌
|
|
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
|
-
|
|
20
|
-
self.
|
|
19
|
+
out_dir = self._output_dir.text().strip() or None
|
|
20
|
+
self._start_batch(apply_vhs_effect, paths, lambda p: {"output": out_dir})
|
pyvideokit_gui/youtube_panel.py
CHANGED
|
@@ -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.
|
|
12
|
-
self.
|
|
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
|
-
|
|
16
|
-
if not
|
|
17
|
-
self._status.setText("❌
|
|
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
|
-
|
|
20
|
-
self.
|
|
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
|
+
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
|
+

|
|
694
|
+

|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|