better-ffmpeg-progress 2.1.6__tar.gz → 2.1.8__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-2.1.6 → better_ffmpeg_progress-2.1.8}/PKG-INFO +21 -14
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/README.md +18 -11
- better_ffmpeg_progress-2.1.8/better_ffmpeg_progress/better_ffmpeg_progress.py +277 -0
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/better_ffmpeg_progress.egg-info/PKG-INFO +21 -14
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/better_ffmpeg_progress.egg-info/requires.txt +1 -1
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/setup.py +3 -3
- better_ffmpeg_progress-2.1.6/better_ffmpeg_progress/better_ffmpeg_progress.py +0 -183
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/LICENSE +0 -0
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/better_ffmpeg_progress/__init__.py +0 -0
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/better_ffmpeg_progress.egg-info/SOURCES.txt +0 -0
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/better_ffmpeg_progress.egg-info/dependency_links.txt +0 -0
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/better_ffmpeg_progress.egg-info/top_level.txt +0 -0
- {better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: better-ffmpeg-progress
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.8
|
|
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
|
|
@@ -9,42 +9,41 @@ Keywords: ffmpeg,progress
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: ffmpeg-python
|
|
16
|
-
Requires-Dist:
|
|
16
|
+
Requires-Dist: rich
|
|
17
17
|
|
|
18
18
|
<div align="center">
|
|
19
19
|
|
|
20
|
-
[](https://pypistats.org/packages/
|
|
21
|
-
[](https://pypistats.org/packages/
|
|
20
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
21
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
22
22
|
[](LICENSE.txt)
|
|
23
23
|
|
|
24
24
|
# Better FFmpeg Progress
|
|
25
25
|
|
|
26
|
-
Runs an FFmpeg command and uses [
|
|
26
|
+
Runs an FFmpeg command and uses [Rich](https://github.com/Textualize/rich) to show a progress bar.
|
|
27
27
|
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
## Example:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
|
|
33
|
+
⠏ Processing... ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Where:
|
|
37
37
|
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
41
|
-
- `00:19` is the time elapsed since the FFmpeg process started.
|
|
42
|
-
- `00:34` is the estimated time required for the FFmpeg process to complete.
|
|
43
|
-
- `1.07` shows how many seconds of the input file are processed per second.
|
|
38
|
+
- `23%` is the percentage progress.
|
|
39
|
+
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
40
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
44
41
|
|
|
45
42
|
## Installation:
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
```
|
|
45
|
+
pip install better-ffmpeg-progress --upgrade
|
|
46
|
+
```
|
|
48
47
|
|
|
49
48
|
## Usage:
|
|
50
49
|
|
|
@@ -141,3 +140,11 @@ The `run` method takes the following **optional** arguments:
|
|
|
141
140
|
[02/05/2024]
|
|
142
141
|
|
|
143
142
|
- [v2.1.5] Fix issue [#23](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/23) and make an error message more specific. [Here](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/a6ef7f26d080b684144021301f3b2aa5e0834dae) is the relevant commit.
|
|
143
|
+
|
|
144
|
+
[18/10/2024]
|
|
145
|
+
|
|
146
|
+
- [v2.1.6] Notify the user if the input filepath or filename is incorrect.
|
|
147
|
+
- [v2.1.7] Refactor to use threads and queues.
|
|
148
|
+
|
|
149
|
+
[19/10/2024]
|
|
150
|
+
- [v2.1.8] Use [Rich](https://github.com/Textualize/rich) instead of [tqdm](https://github.com/tqdm/tqdm) and format code with [Ruff](https://github.com/astral-sh/ruff).
|
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
[](https://pypistats.org/packages/
|
|
4
|
-
[](https://pypistats.org/packages/
|
|
3
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
4
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
5
5
|
[](LICENSE.txt)
|
|
6
6
|
|
|
7
7
|
# Better FFmpeg Progress
|
|
8
8
|
|
|
9
|
-
Runs an FFmpeg command and uses [
|
|
9
|
+
Runs an FFmpeg command and uses [Rich](https://github.com/Textualize/rich) to show a progress bar.
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
13
|
## Example:
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
|
|
16
|
+
⠏ Processing... ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Where:
|
|
20
20
|
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
24
|
-
- `00:19` is the time elapsed since the FFmpeg process started.
|
|
25
|
-
- `00:34` is the estimated time required for the FFmpeg process to complete.
|
|
26
|
-
- `1.07` shows how many seconds of the input file are processed per second.
|
|
21
|
+
- `23%` is the percentage progress.
|
|
22
|
+
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
23
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
27
24
|
|
|
28
25
|
## Installation:
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
```
|
|
28
|
+
pip install better-ffmpeg-progress --upgrade
|
|
29
|
+
```
|
|
31
30
|
|
|
32
31
|
## Usage:
|
|
33
32
|
|
|
@@ -124,3 +123,11 @@ The `run` method takes the following **optional** arguments:
|
|
|
124
123
|
[02/05/2024]
|
|
125
124
|
|
|
126
125
|
- [v2.1.5] Fix issue [#23](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/23) and make an error message more specific. [Here](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/a6ef7f26d080b684144021301f3b2aa5e0834dae) is the relevant commit.
|
|
126
|
+
|
|
127
|
+
[18/10/2024]
|
|
128
|
+
|
|
129
|
+
- [v2.1.6] Notify the user if the input filepath or filename is incorrect.
|
|
130
|
+
- [v2.1.7] Refactor to use threads and queues.
|
|
131
|
+
|
|
132
|
+
[19/10/2024]
|
|
133
|
+
- [v2.1.8] Use [Rich](https://github.com/Textualize/rich) instead of [tqdm](https://github.com/tqdm/tqdm) and format code with [Ruff](https://github.com/astral-sh/ruff).
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from queue import Empty, Queue
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from threading import Thread
|
|
7
|
+
from typing import Callable, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
from ffmpeg import probe
|
|
10
|
+
from rich.progress import (
|
|
11
|
+
Progress,
|
|
12
|
+
TextColumn,
|
|
13
|
+
BarColumn,
|
|
14
|
+
TaskProgressColumn,
|
|
15
|
+
TimeRemainingColumn,
|
|
16
|
+
TimeElapsedColumn,
|
|
17
|
+
SpinnerColumn,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Metrics:
|
|
23
|
+
"""Data class to store FFmpeg progress information."""
|
|
24
|
+
|
|
25
|
+
percentage: float = 0.0
|
|
26
|
+
seconds_processed: float = 0
|
|
27
|
+
speed: float = None
|
|
28
|
+
eta: Optional[float] = None
|
|
29
|
+
estimated_size: Optional[int] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FfmpegProcess:
|
|
33
|
+
"""
|
|
34
|
+
Args:
|
|
35
|
+
command: A list of arguments to pass to FFmpeg.
|
|
36
|
+
ffmpeg_loglevel: Desired FFmpeg log level. Default is "verbose".
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
FileNotFoundError: If FFmpeg cannot find the input filepath or filename.
|
|
40
|
+
ValueError: If the list of arguments does not include "-i"
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
WANTED_METRICS = frozenset({"out_time_ms", "total_size", "speed"})
|
|
44
|
+
|
|
45
|
+
def __init__(self, command: List[str], ffmpeg_loglevel: str = "verbose"):
|
|
46
|
+
if "-i" not in command:
|
|
47
|
+
raise ValueError("FFmpeg command must include '-i'")
|
|
48
|
+
|
|
49
|
+
self._ffmpeg_args = command + ["-hide_banner", "-loglevel", ffmpeg_loglevel]
|
|
50
|
+
self._ffmpeg_stderr: List[str] = []
|
|
51
|
+
self._output_filepath = Path(command[-1])
|
|
52
|
+
self._metrics = Metrics()
|
|
53
|
+
self._duration_secs: Optional[float] = None
|
|
54
|
+
self._current_size: int = 0
|
|
55
|
+
|
|
56
|
+
self._get_input_file_duration()
|
|
57
|
+
|
|
58
|
+
def _get_input_file_duration(self) -> None:
|
|
59
|
+
"""Use ffprobe to get the duration of the input file"""
|
|
60
|
+
index_of_filepath = self._ffmpeg_args.index("-i") + 1
|
|
61
|
+
self._input_filepath = Path(self._ffmpeg_args[index_of_filepath])
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
self._duration_secs = float(
|
|
65
|
+
probe(self._input_filepath)["format"]["duration"]
|
|
66
|
+
)
|
|
67
|
+
print(
|
|
68
|
+
f"The duration of {self._input_filepath.name} has been detected as {self._duration_secs} seconds"
|
|
69
|
+
)
|
|
70
|
+
# -progress pipe:1 sends progress metrics to stdout AND -stats_period sets the period at which encoding progress/statistics are updated
|
|
71
|
+
# -nostats disables encoding progress/statistics in stderr
|
|
72
|
+
self._ffmpeg_args.extend(
|
|
73
|
+
["-progress", "pipe:1", "-stats_period", "0.1", "-nostats"]
|
|
74
|
+
)
|
|
75
|
+
except Exception:
|
|
76
|
+
print(
|
|
77
|
+
f"Could not detect the duration of {self._input_filepath.name}. Percentage progress, ETA and estimated filesize will be unavailable."
|
|
78
|
+
)
|
|
79
|
+
self._duration_secs = None
|
|
80
|
+
|
|
81
|
+
def _check_output_exists(self) -> bool:
|
|
82
|
+
"""Check if output file exists and handle overwrite prompt."""
|
|
83
|
+
if self._output_filepath.exists() and "-y" not in self._ffmpeg_args:
|
|
84
|
+
choice = input(
|
|
85
|
+
f"{self._output_filepath} already exists. Overwrite? [Y/N]: "
|
|
86
|
+
).lower()
|
|
87
|
+
if choice != "y":
|
|
88
|
+
print(
|
|
89
|
+
"FFmpeg process cancelled. Output file exists and overwrite declined."
|
|
90
|
+
)
|
|
91
|
+
return False
|
|
92
|
+
self._ffmpeg_args.insert(1, "-y")
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
def _update_metrics(
|
|
96
|
+
self, line: str, metric: str, value: str, progress_handler: Optional[Callable]
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Update progress information based on FFmpeg output."""
|
|
99
|
+
if metric == "total_size":
|
|
100
|
+
self._current_size = int(value)
|
|
101
|
+
|
|
102
|
+
elif metric == "out_time_ms":
|
|
103
|
+
self._metrics.seconds_processed = int(value) / 1_000_000
|
|
104
|
+
|
|
105
|
+
if self._duration_secs:
|
|
106
|
+
self._metrics.percentage = (
|
|
107
|
+
self._metrics.seconds_processed / self._duration_secs
|
|
108
|
+
) * 100
|
|
109
|
+
if self._current_size and self._metrics.percentage > 0:
|
|
110
|
+
self._metrics.estimated_size = int(
|
|
111
|
+
self._current_size * (100 / self._metrics.percentage)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
elif metric == "speed":
|
|
115
|
+
speed_float = float(value.rstrip("x"))
|
|
116
|
+
if speed_float > 0:
|
|
117
|
+
self._metrics.speed = float(speed_float)
|
|
118
|
+
if self._duration_secs:
|
|
119
|
+
seconds_remaining = (
|
|
120
|
+
self._duration_secs - self._metrics.seconds_processed
|
|
121
|
+
)
|
|
122
|
+
self._metrics.eta = seconds_remaining / self._metrics.speed
|
|
123
|
+
|
|
124
|
+
if line == "progress=end":
|
|
125
|
+
self._metrics.percentage = 100
|
|
126
|
+
self._metrics.eta = 0
|
|
127
|
+
|
|
128
|
+
if progress_handler:
|
|
129
|
+
progress_handler(
|
|
130
|
+
self._metrics.percentage,
|
|
131
|
+
self._metrics.speed,
|
|
132
|
+
self._metrics.eta,
|
|
133
|
+
self._metrics.estimated_size,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _handle_pipe(pipe, queue: Queue) -> None:
|
|
138
|
+
"""Read from the pipe and put the lines into the queue."""
|
|
139
|
+
try:
|
|
140
|
+
for line in pipe:
|
|
141
|
+
queue.put(line.strip())
|
|
142
|
+
finally:
|
|
143
|
+
pipe.close()
|
|
144
|
+
|
|
145
|
+
def _write_ffmpeg_output(
|
|
146
|
+
self, stderr_output: str, output_file: Optional[Union[str, Path]]
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Write FFmpeg stderr to a txt file."""
|
|
149
|
+
output_dir = Path("ffmpeg_output")
|
|
150
|
+
output_dir.mkdir(exist_ok=True)
|
|
151
|
+
|
|
152
|
+
log_file = output_file or output_dir / f"[{self._input_filepath.name}].txt"
|
|
153
|
+
log_file = Path(log_file)
|
|
154
|
+
|
|
155
|
+
log_file.write_text(stderr_output)
|
|
156
|
+
print(f"Done! FFmpeg log file: {log_file}")
|
|
157
|
+
|
|
158
|
+
def _start_process(
|
|
159
|
+
self,
|
|
160
|
+
progress_bar: Optional[Progress],
|
|
161
|
+
task_id: Optional[int],
|
|
162
|
+
success_handler: Optional[Callable],
|
|
163
|
+
error_handler: Optional[Callable],
|
|
164
|
+
progress_handler: Optional[Callable],
|
|
165
|
+
) -> None:
|
|
166
|
+
process = subprocess.Popen(
|
|
167
|
+
self._ffmpeg_args,
|
|
168
|
+
stdout=subprocess.PIPE,
|
|
169
|
+
stderr=subprocess.PIPE,
|
|
170
|
+
text=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
stdout_queue, stderr_queue = Queue(), Queue()
|
|
174
|
+
Thread(
|
|
175
|
+
target=self._handle_pipe, args=(process.stdout, stdout_queue), daemon=True
|
|
176
|
+
).start()
|
|
177
|
+
Thread(
|
|
178
|
+
target=self._handle_pipe, args=(process.stderr, stderr_queue), daemon=True
|
|
179
|
+
).start()
|
|
180
|
+
|
|
181
|
+
while process.poll() is None:
|
|
182
|
+
try:
|
|
183
|
+
stdout = stdout_queue.get_nowait()
|
|
184
|
+
|
|
185
|
+
if stdout:
|
|
186
|
+
metric, value = stdout.split("=")
|
|
187
|
+
|
|
188
|
+
if metric in self.WANTED_METRICS and value and "N/A" not in value:
|
|
189
|
+
self._update_metrics(stdout, metric, value, progress_handler)
|
|
190
|
+
|
|
191
|
+
if (
|
|
192
|
+
metric == "out_time_ms"
|
|
193
|
+
and progress_bar
|
|
194
|
+
and task_id is not None
|
|
195
|
+
):
|
|
196
|
+
progress_bar.update(
|
|
197
|
+
task_id,
|
|
198
|
+
completed=self._metrics.seconds_processed,
|
|
199
|
+
speed=self._metrics.speed,
|
|
200
|
+
)
|
|
201
|
+
except Empty:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
stderr = stderr_queue.get_nowait()
|
|
206
|
+
|
|
207
|
+
if "No such file" in stderr:
|
|
208
|
+
raise FileNotFoundError(
|
|
209
|
+
f"Input file not found: {self._input_filepath}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
self._ffmpeg_stderr.append(stderr)
|
|
213
|
+
except Empty:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
if process.returncode != 0:
|
|
217
|
+
if error_handler:
|
|
218
|
+
error_handler()
|
|
219
|
+
else:
|
|
220
|
+
sys.exit("\nFFmpeg process failed")
|
|
221
|
+
|
|
222
|
+
if success_handler:
|
|
223
|
+
success_handler()
|
|
224
|
+
|
|
225
|
+
def run(
|
|
226
|
+
self,
|
|
227
|
+
output_file: Optional[Union[str, Path]] = None,
|
|
228
|
+
progress_handler: Optional[Callable] = None,
|
|
229
|
+
success_handler: Optional[Callable] = None,
|
|
230
|
+
error_handler: Optional[Callable] = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Args:
|
|
234
|
+
output_file: Optional filepath to write FFmpeg output to
|
|
235
|
+
progress_handler: Optional function which receives progress metrics
|
|
236
|
+
success_handler: Optional function to handle successful completion of the FFmpeg process
|
|
237
|
+
error_handler: Optional function for FFmpeg process error handling
|
|
238
|
+
"""
|
|
239
|
+
if not self._check_output_exists():
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Clear the terminal
|
|
243
|
+
print("\033[2J\033[H", end="")
|
|
244
|
+
|
|
245
|
+
print(f"Executing: {' '.join(self._ffmpeg_args)}")
|
|
246
|
+
|
|
247
|
+
if self._duration_secs and not progress_handler:
|
|
248
|
+
with Progress(
|
|
249
|
+
SpinnerColumn(),
|
|
250
|
+
TextColumn("[progress.description]{task.description}"),
|
|
251
|
+
BarColumn(),
|
|
252
|
+
TaskProgressColumn(),
|
|
253
|
+
TimeElapsedColumn(),
|
|
254
|
+
TimeRemainingColumn(compact=True),
|
|
255
|
+
refresh_per_second=10,
|
|
256
|
+
) as progress_bar:
|
|
257
|
+
task_id = progress_bar.add_task(
|
|
258
|
+
f"Processing {self._input_filepath.name}", total=self._duration_secs
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
self._start_process(
|
|
262
|
+
progress_bar,
|
|
263
|
+
task_id,
|
|
264
|
+
success_handler,
|
|
265
|
+
error_handler,
|
|
266
|
+
progress_handler,
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
self._start_process(
|
|
270
|
+
None,
|
|
271
|
+
None,
|
|
272
|
+
success_handler,
|
|
273
|
+
error_handler,
|
|
274
|
+
progress_handler,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
self._write_ffmpeg_output("\n".join(self._ffmpeg_stderr), output_file)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: better-ffmpeg-progress
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.8
|
|
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
|
|
@@ -9,42 +9,41 @@ Keywords: ffmpeg,progress
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: ffmpeg-python
|
|
16
|
-
Requires-Dist:
|
|
16
|
+
Requires-Dist: rich
|
|
17
17
|
|
|
18
18
|
<div align="center">
|
|
19
19
|
|
|
20
|
-
[](https://pypistats.org/packages/
|
|
21
|
-
[](https://pypistats.org/packages/
|
|
20
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
21
|
+
[](https://pypistats.org/packages/better-ffmpeg-progress)
|
|
22
22
|
[](LICENSE.txt)
|
|
23
23
|
|
|
24
24
|
# Better FFmpeg Progress
|
|
25
25
|
|
|
26
|
-
Runs an FFmpeg command and uses [
|
|
26
|
+
Runs an FFmpeg command and uses [Rich](https://github.com/Textualize/rich) to show a progress bar.
|
|
27
27
|
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
30
|
## Example:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
|
|
33
|
+
⠏ Processing... ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Where:
|
|
37
37
|
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
-
- `
|
|
41
|
-
- `00:19` is the time elapsed since the FFmpeg process started.
|
|
42
|
-
- `00:34` is the estimated time required for the FFmpeg process to complete.
|
|
43
|
-
- `1.07` shows how many seconds of the input file are processed per second.
|
|
38
|
+
- `23%` is the percentage progress.
|
|
39
|
+
- `0:00:04` is the time (H:MM:SS) elapsed.
|
|
40
|
+
- `00:15` is the estimated time until the FFmpeg process completes.
|
|
44
41
|
|
|
45
42
|
## Installation:
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
```
|
|
45
|
+
pip install better-ffmpeg-progress --upgrade
|
|
46
|
+
```
|
|
48
47
|
|
|
49
48
|
## Usage:
|
|
50
49
|
|
|
@@ -141,3 +140,11 @@ The `run` method takes the following **optional** arguments:
|
|
|
141
140
|
[02/05/2024]
|
|
142
141
|
|
|
143
142
|
- [v2.1.5] Fix issue [#23](https://github.com/CrypticSignal/better-ffmpeg-progress/issues/23) and make an error message more specific. [Here](https://github.com/CrypticSignal/better-ffmpeg-progress/commit/a6ef7f26d080b684144021301f3b2aa5e0834dae) is the relevant commit.
|
|
143
|
+
|
|
144
|
+
[18/10/2024]
|
|
145
|
+
|
|
146
|
+
- [v2.1.6] Notify the user if the input filepath or filename is incorrect.
|
|
147
|
+
- [v2.1.7] Refactor to use threads and queues.
|
|
148
|
+
|
|
149
|
+
[19/10/2024]
|
|
150
|
+
- [v2.1.8] Use [Rich](https://github.com/Textualize/rich) instead of [tqdm](https://github.com/tqdm/tqdm) and format code with [Ruff](https://github.com/astral-sh/ruff).
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
ffmpeg-python
|
|
2
|
-
|
|
2
|
+
rich
|
|
@@ -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="2.1.
|
|
8
|
+
version="2.1.8",
|
|
9
9
|
author="GitHub.com/CrypticSignal",
|
|
10
10
|
author_email="theaudiophile@outlook.com",
|
|
11
11
|
description="Run FFmpeg & see percentage progress + ETA.",
|
|
@@ -13,8 +13,8 @@ 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=["ffmpeg-python", "
|
|
17
|
-
python_requires=">=3.
|
|
16
|
+
install_requires=["ffmpeg-python", "rich"],
|
|
17
|
+
python_requires=">=3.7",
|
|
18
18
|
keywords=["ffmpeg", "progress"],
|
|
19
19
|
classifiers=[
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from ffmpeg import probe
|
|
7
|
-
from tqdm import tqdm
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class FfmpegProcess:
|
|
11
|
-
"""
|
|
12
|
-
Args:
|
|
13
|
-
command (list): A list of arguments to pass to FFmpeg.
|
|
14
|
-
|
|
15
|
-
ffmpeg_loglevel (str, optional): Desired FFmpeg log level. Default is "verbose".
|
|
16
|
-
|
|
17
|
-
Raises:
|
|
18
|
-
ValueError: If the list of arguments does not include "-i".
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(self, command, ffmpeg_loglevel="verbose"):
|
|
22
|
-
if "-i" not in command:
|
|
23
|
-
raise ValueError("FFmpeg command must include '-i'")
|
|
24
|
-
|
|
25
|
-
self._ffmpeg_args = command + ["-hide_banner", "-loglevel", ffmpeg_loglevel]
|
|
26
|
-
self._output_filepath = command[-1]
|
|
27
|
-
|
|
28
|
-
self._set_file_info()
|
|
29
|
-
|
|
30
|
-
self._estimated_size = None
|
|
31
|
-
self._eta = None
|
|
32
|
-
self._percentage_progress = 0
|
|
33
|
-
self._previous_seconds_processed = 0
|
|
34
|
-
self._progress_bar = None
|
|
35
|
-
self._seconds_processed = 0
|
|
36
|
-
self._speed = 0
|
|
37
|
-
self._current_size = 0
|
|
38
|
-
|
|
39
|
-
def _set_file_info(self):
|
|
40
|
-
index_of_filepath = self._ffmpeg_args.index("-i") + 1
|
|
41
|
-
self._filepath = self._ffmpeg_args[index_of_filepath]
|
|
42
|
-
self._can_get_duration = True
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
self._duration_secs = float(probe(self._filepath)["format"]["duration"])
|
|
46
|
-
print(
|
|
47
|
-
f"The duration of {self._filepath} has been detected as {self._duration_secs} seconds."
|
|
48
|
-
)
|
|
49
|
-
except Exception:
|
|
50
|
-
self._can_get_duration = False
|
|
51
|
-
|
|
52
|
-
if self._can_get_duration:
|
|
53
|
-
self._ffmpeg_args += ["-progress", "pipe:1", "-nostats"]
|
|
54
|
-
|
|
55
|
-
def _should_run_ffmpeg(self):
|
|
56
|
-
dirname = os.path.dirname(self._output_filepath)
|
|
57
|
-
self._dir_files = (
|
|
58
|
-
[file for file in os.listdir(dirname)] if dirname else [file for file in os.listdir()]
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if "-y" not in self._ffmpeg_args and self._output_filepath in self._dir_files:
|
|
62
|
-
choice = input(f"{self._output_filepath} already exists. Overwrite? [Y/N]: ").lower()
|
|
63
|
-
|
|
64
|
-
if choice != "y":
|
|
65
|
-
print(
|
|
66
|
-
"FFmpeg will not run as the output filename already exists, and you do not want it to be overwritten."
|
|
67
|
-
)
|
|
68
|
-
return False
|
|
69
|
-
|
|
70
|
-
self._ffmpeg_args.insert(1, "-y")
|
|
71
|
-
return True
|
|
72
|
-
|
|
73
|
-
return True
|
|
74
|
-
|
|
75
|
-
def _update_progress(self, ffmpeg_output, metric_name, value, progress_handler):
|
|
76
|
-
if progress_handler is None:
|
|
77
|
-
if metric_name == "out_time_ms":
|
|
78
|
-
seconds_processed = round(int(value) / 1_000_000, 1)
|
|
79
|
-
seconds_increase = seconds_processed - self._previous_seconds_processed
|
|
80
|
-
self._progress_bar.update(seconds_increase)
|
|
81
|
-
self._previous_seconds_processed = seconds_processed
|
|
82
|
-
|
|
83
|
-
else:
|
|
84
|
-
if metric_name == "total_size":
|
|
85
|
-
self._current_size = int(value)
|
|
86
|
-
|
|
87
|
-
elif metric_name == "out_time_ms":
|
|
88
|
-
self._seconds_processed = int(value) / 1_000_000
|
|
89
|
-
|
|
90
|
-
if self._can_get_duration:
|
|
91
|
-
self._percentage_progress = (
|
|
92
|
-
self._seconds_processed / self._duration_secs
|
|
93
|
-
) * 100
|
|
94
|
-
|
|
95
|
-
if self._current_size is not None and self._percentage_progress != 0.0:
|
|
96
|
-
self._estimated_size = self._current_size * (
|
|
97
|
-
100 / self._percentage_progress
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
elif metric_name == "speed":
|
|
101
|
-
speed_str = value[:-1]
|
|
102
|
-
|
|
103
|
-
if speed_str != "0":
|
|
104
|
-
self._speed = float(speed_str)
|
|
105
|
-
|
|
106
|
-
if self._can_get_duration:
|
|
107
|
-
self._eta = (self._duration_secs - self._seconds_processed) / self._speed
|
|
108
|
-
|
|
109
|
-
if ffmpeg_output == "progress=end":
|
|
110
|
-
self._percentage_progress = 100
|
|
111
|
-
self._eta = 0
|
|
112
|
-
|
|
113
|
-
progress_handler(
|
|
114
|
-
self._percentage_progress, self._speed, self._eta, self._estimated_size
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
def run(
|
|
118
|
-
self,
|
|
119
|
-
progress_handler=None,
|
|
120
|
-
ffmpeg_output_file=None,
|
|
121
|
-
success_handler=None,
|
|
122
|
-
error_handler=None,
|
|
123
|
-
):
|
|
124
|
-
if not self._should_run_ffmpeg():
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
print(f"Attempting to run: {' '.join(self._ffmpeg_args)}\n")
|
|
128
|
-
process = subprocess.Popen(self._ffmpeg_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
129
|
-
|
|
130
|
-
stdout = process.stdout.readline().decode()
|
|
131
|
-
stderr = process.stderr.readline().decode()
|
|
132
|
-
|
|
133
|
-
if "No such file" in stderr:
|
|
134
|
-
print(f"{stderr}It seems like the input filepath or filename is incorrect.")
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
if ffmpeg_output_file is None:
|
|
138
|
-
os.makedirs("ffmpeg_output", exist_ok=True)
|
|
139
|
-
ffmpeg_output_file = os.path.join("ffmpeg_output", f"[{Path(self._filepath).name}].txt")
|
|
140
|
-
|
|
141
|
-
with open(ffmpeg_output_file, "a") as f:
|
|
142
|
-
f.write(stdout)
|
|
143
|
-
|
|
144
|
-
if progress_handler is None and self._can_get_duration:
|
|
145
|
-
self._progress_bar = tqdm(
|
|
146
|
-
total=round(self._duration_secs, 1),
|
|
147
|
-
unit="s",
|
|
148
|
-
dynamic_ncols=True,
|
|
149
|
-
leave=False,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
while process.poll() is None:
|
|
153
|
-
stdout = process.stdout.readline().decode().strip()
|
|
154
|
-
|
|
155
|
-
if not stdout:
|
|
156
|
-
continue
|
|
157
|
-
|
|
158
|
-
metric_name = stdout.split("=")[0]
|
|
159
|
-
wanted_metrics = ["out_time_ms", "total_size", "speed"]
|
|
160
|
-
|
|
161
|
-
if metric_name not in wanted_metrics:
|
|
162
|
-
continue
|
|
163
|
-
|
|
164
|
-
value = stdout.split("=")[1].strip()
|
|
165
|
-
|
|
166
|
-
if not value or "N/A" in value:
|
|
167
|
-
continue
|
|
168
|
-
|
|
169
|
-
self._update_progress(stdout, metric_name, value, progress_handler)
|
|
170
|
-
|
|
171
|
-
if process.returncode != 0:
|
|
172
|
-
if error_handler:
|
|
173
|
-
error_handler()
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
print(
|
|
177
|
-
f"The FFmpeg process encountered an error. The output of FFmpeg can be found in {ffmpeg_output_file}"
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
if success_handler:
|
|
181
|
-
success_handler()
|
|
182
|
-
|
|
183
|
-
print(f"\n\nDone! To see FFmpeg's output, check out {ffmpeg_output_file}")
|
|
File without changes
|
{better_ffmpeg_progress-2.1.6 → better_ffmpeg_progress-2.1.8}/better_ffmpeg_progress/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|