ffmpeg-normalize 1.37.1__tar.gz → 1.37.3__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.
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/PKG-INFO +1 -1
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/pyproject.toml +1 -1
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/__main__.py +10 -6
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_media_file.py +101 -2
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_presets.py +31 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/LICENSE.md +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/README.md +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/__init__.py +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_cmd_utils.py +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_errors.py +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_ffmpeg_normalize.py +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_logger.py +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_streams.py +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/data/presets/music.json +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/data/presets/podcast.json +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/data/presets/streaming-video.json +0 -0
- {ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/py.typed +0 -0
|
@@ -197,7 +197,7 @@ def create_parser() -> argparse.ArgumentParser:
|
|
|
197
197
|
help=textwrap.dedent(
|
|
198
198
|
"""\
|
|
199
199
|
Write ReplayGain tags to the original file without normalizing.
|
|
200
|
-
This mode will overwrite the input file and ignore other options.
|
|
200
|
+
This mode will overwrite the input file and ignore other options. It will strip all ReplayGain tags.
|
|
201
201
|
Only works with EBU normalization, and only with .mp3, .mp4/.m4a, .ogg, .opus for now.
|
|
202
202
|
"""
|
|
203
203
|
),
|
|
@@ -607,13 +607,17 @@ def main() -> None:
|
|
|
607
607
|
# Handle --list-presets
|
|
608
608
|
preset_manager = PresetManager()
|
|
609
609
|
if cli_args.list_presets:
|
|
610
|
-
|
|
611
|
-
if
|
|
610
|
+
presets_with_source = preset_manager.get_presets_with_source()
|
|
611
|
+
if presets_with_source:
|
|
612
612
|
print("Available presets:")
|
|
613
|
-
for
|
|
614
|
-
|
|
613
|
+
for preset_name, source in presets_with_source:
|
|
614
|
+
source_label = "(user)" if source == "user" else "(builtin)"
|
|
615
|
+
print(f" - {preset_name} {source_label}")
|
|
615
616
|
else:
|
|
616
|
-
print(
|
|
617
|
+
print("No presets found.")
|
|
618
|
+
print()
|
|
619
|
+
print(f"User presets directory: {preset_manager.presets_dir}")
|
|
620
|
+
print("Place custom .json preset files in this directory.")
|
|
617
621
|
sys.exit(0)
|
|
618
622
|
|
|
619
623
|
# Load and apply preset if specified
|
|
@@ -332,8 +332,10 @@ class MediaFile:
|
|
|
332
332
|
"ReplayGain tagging is enabled. Proceeding with tag calculation/application."
|
|
333
333
|
)
|
|
334
334
|
self._run_replaygain()
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
else:
|
|
336
|
+
# Strip any existing ReplayGain tags from the output file
|
|
337
|
+
# since they are no longer accurate after normalization
|
|
338
|
+
self._strip_replaygain_tags(self.output_file)
|
|
337
339
|
_logger.info(f"Normalized file written to {self.output_file}")
|
|
338
340
|
|
|
339
341
|
def _run_replaygain(self) -> None:
|
|
@@ -461,6 +463,103 @@ class MediaFile:
|
|
|
461
463
|
f"Successfully wrote replaygain tags to input file {self.input_file}"
|
|
462
464
|
)
|
|
463
465
|
|
|
466
|
+
def _strip_replaygain_tags(self, output_file: str) -> None:
|
|
467
|
+
"""
|
|
468
|
+
Strip ReplayGain tags from the output file after normalization.
|
|
469
|
+
|
|
470
|
+
This ensures that old ReplayGain tags from the input are removed,
|
|
471
|
+
since they are no longer accurate after normalization.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
output_file (str): Path to the output file to strip tags from
|
|
475
|
+
"""
|
|
476
|
+
_logger.debug(f"Stripping ReplayGain tags from {output_file}")
|
|
477
|
+
|
|
478
|
+
output_file_ext = os.path.splitext(output_file)[1]
|
|
479
|
+
if output_file_ext == ".mp3":
|
|
480
|
+
try:
|
|
481
|
+
mp3 = MP3(output_file, ID3=ID3)
|
|
482
|
+
if not mp3.tags:
|
|
483
|
+
return
|
|
484
|
+
# Remove REPLAYGAIN_* tags
|
|
485
|
+
tags_to_remove = [
|
|
486
|
+
key for key in mp3.tags if key.startswith("TXXX:REPLAYGAIN_")
|
|
487
|
+
]
|
|
488
|
+
for tag in tags_to_remove:
|
|
489
|
+
del mp3.tags[tag]
|
|
490
|
+
if tags_to_remove:
|
|
491
|
+
mp3.save()
|
|
492
|
+
_logger.debug(
|
|
493
|
+
f"Stripped {len(tags_to_remove)} ReplayGain tag(s) from {output_file}"
|
|
494
|
+
)
|
|
495
|
+
except Exception as e:
|
|
496
|
+
_logger.warning(
|
|
497
|
+
f"Could not strip ReplayGain tags from {output_file}: {e}"
|
|
498
|
+
)
|
|
499
|
+
elif output_file_ext in [".mp4", ".m4a", ".m4v", ".mov"]:
|
|
500
|
+
try:
|
|
501
|
+
mp4 = MP4(output_file)
|
|
502
|
+
if not mp4.tags:
|
|
503
|
+
return
|
|
504
|
+
# Remove REPLAYGAIN_* tags
|
|
505
|
+
tags_to_remove = [
|
|
506
|
+
key
|
|
507
|
+
for key in mp4.tags
|
|
508
|
+
if "REPLAYGAIN_" in key.upper() or "REPLAYGAIN" in key.upper()
|
|
509
|
+
]
|
|
510
|
+
for tag in tags_to_remove:
|
|
511
|
+
del mp4.tags[tag]
|
|
512
|
+
if tags_to_remove:
|
|
513
|
+
mp4.save()
|
|
514
|
+
_logger.debug(
|
|
515
|
+
f"Stripped {len(tags_to_remove)} ReplayGain tag(s) from {output_file}"
|
|
516
|
+
)
|
|
517
|
+
except Exception as e:
|
|
518
|
+
_logger.warning(
|
|
519
|
+
f"Could not strip ReplayGain tags from {output_file}: {e}"
|
|
520
|
+
)
|
|
521
|
+
elif output_file_ext == ".ogg":
|
|
522
|
+
try:
|
|
523
|
+
ogg = OggVorbis(output_file)
|
|
524
|
+
# Remove REPLAYGAIN_* tags (case-insensitive)
|
|
525
|
+
tags_to_remove = [
|
|
526
|
+
key for key in ogg.keys() if key.upper().startswith("REPLAYGAIN_")
|
|
527
|
+
]
|
|
528
|
+
for tag in tags_to_remove:
|
|
529
|
+
del ogg[tag]
|
|
530
|
+
if tags_to_remove:
|
|
531
|
+
ogg.save()
|
|
532
|
+
_logger.debug(
|
|
533
|
+
f"Stripped {len(tags_to_remove)} ReplayGain tag(s) from {output_file}"
|
|
534
|
+
)
|
|
535
|
+
except Exception as e:
|
|
536
|
+
_logger.warning(
|
|
537
|
+
f"Could not strip ReplayGain tags from {output_file}: {e}"
|
|
538
|
+
)
|
|
539
|
+
elif output_file_ext == ".opus":
|
|
540
|
+
try:
|
|
541
|
+
opus = OggOpus(output_file)
|
|
542
|
+
# Remove R128_* tags (Opus uses R128 instead of REPLAYGAIN, case-insensitive)
|
|
543
|
+
tags_to_remove = [
|
|
544
|
+
key for key in opus.keys() if key.upper().startswith("R128_")
|
|
545
|
+
]
|
|
546
|
+
for tag in tags_to_remove:
|
|
547
|
+
del opus[tag]
|
|
548
|
+
if tags_to_remove:
|
|
549
|
+
opus.save()
|
|
550
|
+
_logger.debug(
|
|
551
|
+
f"Stripped {len(tags_to_remove)} R128 tag(s) from {output_file}"
|
|
552
|
+
)
|
|
553
|
+
except Exception as e:
|
|
554
|
+
_logger.warning(
|
|
555
|
+
f"Could not strip ReplayGain tags from {output_file}: {e}"
|
|
556
|
+
)
|
|
557
|
+
else:
|
|
558
|
+
# For other formats, we don't need to strip tags
|
|
559
|
+
_logger.debug(
|
|
560
|
+
f"Skipping ReplayGain tag stripping for {output_file_ext} (not supported/needed)"
|
|
561
|
+
)
|
|
562
|
+
|
|
464
563
|
def _can_write_output_video(self) -> bool:
|
|
465
564
|
"""
|
|
466
565
|
Determine whether the output file can contain video at all.
|
|
@@ -94,6 +94,37 @@ class PresetManager:
|
|
|
94
94
|
|
|
95
95
|
return sorted(presets)
|
|
96
96
|
|
|
97
|
+
def get_presets_with_source(self) -> list[tuple[str, str]]:
|
|
98
|
+
"""Get list of available presets with their source location.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
list[tuple[str, str]]: List of (preset_name, source) tuples where
|
|
102
|
+
source is either "user" or "builtin"
|
|
103
|
+
"""
|
|
104
|
+
user_presets: set[str] = set()
|
|
105
|
+
builtin_presets: set[str] = set()
|
|
106
|
+
|
|
107
|
+
# Get presets from user config directory
|
|
108
|
+
if self.presets_dir.exists():
|
|
109
|
+
for file in self.presets_dir.glob("*.json"):
|
|
110
|
+
user_presets.add(file.stem)
|
|
111
|
+
|
|
112
|
+
# Get default presets from package
|
|
113
|
+
if self.default_presets_dir.exists():
|
|
114
|
+
for file in self.default_presets_dir.glob("*.json"):
|
|
115
|
+
builtin_presets.add(file.stem)
|
|
116
|
+
|
|
117
|
+
result = []
|
|
118
|
+
all_presets = sorted(user_presets | builtin_presets)
|
|
119
|
+
for preset in all_presets:
|
|
120
|
+
# User presets take precedence
|
|
121
|
+
if preset in user_presets:
|
|
122
|
+
result.append((preset, "user"))
|
|
123
|
+
else:
|
|
124
|
+
result.append((preset, "builtin"))
|
|
125
|
+
|
|
126
|
+
return result
|
|
127
|
+
|
|
97
128
|
def load_preset(self, preset_name: str) -> dict[str, Any]:
|
|
98
129
|
"""Load a preset file by name.
|
|
99
130
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/_ffmpeg_normalize.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/data/presets/music.json
RENAMED
|
File without changes
|
{ffmpeg_normalize-1.37.1 → ffmpeg_normalize-1.37.3}/src/ffmpeg_normalize/data/presets/podcast.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|