diff --git a/ffmpeg_normalize/_cmd_utils.py b/ffmpeg_normalize/_cmd_utils.py index 8b3cfe5..1ecc83f 100644 --- a/ffmpeg_normalize/_cmd_utils.py +++ b/ffmpeg_normalize/_cmd_utils.py @@ -20,52 +20,17 @@ r"Duration: (?P\d{2}):(?P\d{2}):(?P\d{2})\.(?P\d{2})" ) - -def to_ms(s: str | None = None, decimals: int | None = None, **kwargs) -> int: - """This function converts a string with time format "hh:mm:ss:ms" to milliseconds - - Args: - s (str): String with time format "hh:mm:ss:ms", if not provided, the function will use the keyword arguments (optional) - decimals (int): Number of decimals to round to (optional) - - Keyword Args: - hour: Number of hours (optional) - min: Number of minutes (optional) - sec: Number of seconds (optional) - ms: Number of milliseconds (optional) - - Returns: - int: Integer with the number of milliseconds - """ - if s: - hour = int(s[0:2]) - minute = int(s[3:5]) - sec = int(s[6:8]) - ms = int(s[10:11]) - else: - hour = int(kwargs.get("hour", 0)) - minute = int(kwargs.get("min", 0)) - sec = int(kwargs.get("sec", 0)) - ms = int(kwargs.get("ms", 0)) - - result = (hour * 60 * 60 * 1000) + (minute * 60 * 1000) + (sec * 1000) + ms - if decimals and isinstance(decimals, int): - return round(result, decimals) - return result - - class CommandRunner: """ Wrapper for running ffmpeg commands """ - def __init__(self, cmd: list[str], dry: bool = False): + def __init__(self, dry: bool = False): """Create a CommandRunner object Args: cmd: Command to run as a list of strings dry: Dry run mode. Defaults to False. """ - self.cmd = cmd self.dry = dry self.output: str | None = None @@ -102,7 +67,7 @@ def prune_ffmpeg_progress_from_output(output: str) -> str: ) - def run_ffmpeg_command(self) -> Iterator[int]: + def run_ffmpeg_command(self, cmd: list[str]) -> Iterator[int]: """ Run an ffmpeg command @@ -110,8 +75,8 @@ def run_ffmpeg_command(self) -> Iterator[int]: int: Progress percentage """ # wrapper for 'ffmpeg-progress-yield' - logger.debug(f"Running command: {self.cmd}") - ff = FfmpegProgress(self.cmd, dry_run=self.dry) + logger.debug(f"Running command: {cmd}") + ff = FfmpegProgress(cmd, dry_run=self.dry) yield from ff.run_command_with_progress() self.output = ff.stderr @@ -122,38 +87,40 @@ def run_ffmpeg_command(self) -> Iterator[int]: ) - def run_command(self) -> None: + def run_command(self, cmd: list[str]) -> CommandRunner: """ - Run the actual command (not ffmpeg) + Run a command with subprocess + + Returns: + CommandRunner: itself Raises: RuntimeError: If command returns non-zero exit code """ - logger.debug(f"Running command: {self.cmd}") + logger.debug(f"Running command: {cmd}") if self.dry: logger.debug("Dry mode specified, not actually running command") - return + return self p = subprocess.Popen( - self.cmd, + cmd, stdin=subprocess.PIPE, # Apply stdin isolation by creating separate pipe. stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False, ) - # simple running of command stdout_bytes, stderr_bytes = p.communicate() stdout = stdout_bytes.decode("utf8", errors="replace") stderr = stderr_bytes.decode("utf8", errors="replace") - if p.returncode == 0: - self.output = stdout + stderr - else: - raise RuntimeError(f"Error running command {self.cmd}: {stderr}") + if p.returncode != 0: + raise RuntimeError(f"Error running command {cmd}: {stderr}") + self.output = stdout + stderr + return self def get_output(self) -> str: if self.output is None: @@ -217,10 +184,8 @@ def ffmpeg_has_loudnorm() -> bool: Returns: bool: True if loudnorm is supported, False otherwise """ - cmd_runner = CommandRunner([get_ffmpeg_exe(), "-filters"]) - cmd_runner.run_command() - - supports_loudnorm = "loudnorm" in cmd_runner.get_output() + output = CommandRunner().run_command([get_ffmpeg_exe(), "-filters"]).get_output() + supports_loudnorm = "loudnorm" in output if not supports_loudnorm: logger.error( "Your ffmpeg does not support the 'loudnorm' filter. " diff --git a/ffmpeg_normalize/_media_file.py b/ffmpeg_normalize/_media_file.py index 18cfbd0..73c9116 100644 --- a/ffmpeg_normalize/_media_file.py +++ b/ffmpeg_normalize/_media_file.py @@ -9,7 +9,7 @@ from tqdm import tqdm -from ._cmd_utils import DUR_REGEX, NUL, CommandRunner, to_ms +from ._cmd_utils import DUR_REGEX, NUL, CommandRunner from ._errors import FFmpegNormalizeError from ._logger import setup_custom_logger from ._streams import AudioStream, SubtitleStream, VideoStream @@ -23,6 +23,15 @@ ONE_STREAM = {"aac", "ast", "flac", "mp3", "wav"} +def _to_ms(**kwargs: str) -> int: + hour = int(kwargs.get("hour", 0)) + minute = int(kwargs.get("min", 0)) + sec = int(kwargs.get("sec", 0)) + ms = int(kwargs.get("ms", 0)) + + return (hour * 60 * 60 * 1000) + (minute * 60 * 1000) + (sec * 1000) + ms + + class StreamDict(TypedDict): audio: dict[int, AudioStream] video: dict[int, VideoStream] @@ -94,9 +103,7 @@ def parse_streams(self) -> None: NUL, ] - cmd_runner = CommandRunner(cmd) - cmd_runner.run_command() - output = cmd_runner.get_output() + output = CommandRunner().run_command(cmd).get_output() logger.debug("Stream parsing command output:") logger.debug(output) @@ -105,20 +112,17 @@ def parse_streams(self) -> None: duration = None for line in output_lines: - if "Duration" in line: - duration_search = DUR_REGEX.search(line) - if not duration_search: - logger.warning("Could not extract duration from input file!") - else: - duration = to_ms(None, None, **duration_search.groupdict()) / 1000 + if duration_search := DUR_REGEX.search(line): + duration = _to_ms(**duration_search.groupdict()) / 1000 logger.debug(f"Found duration: {duration} s") + else: + logger.warning("Could not extract duration from input file!") if not line.startswith("Stream"): continue - stream_id_match = re.search(r"#0:([\d]+)", line) - if stream_id_match: + if stream_id_match := re.search(r"#0:([\d]+)", line): stream_id = int(stream_id_match.group(1)) if stream_id in self._stream_ids(): continue @@ -376,8 +380,7 @@ def _second_pass(self) -> Iterator[int]: # if dry run, only show sample command if self.ffmpeg_normalize.dry_run: cmd.append(self.output_file) - cmd_runner = CommandRunner(cmd, dry=True) - cmd_runner.run_command() + CommandRunner(dry=True).run_command(cmd) yield 100 return @@ -386,9 +389,8 @@ def _second_pass(self) -> Iterator[int]: cmd.append(temp_file) try: - cmd_runner = CommandRunner(cmd) try: - yield from cmd_runner.run_ffmpeg_command() + yield from CommandRunner().run_ffmpeg_command(cmd) except Exception as e: cmd_str = " ".join([shlex.quote(c) for c in cmd]) logger.error(f"Error while running command {cmd_str}! Error: {e}") diff --git a/ffmpeg_normalize/_streams.py b/ffmpeg_normalize/_streams.py index e98d2c8..b621864 100644 --- a/ffmpeg_normalize/_streams.py +++ b/ffmpeg_normalize/_streams.py @@ -229,8 +229,8 @@ def parse_astats(self) -> Iterator[int]: NUL, ] - cmd_runner = CommandRunner(cmd) - yield from cmd_runner.run_ffmpeg_command() + cmd_runner = CommandRunner() + yield from cmd_runner.run_ffmpeg_command(cmd) output = cmd_runner.get_output() logger.debug( @@ -298,8 +298,8 @@ def parse_loudnorm_stats(self) -> Iterator[int]: NUL, ] - cmd_runner = CommandRunner(cmd) - yield from cmd_runner.run_ffmpeg_command() + cmd_runner = CommandRunner() + yield from cmd_runner.run_ffmpeg_command(cmd) output = cmd_runner.get_output() logger.debug(