437 lines
16 KiB
Python
437 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
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 write_config(self, data: dict) -> None:
|
|
config_dir = self.home_dir / ".local" / "etc"
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
(config_dir / "ffx.json").write_text(json.dumps(data), encoding="utf-8")
|
|
|
|
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_no_pattern_stream_remove_list_clears_copied_stream_metadata(self):
|
|
source_filename = "remove_tags_s01e01.mkv"
|
|
self.write_config(
|
|
{
|
|
"metadata": {
|
|
"streams": {
|
|
"remove": ["BPS"],
|
|
}
|
|
}
|
|
}
|
|
)
|
|
source_path = create_source_fixture(
|
|
self.workdir,
|
|
source_filename,
|
|
[
|
|
SourceTrackSpec(
|
|
TrackType.VIDEO,
|
|
identity="video-0",
|
|
extra_tags={"BPS": "remove-me", "KEEP_ME": "video-keep"},
|
|
),
|
|
SourceTrackSpec(
|
|
TrackType.AUDIO,
|
|
identity="audio-1",
|
|
language="eng",
|
|
title="Main Audio",
|
|
extra_tags={"BPS": "remove-me", "KEEP_ME": "audio-keep"},
|
|
),
|
|
],
|
|
)
|
|
|
|
completed = run_ffx_convert(
|
|
self.workdir,
|
|
self.home_dir,
|
|
self.database_path,
|
|
"--video-encoder",
|
|
"copy",
|
|
"--no-pattern",
|
|
"--no-tmdb",
|
|
"--no-prompt",
|
|
"--no-signature",
|
|
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"],
|
|
)
|
|
self.assertEqual(get_tag(streams[0], "THIS_IS"), "video-0")
|
|
self.assertEqual(get_tag(streams[0], "KEEP_ME"), "video-keep")
|
|
self.assertIsNone(get_tag(streams[0], "BPS"))
|
|
self.assertEqual(get_tag(streams[1], "THIS_IS"), "audio-1")
|
|
self.assertEqual(get_tag(streams[1], "KEEP_ME"), "audio-keep")
|
|
self.assertIsNone(get_tag(streams[1], "BPS"))
|
|
|
|
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"
|
|
self.write_config(
|
|
{
|
|
"metadata": {
|
|
"streams": {
|
|
"remove": ["BPS"],
|
|
}
|
|
}
|
|
}
|
|
)
|
|
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",
|
|
extra_tags={"BPS": "remove-me", "EXTERNAL_KEEP": "keep-me"},
|
|
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")
|
|
self.assertEqual(get_tag(subtitle_stream, "EXTERNAL_KEEP"), "keep-me")
|
|
self.assertIsNone(get_tag(subtitle_stream, "BPS"))
|
|
|
|
extracted_subtitle = extract_first_subtitle_text(self.workdir, output_path)
|
|
self.assertIn("external subtitle payload", extracted_subtitle)
|
|
self.assertNotIn("embedded subtitle payload", extracted_subtitle)
|
|
|
|
def test_subtitle_prefix_uses_configured_base_directory_when_directory_is_omitted(self):
|
|
source_filename = "substitute_default_s01e01.mkv"
|
|
subtitle_prefix = "substitute_default"
|
|
subtitles_base_dir = self.home_dir / ".local" / "var" / "sync" / "subtitles"
|
|
resolved_subtitle_dir = subtitles_base_dir / subtitle_prefix
|
|
resolved_subtitle_dir.mkdir(parents=True, exist_ok=True)
|
|
self.write_config(
|
|
{
|
|
"subtitlesDirectory": "~/.local/var/sync/subtitles",
|
|
"metadata": {
|
|
"streams": {
|
|
"remove": ["BPS"],
|
|
}
|
|
}
|
|
}
|
|
)
|
|
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",
|
|
extra_tags={"BPS": "remove-me", "EXTERNAL_KEEP": "keep-me"},
|
|
subtitle_lines=("embedded subtitle payload",),
|
|
),
|
|
],
|
|
)
|
|
|
|
write_vtt(
|
|
resolved_subtitle_dir / f"{subtitle_prefix}_s01e01_2_deu.vtt",
|
|
("external subtitle payload",),
|
|
)
|
|
|
|
prepare_pattern_database(
|
|
self.database_path,
|
|
r"^substitute_default_(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-prefix",
|
|
subtitle_prefix,
|
|
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")
|
|
self.assertEqual(get_tag(subtitle_stream, "EXTERNAL_KEEP"), "keep-me")
|
|
self.assertIsNone(get_tag(subtitle_stream, "BPS"))
|
|
|
|
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()
|