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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: better-ffmpeg-progress
3
- Version: 2.1.6
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.6
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: tqdm
16
+ Requires-Dist: rich
17
17
 
18
18
  <div align="center">
19
19
 
20
- [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
21
- [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
20
+ [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/better-ffmpeg-progress)
21
+ [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/better-ffmpeg-progress)
22
22
  [![GitHub](https://img.shields.io/github/license/crypticsignal/better-ffmpeg-progress?label=License&color=blue)](LICENSE.txt)
23
23
 
24
24
  # Better FFmpeg Progress
25
25
 
26
- Runs an FFmpeg command and uses [tqdm](https://github.com/tqdm/tqdm) to show a progress bar.
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
- 39%|███████████████████████████████████████████ | 23.6/60.2 [00:19<00:34, 1.07s/s]
33
+ ⠏ Processing... ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
34
34
  ```
35
35
 
36
36
  Where:
37
37
 
38
- - `39%` is the percentage progress.
39
- - `23.6` seconds of the input file have been processed.
40
- - `60.2` is the duration of the input file in seconds.
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
- `pip3 install better-ffmpeg-progress --upgrade`
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
- [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
4
- [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
3
+ [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/better-ffmpeg-progress)
4
+ [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/better-ffmpeg-progress)
5
5
  [![GitHub](https://img.shields.io/github/license/crypticsignal/better-ffmpeg-progress?label=License&color=blue)](LICENSE.txt)
6
6
 
7
7
  # Better FFmpeg Progress
8
8
 
9
- Runs an FFmpeg command and uses [tqdm](https://github.com/tqdm/tqdm) to show a progress bar.
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
- 39%|███████████████████████████████████████████ | 23.6/60.2 [00:19<00:34, 1.07s/s]
16
+ ⠏ Processing... ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
17
17
  ```
18
18
 
19
19
  Where:
20
20
 
21
- - `39%` is the percentage progress.
22
- - `23.6` seconds of the input file have been processed.
23
- - `60.2` is the duration of the input file in seconds.
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
- `pip3 install better-ffmpeg-progress --upgrade`
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.6
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.6
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: tqdm
16
+ Requires-Dist: rich
17
17
 
18
18
  <div align="center">
19
19
 
20
- [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
21
- [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/pypistats)
20
+ [![PyPI downloads](https://img.shields.io/pypi/dm/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/better-ffmpeg-progress)
21
+ [![PyPI downloads](https://img.shields.io/pypi/dw/better-ffmpeg-progress?label=PyPI&color=blue)](https://pypistats.org/packages/better-ffmpeg-progress)
22
22
  [![GitHub](https://img.shields.io/github/license/crypticsignal/better-ffmpeg-progress?label=License&color=blue)](LICENSE.txt)
23
23
 
24
24
  # Better FFmpeg Progress
25
25
 
26
- Runs an FFmpeg command and uses [tqdm](https://github.com/tqdm/tqdm) to show a progress bar.
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
- 39%|███████████████████████████████████████████ | 23.6/60.2 [00:19<00:34, 1.07s/s]
33
+ ⠏ Processing... ━━━━━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 23% 0:00:04 00:15
34
34
  ```
35
35
 
36
36
  Where:
37
37
 
38
- - `39%` is the percentage progress.
39
- - `23.6` seconds of the input file have been processed.
40
- - `60.2` is the duration of the input file in seconds.
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
- `pip3 install better-ffmpeg-progress --upgrade`
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).
@@ -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.6",
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", "tqdm"],
17
- python_requires=">=3.6",
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}")