Add a dedicated class for temporary files management.

This commit is contained in:
Frédéric Tronel
2026-08-31 11:12:47 +02:00
parent d1df44c82c
commit 5b2afd29b3
7 changed files with 62 additions and 36 deletions
+6 -1
View File
@@ -15,6 +15,7 @@ import coloredlogs
from tscut.exceptions import TSCutError from tscut.exceptions import TSCutError
from tscut.models import ProcessingOptions from tscut.models import ProcessingOptions
from tscut.pipeline import process_recording from tscut.pipeline import process_recording
from tscut.temporaries import TemporaryFiles
from tscut.tools.discovery import check_required_tools from tscut.tools.discovery import check_required_tools
from tscut.tools.timeframe import compare_time_interval, parse_time_interval from tscut.tools.timeframe import compare_time_interval, parse_time_interval
@@ -110,9 +111,13 @@ def main() -> int:
logger.debug("Configuration: %s", config) logger.debug("Configuration: %s", config)
try: try:
process_recording(config) temporaries = TemporaryFiles(config.keep_temporaries)
process_recording(config, temporaries)
except TSCutError as exc: except TSCutError as exc:
logger.error("%s", exc) logger.error("%s", exc)
return 1 return 1
finally:
logger.info("Cleaning temporary files")
temporaries.cleanup()
return 0 return 0
+2 -2
View File
@@ -5,8 +5,8 @@
import logging import logging
from typing import IO from typing import IO
from tscut.tools.mkvtoolnix import extract_mkv_part
from tscut.exceptions import TemporaryFileError from tscut.exceptions import TemporaryFileError
from tscut.tools.mkvtoolnix import extract_mkv_part
logger = logging.getLogger(__name__) 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, extract_mkv_part(mkvmerge_path=mkvmerge_path, input_file=input_file, output_file=internal_mkv,
begin=begin, end=end) begin=begin, end=end)
temporaries.append(internal_mkv) temporaries.add(internal_mkv)
+11 -10
View File
@@ -12,7 +12,7 @@ from os import (
write, write,
) )
from shutil import copyfile from shutil import copyfile
from typing import IO, Any, BinaryIO, Sequence, TextIO from typing import BinaryIO, Sequence, TextIO
import hexdump import hexdump
from tqdm import tqdm 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.h264.avc import get_avc_config_from_h264
from tscut.matroska.codec import dump_codec_private_data from tscut.matroska.codec import dump_codec_private_data
from tscut.models import CutResult, PreparedMedia, ProcessingOptions from tscut.models import CutResult, PreparedMedia, ProcessingOptions
from tscut.temporaries import TemporaryFiles
from tscut.tools.ffmpeg import extract_all_streams from tscut.tools.ffmpeg import extract_all_streams
from tscut.tools.ffprobe import ( from tscut.tools.ffprobe import (
get_nearest_iframe, get_nearest_iframe,
@@ -94,7 +95,7 @@ def concatenate_h264_ts_parts(h264_ts_parts: Sequence[TextIO], output: TextIO) -
first = False first = False
def cut_recording(media: PreparedMedia, options: ProcessingOptions, def cut_recording(media: PreparedMedia, options: ProcessingOptions,
temporaries: list[IO[Any]]) -> CutResult: temporaries: TemporaryFiles) -> CutResult:
# Pour chaque portion # Pour chaque portion
partnum = 0 partnum = 0
@@ -243,10 +244,10 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions,
remove_video_tracks_from_mkv(mkvmerge_path=options.tools_paths['mkvmerge'], remove_video_tracks_from_mkv(mkvmerge_path=options.tools_paths['mkvmerge'],
input_file=internal_mkv, output_file=internal_novideo_mkv) input_file=internal_mkv, output_file=internal_novideo_mkv)
temporaries.append(internal_mkv) temporaries.add(internal_mkv)
temporaries.append(internal_h264) temporaries.add(internal_h264)
temporaries.append(internal_h264_ts) temporaries.add(internal_h264_ts)
temporaries.append(internal_novideo_mkv) temporaries.add(internal_novideo_mkv)
h264parts.append(internal_h264) h264parts.append(internal_h264)
h264_ts.append(internal_h264_ts) 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, part = merge_mkvs(mkvmerge_path=options.tools_paths['mkvmerge'], inputs=subparts,
output_name=f'part-{partnum:d}.mkv', concatenate=True) output_name=f'part-{partnum:d}.mkv', concatenate=True)
mkvparts.append(part) mkvparts.append(part)
temporaries.append(part) temporaries.add(part)
pos = pos+tail_iframe_ts-ts1 pos = pos+tail_iframe_ts-ts1
@@ -301,7 +302,7 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions,
logger.info('Merging all H264 tracks') logger.info('Merging all H264 tracks')
concatenate_h264_parts(h264parts=h264parts, output=full_h264) concatenate_h264_parts(h264parts=h264parts, output=full_h264)
temporaries.append(full_h264) temporaries.add(full_h264)
try: try:
full_h264_ts = open(f'{media.basename}-ts.txt', 'w+', encoding='utf8') 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') logger.info('Merging H264 timestamps')
concatenate_h264_ts_parts(h264_ts_parts=h264_ts, output=full_h264_ts) 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_novideo_name = f'{media.basename}-novideo.mkv'
final_with_video_name = f'{media.basename}-video.mkv' final_with_video_name = f'{media.basename}-video.mkv'
@@ -331,7 +332,7 @@ def cut_recording(media: PreparedMedia, options: ProcessingOptions,
except OSError: except OSError:
raise TemporaryFileError(f"Impossible to open file: {final_novideo_name}") raise TemporaryFileError(f"Impossible to open file: {final_novideo_name}")
temporaries.append(final_novideo) temporaries.add(final_novideo)
full_h264_ts.seek(0) full_h264_ts.seek(0)
+1 -1
View File
@@ -57,5 +57,5 @@ class PreparedMedia:
@dataclass @dataclass
class CutResult: class CutResult:
filename: str filename: str
movie: BinaryIO movie: IO[bytes]
check_positions: list[timedelta] check_positions: list[timedelta]
+9 -19
View File
@@ -5,9 +5,8 @@
import logging import logging
import os import os
from datetime import datetime, timedelta from datetime import datetime, timedelta
from os import unlink
from shutil import copyfile, move from shutil import copyfile, move
from typing import IO, Any, BinaryIO from typing import BinaryIO
import hexdump 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.matroska.codec import dump_codec_private_data
from tscut.models import PreparedMedia, ProcessingOptions, SupportedFormat from tscut.models import PreparedMedia, ProcessingOptions, SupportedFormat
from tscut.subtitles.ocr import do_ocr, extract_srt, get_tesseract_supported_lang 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.ffmpeg import ffmpeg_convert
from tscut.tools.ffprobe import ( from tscut.tools.ffprobe import (
find_subtitles_tracks, find_subtitles_tracks,
@@ -42,10 +42,8 @@ def detect_supported_format(format_name: str) -> SupportedFormat:
f"Unsupported media format: {format_name}" 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) nb_parts = len(options.parts)
temporaries : list[IO[Any]] = []
basename = os.path.splitext(os.path.basename(options.input_file))[0] basename = os.path.splitext(os.path.basename(options.input_file))[0]
mp4filename = basename+'.mp4' mp4filename = basename+'.mp4'
mkvfilename = basename+'.mkv' mkvfilename = basename+'.mkv'
@@ -88,7 +86,7 @@ def process_recording(options: ProcessingOptions) -> None:
with open(mp4filename, 'wb+') as mp4: with open(mp4filename, 'wb+') as mp4:
ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'], ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'],
input_file, 'mpegts', mp4, 'mp4', duration) input_file, 'mpegts', mp4, 'mp4', duration)
temporaries.append(mp4) temporaries.add(mp4)
logger.info("Converting MP4 to MKV.") logger.info("Converting MP4 to MKV.")
try: try:
mkv = open(mkvfilename, 'wb+') mkv = open(mkvfilename, 'wb+')
@@ -98,7 +96,7 @@ def process_recording(options: ProcessingOptions) -> None:
ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'], ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'],
mp4, 'mp4', mkv, 'matroska', duration) mp4, 'mp4', mkv, 'matroska', duration)
if nb_parts > 0: if nb_parts > 0:
temporaries.append(mkv) temporaries.add(mkv)
except OSError: except OSError:
raise TemporaryFileError(f"Impossible to create: {mp4filename}") 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'], ffmpeg_convert(options.tools_paths['ffmpeg'], options.tools_paths['ffprobe'],
input_file, 'mp4', mkv, 'matroska', duration) input_file, 'mp4', mkv, 'matroska', duration)
if nb_parts > 0: if nb_parts > 0:
temporaries.append(mkv) temporaries.add(mkv)
else: else:
logger.info("Already in MKV") logger.info("Already in MKV")
mkv = input_file mkv = input_file
@@ -198,7 +196,7 @@ def process_recording(options: ProcessingOptions) -> None:
move(final_with_video_name, options.output_file) move(final_with_video_name, options.output_file)
else: else:
# Final cut is not any more the final step. # 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) duration = get_movie_duration(options.tools_paths['ffprobe'], final_with_video)
supported_langs = get_tesseract_supported_lang(options.tools_paths['tesseract']) supported_langs = get_tesseract_supported_lang(options.tools_paths['tesseract'])
logger.info('Supported lang: %s', supported_langs) logger.info('Supported lang: %s', supported_langs)
@@ -238,8 +236,8 @@ def process_recording(options: ProcessingOptions) -> None:
except OSError: except OSError:
raise TemporaryFileError(f"Impossible to open {sub_name}") raise TemporaryFileError(f"Impossible to open {sub_name}")
temporaries.append(idx) temporaries.add(idx)
temporaries.append(sub) temporaries.add(sub)
ocr = do_ocr(options.tools_paths['vobsubocr'], list_of_subtitles, duration, ocr = do_ocr(options.tools_paths['vobsubocr'], list_of_subtitles, duration,
temporaries, options.dump_memory) temporaries, options.dump_memory)
@@ -253,14 +251,6 @@ def process_recording(options: ProcessingOptions) -> None:
else: else:
move(final_with_video_name, options.output_file) 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) d = datetime(1,1,1)
for c in checks: for c in checks:
logger.info("Please check cut smoothness at %s", (c+d).strftime("%H:%M:%S")) logger.info("Please check cut smoothness at %s", (c+d).strftime("%H:%M:%S"))
+4 -3
View File
@@ -17,7 +17,7 @@ from os import (
write, write,
) )
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from typing import IO, Any, Sequence from typing import Sequence
from iso639 import Lang from iso639 import Lang
from iso639.exceptions import InvalidLanguageValue from iso639.exceptions import InvalidLanguageValue
@@ -25,6 +25,7 @@ from tqdm import tqdm
from typeguard import typechecked from typeguard import typechecked
from tscut.exceptions import ExternalToolError from tscut.exceptions import ExternalToolError
from tscut.temporaries import TemporaryFiles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -130,7 +131,7 @@ def extract_srt(mkvextract:str, filename:str, subtitles:dict[str, list[int]],
@typechecked @typechecked
def do_ocr(vobsubocr:str, idxs: Sequence[tuple[str,str,str,str]], duration:timedelta, 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 = [] res = []
for idx_name, _, lang, iso in idxs: 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 pos = 0
while pos < srt_length: while pos < srt_length:
pos+=write(outfd, buf[pos:]) pos+=write(outfd, buf[pos:])
temporaries.append(dump_srt) temporaries.add(dump_srt)
except OSError: except OSError:
logger.error('Impossible to create file: %s', srtname) logger.error('Impossible to create file: %s', srtname)
return None return None
+29
View File
@@ -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)