Prefixless subtitle sidecar files
This commit is contained in:
@@ -421,6 +421,59 @@ class SubtrackMappingBundleTests(unittest.TestCase):
|
||||
self.assertIn("external subtitle payload", extracted_subtitle)
|
||||
self.assertNotIn("embedded subtitle payload", extracted_subtitle)
|
||||
|
||||
def test_subtitle_directory_without_prefix_uses_source_basename(self):
|
||||
source_filename = "basename_substitute.mkv"
|
||||
subtitle_directory = self.workdir / "sidecars"
|
||||
subtitle_directory.mkdir()
|
||||
source_path = create_source_fixture(
|
||||
self.workdir,
|
||||
source_filename,
|
||||
[
|
||||
SourceTrackSpec(TrackType.VIDEO, identity="video-0"),
|
||||
SourceTrackSpec(TrackType.AUDIO, identity="audio-1", language="eng"),
|
||||
SourceTrackSpec(
|
||||
TrackType.SUBTITLE,
|
||||
identity="embedded-subtitle",
|
||||
language="eng",
|
||||
subtitle_lines=("embedded subtitle payload",),
|
||||
),
|
||||
],
|
||||
)
|
||||
write_vtt(
|
||||
subtitle_directory / "basename_substitute_2_deu_DEF.vtt",
|
||||
("external subtitle payload",),
|
||||
)
|
||||
|
||||
completed = run_ffx_convert(
|
||||
self.workdir,
|
||||
self.home_dir,
|
||||
self.database_path,
|
||||
"--video-encoder",
|
||||
"copy",
|
||||
"--no-pattern",
|
||||
"--no-tmdb",
|
||||
"--no-prompt",
|
||||
"--no-signature",
|
||||
"--subtitle-directory",
|
||||
str(subtitle_directory),
|
||||
str(source_path),
|
||||
)
|
||||
self.assertCompleted(completed)
|
||||
self.assertIn("matched subtitle tracks #2", completed.stdout)
|
||||
self.assertIn("Substituting subtitle stream #2", completed.stdout)
|
||||
|
||||
output_path = expected_output_path(self.workdir, source_filename)
|
||||
subtitle_stream = [
|
||||
stream
|
||||
for stream in ffprobe_json(output_path)["streams"]
|
||||
if stream["codec_type"] == "subtitle"
|
||||
][0]
|
||||
self.assertEqual("deu", get_tag(subtitle_stream, "language"))
|
||||
|
||||
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"
|
||||
|
||||
@@ -6,7 +6,9 @@ from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import click
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
||||
@@ -17,6 +19,10 @@ if str(SRC_ROOT) not in sys.path:
|
||||
|
||||
|
||||
from ffx import cli # noqa: E402
|
||||
from ffx.logging_utils import get_ffx_logger # noqa: E402
|
||||
from ffx.media_descriptor import MediaDescriptor # noqa: E402
|
||||
from ffx.track_descriptor import TrackDescriptor # noqa: E402
|
||||
from ffx.track_type import TrackType # noqa: E402
|
||||
|
||||
|
||||
class SubtitleDirectoryCliTests(unittest.TestCase):
|
||||
@@ -48,6 +54,35 @@ class SubtitleDirectoryCliTests(unittest.TestCase):
|
||||
env={**os.environ, "HOME": str(self.home_dir)},
|
||||
)
|
||||
|
||||
def make_subtitle_descriptor(self, indices=(2, 3, 4)) -> MediaDescriptor:
|
||||
return MediaDescriptor(
|
||||
context={"logger": get_ffx_logger()},
|
||||
track_descriptors=[
|
||||
TrackDescriptor(
|
||||
index=index,
|
||||
source_index=index,
|
||||
sub_index=subIndex,
|
||||
track_type=TrackType.SUBTITLE,
|
||||
)
|
||||
for subIndex, index in enumerate(indices)
|
||||
],
|
||||
)
|
||||
|
||||
def make_import_context(
|
||||
self,
|
||||
subtitleDirectory: Path,
|
||||
noPrompt: bool,
|
||||
yes: bool = False,
|
||||
) -> dict:
|
||||
return {
|
||||
"subtitle_match_source_basename": True,
|
||||
"subtitle_directory": str(subtitleDirectory),
|
||||
"subtitle_prefix": "",
|
||||
"subtitle_extension": "vtt",
|
||||
"no_prompt": noPrompt,
|
||||
"yes": yes,
|
||||
}
|
||||
|
||||
def test_subtitle_prefix_without_directory_or_default_fails(self):
|
||||
result = self.invoke_convert("--subtitle-prefix", "dball")
|
||||
|
||||
@@ -79,6 +114,143 @@ class SubtitleDirectoryCliTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(0, result.exit_code, result.output)
|
||||
|
||||
def test_explicit_directory_without_prefix_enables_basename_matching(self):
|
||||
explicitSubtitleDirectory = self.home_dir / "manual-subtitles"
|
||||
explicitSubtitleDirectory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
enabled, directory, prefix, matchBasename = cli.resolveSubtitleImportOptions(
|
||||
{},
|
||||
str(explicitSubtitleDirectory),
|
||||
"",
|
||||
)
|
||||
|
||||
self.assertTrue(enabled)
|
||||
self.assertEqual(str(explicitSubtitleDirectory), directory)
|
||||
self.assertEqual("", prefix)
|
||||
self.assertTrue(matchBasename)
|
||||
|
||||
def test_subtitle_extension_accepts_optional_leading_dot(self):
|
||||
self.assertEqual("mkv", cli.normalizeSubtitleExtension(None, None, "mkv"))
|
||||
self.assertEqual("mkv", cli.normalizeSubtitleExtension(None, None, ".mkv"))
|
||||
|
||||
def test_subtitle_extension_rejects_multiple_leading_dots(self):
|
||||
with self.assertRaises(click.BadParameter):
|
||||
cli.normalizeSubtitleExtension(None, None, "..mkv")
|
||||
|
||||
def test_complete_basename_set_does_not_prompt(self):
|
||||
subtitleDirectory = Path(__file__).resolve().parents[1] / "assets" / "subtitles"
|
||||
descriptor = self.make_subtitle_descriptor()
|
||||
context = self.make_import_context(subtitleDirectory, noPrompt=True)
|
||||
|
||||
with patch("ffx.cli.click.confirm") as mockedConfirm:
|
||||
result = cli.importExternalSubtitles(
|
||||
context,
|
||||
descriptor,
|
||||
"A2_t01",
|
||||
-1,
|
||||
-1,
|
||||
)
|
||||
|
||||
self.assertEqual([], result["missing_track_indices"])
|
||||
mockedConfirm.assert_not_called()
|
||||
|
||||
def test_incomplete_basename_set_fails_with_no_prompt(self):
|
||||
descriptor = self.make_subtitle_descriptor()
|
||||
subtitleDirectory = self.home_dir / "partial-subtitles"
|
||||
subtitleDirectory.mkdir()
|
||||
(subtitleDirectory / "episode_2_deu.vtt").write_text(
|
||||
"WEBVTT\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = self.make_import_context(subtitleDirectory, noPrompt=True)
|
||||
|
||||
with patch("ffx.cli.click.confirm") as mockedConfirm:
|
||||
with self.assertRaisesRegex(click.ClickException, "--no-prompt is set"):
|
||||
cli.importExternalSubtitles(
|
||||
context,
|
||||
descriptor,
|
||||
"episode",
|
||||
-1,
|
||||
-1,
|
||||
)
|
||||
|
||||
mockedConfirm.assert_not_called()
|
||||
|
||||
def test_incomplete_basename_set_can_be_confirmed(self):
|
||||
descriptor = self.make_subtitle_descriptor()
|
||||
subtitleDirectory = self.home_dir / "partial-subtitles"
|
||||
subtitleDirectory.mkdir()
|
||||
(subtitleDirectory / "episode_2_deu.vtt").write_text(
|
||||
"WEBVTT\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = self.make_import_context(subtitleDirectory, noPrompt=False)
|
||||
|
||||
with patch("ffx.cli.click.confirm", return_value=True) as mockedConfirm:
|
||||
result = cli.importExternalSubtitles(
|
||||
context,
|
||||
descriptor,
|
||||
"episode",
|
||||
-1,
|
||||
-1,
|
||||
)
|
||||
|
||||
self.assertEqual([3, 4], result["missing_track_indices"])
|
||||
mockedConfirm.assert_called_once()
|
||||
|
||||
def test_incomplete_basename_set_with_yes_does_not_prompt(self):
|
||||
descriptor = self.make_subtitle_descriptor()
|
||||
subtitleDirectory = self.home_dir / "partial-subtitles"
|
||||
subtitleDirectory.mkdir()
|
||||
(subtitleDirectory / "episode_2_deu.vtt").write_text(
|
||||
"WEBVTT\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = self.make_import_context(
|
||||
subtitleDirectory,
|
||||
noPrompt=False,
|
||||
yes=True,
|
||||
)
|
||||
|
||||
with patch("ffx.cli.click.confirm") as mockedConfirm:
|
||||
result = cli.importExternalSubtitles(
|
||||
context,
|
||||
descriptor,
|
||||
"episode",
|
||||
-1,
|
||||
-1,
|
||||
)
|
||||
|
||||
self.assertEqual([2], result["imported_track_indices"])
|
||||
self.assertEqual([3, 4], result["missing_track_indices"])
|
||||
mockedConfirm.assert_not_called()
|
||||
|
||||
def test_yes_takes_precedence_over_no_prompt_for_incomplete_set(self):
|
||||
descriptor = self.make_subtitle_descriptor()
|
||||
subtitleDirectory = self.home_dir / "partial-subtitles"
|
||||
subtitleDirectory.mkdir()
|
||||
(subtitleDirectory / "episode_2_deu.vtt").write_text(
|
||||
"WEBVTT\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
context = self.make_import_context(
|
||||
subtitleDirectory,
|
||||
noPrompt=True,
|
||||
yes=True,
|
||||
)
|
||||
|
||||
with patch("ffx.cli.click.confirm") as mockedConfirm:
|
||||
result = cli.importExternalSubtitles(
|
||||
context,
|
||||
descriptor,
|
||||
"episode",
|
||||
-1,
|
||||
-1,
|
||||
)
|
||||
|
||||
self.assertEqual([3, 4], result["missing_track_indices"])
|
||||
mockedConfirm.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -7,6 +7,7 @@ import unittest
|
||||
|
||||
|
||||
SRC_ROOT = Path(__file__).resolve().parents[2] / "src"
|
||||
ASSETS_ROOT = Path(__file__).resolve().parents[1] / "assets"
|
||||
|
||||
if str(SRC_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_ROOT))
|
||||
@@ -20,18 +21,19 @@ from ffx.track_type import TrackType # noqa: E402
|
||||
|
||||
|
||||
class MediaDescriptorImportSubtitlesTests(unittest.TestCase):
|
||||
def make_descriptor(self) -> MediaDescriptor:
|
||||
def make_descriptor(self, indices=(3,)) -> MediaDescriptor:
|
||||
return MediaDescriptor(
|
||||
context={"logger": get_ffx_logger()},
|
||||
track_descriptors=[
|
||||
TrackDescriptor(
|
||||
index=3,
|
||||
source_index=3,
|
||||
sub_index=0,
|
||||
index=index,
|
||||
source_index=index,
|
||||
sub_index=subIndex,
|
||||
track_type=TrackType.SUBTITLE,
|
||||
tags={"language": "eng", "title": "DB Subtitle"},
|
||||
disposition_set={TrackDisposition.DEFAULT},
|
||||
)
|
||||
for subIndex, index in enumerate(indices)
|
||||
],
|
||||
)
|
||||
|
||||
@@ -74,6 +76,110 @@ class MediaDescriptorImportSubtitlesTests(unittest.TestCase):
|
||||
self.assertEqual("deu", track.getTags()["language"])
|
||||
self.assertEqual({TrackDisposition.FORCED}, track.getDispositionSet())
|
||||
|
||||
def test_strict_basename_import_recognizes_vtt_asset_set(self):
|
||||
descriptor = self.make_descriptor(indices=(2, 3, 4))
|
||||
|
||||
result = descriptor.importSubtitles(
|
||||
str(ASSETS_ROOT / "subtitles"),
|
||||
"A2_t01",
|
||||
strict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(3, result["candidate_count"])
|
||||
self.assertEqual([2, 3, 4], result["imported_track_indices"])
|
||||
self.assertEqual([], result["missing_track_indices"])
|
||||
self.assertEqual(
|
||||
[
|
||||
"A2_t01_2_deu_DEF.vtt",
|
||||
"A2_t01_3_eng.vtt",
|
||||
"A2_t01_4_eng.vtt",
|
||||
],
|
||||
[
|
||||
Path(track.getExternalSourceFilePath()).name
|
||||
for track in descriptor.getSubtitleTracks()
|
||||
],
|
||||
)
|
||||
|
||||
def test_strict_basename_import_accepts_dotted_mkv_extension(self):
|
||||
descriptor = self.make_descriptor(indices=(2, 3, 4))
|
||||
|
||||
result = descriptor.importSubtitles(
|
||||
str(ASSETS_ROOT / "subtitles"),
|
||||
"A2_t01",
|
||||
extension=".mkv",
|
||||
strict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(3, result["candidate_count"])
|
||||
self.assertEqual([2, 3, 4], result["imported_track_indices"])
|
||||
self.assertEqual([], result["missing_track_indices"])
|
||||
self.assertTrue(
|
||||
all(
|
||||
track.getExternalSourceFilePath().endswith(".mkv")
|
||||
for track in descriptor.getSubtitleTracks()
|
||||
)
|
||||
)
|
||||
|
||||
def test_strict_basename_import_reports_missing_tracks(self):
|
||||
descriptor = self.make_descriptor(indices=(2, 3, 4))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sidecarPath = Path(tmpdir) / "episode_2_deu.vtt"
|
||||
sidecarPath.write_text("WEBVTT\n\n", encoding="utf-8")
|
||||
|
||||
result = descriptor.importSubtitles(
|
||||
tmpdir,
|
||||
"episode",
|
||||
strict=True,
|
||||
)
|
||||
|
||||
self.assertEqual([2], result["imported_track_indices"])
|
||||
self.assertEqual([3, 4], result["missing_track_indices"])
|
||||
|
||||
def test_strict_basename_import_rejects_too_many_files(self):
|
||||
descriptor = self.make_descriptor(indices=(2,))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
for filename in ("episode_2_deu.vtt", "episode_3_eng.vtt"):
|
||||
(Path(tmpdir) / filename).write_text("WEBVTT\n\n", encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "2 matching .* for 1 subtitle tracks"):
|
||||
descriptor.importSubtitles(tmpdir, "episode", strict=True)
|
||||
|
||||
def test_strict_basename_import_rejects_unknown_track_index(self):
|
||||
descriptor = self.make_descriptor(indices=(2, 3, 4))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
(Path(tmpdir) / "episode_9_eng.vtt").write_text(
|
||||
"WEBVTT\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "track index pattern does not match"):
|
||||
descriptor.importSubtitles(tmpdir, "episode", strict=True)
|
||||
|
||||
def test_strict_basename_import_rejects_malformed_filtered_filename(self):
|
||||
descriptor = self.make_descriptor(indices=(2, 3, 4))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
(Path(tmpdir) / "episode_s01e01_2_deu.vtt").write_text(
|
||||
"WEBVTT\n\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "expected pattern"):
|
||||
descriptor.importSubtitles(tmpdir, "episode", strict=True)
|
||||
|
||||
def test_strict_basename_import_rejects_duplicate_track_indices(self):
|
||||
descriptor = self.make_descriptor(indices=(2, 3, 4))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
for filename in ("episode_2_deu.vtt", "episode_2_eng.vtt"):
|
||||
(Path(tmpdir) / filename).write_text("WEBVTT\n\n", encoding="utf-8")
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "Multiple external subtitle files"):
|
||||
descriptor.importSubtitles(tmpdir, "episode", strict=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user