better-ffmpeg-progress 3.2.6__tar.gz → 4.0.1__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.
Files changed (20) hide show
  1. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/PKG-INFO +31 -7
  2. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/README.md +30 -7
  3. better_ffmpeg_progress-4.0.1/better_ffmpeg_progress/__init__.py +17 -0
  4. better_ffmpeg_progress-4.0.1/better_ffmpeg_progress/better_ffmpeg_progress.py +200 -0
  5. better_ffmpeg_progress-4.0.1/better_ffmpeg_progress/enums.py +13 -0
  6. better_ffmpeg_progress-4.0.1/better_ffmpeg_progress/exceptions.py +14 -0
  7. better_ffmpeg_progress-4.0.1/better_ffmpeg_progress/progress_bars.py +130 -0
  8. better_ffmpeg_progress-4.0.1/better_ffmpeg_progress/terminate_process.py +249 -0
  9. better_ffmpeg_progress-4.0.1/better_ffmpeg_progress/utils.py +151 -0
  10. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/better_ffmpeg_progress.egg-info/PKG-INFO +31 -7
  11. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/better_ffmpeg_progress.egg-info/SOURCES.txt +4 -0
  12. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/better_ffmpeg_progress.egg-info/requires.txt +1 -0
  13. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/setup.py +2 -2
  14. better_ffmpeg_progress-3.2.6/better_ffmpeg_progress/__init__.py +0 -1
  15. better_ffmpeg_progress-3.2.6/better_ffmpeg_progress/better_ffmpeg_progress.py +0 -370
  16. better_ffmpeg_progress-3.2.6/better_ffmpeg_progress/utils.py +0 -10
  17. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/LICENSE +0 -0
  18. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/better_ffmpeg_progress.egg-info/dependency_links.txt +0 -0
  19. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/better_ffmpeg_progress.egg-info/top_level.txt +0 -0
  20. {better_ffmpeg_progress-3.2.6 → better_ffmpeg_progress-4.0.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: better-ffmpeg-progress
3
- Version: 3.2.6
3
+ Version: 4.0.1
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
@@ -13,6 +13,7 @@ Requires-Python: >=3.7
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: psutil
16
+ Requires-Dist: requests
16
17
  Requires-Dist: rich
17
18
  Requires-Dist: tqdm
18
19
 
@@ -53,13 +54,37 @@ pip install better-ffmpeg-progress --upgrade
53
54
  ## Usage
54
55
  Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()` or `subprocess.Popen()`.
55
56
 
56
- Here's a simple example:
57
+ Example:
57
58
  ```py
58
- from better_ffmpeg_progress import FfmpegProcess
59
+ from better_ffmpeg_progress import FfmpegProcess, FfmpegProcessError
59
60
 
60
- process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:v", "libx265", "output.mp4"])
61
- # return_code will be 0 if the process completed successfully, otherwise it will be 1
62
- return_code = process.run()
61
+ command = [
62
+ "ffmpeg",
63
+ "-i",
64
+ "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
65
+ "-c:v",
66
+ "libx264",
67
+ "-preset",
68
+ "ultrafast",
69
+ "-c:a",
70
+ "copy",
71
+ "-f",
72
+ "null",
73
+ "-",
74
+ ]
75
+
76
+ try:
77
+ process = FfmpegProcess(command)
78
+ # Uncomment the line below if you want to use tqdm instead of rich for the progress bar
79
+ # process.use_tqdm = True
80
+
81
+ # Run the FFmpeg command and show a progress bar
82
+ process.run()
83
+ # The FFmpeg process failed if the return code is not 0
84
+ if process.return_code != 0:
85
+ pass
86
+ except FfmpegProcessError as e:
87
+ print(f"An error occurred when running the better-ffmpeg-process package:\n{e}")
63
88
  ```
64
89
  ## Optional Arguments
65
90
  An instance of the `FfmpegProcess` class takes the following **optional** arguments:
@@ -71,4 +96,3 @@ An instance of the `FfmpegProcess` class takes the following **optional** argume
71
96
 
72
97
  The `run` method takes the following **optional** arguments:
73
98
  - `print_command` - Print the FFmpeg command being executed. Default: `False`
74
- - `use_tqdm` - Use [tqdm](https://github.com/tqdm/tqdm) instead of [Rich](https://github.com/Textualize/rich) for the progress bar. Default: `False`
@@ -35,13 +35,37 @@ pip install better-ffmpeg-progress --upgrade
35
35
  ## Usage
36
36
  Create an instance of the `FfmpegProcess` class and supply a list of arguments like you would to `subprocess.run()` or `subprocess.Popen()`.
37
37
 
38
- Here's a simple example:
38
+ Example:
39
39
  ```py
40
- from better_ffmpeg_progress import FfmpegProcess
40
+ from better_ffmpeg_progress import FfmpegProcess, FfmpegProcessError
41
41
 
42
- process = FfmpegProcess(["ffmpeg", "-i", "input.mp4", "-c:v", "libx265", "output.mp4"])
43
- # return_code will be 0 if the process completed successfully, otherwise it will be 1
44
- return_code = process.run()
42
+ command = [
43
+ "ffmpeg",
44
+ "-i",
45
+ "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
46
+ "-c:v",
47
+ "libx264",
48
+ "-preset",
49
+ "ultrafast",
50
+ "-c:a",
51
+ "copy",
52
+ "-f",
53
+ "null",
54
+ "-",
55
+ ]
56
+
57
+ try:
58
+ process = FfmpegProcess(command)
59
+ # Uncomment the line below if you want to use tqdm instead of rich for the progress bar
60
+ # process.use_tqdm = True
61
+
62
+ # Run the FFmpeg command and show a progress bar
63
+ process.run()
64
+ # The FFmpeg process failed if the return code is not 0
65
+ if process.return_code != 0:
66
+ pass
67
+ except FfmpegProcessError as e:
68
+ print(f"An error occurred when running the better-ffmpeg-process package:\n{e}")
45
69
  ```
46
70
  ## Optional Arguments
47
71
  An instance of the `FfmpegProcess` class takes the following **optional** arguments:
@@ -52,5 +76,4 @@ An instance of the `FfmpegProcess` class takes the following **optional** argume
52
76
  - `print_stderr_new_line` - If better progress information cannot be shown, print FFmpeg stderr in a new line instead of replacing the current line in the terminal. Default: `False`
53
77
 
54
78
  The `run` method takes the following **optional** arguments:
55
- - `print_command` - Print the FFmpeg command being executed. Default: `False`
56
- - `use_tqdm` - Use [tqdm](https://github.com/tqdm/tqdm) instead of [Rich](https://github.com/Textualize/rich) for the progress bar. Default: `False`
79
+ - `print_command` - Print the FFmpeg command being executed. Default: `False`
@@ -0,0 +1,17 @@
1
+ from .better_ffmpeg_progress import FfmpegProcess
2
+ from .enums import FfmpegLogLevel
3
+ from .exceptions import (
4
+ FfmpegProcessError,
5
+ FfmpegCommandError,
6
+ FfmpegProcessUserCancelledError,
7
+ FfmpegProcessInterruptedError,
8
+ )
9
+
10
+ __all__ = [
11
+ "FfmpegProcess",
12
+ "FfmpegLogLevel",
13
+ "FfmpegProcessError",
14
+ "FfmpegCommandError",
15
+ "FfmpegProcessUserCancelledError",
16
+ "FfmpegProcessInterruptedError",
17
+ ]
@@ -0,0 +1,200 @@
1
+ import os
2
+ from pathlib import Path
3
+ import subprocess
4
+ from sys import exit
5
+ from typing import List, Optional, Union
6
+
7
+ from .enums import FfmpegLogLevel
8
+ from .exceptions import (
9
+ FfmpegProcessError,
10
+ FfmpegCommandError,
11
+ FfmpegProcessUserCancelledError,
12
+ FfmpegProcessInterruptedError,
13
+ )
14
+ from .utils import (
15
+ validate_ffmpeg_command,
16
+ get_media_duration,
17
+ check_shell_needed_for_command,
18
+ )
19
+ from .terminate_process import terminate_ffmpeg_process
20
+ from .progress_bars import use_rich, use_tqdm
21
+
22
+ _FFMPEG_OVERWRITE_FLAG = "-y"
23
+
24
+
25
+ class FfmpegProcess:
26
+ def _handle_overwrite_prompt(self) -> None:
27
+ """
28
+ Handles the logic for overwriting an existing output file.
29
+ Modifies self._ffmpeg_command to include -y if overwrite is confirmed.
30
+ Raises FfmpegProcessUserCancelledError or FfmpegProcessInterruptedError if not proceeding.
31
+ """
32
+ try:
33
+ answer = (
34
+ input(
35
+ f"Output file {self._output_filepath} already exists. Overwrite? [y/N]: "
36
+ )
37
+ .strip()
38
+ .lower()
39
+ )
40
+ if answer == "y":
41
+ # Insert -y
42
+ self._ffmpeg_command.insert(1, _FFMPEG_OVERWRITE_FLAG)
43
+ else:
44
+ raise FfmpegProcessUserCancelledError(
45
+ "FFmpeg process cancelled. Output file exists and overwrite declined."
46
+ )
47
+ except KeyboardInterrupt as e:
48
+ raise FfmpegProcessInterruptedError(
49
+ "[KeyboardInterrupt] FFmpeg process cancelled during overwrite prompt."
50
+ ) from e
51
+ except EOFError as e:
52
+ raise FfmpegProcessInterruptedError(
53
+ "Input error (EOF) during overwrite prompt. FFmpeg process cancelled."
54
+ ) from e
55
+
56
+ def __init__(
57
+ self,
58
+ command: List[str],
59
+ ffmpeg_log_level: Optional[Union[FfmpegLogLevel, str]] = None,
60
+ ffmpeg_log_file: Optional[Union[str, Path]] = None,
61
+ print_detected_duration: bool = False,
62
+ ):
63
+ # Raises FfmpegCommandError if the command is invalid
64
+ validate_ffmpeg_command(command)
65
+
66
+ ffmpeg_log_level_val: str
67
+
68
+ if ffmpeg_log_level is None:
69
+ ffmpeg_log_level_val = FfmpegLogLevel.VERBOSE.value
70
+ elif isinstance(ffmpeg_log_level, FfmpegLogLevel):
71
+ ffmpeg_log_level_val = ffmpeg_log_level.value
72
+ elif isinstance(ffmpeg_log_level, str):
73
+ try:
74
+ ffmpeg_log_level_val = FfmpegLogLevel(ffmpeg_log_level.lower()).value
75
+ except ValueError:
76
+ valid_levels = [e.value for e in FfmpegLogLevel]
77
+ raise FfmpegCommandError(
78
+ f"Invalid ffmpeg_log_level string: '{ffmpeg_log_level}'. "
79
+ f"Must be one of {valid_levels} (case-insensitive)."
80
+ )
81
+ else:
82
+ raise TypeError(
83
+ f"ffmpeg_log_level must be an FfmpegLogLevel enum instance, a string, or None, "
84
+ f"not {type(ffmpeg_log_level).__name__}"
85
+ )
86
+ self._ffmpeg_log_level_val = ffmpeg_log_level_val
87
+
88
+ input_file_index = command.index("-i")
89
+ input_file_path_str = command[input_file_index + 1]
90
+ self._input_filepath = Path(input_file_path_str)
91
+ # Assumes last argument is output
92
+ self._output_filepath = Path(command[-1])
93
+
94
+ self._ffmpeg_log_file = Path(
95
+ ffmpeg_log_file or f"{self._input_filepath.name}_ffmpeg_log.txt"
96
+ )
97
+ self._print_detected_duration = print_detected_duration
98
+ self._duration_secs = get_media_duration(input_file_path_str)
99
+
100
+ if self._print_detected_duration and self._duration_secs is not None:
101
+ print(f"Detected duration: {self._duration_secs:.2f} seconds")
102
+ elif self._print_detected_duration and self._duration_secs is None:
103
+ print(
104
+ "Could not detect duration. Progress bar may not show time remaining."
105
+ )
106
+
107
+ self._ffmpeg_command = [
108
+ command[0],
109
+ "-hide_banner",
110
+ "-loglevel",
111
+ self._ffmpeg_log_level_val,
112
+ "-progress",
113
+ "pipe:1", # stdout
114
+ "-nostats",
115
+ ]
116
+ self._ffmpeg_command.extend(command[1:])
117
+
118
+ is_overwrite_in_user_command = any(
119
+ arg == _FFMPEG_OVERWRITE_FLAG for arg in command[1:]
120
+ )
121
+
122
+ if self._output_filepath.exists() and not is_overwrite_in_user_command:
123
+ self._handle_overwrite_prompt()
124
+
125
+ self._process: Optional[subprocess.Popen] = None
126
+ self._return_code: Optional[int] = None
127
+ self.use_tqdm: bool = False
128
+
129
+ def run(
130
+ self,
131
+ print_command: bool = False,
132
+ ) -> int:
133
+ if print_command:
134
+ cmd_str = (
135
+ " ".join(self._ffmpeg_command)
136
+ if isinstance(self._ffmpeg_command, list)
137
+ else self._ffmpeg_command
138
+ )
139
+ print(f"Executing: {cmd_str}")
140
+
141
+ self._shell_needed = check_shell_needed_for_command(
142
+ self._ffmpeg_command
143
+ if isinstance(self._ffmpeg_command, list)
144
+ else [self._ffmpeg_command]
145
+ )
146
+
147
+ current_ffmpeg_command = self._ffmpeg_command
148
+ if self._shell_needed and isinstance(current_ffmpeg_command, list):
149
+ current_ffmpeg_command = " ".join(current_ffmpeg_command)
150
+
151
+ self._return_code = 1
152
+
153
+ try:
154
+ with open(self._ffmpeg_log_file, "w", encoding="utf-8") as f:
155
+ creationflags = 0
156
+
157
+ if os.name == "nt":
158
+ creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
159
+
160
+ self._process = subprocess.Popen(
161
+ current_ffmpeg_command,
162
+ shell=self._shell_needed,
163
+ stdout=subprocess.PIPE,
164
+ stderr=f,
165
+ creationflags=creationflags,
166
+ )
167
+ except Exception as e:
168
+ # self._return_code remains None as the process didn't start
169
+ raise FfmpegProcessError(f"Error starting FFmpeg process: {e}") from e
170
+
171
+ try:
172
+ if self.use_tqdm:
173
+ self._return_code = use_tqdm(self, self._process)
174
+ else:
175
+ self._return_code = use_rich(self, self._process)
176
+ except KeyboardInterrupt:
177
+ self._terminate()
178
+ self._return_code = 1
179
+ finally:
180
+ if self._process:
181
+ self._return_code = self._process.returncode
182
+
183
+ if self._process.stdout:
184
+ self._process.stdout.close()
185
+
186
+ return self._return_code
187
+
188
+ def _terminate(self):
189
+ if self._process:
190
+ terminate_ffmpeg_process(self._process)
191
+ else:
192
+ exit()
193
+
194
+ @property
195
+ def return_code(self) -> Optional[int]:
196
+ """
197
+ The return code of the FFmpeg process.
198
+ None if the process has not started or failed to start.
199
+ """
200
+ return self._return_code
@@ -0,0 +1,13 @@
1
+ from enum import Enum
2
+
3
+
4
+ class FfmpegLogLevel(Enum):
5
+ QUIET = "quiet"
6
+ PANIC = "panic"
7
+ FATAL = "fatal"
8
+ ERROR = "error"
9
+ WARNING = "warning"
10
+ INFO = "info"
11
+ VERBOSE = "verbose"
12
+ DEBUG = "debug"
13
+ TRACE = "trace"
@@ -0,0 +1,14 @@
1
+ class FfmpegProcessError(Exception):
2
+ pass
3
+
4
+
5
+ class FfmpegCommandError(FfmpegProcessError):
6
+ pass
7
+
8
+
9
+ class FfmpegProcessUserCancelledError(FfmpegProcessError):
10
+ pass
11
+
12
+
13
+ class FfmpegProcessInterruptedError(FfmpegProcessError):
14
+ pass
@@ -0,0 +1,130 @@
1
+ import os
2
+ import subprocess
3
+ from typing import TYPE_CHECKING
4
+
5
+ from rich.progress import (
6
+ Progress,
7
+ TextColumn,
8
+ BarColumn,
9
+ TaskProgressColumn,
10
+ TimeRemainingColumn,
11
+ TimeElapsedColumn,
12
+ )
13
+ from tqdm import tqdm
14
+
15
+ from .utils import parse_ffmpeg_progress_line
16
+
17
+ if TYPE_CHECKING:
18
+ from .better_ffmpeg_progress import FfmpegProcess
19
+
20
+
21
+ def use_rich(
22
+ ffmpeg_process_instance: "FfmpegProcess", process: subprocess.Popen
23
+ ) -> int:
24
+ task_id = None
25
+ progress_bar_instance = Progress(
26
+ TextColumn("[progress.description]{task.description}"),
27
+ BarColumn(),
28
+ TaskProgressColumn(),
29
+ TimeElapsedColumn(),
30
+ TimeRemainingColumn(compact=True),
31
+ refresh_per_second=2,
32
+ )
33
+
34
+ duration_secs = ffmpeg_process_instance._duration_secs
35
+ input_filename = ffmpeg_process_instance._input_filepath.name
36
+ log_file = ffmpeg_process_instance._ffmpeg_log_file
37
+
38
+ with progress_bar_instance as progress_bar:
39
+ if duration_secs:
40
+ task_id = progress_bar.add_task(
41
+ f"Processing {input_filename}",
42
+ total=duration_secs,
43
+ )
44
+ update_progress = progress_bar.update
45
+
46
+ for line_bytes in process.stdout:
47
+ stripped_line_bytes = line_bytes.strip()
48
+ progress_val_secs = parse_ffmpeg_progress_line(
49
+ stripped_line_bytes, duration_secs
50
+ )
51
+ if progress_val_secs is not None and task_id is not None:
52
+ update_progress(task_id, completed=progress_val_secs)
53
+ else:
54
+ print(f"Processing {input_filename}...")
55
+
56
+ process.wait()
57
+
58
+ if task_id is not None:
59
+ if process.returncode == 0:
60
+ update_progress(task_id, completed=duration_secs)
61
+ progress_bar.columns = (
62
+ TextColumn("[progress.description]{task.description}"),
63
+ BarColumn(),
64
+ TaskProgressColumn(),
65
+ )
66
+ update_progress(
67
+ task_id,
68
+ description=f"✓ Processed {input_filename}",
69
+ )
70
+ else:
71
+ progress_bar.update(
72
+ task_id,
73
+ description=f"FFmpeg process failed. Check {log_file} for details.",
74
+ )
75
+
76
+ if process.returncode != 0 and task_id is None:
77
+ print(f"FFmpeg process failed. Check {log_file} for details.")
78
+
79
+ return 1 if process.returncode != 0 else 0
80
+
81
+
82
+ def use_tqdm(
83
+ ffmpeg_process_instance: "FfmpegProcess", process: subprocess.Popen
84
+ ) -> int:
85
+ progress_bar = None
86
+ try:
87
+ width = os.get_terminal_size().columns
88
+ except OSError:
89
+ width = 80
90
+
91
+ duration_secs = ffmpeg_process_instance._duration_secs
92
+ input_filename = ffmpeg_process_instance._input_filepath.name
93
+ log_file = ffmpeg_process_instance._ffmpeg_log_file
94
+
95
+ if duration_secs:
96
+ progress_bar = tqdm(
97
+ mininterval=0.5,
98
+ total=duration_secs,
99
+ desc=f"Processing {input_filename}",
100
+ ncols=80,
101
+ dynamic_ncols=True if width < 80 else False,
102
+ bar_format="{desc} {bar} {percentage:.1f}% [{elapsed}<{remaining}, {rate_fmt}{postfix}]",
103
+ )
104
+
105
+ for line_bytes in process.stdout:
106
+ stripped_line_bytes = line_bytes.strip()
107
+ progress_val_secs = parse_ffmpeg_progress_line(
108
+ stripped_line_bytes, duration_secs
109
+ )
110
+ if progress_val_secs is not None and progress_bar is not None:
111
+ progress_bar.n = progress_val_secs
112
+ progress_bar.refresh()
113
+ else:
114
+ print(f"Processing {input_filename}...")
115
+
116
+ process.wait()
117
+
118
+ if process.returncode == 0:
119
+ if progress_bar:
120
+ progress_bar.n = duration_secs
121
+ progress_bar.set_description(f"✓ Processed {input_filename}")
122
+ progress_bar.close()
123
+ else:
124
+ print(f"✓ Processed {input_filename}")
125
+ else:
126
+ if progress_bar:
127
+ progress_bar.close()
128
+ print(f"FFmpeg process failed. Check {log_file} for details.")
129
+
130
+ return 1 if process.returncode != 0 else 0