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.
- 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 +8 -8
- pyvideokit_gui/extract_audio_panel.py +7 -7
- pyvideokit_gui/vhs_panel.py +7 -7
- pyvideokit_gui/youtube_panel.py +7 -7
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.2.dist-info}/METADATA +33 -22
- pyvideokit_gui-0.4.2.dist-info/RECORD +18 -0
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.2.dist-info}/WHEEL +1 -1
- pyvideokit_gui-0.3.0.dist-info/RECORD +0 -18
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.2.dist-info}/entry_points.txt +0 -0
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.2.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 QCheckBox, QHBoxLayout,
|
|
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.
|
|
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.
|
|
25
|
+
self._output_dir = self._output_dir_row(layout)
|
|
26
26
|
|
|
27
27
|
def _run(self):
|
|
28
|
-
|
|
29
|
-
if not
|
|
30
|
-
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.")
|
|
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.
|
|
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/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,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyVideoKit-GUI
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
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
|
|
688
|
-
Requires-Dist:
|
|
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
|

|
|
694
700
|

|
|
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
|
-
-
|
|
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
|
-
-
|
|
707
|
-
-
|
|
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** *(
|
|
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
|
|
801
|
-
|
|
802
|
-
| 🎞️ Convert to FFV1 | Convert
|
|
803
|
-
| ✂️ Trim
|
|
804
|
-
| 🔗 Concat
|
|
805
|
-
| 🎬 Fade
|
|
806
|
-
| 📼 VHS Effect
|
|
807
|
-
| 🔊 Extract Audio
|
|
808
|
-
| 📺 YouTube
|
|
809
|
-
|
|
810
|
-
|
|
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,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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|