ffmpeg-normalize 1.36.0__tar.gz → 1.36.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 (17) hide show
  1. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/PKG-INFO +9 -8
  2. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/README.md +8 -7
  3. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/pyproject.toml +1 -1
  4. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/__main__.py +12 -0
  5. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/_cmd_utils.py +73 -0
  6. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/_ffmpeg_normalize.py +23 -1
  7. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/LICENSE.md +0 -0
  8. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/__init__.py +0 -0
  9. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/_errors.py +0 -0
  10. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/_logger.py +0 -0
  11. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/_media_file.py +0 -0
  12. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/_presets.py +0 -0
  13. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/_streams.py +0 -0
  14. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/data/presets/music.json +0 -0
  15. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/data/presets/podcast.json +0 -0
  16. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/data/presets/streaming-video.json +0 -0
  17. {ffmpeg_normalize-1.36.0 → ffmpeg_normalize-1.36.1}/src/ffmpeg_normalize/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ffmpeg-normalize
3
- Version: 1.36.0
3
+ Version: 1.36.1
4
4
  Summary: Normalize audio via ffmpeg
5
5
  Keywords: ffmpeg,normalize,audio
6
6
  Author: Werner Robitza
@@ -54,7 +54,14 @@ This program normalizes media files to a certain loudness level using the EBU R1
54
54
  - Docker support — Run via Docker container
55
55
  - Python API — Use programmatically in your Python projects
56
56
  - Shell completions — Available for bash, zsh, and fish
