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.
- 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.0.dist-info}/METADATA +24 -19
- pyvideokit_gui-0.4.0.dist-info/RECORD +18 -0
- pyvideokit_gui-0.3.0.dist-info/RECORD +0 -18
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/WHEEL +0 -0
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/entry_points.txt +0 -0
- {pyvideokit_gui-0.3.0.dist-info → pyvideokit_gui-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {pyvideokit_gui-0.3.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 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,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
|
|
@@ -693,19 +693,21 @@ Dynamic: license-file
|
|
|
693
693
|

|
|
694
694
|

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