from __future__ import annotations import json import logging from pathlib import Path import sys from types import SimpleNamespace import unittest from unittest.mock import patch SRC_ROOT = Path(__file__).resolve().parents[2] / "src" if str(SRC_ROOT) not in sys.path: sys.path.insert(0, str(SRC_ROOT)) class StaticConfig: def getData(self): return {} class DummyPatternController: def __init__(self, context): self.context = context def matchFilename(self, filename): return {} def make_logger(name: str) -> logging.Logger: logger = logging.getLogger(name) logger.handlers = [] logger.setLevel(logging.DEBUG) logger.propagate = False logger.addHandler(logging.NullHandler()) return logger class FilePropertiesProbeTests(unittest.TestCase): def import_module(self): try: import ffx.file_properties as file_properties_module except ModuleNotFoundError as ex: if ex.name == "sqlalchemy": self.skipTest("sqlalchemy is not installed in this environment") raise return file_properties_module def make_context(self): return { "logger": make_logger("ffx-test-file-properties-probe"), "config": StaticConfig(), "database": {"session": object()}, "use_pattern": False, } def sample_probe_data(self): return { "format": { "filename": "/tmp/example_s01e01.mkv", "nb_streams": 2, "format_name": "matroska,webm", }, "streams": [ { "index": 0, "codec_name": "h264", "codec_type": "video", "disposition": {"default": 1}, "tags": {}, }, { "index": 1, "codec_name": "aac", "codec_type": "audio", "channel_layout": "stereo", "channels": 2, "disposition": {"default": 0}, "tags": {"language": "eng"}, }, ], } def test_format_and_stream_accessors_share_one_combined_probe(self): file_properties_module = self.import_module() probe_output = self.sample_probe_data() with ( patch.object(file_properties_module, "PatternController", DummyPatternController), patch.object( file_properties_module, "executeProcess", return_value=(json.dumps(probe_output), "", 0), ) as mocked_execute, ): file_properties = file_properties_module.FileProperties( self.make_context(), "/tmp/example_s01e01.mkv", ) self.assertEqual(probe_output["format"], file_properties.getFormatData()) self.assertEqual(probe_output["streams"], file_properties.getStreamData()) mocked_execute.assert_called_once_with( file_properties_module.FileProperties.FFPROBE_COMMAND_TOKENS + ["/tmp/example_s01e01.mkv"] ) def test_cropdetect_uses_configured_window_and_caches_results(self): file_properties_module = self.import_module() file_properties_module.FileProperties._clear_cropdetect_cache() cropdetect_stderr = "\n".join( [ "[Parsed_cropdetect_0] crop=1440:1080:240:0", "[Parsed_cropdetect_0] crop=1440:1080:240:0", "[Parsed_cropdetect_0] crop=1438:1080:242:0", ] ) context = self.make_context() context["cropdetect"] = {"seek_seconds": 15, "duration_seconds": 45} with ( patch.object( file_properties_module.os, "stat", return_value=SimpleNamespace(st_mtime_ns=1234, st_size=5678), ), patch.object(file_properties_module, "PatternController", DummyPatternController), patch.object( file_properties_module, "executeProcess", return_value=("", cropdetect_stderr, 0), ) as mocked_execute, ): file_properties = file_properties_module.FileProperties( context, "/tmp/example_s01e01.mkv", ) first = file_properties.findCropArguments() second = file_properties.findCropArguments() self.assertEqual(first, second) self.assertEqual( { "output_width": "1440", "output_height": "1080", "x_offset": "240", "y_offset": "0", }, first, ) mocked_execute.assert_called_once_with( list(file_properties_module.FFMPEG_COMMAND_TOKENS) + [ "-ss", "15", "-i", "/tmp/example_s01e01.mkv", "-t", "45", "-vf", "cropdetect", ] + list(file_properties_module.FFMPEG_NULL_OUTPUT_TOKENS), context=context, ) file_properties_module.FileProperties._clear_cropdetect_cache() if __name__ == "__main__": unittest.main()