better-ffmpeg-progress 3.2.2__tar.gz → 3.2.4__tar.gz
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.
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/PKG-INFO +1 -2
- better_ffmpeg_progress-3.2.4/better_ffmpeg_progress/better_ffmpeg_progress.py +350 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress.egg-info/PKG-INFO +1 -2
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress.egg-info/requires.txt +0 -1
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/setup.py +2 -2
- better_ffmpeg_progress-3.2.2/better_ffmpeg_progress/better_ffmpeg_progress.py +0 -271
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/LICENSE +0 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/README.md +0 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress/__init__.py +0 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress/utils.py +0 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress.egg-info/SOURCES.txt +0 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress.egg-info/dependency_links.txt +0 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress.egg-info/top_level.txt +0 -0
- {better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: better-ffmpeg-progress
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.4
|
|
4
4
|
Summary: Run FFmpeg & see percentage progress + ETA.
|
|
5
5
|
Home-page: https://github.com/CrypticSignal/better-ffmpeg-progress
|
|
6
6
|
Author: GitHub.com/CrypticSignal
|
|
@@ -12,7 +12,6 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.7
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: ffmpeg-python
|
|
16
15
|
Requires-Dist: psutil
|
|
17
16
|
Requires-Dist: rich
|
|
18
17
|
Requires-Dist: tqdm
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import psutil
|
|
5
|
+
import shutil
|
|
6
|
+
from signal import CTRL_BREAK_EVENT
|
|
7
|
+
import subprocess
|
|
8
|
+
from typing import List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from rich.progress import (
|
|
11
|
+
Progress,
|
|
12
|
+
TextColumn,
|
|
13
|
+
BarColumn,
|
|
14
|
+
TaskProgressColumn,
|
|
15
|
+
TimeRemainingColumn,
|
|
16
|
+
TimeElapsedColumn,
|
|
17
|
+
)
|
|
18
|
+
from tqdm import tqdm
|
|
19
|
+
|
|
20
|
+
OUT_TIME_PREFIX = b"out_time_"
|
|
21
|
+
# Convert microseconds to seconds
|
|
22
|
+
MULTIPLIER = 1e-6
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FfmpegLogLevel(Enum):
|
|
26
|
+
QUIET = "quiet"
|
|
27
|
+
PANIC = "panic"
|
|
28
|
+
FATAL = "fatal"
|
|
29
|
+
ERROR = "error"
|
|
30
|
+
WARNING = "warning"
|
|
31
|
+
INFO = "info"
|
|
32
|
+
VERBOSE = "verbose"
|
|
33
|
+
DEBUG = "debug"
|
|
34
|
+
TRACE = "trace"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FfmpegProcess:
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _validate_command(command: List[str]) -> bool:
|
|
40
|
+
if not shutil.which("ffmpeg"):
|
|
41
|
+
print("Error: FFmpeg executable not found in PATH")
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
if "-i" not in command:
|
|
45
|
+
print("Error: FFmpeg command must include '-i'")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
input_idx = command.index("-i") + 1
|
|
49
|
+
if input_idx >= len(command):
|
|
50
|
+
print("Error: No input file specified after -i")
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
input_file = Path(command[input_idx])
|
|
54
|
+
if not input_file.exists():
|
|
55
|
+
print(f"Error: Input file not found: {input_file}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
if input_idx + 1 >= len(command):
|
|
59
|
+
print("Error: No output file specified")
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def _should_overwrite(self):
|
|
65
|
+
if self._output_filepath.exists() and "-y" not in self._ffmpeg_command:
|
|
66
|
+
try:
|
|
67
|
+
if (
|
|
68
|
+
input(
|
|
69
|
+
f"{self._output_filepath} already exists. Overwrite? [Y/N]: "
|
|
70
|
+
).lower()
|
|
71
|
+
!= "y"
|
|
72
|
+
):
|
|
73
|
+
print(
|
|
74
|
+
"FFmpeg process cancelled. Output file exists and overwrite declined."
|
|
75
|
+
)
|
|
76
|
+
return False
|
|
77
|
+
except EOFError:
|
|
78
|
+
print("Input error. FFmpeg process cancelled.")
|
|
79
|
+
return False
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
print("[KeyboardInterrupt] FFmpeg process cancelled.")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
self._ffmpeg_command.insert(1, "-y")
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _get_duration(filepath: Path) -> Optional[float]:
|
|
89
|
+
try:
|
|
90
|
+
output = subprocess.check_output(
|
|
91
|
+
[
|
|
92
|
+
"ffprobe",
|
|
93
|
+
"-v",
|
|
94
|
+
"error",
|
|
95
|
+
"-show_entries",
|
|
96
|
+
"format=duration",
|
|
97
|
+
"-of",
|
|
98
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
99
|
+
"-i",
|
|
100
|
+
str(filepath),
|
|
101
|
+
],
|
|
102
|
+
text=True,
|
|
103
|
+
)
|
|
104
|
+
return float(output)
|
|
105
|
+
except (subprocess.CalledProcessError, ValueError, FileNotFoundError):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _check_shell_needed(command):
|
|
110
|
+
shell_operators = {"|", ">", "<", ">>", "&&", "||"}
|
|
111
|
+
return any(op in item for item in command for op in shell_operators)
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
command: List[str],
|
|
116
|
+
ffmpeg_log_level: Optional[Union[FfmpegLogLevel, str]] = None,
|
|
117
|
+
ffmpeg_log_file: Optional[Union[str, Path]] = None,
|
|
118
|
+
print_detected_duration: bool = False,
|
|
119
|
+
print_stderr_new_line: bool = False,
|
|
120
|
+
):
|
|
121
|
+
if not self._validate_command(command):
|
|
122
|
+
self._return_code = 1
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
if ffmpeg_log_level and ffmpeg_log_level not in FfmpegLogLevel:
|
|
126
|
+
print(
|
|
127
|
+
f"ffmpeg_log_level must be a lowercase variant of: {list(FfmpegLogLevel.__members__)}"
|
|
128
|
+
)
|
|
129
|
+
self._return_code = 1
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
input_idx = command.index("-i") + 1
|
|
133
|
+
self._input_filepath = Path(command[input_idx])
|
|
134
|
+
self._output_filepath = Path(command[-1])
|
|
135
|
+
self._ffmpeg_log_level = (
|
|
136
|
+
ffmpeg_log_level if ffmpeg_log_level else FfmpegLogLevel.VERBOSE.value
|
|
137
|
+
)
|
|
138
|
+
self._ffmpeg_log_file = Path(
|
|
139
|
+
ffmpeg_log_file or f"{self._input_filepath.name}_log.txt"
|
|
140
|
+
)
|
|
141
|
+
self._print_detected_duration = print_detected_duration
|
|
142
|
+
self._print_stderr_new_line = print_stderr_new_line
|
|
143
|
+
self._duration_secs = self._get_duration(self._input_filepath)
|
|
144
|
+
|
|
145
|
+
if self._print_detected_duration and self._duration_secs is not None:
|
|
146
|
+
print(f"Detected duration: {self._duration_secs} seconds")
|
|
147
|
+
|
|
148
|
+
self._ffmpeg_command = [
|
|
149
|
+
command[0],
|
|
150
|
+
"-hide_banner",
|
|
151
|
+
"-loglevel",
|
|
152
|
+
self._ffmpeg_log_level,
|
|
153
|
+
"-progress",
|
|
154
|
+
"pipe:1",
|
|
155
|
+
"-nostats",
|
|
156
|
+
]
|
|
157
|
+
self._ffmpeg_command.extend(command[1:])
|
|
158
|
+
|
|
159
|
+
if not self._should_overwrite():
|
|
160
|
+
self._return_code = 1
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
def _use_rich(self, process) -> int:
|
|
164
|
+
task_id = None
|
|
165
|
+
|
|
166
|
+
with Progress(
|
|
167
|
+
TextColumn("[progress.description]{task.description}"),
|
|
168
|
+
BarColumn(),
|
|
169
|
+
TaskProgressColumn(),
|
|
170
|
+
TimeElapsedColumn(),
|
|
171
|
+
TimeRemainingColumn(compact=True),
|
|
172
|
+
refresh_per_second=2,
|
|
173
|
+
) as progress_bar:
|
|
174
|
+
if self._duration_secs:
|
|
175
|
+
task_id = progress_bar.add_task(
|
|
176
|
+
f"Processing {self._input_filepath.name}",
|
|
177
|
+
total=self._duration_secs,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
update_progress = progress_bar.update
|
|
181
|
+
duration = self._duration_secs
|
|
182
|
+
|
|
183
|
+
for line in process.stdout:
|
|
184
|
+
if line[:9] == OUT_TIME_PREFIX:
|
|
185
|
+
value = line[12:-1]
|
|
186
|
+
# Check if the first byte is a digit
|
|
187
|
+
try:
|
|
188
|
+
if 48 <= value[0] <= 57:
|
|
189
|
+
value_int = int(value) * MULTIPLIER
|
|
190
|
+
if value_int <= duration:
|
|
191
|
+
update_progress(task_id, completed=value_int)
|
|
192
|
+
except (IndexError, ValueError):
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
process.wait()
|
|
196
|
+
|
|
197
|
+
if process.returncode == 0 and task_id is not None:
|
|
198
|
+
update_progress(task_id, completed=duration)
|
|
199
|
+
progress_bar.columns = (
|
|
200
|
+
TextColumn("[progress.description]{task.description}"),
|
|
201
|
+
BarColumn(),
|
|
202
|
+
TaskProgressColumn(),
|
|
203
|
+
)
|
|
204
|
+
update_progress(
|
|
205
|
+
task_id,
|
|
206
|
+
description=f"✓ Processed {self._input_filepath.name}",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if process.returncode != 0:
|
|
210
|
+
if task_id is not None:
|
|
211
|
+
progress_bar.update(
|
|
212
|
+
task_id,
|
|
213
|
+
description=f"FFmpeg process failed. Check out {self._ffmpeg_log_file} for details.",
|
|
214
|
+
)
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
def _use_tqdm(self, process) -> int:
|
|
220
|
+
progress_bar = None
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
width = os.get_terminal_size().columns
|
|
224
|
+
except OSError:
|
|
225
|
+
width = 80
|
|
226
|
+
|
|
227
|
+
if self._duration_secs:
|
|
228
|
+
progress_bar = tqdm(
|
|
229
|
+
mininterval=0.5,
|
|
230
|
+
total=self._duration_secs,
|
|
231
|
+
desc=f"Processing {self._input_filepath.name}",
|
|
232
|
+
ncols=80,
|
|
233
|
+
dynamic_ncols=True if width < 80 else False,
|
|
234
|
+
bar_format="{desc} {bar} {percentage:.1f}% {elapsed} {remaining}",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
duration = self._duration_secs
|
|
238
|
+
|
|
239
|
+
for line in process.stdout:
|
|
240
|
+
if line[:9] == OUT_TIME_PREFIX:
|
|
241
|
+
value = line[12:-1]
|
|
242
|
+
try:
|
|
243
|
+
# Check if the first byte is a digit
|
|
244
|
+
if 48 <= value[0] <= 57:
|
|
245
|
+
value_int = int(value) * MULTIPLIER
|
|
246
|
+
if value_int <= duration:
|
|
247
|
+
progress_bar.n = value_int
|
|
248
|
+
progress_bar.refresh()
|
|
249
|
+
except (IndexError, ValueError):
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
process.wait()
|
|
253
|
+
|
|
254
|
+
if process.returncode == 0 and progress_bar is not None:
|
|
255
|
+
progress_bar.n = duration
|
|
256
|
+
progress_bar.set_description(f"✓ Processed {self._input_filepath.name}")
|
|
257
|
+
progress_bar.close()
|
|
258
|
+
|
|
259
|
+
if process.returncode != 0:
|
|
260
|
+
print(
|
|
261
|
+
f"FFmpeg process failed. Check out {self._ffmpeg_log_file} for details."
|
|
262
|
+
)
|
|
263
|
+
return 1
|
|
264
|
+
|
|
265
|
+
return 0
|
|
266
|
+
|
|
267
|
+
def run(
|
|
268
|
+
self,
|
|
269
|
+
print_command: bool = False,
|
|
270
|
+
use_tqdm: bool = False,
|
|
271
|
+
) -> int:
|
|
272
|
+
if hasattr(self, "_return_code") and self._return_code != 0:
|
|
273
|
+
return 1
|
|
274
|
+
|
|
275
|
+
if print_command:
|
|
276
|
+
print(f"Executing: {' '.join(self._ffmpeg_command)}")
|
|
277
|
+
|
|
278
|
+
self._shell_needed = self._check_shell_needed(self._ffmpeg_command)
|
|
279
|
+
|
|
280
|
+
if self._shell_needed:
|
|
281
|
+
self._ffmpeg_command = " ".join(self._ffmpeg_command)
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
with open(self._ffmpeg_log_file, "w") as f:
|
|
285
|
+
self._process = subprocess.Popen(
|
|
286
|
+
self._ffmpeg_command,
|
|
287
|
+
shell=self._shell_needed,
|
|
288
|
+
stdout=subprocess.PIPE,
|
|
289
|
+
stderr=f,
|
|
290
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
|
291
|
+
if os.name == "nt"
|
|
292
|
+
else 0,
|
|
293
|
+
)
|
|
294
|
+
except Exception as e:
|
|
295
|
+
print(f"Error starting FFmpeg process: {e}")
|
|
296
|
+
return 1
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
return (
|
|
300
|
+
self._use_tqdm(self._process)
|
|
301
|
+
if use_tqdm
|
|
302
|
+
else self._use_rich(self._process)
|
|
303
|
+
)
|
|
304
|
+
except KeyboardInterrupt:
|
|
305
|
+
print("\n[KeyboardInterrupt] Terminating FFmpeg process...")
|
|
306
|
+
self._terminate()
|
|
307
|
+
print("Done!")
|
|
308
|
+
return 1
|
|
309
|
+
finally:
|
|
310
|
+
if self._process and self._process.poll() is None:
|
|
311
|
+
self._terminate()
|
|
312
|
+
|
|
313
|
+
def _terminate(self):
|
|
314
|
+
if not self._process or self._process.poll() is not None:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
proc = psutil.Process(self._process.pid)
|
|
319
|
+
children = proc.children(recursive=True)
|
|
320
|
+
|
|
321
|
+
# Windows. Use CTRL_BREAK_EVENT to terminate entire process group
|
|
322
|
+
if os.name == "nt":
|
|
323
|
+
if self._shell_needed:
|
|
324
|
+
# If running under a shell, terminate shell first
|
|
325
|
+
proc.terminate()
|
|
326
|
+
else:
|
|
327
|
+
# Send CTRL_BREAK_EVENT to entire process group
|
|
328
|
+
os.kill(self._process.pid, CTRL_BREAK_EVENT)
|
|
329
|
+
# Unix
|
|
330
|
+
else:
|
|
331
|
+
for child in children:
|
|
332
|
+
try:
|
|
333
|
+
child.terminate()
|
|
334
|
+
except psutil.NoSuchProcess:
|
|
335
|
+
pass
|
|
336
|
+
proc.terminate()
|
|
337
|
+
|
|
338
|
+
# Wait up to 1 second for processes to exit gracefully
|
|
339
|
+
gone, still_alive = psutil.wait_procs(children + [proc], timeout=1)
|
|
340
|
+
|
|
341
|
+
# Force kill any remaining processes
|
|
342
|
+
for p in still_alive:
|
|
343
|
+
try:
|
|
344
|
+
p.kill()
|
|
345
|
+
except psutil.NoSuchProcess:
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
except (psutil.NoSuchProcess, ProcessLookupError, KeyboardInterrupt):
|
|
349
|
+
# As a last resort, force kill the main process
|
|
350
|
+
self._process.kill()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: better-ffmpeg-progress
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.4
|
|
4
4
|
Summary: Run FFmpeg & see percentage progress + ETA.
|
|
5
5
|
Home-page: https://github.com/CrypticSignal/better-ffmpeg-progress
|
|
6
6
|
Author: GitHub.com/CrypticSignal
|
|
@@ -12,7 +12,6 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Requires-Python: >=3.7
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: ffmpeg-python
|
|
16
15
|
Requires-Dist: psutil
|
|
17
16
|
Requires-Dist: rich
|
|
18
17
|
Requires-Dist: tqdm
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as f:
|
|
|
5
5
|
|
|
6
6
|
setuptools.setup(
|
|
7
7
|
name="better-ffmpeg-progress",
|
|
8
|
-
version="3.2.
|
|
8
|
+
version="3.2.4",
|
|
9
9
|
author="GitHub.com/CrypticSignal",
|
|
10
10
|
author_email="theaudiophile@outlook.com",
|
|
11
11
|
description="Run FFmpeg & see percentage progress + ETA.",
|
|
@@ -13,7 +13,7 @@ setuptools.setup(
|
|
|
13
13
|
long_description_content_type="text/markdown",
|
|
14
14
|
url="https://github.com/CrypticSignal/better-ffmpeg-progress",
|
|
15
15
|
packages=["better_ffmpeg_progress"],
|
|
16
|
-
install_requires=["
|
|
16
|
+
install_requires=["psutil", "rich", "tqdm"],
|
|
17
17
|
python_requires=">=3.7",
|
|
18
18
|
keywords=["ffmpeg", "progress"],
|
|
19
19
|
classifiers=[
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
import psutil
|
|
5
|
-
from queue import Queue
|
|
6
|
-
import shutil
|
|
7
|
-
import subprocess
|
|
8
|
-
from typing import List, Optional, Union
|
|
9
|
-
|
|
10
|
-
from ffmpeg import probe
|
|
11
|
-
from rich.progress import (
|
|
12
|
-
Progress,
|
|
13
|
-
TextColumn,
|
|
14
|
-
BarColumn,
|
|
15
|
-
TaskProgressColumn,
|
|
16
|
-
TimeRemainingColumn,
|
|
17
|
-
TimeElapsedColumn,
|
|
18
|
-
SpinnerColumn,
|
|
19
|
-
)
|
|
20
|
-
from tqdm import tqdm
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class FfmpegLogLevel(Enum):
|
|
24
|
-
QUIET = "quiet"
|
|
25
|
-
PANIC = "panic"
|
|
26
|
-
FATAL = "fatal"
|
|
27
|
-
ERROR = "error"
|
|
28
|
-
WARNING = "warning"
|
|
29
|
-
INFO = "info"
|
|
30
|
-
VERBOSE = "verbose"
|
|
31
|
-
DEBUG = "debug"
|
|
32
|
-
TRACE = "trace"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class FfmpegProcess:
|
|
36
|
-
@classmethod
|
|
37
|
-
def _validate_command(cls, command: List[str]) -> bool:
|
|
38
|
-
if not shutil.which("ffmpeg"):
|
|
39
|
-
print("Error: FFmpeg executable not found in PATH")
|
|
40
|
-
return False
|
|
41
|
-
|
|
42
|
-
if "-i" not in command:
|
|
43
|
-
print("Error: FFmpeg command must include '-i'")
|
|
44
|
-
return False
|
|
45
|
-
|
|
46
|
-
input_idx = command.index("-i") + 1
|
|
47
|
-
if input_idx >= len(command):
|
|
48
|
-
print("Error: No input file specified after -i")
|
|
49
|
-
return False
|
|
50
|
-
|
|
51
|
-
input_file = Path(command[input_idx])
|
|
52
|
-
if not input_file.exists():
|
|
53
|
-
print(f"Error: Input file not found: {input_file}")
|
|
54
|
-
return False
|
|
55
|
-
|
|
56
|
-
if input_idx + 1 >= len(command):
|
|
57
|
-
print("Error: No output file specified")
|
|
58
|
-
return False
|
|
59
|
-
|
|
60
|
-
return True
|
|
61
|
-
|
|
62
|
-
def __init__(
|
|
63
|
-
self,
|
|
64
|
-
command: List[str],
|
|
65
|
-
ffmpeg_log_level: Optional[FfmpegLogLevel] = None,
|
|
66
|
-
ffmpeg_log_file: Optional[Union[str, Path]] = None,
|
|
67
|
-
print_detected_duration: bool = False,
|
|
68
|
-
print_stderr_new_line: bool = False,
|
|
69
|
-
):
|
|
70
|
-
if not self._validate_command(command):
|
|
71
|
-
self.return_code = 1
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
if ffmpeg_log_level and ffmpeg_log_level not in FfmpegLogLevel:
|
|
75
|
-
print(
|
|
76
|
-
f"ffmpeg_log_level must be a lowercase variant of: {list(FfmpegLogLevel.__members__)}"
|
|
77
|
-
)
|
|
78
|
-
self.return_code = 1
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
input_idx = command.index("-i") + 1
|
|
82
|
-
self._input_filepath = Path(command[input_idx])
|
|
83
|
-
self._output_filepath = Path(command[-1])
|
|
84
|
-
self._ffmpeg_log_level = (
|
|
85
|
-
ffmpeg_log_level if ffmpeg_log_level else FfmpegLogLevel.VERBOSE.value
|
|
86
|
-
)
|
|
87
|
-
self._ffmpeg_log_file = Path(
|
|
88
|
-
ffmpeg_log_file
|
|
89
|
-
if ffmpeg_log_file
|
|
90
|
-
else f"{self._input_filepath.name}_log.txt"
|
|
91
|
-
)
|
|
92
|
-
self._print_detected_duration = print_detected_duration
|
|
93
|
-
self._print_stderr_new_line = print_stderr_new_line
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
probe_data = probe(self._input_filepath)
|
|
97
|
-
self._duration_secs = float(probe_data["format"]["duration"])
|
|
98
|
-
if self._print_detected_duration:
|
|
99
|
-
print(f"Detected duration: {self._duration_secs} seconds")
|
|
100
|
-
except Exception:
|
|
101
|
-
self._duration_secs = None
|
|
102
|
-
|
|
103
|
-
self._ffmpeg_command = [
|
|
104
|
-
command[0],
|
|
105
|
-
"-hide_banner",
|
|
106
|
-
"-loglevel",
|
|
107
|
-
self._ffmpeg_log_level,
|
|
108
|
-
"-progress",
|
|
109
|
-
"pipe:1",
|
|
110
|
-
"-nostats",
|
|
111
|
-
*command[1:],
|
|
112
|
-
]
|
|
113
|
-
|
|
114
|
-
self.return_code = 0
|
|
115
|
-
|
|
116
|
-
def _use_rich(self, process) -> int:
|
|
117
|
-
with Progress(
|
|
118
|
-
SpinnerColumn(),
|
|
119
|
-
TextColumn("[progress.description]{task.description}"),
|
|
120
|
-
BarColumn(),
|
|
121
|
-
TaskProgressColumn(),
|
|
122
|
-
TimeElapsedColumn(),
|
|
123
|
-
TimeRemainingColumn(compact=True),
|
|
124
|
-
refresh_per_second=10,
|
|
125
|
-
) as progress_bar:
|
|
126
|
-
task_id = None
|
|
127
|
-
|
|
128
|
-
if self._duration_secs:
|
|
129
|
-
task_id = progress_bar.add_task(
|
|
130
|
-
f"Processing {self._input_filepath.name}",
|
|
131
|
-
total=self._duration_secs,
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
if task_id is not None:
|
|
135
|
-
while process.poll() is None:
|
|
136
|
-
stdout = process.stdout.readline()
|
|
137
|
-
|
|
138
|
-
if not stdout.startswith("out_time_ms"):
|
|
139
|
-
continue
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
value = int(stdout.split("=")[1]) / 1_000_000
|
|
143
|
-
except ValueError:
|
|
144
|
-
continue
|
|
145
|
-
else:
|
|
146
|
-
if value <= self._duration_secs:
|
|
147
|
-
progress_bar.update(task_id, completed=value)
|
|
148
|
-
|
|
149
|
-
if process.returncode == 0:
|
|
150
|
-
progress_bar.update(task_id, completed=self._duration_secs)
|
|
151
|
-
progress_bar.columns = (
|
|
152
|
-
TextColumn("[progress.description]{task.description}"),
|
|
153
|
-
BarColumn(),
|
|
154
|
-
TaskProgressColumn(),
|
|
155
|
-
)
|
|
156
|
-
progress_bar.update(
|
|
157
|
-
task_id,
|
|
158
|
-
description=f"✓ Processed {self._input_filepath.name}",
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
if process.returncode != 0:
|
|
162
|
-
progress_bar.update(
|
|
163
|
-
task_id,
|
|
164
|
-
description=f"The FFmpeg process did not complete successfully. Check out {self._ffmpeg_log_file} for details.",
|
|
165
|
-
)
|
|
166
|
-
return 1
|
|
167
|
-
|
|
168
|
-
return 0
|
|
169
|
-
|
|
170
|
-
def _use_tqdm(self, process) -> int:
|
|
171
|
-
progress_bar = None
|
|
172
|
-
width, _ = os.get_terminal_size()
|
|
173
|
-
|
|
174
|
-
if self._duration_secs:
|
|
175
|
-
progress_bar = tqdm(
|
|
176
|
-
total=self._duration_secs,
|
|
177
|
-
desc=f"Processing {self._input_filepath.name}",
|
|
178
|
-
ncols=80,
|
|
179
|
-
dynamic_ncols=True if width < 80 else False,
|
|
180
|
-
bar_format="{desc} {bar} {percentage:.1f}% {elapsed} {remaining}",
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
if progress_bar is not None:
|
|
184
|
-
while process.poll() is None:
|
|
185
|
-
stdout = process.stdout.readline()
|
|
186
|
-
|
|
187
|
-
if not stdout.startswith("out_time_ms"):
|
|
188
|
-
continue
|
|
189
|
-
|
|
190
|
-
try:
|
|
191
|
-
value = int(stdout.split("=")[1]) / 1_000_000
|
|
192
|
-
except ValueError:
|
|
193
|
-
continue
|
|
194
|
-
else:
|
|
195
|
-
if value <= self._duration_secs:
|
|
196
|
-
progress_bar.n = value
|
|
197
|
-
progress_bar.refresh()
|
|
198
|
-
|
|
199
|
-
if process.returncode == 0:
|
|
200
|
-
progress_bar.n = self._duration_secs
|
|
201
|
-
progress_bar.set_description(f"✓ Processed {self._input_filepath.name}")
|
|
202
|
-
|
|
203
|
-
progress_bar.close()
|
|
204
|
-
|
|
205
|
-
if process.returncode != 0:
|
|
206
|
-
print(
|
|
207
|
-
f"The FFmpeg process did not complete successfully. Check out {self._ffmpeg_log_file} for details."
|
|
208
|
-
)
|
|
209
|
-
return 1
|
|
210
|
-
|
|
211
|
-
return 0
|
|
212
|
-
|
|
213
|
-
def run(
|
|
214
|
-
self,
|
|
215
|
-
print_command: bool = False,
|
|
216
|
-
use_tqdm: bool = False,
|
|
217
|
-
) -> int:
|
|
218
|
-
if hasattr(self, "return_code") and self.return_code != 0:
|
|
219
|
-
return 1
|
|
220
|
-
|
|
221
|
-
if self._output_filepath.exists() and "-y" not in self._ffmpeg_command:
|
|
222
|
-
if (
|
|
223
|
-
input(
|
|
224
|
-
f"{self._output_filepath} already exists. Overwrite? [Y/N]: "
|
|
225
|
-
).lower()
|
|
226
|
-
!= "y"
|
|
227
|
-
):
|
|
228
|
-
print(
|
|
229
|
-
"FFmpeg process cancelled. Output file exists and overwrite declined."
|
|
230
|
-
)
|
|
231
|
-
return 1
|
|
232
|
-
self._ffmpeg_command.insert(1, "-y")
|
|
233
|
-
|
|
234
|
-
if print_command:
|
|
235
|
-
print(f"Executing: {' '.join(self._ffmpeg_command)}")
|
|
236
|
-
|
|
237
|
-
def _contains_shell_operators(command: List[str]) -> bool:
|
|
238
|
-
shell_operators = {"|", ">", "<", ">>", "&&", "||"}
|
|
239
|
-
return any(op in command for op in shell_operators)
|
|
240
|
-
|
|
241
|
-
# If command contains shell operators, turn the list into a string.
|
|
242
|
-
if _contains_shell_operators(self._ffmpeg_command):
|
|
243
|
-
self._ffmpeg_command = " ".join(self._ffmpeg_command)
|
|
244
|
-
|
|
245
|
-
try:
|
|
246
|
-
with open(self._ffmpeg_log_file, "w") as f:
|
|
247
|
-
process = subprocess.Popen(
|
|
248
|
-
self._ffmpeg_command,
|
|
249
|
-
shell=isinstance(self._ffmpeg_command, str),
|
|
250
|
-
stdout=subprocess.PIPE,
|
|
251
|
-
stderr=f,
|
|
252
|
-
text=True,
|
|
253
|
-
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
|
254
|
-
if os.name == "nt"
|
|
255
|
-
else 0,
|
|
256
|
-
)
|
|
257
|
-
except Exception as e:
|
|
258
|
-
print(f"Error starting FFmpeg process: {e}")
|
|
259
|
-
return 1
|
|
260
|
-
|
|
261
|
-
try:
|
|
262
|
-
if use_tqdm:
|
|
263
|
-
return self._use_tqdm(process)
|
|
264
|
-
else:
|
|
265
|
-
return self._use_rich(process)
|
|
266
|
-
except KeyboardInterrupt:
|
|
267
|
-
try:
|
|
268
|
-
psutil.Process(process.pid).terminate()
|
|
269
|
-
except psutil.NoSuchProcess:
|
|
270
|
-
pass
|
|
271
|
-
return 1
|
|
File without changes
|
|
File without changes
|
{better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress/__init__.py
RENAMED
|
File without changes
|
{better_ffmpeg_progress-3.2.2 → better_ffmpeg_progress-3.2.4}/better_ffmpeg_progress/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|