diff --git a/src/tscut/cli.py b/src/tscut/cli.py index 4e0bc35..cf19e77 100644 --- a/src/tscut/cli.py +++ b/src/tscut/cli.py @@ -15,6 +15,7 @@ import coloredlogs from tscut.exceptions import TSCutError from tscut.models import ProcessingOptions from tscut.pipeline import process_recording +from tscut.temporaries import TemporaryFiles from tscut.tools.discovery import check_required_tools from tscut.tools.timeframe import compare_time_interval, parse_time_interval @@ -110,9 +111,13 @@ def main() -> int: logger.debug("Configuration: %s", config) try: - process_recording(config) + temporaries = TemporaryFiles(config.keep_temporaries) + process_recording(config, temporaries) except TSCutError as exc: logger.error("%s", exc) return 1 + finally: + logger.info("Cleaning temporary files") + temporaries.cleanup() return 0 diff --git a/src/tscut/coarse.py b/src/tscut/coarse.py index b94b50f..01d1612 100644 --- a/src/tscut/coarse.py +++ b/src/tscut/coarse.py @@ -5,8 +5,8 @@ import logging from typing import IO -from tscut.tools.mkvtoolnix import extract_mkv_part from tscut.exceptions import TemporaryFileError +from tscut.tools.mkvtoolnix import extract_mkv_part logger = logging.getLogger(__name__) @@ -27,4 +27,4 @@ def do_coarse_processing(ffmpeg_path:str, ffprobe_path:str, mkvmerge_path:str, extract_mkv_part(mkvmerge_path=mkvmerge_path, input_file=input_file, output_file=internal_mkv, begin=begin, end=end) - temporaries.append(internal_mkv) + temporaries.add(internal_mkv) diff --git a/src/tscut/cutting.py b/src/tscut/cutting.py index f9a52eb..b8f5313 100644 --- a/src/tscut/cutting.py +++ b/src/tscut/cutting.py @@ -12,7 +12,7 @@ from os import ( write, ) from shutil import copyfile -from typing import IO, Any, BinaryIO, Sequence, TextIO +from typing import BinaryIO, Sequence, TextIO import hexdump from tqdm import tqdm @@ -22,6 +22,7 @@ from tscut.exceptions import InvalidMediaError, TemporaryFileError from tscut.h264.avc import get_avc_config_from_h264 from tscut.matroska.codec import dump_codec_private_data from tscut.models import CutResult, PreparedMedia, ProcessingOptions +from tscut.temporaries import TemporaryFiles from tscut.tools.ffmpeg import extract_all_streams from tscut.tools.ffprobe import ( get_nearest_iframe, @@ -94,7 +95,7 @@ def concatenate_h264_ts_parts(h264_ts_parts: Sequence[TextIO], output: TextIO) - first = False def cut_recording(media: PreparedMedia, options: ProcessingOptions, - temporaries: list[IO[Any]]) -> CutResult: + temporaries: TemporaryFiles) -> CutResult: # Pour chaque portion partnum = 0 @@ -243,10 +244,10 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions, remove_video_tracks_from_mkv(mkvmerge_path=options.tools_paths['mkvmerge'], input_file=internal_mkv, output_file=internal_novideo_mkv) - temporaries.append(internal_mkv) - temporaries.append(internal_h264) - temporaries.append(internal_h264_ts) - temporaries.append(internal_novideo_mkv) + temporaries.add(internal_mkv) + temporaries.add(internal_h264) + temporaries.add(internal_h264_ts) + temporaries.add(internal_novideo_mkv) h264parts.append(internal_h264) h264_ts.append(internal_h264_ts) @@ -280,7 +281,7 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions, part = merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], inputs=subparts, output_name=f'part-{partnum:d}.mkv', concatenate=True) mkvparts.append(part) - temporaries.append(part) + temporaries.add(part) pos = pos+tail_iframe_ts-ts1 @@ -301,7 +302,7 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions, logger.info('Merging all H264 tracks') concatenate_h264_parts(h264parts=h264parts, output=full_h264) - temporaries.append(full_h264) + temporaries.add(full_h264) try: full_h264_ts = open(f'{media.basename}-ts.txt', 'w+', encoding='utf8') @@ -310,7 +311,7 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions, logger.info('Merging H264 timestamps') concatenate_h264_ts_parts(h264_ts_parts=h264_ts, output=full_h264_ts) - temporaries.append(full_h264_ts) + temporaries.add(full_h264_ts) final_novideo_name = f'{media.basename}-novideo.mkv' final_with_video_name = f'{media.basename}-video.mkv' @@ -331,7 +332,7 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions, except OSError: raise TemporaryFileError(f"Impossible to open file: {final_novideo_name}") - temporaries.append(final_novideo) + temporaries.add(final_novideo) full_h264_ts.seek(0) diff --git a/src/tscut/models.py b/src/tscut/models.py index 5fd3f7b..e254180 100644 --- a/src/tscut/models.py +++ b/src/tscut/models.py @@ -57,5 +57,5 @@ class PreparedMedia: @dataclass class CutResult: filename: str - movie: BinaryIO + movie: IO[bytes] check_positions: list[timedelta] diff --git a/src/tscut/pipeline.py b/src/tscut/pipeline.py index 228d5b2..445c2ed 100644 --- a/src/tscut/pipeline.py +++ b/src/tscut/pipeline.py @@ -5,9 +5,8 @@ import logging import os from datetime import datetime, timedelta -from os import unlink from shutil import copyfile, move -from typing import IO, Any, BinaryIO +from typing import BinaryIO import hexdump @@ -17,6 +16,7 @@ from tscut.h264.avc import parse_codec_private from tscut.matroska.codec import dump_codec_private_data from tscut.models import PreparedMedia, ProcessingOptions, SupportedFormat from tscut.subtitles.ocr import do_ocr, extract_srt, get_tesseract_supported_lang +from tscut.temporaries import TemporaryFiles from tscut.tools.ffmpeg import ffmpeg_convert from tscut.tools.ffprobe import ( find_subtitles_tracks, @@ -42,10 +42,8 @@ def detect_supported_format(format_name: str) -> SupportedFormat: f"Unsupported media format: {format_name}" ) -def process_recording(options: ProcessingOptions) -> None: +def process_recording(options: ProcessingOptions, temporaries: TemporaryFiles) -> None: nb_parts = len(options.parts) - temporaries : list[IO[Any]] = [] - basename = os.path.splitext(os.path.basename(options.input_file))[0] mp4filename = basename+'.mp4' mkvfilename = basename+'.mkv' @@ -88,7 +86,7 @@ def process_recording(options: ProcessingOptions) -> None: with open(mp4filename, 'wb+') as mp4: ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'], input_file, 'mpegts', mp4, 'mp4', duration) - temporaries.append(mp4) + temporaries.add(mp4) logger.info("Converting MP4 to MKV.") try: mkv = open(mkvfilename, 'wb+') @@ -98,7 +96,7 @@ def process_recording(options: ProcessingOptions) -> None: ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'], mp4, 'mp4', mkv, 'matroska', duration) if nb_parts > 0: - temporaries.append(mkv) + temporaries.add(mkv) except OSError: raise TemporaryFileError(f"Impossible to create: {mp4filename}") @@ -112,7 +110,7 @@ def process_recording(options: ProcessingOptions) -> None: ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'], input_file, 'mp4', mkv, 'matroska', duration) if nb_parts > 0: - temporaries.append(mkv) + temporaries.add(mkv) else: logger.info("Already in MKV") mkv = input_file @@ -198,7 +196,7 @@ def process_recording(options: ProcessingOptions) -> None: move(final_with_video_name, options.output_file) else: # Final cut is not any more the final step. - temporaries.append(final_with_video) + temporaries.add(final_with_video) duration = get_movie_duration(options.tools_paths['ffprobe'], final_with_video) supported_langs = get_tesseract_supported_lang(options.tools_paths['tesseract']) logger.info('Supported lang: %s', supported_langs) @@ -238,8 +236,8 @@ def process_recording(options: ProcessingOptions) -> None: except OSError: raise TemporaryFileError(f"Impossible to open {sub_name}") - temporaries.append(idx) - temporaries.append(sub) + temporaries.add(idx) + temporaries.add(sub) ocr = do_ocr(options.tools_paths['vobsubocr'], list_of_subtitles, duration, temporaries, options.dump_memory) @@ -253,14 +251,6 @@ def process_recording(options: ProcessingOptions) -> None: else: move(final_with_video_name, options.output_file) - if not options.keep_temporaries: - logger.info("Cleaning temporary files") - for tmp in temporaries: - path = os.path.realpath(tmp.name) - logger.info("Removing: %s", path) - tmp.close() - unlink(path) - d = datetime(1,1,1) for c in checks: logger.info("Please check cut smoothness at %s", (c+d).strftime("%H:%M:%S")) diff --git a/src/tscut/subtitles/ocr.py b/src/tscut/subtitles/ocr.py index f80e9b1..b5ff59b 100644 --- a/src/tscut/subtitles/ocr.py +++ b/src/tscut/subtitles/ocr.py @@ -17,7 +17,7 @@ from os import ( write, ) from subprocess import PIPE, Popen -from typing import IO, Any, Sequence +from typing import Sequence from iso639 import Lang from iso639.exceptions import InvalidLanguageValue @@ -25,6 +25,7 @@ from tqdm import tqdm from typeguard import typechecked from tscut.exceptions import ExternalToolError +from tscut.temporaries import TemporaryFiles logger = logging.getLogger(__name__) @@ -130,7 +131,7 @@ def extract_srt(mkvextract:str, filename:str, subtitles:dict[str, list[int]], @typechecked def do_ocr(vobsubocr:str, idxs: Sequence[tuple[str,str,str,str]], duration:timedelta, - temporaries:list[IO[Any]], dump_mem_fd:bool=False): + temporaries:TemporaryFiles, dump_mem_fd:bool=False): res = [] for idx_name, _, lang, iso in idxs: @@ -177,7 +178,7 @@ def do_ocr(vobsubocr:str, idxs: Sequence[tuple[str,str,str,str]], duration:timed pos = 0 while pos < srt_length: pos+=write(outfd, buf[pos:]) - temporaries.append(dump_srt) + temporaries.add(dump_srt) except OSError: logger.error('Impossible to create file: %s', srtname) return None diff --git a/src/tscut/temporaries.py b/src/tscut/temporaries.py new file mode 100644 index 0000000..18d13ee --- /dev/null +++ b/src/tscut/temporaries.py @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2026 Frédéric Tronel + +"""A class to manage (and clean) all temporary files created during conversion""" + +import logging +import os +from typing import IO, Any + +logger = logging.getLogger(__name__) + +class TemporaryFiles: + def __init__(self, keep: bool = False) -> None: + self._files: list[IO[Any]] = [] + self._keep = keep + + def add(self, file: IO[Any]) -> None: + self._files.append(file) + + def cleanup(self) -> None: + if self._keep: + return + + for f in self._files: + path = os.path.realpath(f.name) + logger.info("Removing: %s", path) + f.close() + os.unlink(path)