from __future__ import annotations from pathlib import Path import tempfile import unittest from tests.support.ffx_bundle import ( PatternTrackSpec, SourceTrackSpec, create_source_fixture, expected_output_path, extract_first_subtitle_text, ffprobe_json, get_tag, prepare_pattern_database, run_ffx_convert, write_vtt, ) from ffx.track_type import TrackType try: import pytest except ImportError: # pragma: no cover - unittest-only environments pytest = None if pytest is not None: pytestmark = [pytest.mark.integration, pytest.mark.subtrack_mapping] class SubtrackMappingBundleTests(unittest.TestCase): def setUp(self): self.tempdir = tempfile.TemporaryDirectory() self.workdir = Path(self.tempdir.name) self.home_dir = self.workdir / "home" self.home_dir.mkdir() self.database_path = self.workdir / "test.db" def tearDown(self): self.tempdir.cleanup() def assertCompleted(self, completed): if completed.returncode != 0: self.fail( "FFX convert failed\n" f"STDOUT:\n{completed.stdout}\n" f"STDERR:\n{completed.stderr}" ) def test_pattern_reorders_and_omits_tracks_preserving_metadata_and_group_order(self): source_filename = "reorder_s01e01.mkv" source_path = create_source_fixture( self.workdir, source_filename, [ SourceTrackSpec(TrackType.VIDEO, identity="video-0", title="Video Zero"), SourceTrackSpec( TrackType.SUBTITLE, identity="subtitle-1", language="eng", title="First Subtitle", subtitle_lines=("first embedded subtitle",), ), SourceTrackSpec( TrackType.AUDIO, identity="audio-2", language="deu", title="German Audio", ), SourceTrackSpec( TrackType.SUBTITLE, identity="subtitle-3", language="fra", title="Second Subtitle", subtitle_lines=("second embedded subtitle",), ), SourceTrackSpec(TrackType.ATTACHMENT, attachment_name="ordered.ttf"), ], ) prepare_pattern_database( self.database_path, r"^reorder_(s[0-9]+e[0-9]+)\.mkv$", [ PatternTrackSpec( index=0, source_index=0, track_type=TrackType.VIDEO, tags={"THIS_IS": "video-0", "title": "Video Zero"}, ), PatternTrackSpec( index=1, source_index=2, track_type=TrackType.AUDIO, tags={"THIS_IS": "audio-2", "language": "deu", "title": "German Audio"}, ), PatternTrackSpec( index=2, source_index=1, track_type=TrackType.SUBTITLE, tags={"THIS_IS": "subtitle-1", "language": "eng", "title": "First Subtitle"}, ), ], ) completed = run_ffx_convert( self.workdir, self.home_dir, self.database_path, "--video-encoder", "copy", "--no-tmdb", "--no-prompt", "--no-signature", str(source_path), ) self.assertCompleted(completed) output_path = expected_output_path(self.workdir, source_filename) self.assertTrue(output_path.is_file(), output_path) streams = ffprobe_json(output_path)["streams"] self.assertEqual( [stream["codec_type"] for stream in streams], ["video", "audio", "subtitle", "attachment"], ) self.assertEqual( [get_tag(streams[index], "THIS_IS") for index in range(3)], ["video-0", "audio-2", "subtitle-1"], ) self.assertNotIn( "subtitle-3", [get_tag(stream, "THIS_IS") for stream in streams if stream["codec_type"] != "attachment"], ) self.assertEqual(streams[-1]["codec_name"], "ttf") extracted_subtitle = extract_first_subtitle_text(self.workdir, output_path) self.assertIn("first embedded subtitle", extracted_subtitle) self.assertNotIn("second embedded subtitle", extracted_subtitle) def test_cli_rearrange_streams_reorders_tracks_without_database_pattern(self): source_filename = "cli_s01e01.mkv" source_path = create_source_fixture( self.workdir, source_filename, [ SourceTrackSpec(TrackType.VIDEO, identity="video-0"), SourceTrackSpec(TrackType.AUDIO, identity="audio-1", language="eng", title="First Audio"), SourceTrackSpec(TrackType.AUDIO, identity="audio-2", language="deu", title="Second Audio"), SourceTrackSpec(TrackType.SUBTITLE, identity="subtitle-3", language="eng", title="Subtitle"), ], ) completed = run_ffx_convert( self.workdir, self.home_dir, self.database_path, "--video-encoder", "copy", "--no-pattern", "--no-tmdb", "--no-prompt", "--no-signature", "--rearrange-streams", "0,2,1,3", str(source_path), ) self.assertCompleted(completed) output_path = expected_output_path(self.workdir, source_filename) streams = ffprobe_json(output_path)["streams"] self.assertEqual( [stream["codec_type"] for stream in streams], ["video", "audio", "audio", "subtitle"], ) self.assertEqual( [get_tag(stream, "THIS_IS") for stream in streams], ["video-0", "audio-2", "audio-1", "subtitle-3"], ) def test_pattern_validation_fails_for_nonexistent_source_track_reference(self): source_filename = "invalid_s01e01.mkv" source_path = create_source_fixture( self.workdir, source_filename, [ SourceTrackSpec(TrackType.VIDEO, identity="video-0"), SourceTrackSpec(TrackType.AUDIO, identity="audio-1"), SourceTrackSpec(TrackType.SUBTITLE, identity="subtitle-2"), ], ) prepare_pattern_database( self.database_path, r"^invalid_(s[0-9]+e[0-9]+)\.mkv$", [ PatternTrackSpec(index=0, source_index=0, track_type=TrackType.VIDEO), PatternTrackSpec(index=1, source_index=99, track_type=TrackType.SUBTITLE), ], ) completed = run_ffx_convert( self.workdir, self.home_dir, self.database_path, "--video-encoder", "copy", "--no-tmdb", "--no-prompt", "--no-signature", str(source_path), ) self.assertNotEqual(completed.returncode, 0) error_output = f"{completed.stdout}\n{completed.stderr}" self.assertIn("non-existent source track #99", error_output) self.assertFalse(expected_output_path(self.workdir, source_filename).exists()) def test_external_subtitle_file_replaces_payload_and_overrides_metadata(self): source_filename = "substitute_s01e01.mkv" source_path = create_source_fixture( self.workdir, source_filename, [ SourceTrackSpec(TrackType.VIDEO, identity="video-0"), SourceTrackSpec(TrackType.AUDIO, identity="audio-1", language="eng", title="Main Audio"), SourceTrackSpec( TrackType.SUBTITLE, identity="embedded-subtitle", language="eng", title="Embedded Title", subtitle_lines=("embedded subtitle payload",), ), ], ) write_vtt( self.workdir / "substitute_s01e01_2_deu.vtt", ("external subtitle payload",), ) prepare_pattern_database( self.database_path, r"^substitute_(s[0-9]+e[0-9]+)\.mkv$", [ PatternTrackSpec(index=0, source_index=0, track_type=TrackType.VIDEO), PatternTrackSpec(index=1, source_index=1, track_type=TrackType.AUDIO), PatternTrackSpec(index=2, source_index=2, track_type=TrackType.SUBTITLE), ], ) completed = run_ffx_convert( self.workdir, self.home_dir, self.database_path, "--video-encoder", "copy", "--no-tmdb", "--no-prompt", "--no-signature", "--subtitle-directory", str(self.workdir), "--subtitle-prefix", "substitute", str(source_path), ) self.assertCompleted(completed) output_path = expected_output_path(self.workdir, source_filename) streams = ffprobe_json(output_path)["streams"] subtitle_stream = [stream for stream in streams if stream["codec_type"] == "subtitle"][0] self.assertEqual(get_tag(subtitle_stream, "language"), "deu") self.assertEqual(get_tag(subtitle_stream, "title"), "Embedded Title") self.assertEqual(get_tag(subtitle_stream, "THIS_IS"), "embedded-subtitle") extracted_subtitle = extract_first_subtitle_text(self.workdir, output_path) self.assertIn("external subtitle payload", extracted_subtitle) self.assertNotIn("embedded subtitle payload", extracted_subtitle) if __name__ == "__main__": unittest.main()