from __future__ import annotations import json import os from pathlib import Path import sys import tempfile import unittest from unittest.mock import patch import click from click.testing import CliRunner SRC_ROOT = Path(__file__).resolve().parents[2] / "src" if str(SRC_ROOT) not in sys.path: sys.path.insert(0, str(SRC_ROOT)) 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): def setUp(self): self.tempdir = tempfile.TemporaryDirectory() self.home_dir = Path(self.tempdir.name) / "home" self.home_dir.mkdir() self.database_path = Path(self.tempdir.name) / "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 invoke_convert(self, *args: str): runner = CliRunner() return runner.invoke( cli.ffx, [ "--database-file", str(self.database_path), "convert", "--no-tmdb", *args, ], 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") self.assertNotEqual(0, result.exit_code) self.assertIn("no --subtitle-directory was provided", result.output) self.assertIn("no subtitlesDirectory default is configured", result.output) def test_subtitle_prefix_without_directory_fails_when_configured_subdir_is_missing(self): subtitles_base_dir = self.home_dir / ".local" / "var" / "sync" / "subtitles" subtitles_base_dir.mkdir(parents=True, exist_ok=True) self.write_config({"subtitlesDirectory": "~/.local/var/sync/subtitles"}) result = self.invoke_convert("--subtitle-prefix", "dball") self.assertNotEqual(0, result.exit_code) self.assertIn("resolved subtitle directory does not exist", result.output) self.assertIn(str(subtitles_base_dir / "dball"), result.output) def test_explicit_subtitle_directory_wins_over_missing_default(self): explicit_subtitle_directory = self.home_dir / "manual-subtitles" explicit_subtitle_directory.mkdir(parents=True, exist_ok=True) result = self.invoke_convert( "--subtitle-directory", str(explicit_subtitle_directory), "--subtitle-prefix", "dball", ) 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 = self.home_dir / "complete-subtitles" subtitleDirectory.mkdir() for basename in ( "A2_t01_2_deu_DEF", "A2_t01_3_eng", "A2_t01_4_eng", ): (subtitleDirectory / f"{basename}.vtt").write_text( "WEBVTT\n\n", encoding="utf-8", ) 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()