ffmpeg-normalize 1.31.1__py2.py3-none-any.whl → 1.31.3__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ffmpeg_normalize/__main__.py +1 -0
- ffmpeg_normalize/_cmd_utils.py +1 -3
- ffmpeg_normalize/_ffmpeg_normalize.py +3 -0
- ffmpeg_normalize/_media_file.py +24 -12
- ffmpeg_normalize/_streams.py +15 -14
- ffmpeg_normalize/_version.py +1 -1
- {ffmpeg_normalize-1.31.1.dist-info → ffmpeg_normalize-1.31.3.dist-info}/METADATA +11 -1
- ffmpeg_normalize-1.31.3.dist-info/RECORD +16 -0
- ffmpeg_normalize-1.31.1.dist-info/RECORD +0 -16
- {ffmpeg_normalize-1.31.1.dist-info → ffmpeg_normalize-1.31.3.dist-info}/LICENSE +0 -0
- {ffmpeg_normalize-1.31.1.dist-info → ffmpeg_normalize-1.31.3.dist-info}/WHEEL +0 -0
- {ffmpeg_normalize-1.31.1.dist-info → ffmpeg_normalize-1.31.3.dist-info}/entry_points.txt +0 -0
- {ffmpeg_normalize-1.31.1.dist-info → ffmpeg_normalize-1.31.3.dist-info}/top_level.txt +0 -0
ffmpeg_normalize/__main__.py
CHANGED
|
@@ -559,6 +559,7 @@ def main() -> None:
|
|
|
559
559
|
extra_input_options=extra_input_options,
|
|
560
560
|
extra_output_options=extra_output_options,
|
|
561
561
|
output_format=cli_args.output_format,
|
|
562
|
+
extension=cli_args.extension,
|
|
562
563
|
dry_run=cli_args.dry_run,
|
|
563
564
|
progress=cli_args.progress,
|
|
564
565
|
)
|
ffmpeg_normalize/_cmd_utils.py
CHANGED
|
@@ -5,9 +5,8 @@ import os
|
|
|
5
5
|
import re
|
|
6
6
|
import shlex
|
|
7
7
|
import subprocess
|
|
8
|
-
from platform import system
|
|
9
8
|
from shutil import which
|
|
10
|
-
from typing import
|
|
9
|
+
from typing import Any, Iterator
|
|
11
10
|
|
|
12
11
|
from ffmpeg_progress_yield import FfmpegProgress
|
|
13
12
|
|
|
@@ -15,7 +14,6 @@ from ._errors import FFmpegNormalizeError
|
|
|
15
14
|
|
|
16
15
|
_logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
18
|
-
NUL = "NUL" if system() in ("Windows", "cli") else "/dev/null"
|
|
19
17
|
DUR_REGEX = re.compile(
|
|
20
18
|
r"Duration: (?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})\.(?P<ms>\d{2})"
|
|
21
19
|
)
|
|
@@ -79,6 +79,7 @@ class FFmpegNormalize:
|
|
|
79
79
|
extra_input_options (list, optional): Extra input options. Defaults to None.
|
|
80
80
|
extra_output_options (list, optional): Extra output options. Defaults to None.
|
|
81
81
|
output_format (str, optional): Output format. Defaults to None.
|
|
82
|
+
extension (str, optional): Output file extension to use for output files that were not explicitly specified. Defaults to "mkv".
|
|
82
83
|
dry_run (bool, optional): Dry run. Defaults to False.
|
|
83
84
|
debug (bool, optional): Debug. Defaults to False.
|
|
84
85
|
progress (bool, optional): Progress. Defaults to False.
|
|
@@ -117,6 +118,7 @@ class FFmpegNormalize:
|
|
|
117
118
|
extra_input_options: list[str] | None = None,
|
|
118
119
|
extra_output_options: list[str] | None = None,
|
|
119
120
|
output_format: str | None = None,
|
|
121
|
+
extension: str = "mkv",
|
|
120
122
|
dry_run: bool = False,
|
|
121
123
|
debug: bool = False,
|
|
122
124
|
progress: bool = False,
|
|
@@ -197,6 +199,7 @@ class FFmpegNormalize:
|
|
|
197
199
|
self.post_filter = post_filter
|
|
198
200
|
|
|
199
201
|
self.output_format = output_format
|
|
202
|
+
self.extension = extension
|
|
200
203
|
self.dry_run = dry_run
|
|
201
204
|
self.debug = debug
|
|
202
205
|
self.progress = progress
|
ffmpeg_normalize/_media_file.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Iterable, Iterator, Literal, TypedDict
|
|
|
10
10
|
|
|
11
11
|
from tqdm import tqdm
|
|
12
12
|
|
|
13
|
-
from ._cmd_utils import DUR_REGEX,
|
|
13
|
+
from ._cmd_utils import DUR_REGEX, CommandRunner
|
|
14
14
|
from ._errors import FFmpegNormalizeError
|
|
15
15
|
from ._streams import (
|
|
16
16
|
AudioStream,
|
|
@@ -67,7 +67,12 @@ class MediaFile:
|
|
|
67
67
|
self.skip = False
|
|
68
68
|
self.input_file = input_file
|
|
69
69
|
self.output_file = output_file
|
|
70
|
-
|
|
70
|
+
current_ext = os.path.splitext(output_file)[1][1:]
|
|
71
|
+
# we need to check if it's empty, e.g. /dev/null or NUL
|
|
72
|
+
if current_ext == "" or self.output_file == os.devnull:
|
|
73
|
+
self.output_ext = self.ffmpeg_normalize.extension
|
|
74
|
+
else:
|
|
75
|
+
self.output_ext = current_ext
|
|
71
76
|
self.streams: StreamDict = {"audio": {}, "video": {}, "subtitle": {}}
|
|
72
77
|
|
|
73
78
|
self.parse_streams()
|
|
@@ -109,7 +114,7 @@ class MediaFile:
|
|
|
109
114
|
"0",
|
|
110
115
|
"-f",
|
|
111
116
|
"null",
|
|
112
|
-
|
|
117
|
+
os.devnull,
|
|
113
118
|
]
|
|
114
119
|
|
|
115
120
|
output = CommandRunner().run_command(cmd).get_output()
|
|
@@ -424,13 +429,18 @@ class MediaFile:
|
|
|
424
429
|
# if dry run, only show sample command
|
|
425
430
|
if self.ffmpeg_normalize.dry_run:
|
|
426
431
|
cmd.append(self.output_file)
|
|
432
|
+
_logger.warning("Dry run used, not actually running second-pass command")
|
|
427
433
|
CommandRunner(dry=True).run_command(cmd)
|
|
428
434
|
yield 100
|
|
429
435
|
return
|
|
430
436
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
437
|
+
# special case: if output is a null device, write directly to it
|
|
438
|
+
if self.output_file == os.devnull:
|
|
439
|
+
cmd.append(self.output_file)
|
|
440
|
+
else:
|
|
441
|
+
temp_dir = mkdtemp()
|
|
442
|
+
temp_file = os.path.join(temp_dir, f"out.{self.output_ext}")
|
|
443
|
+
cmd.append(temp_file)
|
|
434
444
|
|
|
435
445
|
cmd_runner = CommandRunner()
|
|
436
446
|
try:
|
|
@@ -442,13 +452,15 @@ class MediaFile:
|
|
|
442
452
|
)
|
|
443
453
|
raise e
|
|
444
454
|
else:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
455
|
+
if self.output_file != os.devnull:
|
|
456
|
+
_logger.debug(
|
|
457
|
+
f"Moving temporary file from {temp_file} to {self.output_file}"
|
|
458
|
+
)
|
|
459
|
+
move(temp_file, self.output_file)
|
|
460
|
+
rmtree(temp_dir, ignore_errors=True)
|
|
450
461
|
except Exception as e:
|
|
451
|
-
|
|
462
|
+
if self.output_file != os.devnull:
|
|
463
|
+
rmtree(temp_dir, ignore_errors=True)
|
|
452
464
|
raise e
|
|
453
465
|
|
|
454
466
|
output = cmd_runner.get_output()
|
ffmpeg_normalize/_streams.py
CHANGED
|
@@ -4,9 +4,9 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
from typing import TYPE_CHECKING, Iterator,
|
|
7
|
+
from typing import TYPE_CHECKING, Iterator, Literal, TypedDict, cast
|
|
8
8
|
|
|
9
|
-
from ._cmd_utils import
|
|
9
|
+
from ._cmd_utils import CommandRunner, dict_to_filter_opts
|
|
10
10
|
from ._errors import FFmpegNormalizeError
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -17,6 +17,7 @@ _logger = logging.getLogger(__name__)
|
|
|
17
17
|
|
|
18
18
|
_loudnorm_pattern = re.compile(r"\[Parsed_loudnorm_(\d+)")
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
class EbuLoudnessStatistics(TypedDict):
|
|
21
22
|
input_i: float
|
|
22
23
|
input_tp: float
|
|
@@ -239,7 +240,7 @@ class AudioStream(MediaStream):
|
|
|
239
240
|
"-sn",
|
|
240
241
|
"-f",
|
|
241
242
|
"null",
|
|
242
|
-
|
|
243
|
+
os.devnull,
|
|
243
244
|
]
|
|
244
245
|
|
|
245
246
|
cmd_runner = CommandRunner()
|
|
@@ -310,7 +311,7 @@ class AudioStream(MediaStream):
|
|
|
310
311
|
"-sn",
|
|
311
312
|
"-f",
|
|
312
313
|
"null",
|
|
313
|
-
|
|
314
|
+
os.devnull,
|
|
314
315
|
]
|
|
315
316
|
|
|
316
317
|
cmd_runner = CommandRunner()
|
|
@@ -322,11 +323,13 @@ class AudioStream(MediaStream):
|
|
|
322
323
|
)
|
|
323
324
|
|
|
324
325
|
# only one stream
|
|
325
|
-
self.loudness_statistics["ebu_pass1"] = next(
|
|
326
|
+
self.loudness_statistics["ebu_pass1"] = next(
|
|
327
|
+
iter(AudioStream.prune_and_parse_loudnorm_output(output).values())
|
|
328
|
+
)
|
|
326
329
|
|
|
327
330
|
@staticmethod
|
|
328
331
|
def prune_and_parse_loudnorm_output(
|
|
329
|
-
output: str
|
|
332
|
+
output: str,
|
|
330
333
|
) -> dict[int, EbuLoudnessStatistics]:
|
|
331
334
|
"""
|
|
332
335
|
Prune ffmpeg progress lines from output and parse the loudnorm filter output.
|
|
@@ -344,7 +347,7 @@ class AudioStream(MediaStream):
|
|
|
344
347
|
|
|
345
348
|
@staticmethod
|
|
346
349
|
def _parse_loudnorm_output(
|
|
347
|
-
output_lines: list[str]
|
|
350
|
+
output_lines: list[str],
|
|
348
351
|
) -> dict[int, EbuLoudnessStatistics]:
|
|
349
352
|
"""
|
|
350
353
|
Parse the output of a loudnorm filter to get the EBU loudness statistics.
|
|
@@ -403,7 +406,9 @@ class AudioStream(MediaStream):
|
|
|
403
406
|
# convert to floats
|
|
404
407
|
loudnorm_stats[key] = float(loudnorm_stats[key])
|
|
405
408
|
|
|
406
|
-
result[stream_index] = cast(
|
|
409
|
+
result[stream_index] = cast(
|
|
410
|
+
EbuLoudnessStatistics, loudnorm_stats
|
|
411
|
+
)
|
|
407
412
|
stream_index = -1
|
|
408
413
|
except Exception as e:
|
|
409
414
|
raise FFmpegNormalizeError(
|
|
@@ -504,15 +509,11 @@ class AudioStream(MediaStream):
|
|
|
504
509
|
"offset": self._constrain(
|
|
505
510
|
stats["target_offset"], -99, 99, name="target_offset"
|
|
506
511
|
),
|
|
507
|
-
"measured_i": self._constrain(
|
|
508
|
-
stats["input_i"], -99, 0, name="input_i"
|
|
509
|
-
),
|
|
512
|
+
"measured_i": self._constrain(stats["input_i"], -99, 0, name="input_i"),
|
|
510
513
|
"measured_lra": self._constrain(
|
|
511
514
|
stats["input_lra"], 0, 99, name="input_lra"
|
|
512
515
|
),
|
|
513
|
-
"measured_tp": self._constrain(
|
|
514
|
-
stats["input_tp"], -99, 99, name="input_tp"
|
|
515
|
-
),
|
|
516
|
+
"measured_tp": self._constrain(stats["input_tp"], -99, 99, name="input_tp"),
|
|
516
517
|
"measured_thresh": self._constrain(
|
|
517
518
|
stats["input_thresh"], -99, 0, name="input_thresh"
|
|
518
519
|
),
|
ffmpeg_normalize/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.31.
|
|
1
|
+
__version__ = "1.31.3"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ffmpeg-normalize
|
|
3
|
-
Version: 1.31.
|
|
3
|
+
Version: 1.31.3
|
|
4
4
|
Summary: Normalize audio via ffmpeg
|
|
5
5
|
Home-page: https://github.com/slhck/ffmpeg-normalize
|
|
6
6
|
Author: Werner Robitza
|
|
@@ -671,6 +671,16 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
671
671
|
# Changelog
|
|
672
672
|
|
|
673
673
|
|
|
674
|
+
## v1.31.3 (2025-04-14)
|
|
675
|
+
|
|
676
|
+
* Swap NUL to os.devnull.
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
## v1.31.2 (2025-03-19)
|
|
680
|
+
|
|
681
|
+
* Fix: special handling of /dev/null.
|
|
682
|
+
|
|
683
|
+
|
|
674
684
|
## v1.31.1 (2025-02-19)
|
|
675
685
|
|
|
676
686
|
* Fix bitrate setting for libvorbis, fixes #277.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
ffmpeg_normalize/__init__.py,sha256=aAhlk93ZE6SQcWUDzZQcw9vJh0bJcKEUNFGhVc5ZIto,453
|
|
2
|
+
ffmpeg_normalize/__main__.py,sha256=qOiycud87-wBceNLujaPZgWCTwJr7kaJM-IBvz4D7f4,19431
|
|
3
|
+
ffmpeg_normalize/_cmd_utils.py,sha256=iGzO3iOylDUOnx-FCKd84BMxiIhmIthxU1tg7kvf4Ss,5269
|
|
4
|
+
ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
|
|
5
|
+
ffmpeg_normalize/_ffmpeg_normalize.py,sha256=JIVE03ZJi-jOvnc7OuftQEqloQwwyO7ZD3PIMFmIvbo,11283
|
|
6
|
+
ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
|
|
7
|
+
ffmpeg_normalize/_media_file.py,sha256=c_u-f1XXLg26RkN825-YmleSZLVqLu42EBxybiyhdqU,18893
|
|
8
|
+
ffmpeg_normalize/_streams.py,sha256=C0b8GT4EK0W02x8pM99H1-BjjO-pLxYFNvZ4R7gu4CM,20223
|
|
9
|
+
ffmpeg_normalize/_version.py,sha256=TITwbgAbYbb0TTsjNH09bidAABZM-hqec2XVSZ9BDPY,23
|
|
10
|
+
ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
ffmpeg_normalize-1.31.3.dist-info/LICENSE,sha256=zeqAHGWrSIwdPHsZMZv1_N0gGFO1xxjcZEz9CplR4EM,1086
|
|
12
|
+
ffmpeg_normalize-1.31.3.dist-info/METADATA,sha256=9QkgKN6BohVDpX0ot_EZdUwRz8zLiHPN7FY2rks9s2k,60327
|
|
13
|
+
ffmpeg_normalize-1.31.3.dist-info/WHEEL,sha256=fS9sRbCBHs7VFcwJLnLXN1MZRR0_TVTxvXKzOnaSFs8,110
|
|
14
|
+
ffmpeg_normalize-1.31.3.dist-info/entry_points.txt,sha256=X0EC5ptb0iGOxrk3Aa65dVQtvUixngLd_2-iAtSixdc,68
|
|
15
|
+
ffmpeg_normalize-1.31.3.dist-info/top_level.txt,sha256=wnUkr17ckPrrU1JsxZQiXbEBUnHKsC64yck-MemEBuI,17
|
|
16
|
+
ffmpeg_normalize-1.31.3.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
ffmpeg_normalize/__init__.py,sha256=aAhlk93ZE6SQcWUDzZQcw9vJh0bJcKEUNFGhVc5ZIto,453
|
|
2
|
-
ffmpeg_normalize/__main__.py,sha256=AuyIcMU1WxGamczZtZyISfgMsEIt3Y1kSJ87dCoH9J4,19393
|
|
3
|
-
ffmpeg_normalize/_cmd_utils.py,sha256=S7PLXQAZHmJ30RM9K6b--vXuxMf-cQHtaFOPtILxz-4,5360
|
|
4
|
-
ffmpeg_normalize/_errors.py,sha256=brTQ4osJ4fTA8wnyMPVVYfGwJ0wqeShRFydTEwi_VEY,48
|
|
5
|
-
ffmpeg_normalize/_ffmpeg_normalize.py,sha256=VoxhER57Ew0RKC_WcGpoCmyKKoqsHYuqNFMm7z4BPM4,11080
|
|
6
|
-
ffmpeg_normalize/_logger.py,sha256=3Ap4Fxg7xGrzz7h4IGuNEf0KKstx0Rq_eLbHPrHzcrI,1841
|
|
7
|
-
ffmpeg_normalize/_media_file.py,sha256=BnFTOGOAxzofGS5k7ueG3k6LsUxro_VM6qWCQH5o81A,18258
|
|
8
|
-
ffmpeg_normalize/_streams.py,sha256=LIllXl4SKLxlyPVjD3ieHqc_byF2eUTjnK-clh2g_CY,20211
|
|
9
|
-
ffmpeg_normalize/_version.py,sha256=VFBXpgylen4bFLDe4KfNX1OmaXu_M6UnJYtTvQRQz4c,23
|
|
10
|
-
ffmpeg_normalize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
ffmpeg_normalize-1.31.1.dist-info/LICENSE,sha256=zeqAHGWrSIwdPHsZMZv1_N0gGFO1xxjcZEz9CplR4EM,1086
|
|
12
|
-
ffmpeg_normalize-1.31.1.dist-info/METADATA,sha256=Trwdo0wpKwL38BgLgBLXzAWhqyMjhEnKDgL2jFHtMXU,60209
|
|
13
|
-
ffmpeg_normalize-1.31.1.dist-info/WHEEL,sha256=fS9sRbCBHs7VFcwJLnLXN1MZRR0_TVTxvXKzOnaSFs8,110
|
|
14
|
-
ffmpeg_normalize-1.31.1.dist-info/entry_points.txt,sha256=X0EC5ptb0iGOxrk3Aa65dVQtvUixngLd_2-iAtSixdc,68
|
|
15
|
-
ffmpeg_normalize-1.31.1.dist-info/top_level.txt,sha256=wnUkr17ckPrrU1JsxZQiXbEBUnHKsC64yck-MemEBuI,17
|
|
16
|
-
ffmpeg_normalize-1.31.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|