57
- - Album Batch normalization – Process files jointy, preserving relative loudness
57
+ - Album Batch normalization – Process files jointly, preserving relative loudness
58
+
59
+ ## 🚀 Quick Start
60
+
61
+ 1. Install a recent version of [ffmpeg](https://ffmpeg.org/download.html)
62
+ 2. Run `pip3 install ffmpeg-normalize` and `ffmpeg-normalize /path/to/your/file.mp4`, alternatively install [`uv`](https://docs.astral.sh/uv/getting-started/installation/) and run `uvx ffmpeg-normalize /path/to/your/file.mp4`
63
+ 3. Done! 🎧 (the normalized file will be called `normalized/file.mkv`)
64
+
58
65
 
59
66
  ## 🆕 What's New
60
67
 
@@ -99,12 +106,6 @@ Other recent additions:
99
106
 
100
107
  See the [full changelog](https://github.com/slhck/ffmpeg-normalize/blob/master/CHANGELOG.md) for all updates.
101
108
 
102
- ## 🚀 Quick Start
103
-
104
- 1. Install a recent version of [ffmpeg](https://ffmpeg.org/download.html)
105
- 2. Run `pip3 install ffmpeg-normalize` and `ffmpeg-normalize /path/to/your/file.mp4`, alternatively install [`uv`](https://docs.astral.sh/uv/getting-started/installation/) and run `uvx ffmpeg-normalize /path/to/your/file.mp4`
106
- 3. Done! 🎧 (the normalized file will be called `normalized/file.mkv`)
107
-
108
109
  ## 📓 Documentation
109
110
 
110
111
  Check out our [documentation](https://slhck.info/ffmpeg-normalize/) for more info!
@@ -22,7 +22,14 @@ This program normalizes media files to a certain loudness level using the EBU R1
22
22
  - Docker support — Run via Docker container
23
23
  - Python API — Use programmatically in your Python projects
24
24
  - Shell completions — Available for bash, zsh, and fish
25
- - Album Batch normalization – Process files jointy, preserving relative loudness
25
+ - Album Batch normalization – Process files jointly, preserving relative loudness
26
+
27
+ ## 🚀 Quick Start
28
+
29
+ 1. Install a recent version of [ffmpeg](https://ffmpeg.org/download.html)
30
+ 2. Run `pip3 install ffmpeg-normalize` and `ffmpeg-normalize /path/to/your/file.mp4`, alternatively install [`uv`](https://docs.astral.sh/uv/getting-started/installation/) and run `uvx ffmpeg-normalize /path/to/your/file.mp4`
31
+ 3. Done! 🎧 (the normalized file will be called `normalized/file.mkv`)
32
+
26
33
 
27
34
  ## 🆕 What's New
28
35
 
@@ -67,12 +74,6 @@ Other recent additions:
67
74
 
68
75
  See the [full changelog](https://github.com/slhck/ffmpeg-normalize/blob/master/CHANGELOG.md) for all updates.
69
76
 
70
- ## 🚀 Quick Start
71
-
72
- 1. Install a recent version of [ffmpeg](https://ffmpeg.org/download.html)
73
- 2. Run `pip3 install ffmpeg-normalize` and `ffmpeg-normalize /path/to/your/file.mp4`, alternatively install [`uv`](https://docs.astral.sh/uv/getting-started/installation/) and run `uvx ffmpeg-normalize /path/to/your/file.mp4`
74
- 3. Done! 🎧 (the normalized file will be called `normalized/file.mkv`)
75
-
76
77
  ## 📓 Documentation
77
78
 
78
79
  Check out our [documentation](https://slhck.info/ffmpeg-normalize/) for more info!
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "ffmpeg-normalize"
7
- version = "1.36.0"
7
+ version = "1.36.1"
8
8
  description = "Normalize audio via ffmpeg"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -723,6 +723,18 @@ def main() -> None:
723
723
  if not input_files:
724
724
  error("No input files specified. Use positional arguments or --input-list.")
725
725
 
726
+ # Validate all input files upfront before processing
727
+ _logger.debug("Validating all input files before processing...")
728
+ validation_errors = FFmpegNormalize.validate_input_files(input_files)
729
+ if validation_errors:
730
+ _logger.error("Validation failed for the following files:")
731
+ for err in validation_errors:
732
+ _logger.error(f" - {err}")
733
+ error(
734
+ f"Validation failed for {len(validation_errors)} file(s). "
735
+ "Please fix the issues above and try again."
736
+ )
737
+
726
738
  for index, input_file in enumerate(input_files):
727
739
  if cli_args.output is not None and index < len(cli_args.output):
728
740
  if cli_args.output_folder and cli_args.output_folder != "normalized":
@@ -190,3 +190,76 @@ def ffmpeg_has_loudnorm() -> bool:
190
190
  "Please make sure you are running ffmpeg v4.2 or above."
191
191
  )
192
192
  return supports_loudnorm
193
+
194
+
195
+ def validate_input_file(input_file: str) -> tuple[bool, str | None]:
196
+ """
197
+ Validate that an input file exists, is readable, and contains audio streams.
198
+
199
+ This function performs a lightweight probe of the file using ffmpeg to check
200
+ if it can be read and contains at least one audio stream.
201
+
202
+ Args:
203
+ input_file: Path to the input file to validate
204
+
205
+ Returns:
206
+ tuple: (is_valid, error_message)
207
+ - is_valid: True if the file is valid, False otherwise
208
+ - error_message: None if valid, otherwise a descriptive error message
209
+ """
210
+ # Check if file exists
211
+ if not os.path.exists(input_file):
212
+ return False, f"File does not exist: {input_file}"
213
+
214
+ # Check if it's actually a file (not a directory)
215
+ if not os.path.isfile(input_file):
216
+ return False, f"Path is not a file: {input_file}"
217
+
218
+ # Check if file is readable
219
+ if not os.access(input_file, os.R_OK):
220
+ return False, f"File is not readable (permission denied): {input_file}"
221
+
222
+ # Check if file has audio streams using ffmpeg probe
223
+ ffmpeg_exe = get_ffmpeg_exe()
224
+ cmd = [
225
+ ffmpeg_exe,
226
+ "-i",
227
+ input_file,
228
+ "-c",
229
+ "copy",
230
+ "-t",
231
+ "0",
232
+ "-map",
233
+ "0",
234
+ "-f",
235
+ "null",
236
+ os.devnull,
237
+ ]
238
+
239
+ try:
240
+ output = CommandRunner().run_command(cmd).get_output()
241
+ except RuntimeError as e:
242
+ error_str = str(e)
243
+ # Extract a cleaner error message from ffmpeg output
244
+ if "Invalid data found" in error_str:
245
+ return False, f"Invalid or corrupted media file: {input_file}"
246
+ if "No such file or directory" in error_str:
247
+ return False, f"File not found or cannot be opened: {input_file}"
248
+ if "Permission denied" in error_str:
249
+ return False, f"Permission denied when reading file: {input_file}"
250
+ if "does not contain any stream" in error_str:
251
+ return False, f"File contains no media streams: {input_file}"
252
+ # Generic error for other ffmpeg failures
253
+ return False, f"Cannot read media file: {input_file}"
254
+
255
+ # Check for audio streams in the output
256
+ has_audio = False
257
+ for line in output.split("\n"):
258
+ if line.strip().startswith("Stream") and "Audio" in line:
259
+ has_audio = True
260
+ break
261
+
262
+ if not has_audio:
263
+ return False, f"File does not contain any audio streams: {input_file}"
264
+
265
+ return True, None
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Literal
9
9
 
10
10
  from tqdm import tqdm
11
11
 
12
- from ._cmd_utils import ffmpeg_has_loudnorm, get_ffmpeg_exe
12
+ from ._cmd_utils import ffmpeg_has_loudnorm, get_ffmpeg_exe, validate_input_file
13
13
  from ._errors import FFmpegNormalizeError
14
14
  from ._media_file import MediaFile
15
15
 
@@ -317,6 +317,28 @@ class FFmpegNormalize:
317
317
  self.media_files.append(MediaFile(self, input_file, output_file))
318
318
  self.file_count += 1
319
319
 
320
+ @staticmethod
321
+ def validate_input_files(input_files: list[str]) -> list[str]:
322
+ """
323
+ Validate all input files before processing.
324
+
325
+ This method checks that each input file exists, is readable, and contains
326
+ at least one audio stream. All files are validated upfront so that users
327
+ can fix all issues before rerunning the batch.
328
+
329
+ Args:
330
+ input_files: List of input file paths to validate
331
+
332
+ Returns:
333
+ list: List of error messages for invalid files. Empty if all files are valid.
334
+ """
335
+ errors = []
336
+ for input_file in input_files:
337
+ is_valid, error_msg = validate_input_file(input_file)
338
+ if not is_valid and error_msg:
339
+ errors.append(error_msg)
340
+ return errors
341
+
320
342
  def _calculate_batch_reference(self) -> float | None:
321
343
  """
322
344
  Calculate the batch reference loudness by averaging measurements across all files.