Linting and typing fixes.

This commit is contained in:
Frédéric Tronel
2026-08-29 16:07:29 +02:00
parent 294d857206
commit 0ff2e3a6dd
7 changed files with 43 additions and 77 deletions
+3 -4
View File
@@ -3,10 +3,6 @@
# Copyright (C) 2026 Frédéric Tronel # Copyright (C) 2026 Frédéric Tronel
from typing import IO
from typeguard import typechecked
class TSCutError(Exception): class TSCutError(Exception):
"""Base exception for tscut.""" """Base exception for tscut."""
@@ -21,3 +17,6 @@ class ExternalToolError(TSCutError):
class InvalidMediaError(TSCutError): class InvalidMediaError(TSCutError):
"""Raised when input media cannot be processed.""" """Raised when input media cannot be processed."""
class UnimplementedFeature(TSCutError):
"""Raised when a rare feature is encountered and not yet implemented"""
+1 -1
View File
@@ -284,7 +284,7 @@ def write_scaling_list(buf:bytearray, bit_position: int, size, matrix:list[int],
bit_position = write_signed_exp_golomb(buf, bit_position, delta) bit_position = write_signed_exp_golomb(buf, bit_position, delta)
else: else:
logger.error('Not yet implemented') logger.error('Not yet implemented')
exit(-1) raise UnimplementedFeature("Optimized scaling list of H264 header is not implemented")
# reverse = deltas.reverse() # reverse = deltas.reverse()
# compressed = False # compressed = False
# while len(reverse)>0: # while len(reverse)>0:
+20 -42
View File
@@ -3,31 +3,24 @@
# Copyright (C) 2026 Frédéric Tronel # Copyright (C) 2026 Frédéric Tronel
import logging import logging
from math import floor, ceil, log
from datetime import timedelta from datetime import timedelta
from os import (
read,
SEEK_SET,
lseek,
memfd_create,
set_inheritable,
write,
close
)
from typing import IO
from subprocess import PIPE, Popen
from io import TextIOWrapper 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 typeguard import typechecked
from tqdm import tqdm from tqdm import tqdm
from typeguard import typechecked
from tscut.exceptions import ExternalToolError, InvalidMediaError
from tscut.tools.ffprobe import ( from tscut.tools.ffprobe import (
get_frames_in_stream,
get_video_dimensions, get_video_dimensions,
with_subtitles, with_subtitles,
get_frames_in_stream,
) )
from tscut.tools.ppm import dump_ppm from tscut.tools.ppm import dump_ppm
from tscut.tools.timeframe import parse_timestamp, get_packet_duration from tscut.tools.timeframe import get_packet_duration, parse_timestamp
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -46,10 +39,7 @@ def ffmpeg_convert(ffmpeg_path:str, ffprobe_path:str, input_file: IO[bytes], inp
set_inheritable(infd, True) set_inheritable(infd, True)
set_inheritable(outfd, True) set_inheritable(outfd, True)
if logger.getEffectiveLevel() == logging.DEBUG: log_level = [] if logger.getEffectiveLevel() == logging.DEBUG else ['-loglevel', 'quiet']
log_level = []
else:
log_level = [ '-loglevel', 'quiet' ]
params = [ffmpeg_path, '-y',]+log_level+['-progress', '/dev/stdout', '-canvas_size', params = [ffmpeg_path, '-y',]+log_level+['-progress', '/dev/stdout', '-canvas_size',
f'{width:d}x{height:d}', '-f', input_format, f'{width:d}x{height:d}', '-f', input_format,
@@ -82,7 +72,7 @@ def ffmpeg_convert(ffmpeg_path:str, ffprobe_path:str, input_file: IO[bytes], inp
@typechecked @typechecked
def extract_pictures(ffmpeg_path:str, input_file:IO[bytes], begin:timedelta, nb_frames:int, def extract_pictures(ffmpeg_path:str, input_file:IO[bytes], begin:timedelta, nb_frames:int,
width:int=640, height:int=480) -> tuple[bytes,int]|tuple[None,None]: width:int=640, height:int=480) -> tuple[bytes,int]:
""" """
Extract pictures from a video file using FFmpeg. Extract pictures from a video file using FFmpeg.
@@ -126,13 +116,13 @@ def extract_pictures(ffmpeg_path:str, input_file:IO[bytes], begin:timedelta, nb_
status = ffmpeg.wait() status = ffmpeg.wait()
if status != 0: if status != 0:
logger.error('Conversion failed with status code: %d', status) logger.error('Conversion failed with status code: %d', status)
return None, None raise ExternalToolError("")
lseek(outfd, 0, SEEK_SET) lseek(outfd, 0, SEEK_SET)
images = read(outfd,length) images = read(outfd,length)
if len(images) != length: if len(images) != length:
logger.error("Received %d bytes but %d were expected.", len(images), length) logger.error("Received %d bytes but %d were expected.", len(images), length)
return None, None raise InvalidMediaError("")
lseek(outfd, 0, SEEK_SET) lseek(outfd, 0, SEEK_SET)
return images, outfd return images, outfd
@@ -141,7 +131,7 @@ def extract_pictures(ffmpeg_path:str, input_file:IO[bytes], begin:timedelta, nb_
def extract_sound(ffmpeg_path:str, input_file: IO[bytes], begin:timedelta, output_filename:str, def extract_sound(ffmpeg_path:str, input_file: IO[bytes], begin:timedelta, output_filename:str,
packet_duration:int, sub_channel:int=0, packet_duration:int, sub_channel:int=0,
nb_packets:int=0, sample_rate:int=48000, nb_packets:int=0, sample_rate:int=48000,
nb_channels:int=2) -> tuple[bytes,int]|tuple[None,None]: nb_channels:int=2) -> tuple[bytes,int]:
outfd = memfd_create(output_filename, flags=0) outfd = memfd_create(output_filename, flags=0)
infd = input_file.fileno() infd = input_file.fileno()
lseek(infd, 0, SEEK_SET) lseek(infd, 0, SEEK_SET)
@@ -160,7 +150,7 @@ def extract_sound(ffmpeg_path:str, input_file: IO[bytes], begin:timedelta, outpu
status = ffmpeg.wait() status = ffmpeg.wait()
if status != 0: if status != 0:
logger.error('Sound extraction returns error code: %d', status) logger.error('Sound extraction returns error code: %d', status)
return None, None raise ExternalToolError("")
lseek(outfd, 0, SEEK_SET) lseek(outfd, 0, SEEK_SET)
sound = read(outfd, length) sound = read(outfd, length)
@@ -169,7 +159,7 @@ def extract_sound(ffmpeg_path:str, input_file: IO[bytes], begin:timedelta, outpu
logger.info("Received %d bytes but %d were expected (channels=%d, freq=%d, packets=%d,\ 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, duration=%d ms).", len(sound), length, nb_channels, sample_rate, nb_packets,
packet_duration) packet_duration)
return None, None raise InvalidMediaError("")
return sound, outfd return sound, outfd
@@ -236,9 +226,6 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes],
images_bytes, memfd = extract_pictures(ffmpeg_path, input_file=input_file, images_bytes, memfd = extract_pictures(ffmpeg_path, input_file=input_file,
begin=begin, nb_frames=nb_frames, begin=begin, nb_frames=nb_frames,
width=width, height=height) width=width, height=height)
if images_bytes is None or memfd is None:
logger.error('Impossible to extract picture from video stream.')
exit(-1)
memfds.append(memfd) memfds.append(memfd)
if dump_mem_fd: if dump_mem_fd:
@@ -262,14 +249,10 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes],
logger.debug('Audio stream: %s', stream) logger.debug('Audio stream: %s', stream)
sample_rate = int(stream['sample_rate']) sample_rate = int(stream['sample_rate'])
nb_channels = int(stream['channels']) nb_channels = int(stream['channels'])
if 'bit_rate' in stream: bit_rate = int(stream['bit_rate']) if 'bit_rate' in stream else 128000
bit_rate = int(stream['bit_rate'])
else:
bit_rate = 128000
codec = stream['codec_name'] codec = stream['codec_name']
if 'tags' in stream: if 'tags' in stream and 'language' in stream['tags']:
if 'language' in stream['tags']: generic_codec_params.extend([f'-metadata:s:a:{audio_id:d}',
generic_codec_params.extend([f'-metadata:s:a:{audio_id:d}',
f"language={stream['tags']['language']}"]) f"language={stream['tags']['language']}"])
packets = get_frames_in_stream(ffprobe_path, input_file=input_file, begin=begin, packets = get_frames_in_stream(ffprobe_path, input_file=input_file, begin=begin,
end=end, stream_kind='a', sub_stream_id=audio_id) end=end, stream_kind='a', sub_stream_id=audio_id)
@@ -294,10 +277,6 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes],
output_filename=tmpname, output_filename=tmpname,
sample_rate=sample_rate, nb_channels=nb_channels) sample_rate=sample_rate, nb_channels=nb_channels)
if sound_bytes is None or memfd is None:
logger.error('Impossible to extract sound track')
exit(-1)
memfds.append(memfd) memfds.append(memfd)
if dump_mem_fd: if dump_mem_fd:
@@ -325,9 +304,8 @@ def extract_all_streams(ffmpeg_path:str, ffprobe_path:str, input_file:IO[bytes],
logger.info("Extracting a subtitle stream: s:%d", subtitle_id) logger.info("Extracting a subtitle stream: s:%d", subtitle_id)
codec = stream['codec_name'] codec = stream['codec_name']
generic_input_params.extend(['-i', './empty.idx']) generic_input_params.extend(['-i', './empty.idx'])
if 'tags' in stream: if 'tags' in stream and 'language' in stream['tags']:
if 'language' in stream['tags']: generic_codec_params.extend([f'-metadata:s:s:{subtitle_id:d}',
generic_codec_params.extend([f'-metadata:s:s:{subtitle_id:d}',
f"language={stream['tags']['language']}"]) f"language={stream['tags']['language']}"])
generic_codec_params.extend([f'-c:s:{subtitle_id:d}', 'copy']) generic_codec_params.extend([f'-c:s:{subtitle_id:d}', 'copy'])
subtitle_id=subtitle_id+1 subtitle_id=subtitle_id+1
+13 -21
View File
@@ -4,17 +4,17 @@
import json import json
import logging import logging
from math import floor
from datetime import timedelta
import os import os
from datetime import timedelta
from io import BytesIO
from math import floor
from os import ( from os import (
SEEK_SET, SEEK_SET,
lseek, lseek,
set_inheritable, set_inheritable,
) )
from typing import IO
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
from io import BytesIO from typing import IO
from typeguard import typechecked from typeguard import typechecked
@@ -63,9 +63,8 @@ def get_frame_rate(ffprobe_path:str, input_file: IO[bytes]) -> float|None:
out = json.load(BytesIO(out)) out = json.load(BytesIO(out))
if 'frames' in out: if 'frames' in out:
for frame in out['frames']: for frame in out['frames']:
if 'interlaced_frame' in frame: if 'interlaced_frame' in frame and frame['interlaced_frame'] == 1:
if frame['interlaced_frame'] == 1: interlaced = True
interlaced = True
if 'pts_time' in frame: if 'pts_time' in frame:
ts = float(frame['pts_time']) ts = float(frame['pts_time'])
if min_ts is None: if min_ts is None:
@@ -165,8 +164,7 @@ def get_movie_duration(ffprobe_path:str, input_file: IO[bytes]) -> timedelta|Non
out = json.load(BytesIO(out)) out = json.load(BytesIO(out))
if 'format' in out and 'duration' in out['format']: if 'format' in out and 'duration' in out['format']:
duration = floor(float(out['format']['duration'])) duration = floor(float(out['format']['duration']))
ts = timedelta(seconds=duration) return timedelta(seconds=duration)
return ts
logger.error('Impossible to retrieve duration of movie') logger.error('Impossible to retrieve duration of movie')
return None return None
@@ -327,22 +325,22 @@ def get_nearest_idr_frame(ffprobe_path: str, input_file: IO[bytes], timestamp:ti
status = ffprobe.wait() status = ffprobe.wait()
if status != 0: if status != 0:
logger.error('ffprobe failed with status code: %d', status) logger.error('ffprobe failed with status code: %d', status)
return None return
if 'frames' in frames: if 'frames' in frames:
frames = frames['frames'] frames = frames['frames']
for frame in frames: for frame in frames:
ts = get_ts_frame(frame) ts = get_ts_frame(frame)
if ts is None: if ts is None:
return None return
if tbegin <= ts <= tend: if tbegin <= ts <= tend:
idrs.append(frame) idrs.append(frame)
else: else:
logger.error('Impossible to retrieve IDR frames inside file around [%s,%s]', logger.error('Impossible to retrieve IDR frames inside file around [%s,%s]',
tbegin, tend) tbegin, tend)
return None return
return None return
@typechecked @typechecked
def get_nearest_iframe(ffprobe_path:str, input_file: IO[bytes], def get_nearest_iframe(ffprobe_path:str, input_file: IO[bytes],
@@ -356,14 +354,8 @@ def get_nearest_iframe(ffprobe_path:str, input_file: IO[bytes],
iframe = None iframe = None
while delta < delta_max: while delta < delta_max:
zero = timedelta() zero = timedelta()
if before: tbegin = timestamp - delta if before else timestamp
tbegin = timestamp-delta tend = timestamp + delta if not before else timestamp
else:
tbegin = timestamp
if not before:
tend = timestamp+delta
else:
tend = timestamp
tbegin = max(tbegin, zero) tbegin = max(tbegin, zero)
logger.debug('Looking for an iframe in [%s, %s]', tbegin, tend) logger.debug('Looking for an iframe in [%s, %s]', tbegin, tend)
+2 -2
View File
@@ -5,14 +5,14 @@
import logging import logging
import re import re
from io import BytesIO
from math import ceil, log from math import ceil, log
from os import write from os import write
from typing import IO from typing import IO
from io import BytesIO
from typeguard import typechecked from typeguard import typechecked
@typechecked @typechecked
def dump_ppm(pictures: bytes, prefix: str, temporaries: list[IO[bytes]]) -> None: def dump_ppm(pictures: bytes, prefix: str, temporaries: list[IO[bytes]]) -> None:
""" """
+2 -4
View File
@@ -65,9 +65,8 @@ def parse_timestamp(ts:str) -> timedelta|None:
if us < 0 or us > 1000000: if us < 0 or us > 1000000:
logger.error("milliseconds must be in [0,1000000[") logger.error("milliseconds must be in [0,1000000[")
return None return None
res = timedelta(hours=hour, minutes=minute, seconds=second, microseconds=us)
return res return timedelta(hours=hour, minutes=minute, seconds=second, microseconds=us)
@typechecked @typechecked
def parse_time_interval(interval: str) -> tuple[timedelta, timedelta] | tuple[None, None]: def parse_time_interval(interval: str) -> tuple[timedelta, timedelta] | tuple[None, None]:
@@ -204,8 +203,7 @@ def get_ts_frame(frame: dict) -> timedelta|None:
logger.error('Impossible to find timestamp of frame %s', frame) logger.error('Impossible to find timestamp of frame %s', frame)
return None return None
ts = timedelta(seconds=pts_time) return timedelta(seconds=pts_time)
return ts
@typechecked @typechecked
def get_packet_duration(packet: dict) -> int | None: def get_packet_duration(packet: dict) -> int | None:
+2 -3
View File
@@ -143,8 +143,7 @@ def concatenate_h264_parts(h264parts: list[IO[bytes]], output: IO[bytes]) -> Non
def concatenate_h264_ts_parts(h264_ts_parts: list[IO[bytes]], output: IO[bytes]) -> None: def concatenate_h264_ts_parts(h264_ts_parts: list[IO[bytes]], output: IO[bytes]) -> None:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
header = '# timestamp format v2\n'.encode('ascii') header = '# timestamp format v2\n'
output.write(header) output.write(header)
last = 0. last = 0.
@@ -166,7 +165,7 @@ def concatenate_h264_ts_parts(h264_ts_parts: list[IO[bytes]], output: IO[bytes])
break break
ts = offset + float(line) ts = offset + float(line)
last = max(last,ts) last = max(last,ts)
output.write(f'{ts:f}\n'.encode('ascii')) output.write(f'{ts:f}\n')
if first: if first:
first = False first = False