From 4365e083dcc13cacb2029583b343fed75811773c Mon Sep 17 00:00:00 2001 From: Javanaut Date: Sat, 11 Apr 2026 22:31:04 +0200 Subject: [PATCH] Adapt unmux command to changes in convert command --- src/ffx/cli.py | 36 +++++- tests/integration/test_cli_unmux.py | 106 ++++++++++++++++++ tests/unit/test_cli_unmux_output_directory.py | 94 ++++++++++++++++ 3 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 tests/integration/test_cli_unmux.py create mode 100644 tests/unit/test_cli_unmux_output_directory.py diff --git a/src/ffx/cli.py b/src/ffx/cli.py index 4be3451..5507bef 100755 --- a/src/ffx/cli.py +++ b/src/ffx/cli.py @@ -47,6 +47,10 @@ SUBTITLE_PREFIX_OPTION_HELP = ( "Subtitle filename prefix. Requires --subtitle-directory, or a configured " + "subtitlesDirectory base path that contains a matching / subdirectory." ) +UNMUX_OUTPUT_DIRECTORY_OPTION_HELP = ( + "Write extracted streams here. When omitted together with --subtitles-only and " + + "--label, FFX uses the configured subtitlesDirectory base path plus the label." +) CROPDETECT_SEEK_OPTION_HELP = ( "Start crop detection this many seconds into the input. " + "Useful for skipping logos, intros, or black frames." @@ -160,6 +164,27 @@ def resolveSubtitleImportOptions(context, subtitleDirectory, subtitlePrefix): return True, resolvedSubtitleDirectory, resolvedSubtitlePrefix +def resolveUnmuxOutputDirectory(context, outputDirectory, subtitlesOnly, label): + resolvedOutputDirectory = ( + os.path.expanduser(str(outputDirectory).strip()) + if outputDirectory + else '' + ) + resolvedLabel = str(label).strip() + + if resolvedOutputDirectory or not subtitlesOnly or not resolvedLabel: + return resolvedOutputDirectory, False + + configuredSubtitlesBaseDirectory = context['config'].getSubtitlesDirectoryPath() + if not configuredSubtitlesBaseDirectory: + raise click.ClickException( + "Subtitles-only unmux with --label requires --output-directory or a configured " + + "subtitlesDirectory default in ffx.json." + ) + + return os.path.join(configuredSubtitlesBaseDirectory, resolvedLabel), True + + @click.group() @click.pass_context @@ -416,7 +441,7 @@ def getUnmuxSequence(trackDescriptor: TrackDescriptor, sourcePath, targetPrefix, @click.argument('paths', nargs=-1) @click.option('-l', '--label', type=str, default='', help='Label to be used as filename prefix') -@click.option("-o", "--output-directory", type=str, default='') +@click.option("-o", "--output-directory", type=str, default='', help=UNMUX_OUTPUT_DIRECTORY_OPTION_HELP) @click.option("-s", "--subtitles-only", is_flag=True, default=False) @click.option( '--nice', @@ -454,6 +479,15 @@ def unmux(ctx, ctx.obj['resource_limits']['cpu_limit'] = cpu ctx.obj['resource_limits']['cpu_percent'] = cpu + output_directory, create_output_directory = resolveUnmuxOutputDirectory( + ctx.obj, + output_directory, + subtitles_only, + label, + ) + if create_output_directory and existingSourcePaths and not ctx.obj.get('dry_run', False): + os.makedirs(output_directory, exist_ok=True) + for sourcePath in existingSourcePaths: fp = FileProperties(ctx.obj, sourcePath) diff --git a/tests/integration/test_cli_unmux.py b/tests/integration/test_cli_unmux.py new file mode 100644 index 0000000..a47241a --- /dev/null +++ b/tests/integration/test_cli_unmux.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +from tests.support.ffx_bundle import SourceTrackSpec, create_source_fixture + +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] + + +SRC_ROOT = Path(__file__).resolve().parents[2] / "src" + + +def run_ffx_unmux(workdir: Path, home_dir: Path, database_path: Path, *args: str) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + env["HOME"] = str(home_dir) + existing_pythonpath = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = str(SRC_ROOT) if not existing_pythonpath else f"{SRC_ROOT}{os.pathsep}{existing_pythonpath}" + + command = [ + sys.executable, + "-m", + "ffx", + "--database-file", + str(database_path), + "unmux", + *args, + ] + return subprocess.run(command, cwd=workdir, env=env, capture_output=True, text=True) + + +class UnmuxCliTests(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 unmux failed\n" + f"STDOUT:\n{completed.stdout}\n" + f"STDERR:\n{completed.stderr}" + ) + + def test_subtitles_only_without_output_directory_uses_configured_base_plus_label(self): + self.write_config( + { + "subtitlesDirectory": "~/.local/var/sync/subtitles", + } + ) + source_filename = "unmux_s01e01.mkv" + source_path = create_source_fixture( + self.workdir, + source_filename, + [ + SourceTrackSpec(TrackType.VIDEO, identity="video-0"), + SourceTrackSpec( + TrackType.SUBTITLE, + identity="subtitle-1", + language="eng", + subtitle_lines=("subtitle payload",), + ), + ], + ) + + completed = run_ffx_unmux( + self.workdir, + self.home_dir, + self.database_path, + "--subtitles-only", + "--label", + "dball", + str(source_path), + ) + self.assertCompleted(completed) + + expected_directory = self.home_dir / ".local" / "var" / "sync" / "subtitles" / "dball" + self.assertTrue(expected_directory.is_dir(), expected_directory) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/test_cli_unmux_output_directory.py b/tests/unit/test_cli_unmux_output_directory.py new file mode 100644 index 0000000..f417fc6 --- /dev/null +++ b/tests/unit/test_cli_unmux_output_directory.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from pathlib import Path +import sys +import tempfile +import unittest + +import click + + +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 + + +class StaticConfig: + def __init__(self, subtitles_directory: str = ""): + self._subtitles_directory = subtitles_directory + + def getSubtitlesDirectoryPath(self): + return self._subtitles_directory + + +class UnmuxOutputDirectoryTests(unittest.TestCase): + def test_subtitles_only_with_label_uses_configured_subtitles_base_directory(self): + with tempfile.TemporaryDirectory() as tempdir: + context = { + "config": StaticConfig(str(Path(tempdir) / "subtitles")), + } + + resolved_output_directory, should_create = cli.resolveUnmuxOutputDirectory( + context, + "", + True, + "dball", + ) + + self.assertEqual(str(Path(tempdir) / "subtitles" / "dball"), resolved_output_directory) + self.assertTrue(should_create) + + def test_explicit_output_directory_keeps_existing_behavior(self): + with tempfile.TemporaryDirectory() as tempdir: + context = { + "config": StaticConfig(str(Path(tempdir) / "subtitles")), + } + explicit_output_directory = str(Path(tempdir) / "manual") + + resolved_output_directory, should_create = cli.resolveUnmuxOutputDirectory( + context, + explicit_output_directory, + True, + "dball", + ) + + self.assertEqual(explicit_output_directory, resolved_output_directory) + self.assertFalse(should_create) + + def test_subtitles_only_without_label_keeps_existing_behavior(self): + context = { + "config": StaticConfig("/tmp/subtitles"), + } + + resolved_output_directory, should_create = cli.resolveUnmuxOutputDirectory( + context, + "", + True, + "", + ) + + self.assertEqual("", resolved_output_directory) + self.assertFalse(should_create) + + def test_subtitles_only_with_label_requires_configured_default_when_output_directory_is_missing(self): + context = { + "config": StaticConfig(""), + } + + with self.assertRaises(click.ClickException) as caught: + cli.resolveUnmuxOutputDirectory( + context, + "", + True, + "dball", + ) + + self.assertIn("subtitlesDirectory default", str(caught.exception)) + + +if __name__ == "__main__": + unittest.main()