diff --git a/src/tscut/temporaries.py b/src/tscut/temporaries.py index 1d83aa9..49cb927 100644 --- a/src/tscut/temporaries.py +++ b/src/tscut/temporaries.py @@ -6,7 +6,7 @@ import logging import os -from typing import IO, Any +from typing import IO, Any, Self logger = logging.getLogger(__name__) @@ -34,5 +34,5 @@ class TemporaryFiles: def __enter__(self) -> Self: return self - def __exit__(self, ...) -> None: + def __exit__(self) -> None: self.cleanup() diff --git a/src/tscut/tools/ffmpeg.py b/src/tscut/tools/ffmpeg.py index 9a68e24..a26b78e 100644 --- a/src/tscut/tools/ffmpeg.py +++ b/src/tscut/tools/ffmpeg.py @@ -8,12 +8,12 @@ from io import TextIOWrapper from math import ceil, floor, log from os import SEEK_SET, close, lseek, memfd_create, read, set_inheritable, write from subprocess import PIPE, Popen -from typing import IO +from typing import IO, BinaryIO from tqdm import tqdm from typeguard import typechecked -from tscut.exceptions import ExternalToolError, InvalidMediaError +from tscut.exceptions import ExternalToolError, InvalidMediaError, TemporaryFileError from tscut.tools.ffprobe import ( get_frames_in_stream, get_video_dimensions, @@ -21,13 +21,14 @@ from tscut.tools.ffprobe import ( ) from tscut.tools.ppm import dump_ppm from tscut.tools.timeframe import get_packet_duration, parse_timestamp +from tscut.temporaries import TemporaryFiles logger = logging.getLogger(__name__) @typechecked def ffmpeg_convert(ffmpeg_path:str, ffprobe_path:str, input_file: IO[bytes], input_format:str, - output_file: IO[bytes], output_format:str, duration: timedelta): + output_file: IO[bytes], output_format:str, duration: timedelta) -> None: width, height = get_video_dimensions(ffprobe_path, input_file) if width is None or height is None: return @@ -68,7 +69,7 @@ def ffmpeg_convert(ffmpeg_path:str, ffprobe_path:str, input_file: IO[bytes], inp pb.update() status = ffmpeg.wait() if status != 0: - logger.error('Conversion failed with status code: %d', status) + raise ExternalToolError(f"Conversion failed with status code: {status:d}") @typechecked @@ -116,14 +117,12 @@ def extract_pictures(ffmpeg_path:str, input_file:IO[bytes], begin:timedelta, nb_ with Popen(command, stdout=PIPE, close_fds=False) as ffmpeg: status = ffmpeg.wait() if status != 0: - logger.error('Conversion failed with status code: %d', status) - raise ExternalToolError("") + raise ExternalToolError(f"Conversion failed with status code: {status:d}") lseek(outfd, 0, SEEK_SET) images = read(outfd,length) if len(images) != length: - logger.error("Received %d bytes but %d were expected.", len(images), length) - raise InvalidMediaError("") + raise InvalidMediaError(f"Received {len(images)} bytes but {length} were expected.") lseek(outfd, 0, SEEK_SET) return images, outfd @@ -150,24 +149,25 @@ def extract_sound(ffmpeg_path:str, input_file: IO[bytes], begin:timedelta, outpu with Popen(command, stdout=PIPE, close_fds=False) as ffmpeg: status = ffmpeg.wait() if status != 0: - logger.error('Sound extraction returns error code: %d', status) - raise ExternalToolError("") + raise ExternalToolError(f"Sound extraction returns error code: {status}") lseek(outfd, 0, SEEK_SET) sound = read(outfd, length) if len(sound) != length: - logger.info("Received %d bytes but %d were expected (channels=%d, freq=%d, packets=%d,\ - duration=%d ms).", len(sound), length, nb_channels, sample_rate, nb_packets, - packet_duration) - raise InvalidMediaError("") + raise InvalidMediaError(f"Received {len(sound)} bytes but {length} were expected (\ + channels={nb_channels}, freq={sample_rate} packets={nb_packets},\ + duration={packet_duration} ms).") return sound, outfd @typechecked def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], begin:timedelta, end:timedelta, streams, files_prefix, nb_frames:int, framerate:float, - width:int, height:int, temporaries, dump_mem_fd:bool=False): + width:int, height:int, temporaries:TemporaryFiles, + dump_mem_fd:bool=False) -> tuple[BinaryIO|None, + TextIOWrapper|None, + BinaryIO|None]: # The command line for encoding only video track video_encoder_params = [ ffmpeg_path, '-y', '-loglevel', 'quiet'] video_input_params = [] @@ -258,8 +258,7 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], packets = get_frames_in_stream(ffprobe_path, input_file=input_file, begin=begin, end=end, stream_kind='a', sub_stream_id=audio_id) if packets is None: - logger.error("Impossible to retrieve audio packets") - return None + raise InvalidMediaError("Impossible to retrieve audio packets") nb_packets = len(packets) logger.debug("Found %d packets to be extracted from audio track.", nb_packets) if nb_packets > 0: @@ -283,14 +282,13 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], if dump_mem_fd: try: with open(tmpname,'wb') as output: - temporaries.append(output) + temporaries.add(output) outfd = output.fileno() pos = 0 while pos < len(sound_bytes): pos+=write(outfd, sound_bytes[pos:]) - except OSError: - logger.error('Impossible to create file: %s', tmpname) - return None + except OSError as e: + raise TemporaryFileError(f"Impossible to create file: {tmpname}") from e # We rewind to zero the memory file descriptor lseek(memfd, 0, SEEK_SET) @@ -323,9 +321,8 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], mkv_filename = f'{files_prefix}.mkv' try: mkv_output = open(mkv_filename,'wb+') - except OSError: - logger.error('Impossible to create file: %s', mkv_filename) - return None + except OSError as e: + raise TemporaryFileError(f"Impossible to create file: {mkv_filename}") from e mkvoutfd = mkv_output.fileno() set_inheritable(mkvoutfd, True) @@ -336,17 +333,15 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], with Popen(generic_encoder_params, stdout=PIPE, close_fds=False) as ffmpeg: status = ffmpeg.wait() if status != 0: - logger.error('Encoding failed with status code: %d', status) - return None + raise ExternalToolError(f"Encoding failed with status code: {status}") - temporaries.append(mkv_output) + temporaries.add(mkv_output) h264_filename = f'{files_prefix}.h264' try: h264_output = open(h264_filename,'wb+') - except OSError: - logger.error('Impossible to create file: %s', h264_filename) - return None + except OSError as e: + raise TemporaryFileError(f"Impossible to create file {h264_filename}") from e h264outfd = h264_output.fileno() set_inheritable(h264outfd, True) @@ -365,17 +360,15 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], with Popen(video_encoder_params, stdout=PIPE, close_fds=False) as ffmpeg: status = ffmpeg.wait() if status != 0: - logger.error('Encoding failed with status code: %d', status) - return None + raise ExternalToolError(f"Encoding failed with status code: {status:d}") - temporaries.append(h264_output) + temporaries.add(h264_output) h264_ts_filename = f'{files_prefix}-ts.txt' try: h264_ts_output = open(h264_ts_filename,'w+', encoding='utf8') - except OSError: - logger.error('Impossible to create file: %s', h264_ts_filename) - return None + except OSError as e: + raise TemporaryFileError(f"Impossible to create file: {h264_ts_filename}") from e h264_ts_output.write('# timestamp format v2\n') ts = 0 @@ -385,7 +378,7 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], h264_ts_output.flush() h264_ts_output.seek(0) - temporaries.append(h264_ts_output) + temporaries.add(h264_ts_output) for memfd in memfds: close(memfd) @@ -393,5 +386,5 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes], return h264_output, h264_ts_output, mkv_output # Nothing to be done. We are already at a i-frame boundary. - return None, None + return None, None, None diff --git a/src/tscut/tools/mkvtoolnix.py b/src/tscut/tools/mkvtoolnix.py index c0c9e7f..980260c 100644 --- a/src/tscut/tools/mkvtoolnix.py +++ b/src/tscut/tools/mkvtoolnix.py @@ -11,7 +11,7 @@ from io import TextIOWrapper from os import SEEK_SET, fstat, ftruncate, lseek, read, set_inheritable, write from pathlib import Path from subprocess import PIPE, Popen -from typing import IO +from typing import IO, Sequence import hexdump from tqdm import tqdm @@ -238,7 +238,7 @@ def extract_mkv_part(mkvmerge_path:str, input_file:IO[bytes], output_file:IO[byt # Merge a list of mkv files passed as input, and produce a new MKV as output @typechecked -def merge_mkvs(mkvmerge_path:str, inputs: list[IO[bytes]], output_name:str, +def merge_mkvs(mkvmerge_path:str, inputs: Sequence[IO[bytes]], output_name:str, concatenate: bool=True, timestamps: dict[int, IO[str]] | None = None) -> IO[bytes]: logger = logging.getLogger(__name__) diff --git a/src/tscut/tools/ppm.py b/src/tscut/tools/ppm.py index adfbfa6..6312b0d 100644 --- a/src/tscut/tools/ppm.py +++ b/src/tscut/tools/ppm.py @@ -12,9 +12,11 @@ from typing import IO from typeguard import typechecked +from tscut.temporaries import TemporaryFiles +from tscut.exceptions import TemporaryFileError @typechecked -def dump_ppm(pictures: bytes, prefix: str, temporaries: list[IO[bytes]]) -> None: +def dump_ppm(pictures: bytes, prefix: str, temporaries: TemporaryFiles) -> None: """ Dump PPM pictures from a bytes buffer to files. @@ -71,7 +73,7 @@ def dump_ppm(pictures: bytes, prefix: str, temporaries: list[IO[bytes]]) -> None header_len=2+1+ceil(log(width, 10))+1+ceil(log(height, 10))+1+3+1 try: with open(filename, 'wb') as out: - temporaries.append(out) + temporaries.add(out) outfd = out.fileno() length=header_len+3*width*height nb_bytes = 0 @@ -79,5 +81,5 @@ def dump_ppm(pictures: bytes, prefix: str, temporaries: list[IO[bytes]]) -> None nb_bytes+=write(outfd, pictures[pos+nb_bytes:pos+length]) pos+=length picture+=1 - except OSError: - logger.error('Impossible to create file: %s', filename) + except OSError as e: + raise TemporaryFileError(f"Impossible to create file {filename}") from e diff --git a/src/tscut/tools/timeframe.py b/src/tscut/tools/timeframe.py index 03edab5..e4f104a 100644 --- a/src/tscut/tools/timeframe.py +++ b/src/tscut/tools/timeframe.py @@ -9,6 +9,8 @@ from datetime import timedelta from typeguard import typechecked +from tscut.exceptions import InvalidMediaError + @typechecked def parse_timestamp(ts:str) -> timedelta|None: @@ -206,7 +208,7 @@ def get_ts_frame(frame: dict) -> timedelta|None: return timedelta(seconds=pts_time) @typechecked -def get_packet_duration(packet: dict) -> int | None: +def get_packet_duration(packet: dict) -> int: logger = logging.getLogger(__name__) if 'duration' in packet: @@ -214,7 +216,6 @@ def get_packet_duration(packet: dict) -> int | None: elif 'pkt_duration' in packet: duration = int(packet['pkt_duration']) else: - logger.error('Impossible to find duration of packet %s', packet) - return None + raise InvalidMediaError("Impossible to find duration of packet {packet}") return duration