Extd rename/unmux to pad with zeroes
This commit is contained in:
@@ -8,8 +8,18 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from tests.support.ffx_bundle import SourceTrackSpec, create_source_fixture
|
||||
from tests.support.ffx_bundle import (
|
||||
SourceTrackSpec,
|
||||
build_controller_context,
|
||||
create_source_fixture,
|
||||
dispose_controller_context,
|
||||
)
|
||||
|
||||
from ffx.pattern_controller import PatternController
|
||||
from ffx.show_controller import ShowController
|
||||
from ffx.show_descriptor import ShowDescriptor
|
||||
from ffx.track_codec import TrackCodec
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
from ffx.track_type import TrackType
|
||||
|
||||
try:
|
||||
@@ -66,6 +76,39 @@ class UnmuxCliTests(unittest.TestCase):
|
||||
f"STDERR:\n{completed.stderr}"
|
||||
)
|
||||
|
||||
def seed_matching_show(self, pattern_expression: str, *, indicator_season_digits: int, indicator_episode_digits: int) -> None:
|
||||
context = build_controller_context(self.database_path)
|
||||
try:
|
||||
ShowController(context).updateShow(
|
||||
ShowDescriptor(
|
||||
id=1,
|
||||
name="Unmux Test Show",
|
||||
year=2000,
|
||||
indicator_season_digits=indicator_season_digits,
|
||||
indicator_episode_digits=indicator_episode_digits,
|
||||
)
|
||||
)
|
||||
PatternController(context).savePatternSchema(
|
||||
{
|
||||
"show_id": 1,
|
||||
"pattern": pattern_expression,
|
||||
"quality": 0,
|
||||
"notes": "",
|
||||
},
|
||||
trackDescriptors=[
|
||||
TrackDescriptor(
|
||||
index=0,
|
||||
source_index=0,
|
||||
track_type=TrackType.VIDEO,
|
||||
codec_name=TrackCodec.H264,
|
||||
tags={},
|
||||
disposition_set=set(),
|
||||
)
|
||||
],
|
||||
)
|
||||
finally:
|
||||
dispose_controller_context(context)
|
||||
|
||||
def test_subtitles_only_without_output_directory_uses_configured_base_plus_label(self):
|
||||
self.write_config(
|
||||
{
|
||||
@@ -101,6 +144,85 @@ class UnmuxCliTests(unittest.TestCase):
|
||||
expected_directory = self.home_dir / ".local" / "var" / "sync" / "subtitles" / "dball"
|
||||
self.assertTrue(expected_directory.is_dir(), expected_directory)
|
||||
|
||||
def test_unmux_uses_configured_indicator_digits_in_output_filenames(self):
|
||||
self.write_config(
|
||||
{
|
||||
"defaultIndicatorSeasonDigits": 3,
|
||||
"defaultIndicatorEpisodeDigits": 4,
|
||||
}
|
||||
)
|
||||
source_filename = "unmux_s01e01.mkv"
|
||||
output_directory = self.workdir / "unmux-output"
|
||||
output_directory.mkdir()
|
||||
source_path = create_source_fixture(
|
||||
self.workdir,
|
||||
source_filename,
|
||||
[
|
||||
SourceTrackSpec(TrackType.VIDEO, identity="video-0"),
|
||||
],
|
||||
)
|
||||
|
||||
completed = run_ffx_unmux(
|
||||
self.workdir,
|
||||
self.home_dir,
|
||||
self.database_path,
|
||||
"--label",
|
||||
"dball",
|
||||
"--output-directory",
|
||||
str(output_directory),
|
||||
str(source_path),
|
||||
)
|
||||
self.assertCompleted(completed)
|
||||
|
||||
output_filenames = sorted(path.name for path in output_directory.iterdir())
|
||||
self.assertEqual(1, len(output_filenames), output_filenames)
|
||||
self.assertTrue(
|
||||
output_filenames[0].startswith("dball_S001E0001_"),
|
||||
output_filenames,
|
||||
)
|
||||
|
||||
def test_unmux_prefers_matched_show_indicator_digits_over_config_defaults(self):
|
||||
self.write_config(
|
||||
{
|
||||
"defaultIndicatorSeasonDigits": 4,
|
||||
"defaultIndicatorEpisodeDigits": 4,
|
||||
}
|
||||
)
|
||||
self.seed_matching_show(
|
||||
r"^unmux_([sS][0-9]+[eE][0-9]+)\.mkv$",
|
||||
indicator_season_digits=1,
|
||||
indicator_episode_digits=3,
|
||||
)
|
||||
source_filename = "unmux_s01e01.mkv"
|
||||
output_directory = self.workdir / "unmux-output"
|
||||
output_directory.mkdir()
|
||||
source_path = create_source_fixture(
|
||||
self.workdir,
|
||||
source_filename,
|
||||
[
|
||||
SourceTrackSpec(TrackType.VIDEO, identity="video-0"),
|
||||
],
|
||||
)
|
||||
|
||||
completed = run_ffx_unmux(
|
||||
self.workdir,
|
||||
self.home_dir,
|
||||
self.database_path,
|
||||
"--label",
|
||||
"dball",
|
||||
"--output-directory",
|
||||
str(output_directory),
|
||||
str(source_path),
|
||||
)
|
||||
self.assertCompleted(completed)
|
||||
|
||||
output_filenames = sorted(path.name for path in output_directory.iterdir())
|
||||
self.assertEqual(1, len(output_filenames), output_filenames)
|
||||
self.assertTrue(
|
||||
output_filenames[0].startswith("dball_S1E001_"),
|
||||
output_filenames,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -21,6 +23,8 @@ class RenameCliTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.TemporaryDirectory()
|
||||
self.workspace = Path(self.tempdir.name)
|
||||
self.home_dir = self.workspace / "home"
|
||||
self.home_dir.mkdir()
|
||||
|
||||
def tearDown(self):
|
||||
self.tempdir.cleanup()
|
||||
@@ -30,9 +34,18 @@ class RenameCliTests(unittest.TestCase):
|
||||
source_path.write_bytes(payload)
|
||||
return source_path
|
||||
|
||||
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_rename(self, *args: str):
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli.ffx, ["rename", *args])
|
||||
result = runner.invoke(
|
||||
cli.ffx,
|
||||
["rename", *args],
|
||||
env={**os.environ, "HOME": str(self.home_dir)},
|
||||
)
|
||||
self.assertEqual(0, result.exit_code, result.output)
|
||||
return result
|
||||
|
||||
@@ -41,8 +54,8 @@ class RenameCliTests(unittest.TestCase):
|
||||
|
||||
result = self.invoke_rename("--prefix", "dball", str(source_path))
|
||||
|
||||
target_path = self.workspace / "dball_s2e3.mkv"
|
||||
self.assertIn("demo_S02E03.mkv -> dball_s2e3.mkv", result.output)
|
||||
target_path = self.workspace / "dball_s02e03.mkv"
|
||||
self.assertIn("demo_S02E03.mkv -> dball_s02e03.mkv", result.output)
|
||||
self.assertFalse(source_path.exists())
|
||||
self.assertTrue(target_path.exists())
|
||||
self.assertEqual(b"season-episode", target_path.read_bytes())
|
||||
@@ -58,8 +71,8 @@ class RenameCliTests(unittest.TestCase):
|
||||
str(source_path),
|
||||
)
|
||||
|
||||
target_path = self.workspace / "dball_s1e7_bonus.mp4"
|
||||
self.assertIn("demo_E07.mp4 -> dball_s1e7_bonus.mp4", result.output)
|
||||
target_path = self.workspace / "dball_s01e07_bonus.mp4"
|
||||
self.assertIn("demo_E07.mp4 -> dball_s01e07_bonus.mp4", result.output)
|
||||
self.assertFalse(source_path.exists())
|
||||
self.assertTrue(target_path.exists())
|
||||
self.assertEqual(b"episode-only", target_path.read_bytes())
|
||||
@@ -75,8 +88,8 @@ class RenameCliTests(unittest.TestCase):
|
||||
str(source_path),
|
||||
)
|
||||
|
||||
target_path = self.workspace / "dball_s5e7.webm"
|
||||
self.assertIn("demo_s02e07.webm -> dball_s5e7.webm", result.output)
|
||||
target_path = self.workspace / "dball_s05e07.webm"
|
||||
self.assertIn("demo_s02e07.webm -> dball_s05e07.webm", result.output)
|
||||
self.assertFalse(source_path.exists())
|
||||
self.assertTrue(target_path.exists())
|
||||
|
||||
@@ -90,11 +103,27 @@ class RenameCliTests(unittest.TestCase):
|
||||
str(source_path),
|
||||
)
|
||||
|
||||
target_path = self.workspace / "dball_s1e7.mkv"
|
||||
self.assertIn("demo_E07.mkv -> dball_s1e7.mkv", result.output)
|
||||
target_path = self.workspace / "dball_s01e07.mkv"
|
||||
self.assertIn("demo_E07.mkv -> dball_s01e07.mkv", result.output)
|
||||
self.assertTrue(source_path.exists())
|
||||
self.assertFalse(target_path.exists())
|
||||
|
||||
def test_rename_uses_configured_indicator_digit_lengths(self):
|
||||
self.write_config(
|
||||
{
|
||||
"defaultIndicatorSeasonDigits": 3,
|
||||
"defaultIndicatorEpisodeDigits": 4,
|
||||
}
|
||||
)
|
||||
source_path = self.write_source("demo_E07.mkv")
|
||||
|
||||
result = self.invoke_rename("--prefix", "dball", str(source_path))
|
||||
|
||||
target_path = self.workspace / "dball_s001e0007.mkv"
|
||||
self.assertIn("demo_E07.mkv -> dball_s001e0007.mkv", result.output)
|
||||
self.assertFalse(source_path.exists())
|
||||
self.assertTrue(target_path.exists())
|
||||
|
||||
def test_rename_skips_non_matching_filenames(self):
|
||||
source_path = self.write_source("demo_finale.mkv")
|
||||
|
||||
|
||||
150
tests/unit/test_configure_workstation_script.py
Normal file
150
tests/unit/test_configure_workstation_script.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT_PATH = REPO_ROOT / "tools" / "configure_workstation.sh"
|
||||
BUNDLE_PYTHON = Path.home() / ".local" / "share" / "ffx.venv" / "bin" / "python"
|
||||
|
||||
|
||||
class ConfigureWorkstationScriptTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.TemporaryDirectory()
|
||||
self.home_dir = Path(self.tempdir.name) / "home"
|
||||
self.home_dir.mkdir()
|
||||
self.stub_bin_dir = Path(self.tempdir.name) / "bin"
|
||||
self.stub_bin_dir.mkdir()
|
||||
|
||||
for command_name in ("git", "python3", "ffmpeg", "ffprobe", "cpulimit"):
|
||||
self.write_stub_command(command_name)
|
||||
|
||||
def tearDown(self):
|
||||
self.tempdir.cleanup()
|
||||
|
||||
def write_stub_command(self, name: str, body: str = "") -> None:
|
||||
script_path = self.stub_bin_dir / name
|
||||
script_path.write_text(
|
||||
"#!/usr/bin/env bash\n"
|
||||
+ body
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
script_path.chmod(script_path.stat().st_mode | stat.S_IXUSR)
|
||||
|
||||
def run_script(self, **env_overrides: str) -> subprocess.CompletedProcess[str]:
|
||||
if not BUNDLE_PYTHON.is_file():
|
||||
self.skipTest(f"Missing bundle Python at {BUNDLE_PYTHON}")
|
||||
|
||||
env = {
|
||||
**os.environ,
|
||||
"HOME": str(self.home_dir),
|
||||
"PATH": f"{self.stub_bin_dir}:{os.environ.get('PATH', '')}",
|
||||
"FFX_PYTHON": str(BUNDLE_PYTHON),
|
||||
**env_overrides,
|
||||
}
|
||||
|
||||
return subprocess.run(
|
||||
["bash", str(SCRIPT_PATH)],
|
||||
capture_output=True,
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
text=True,
|
||||
)
|
||||
|
||||
def test_script_seeds_default_config_from_template(self):
|
||||
completed = self.run_script()
|
||||
|
||||
self.assertEqual(
|
||||
0,
|
||||
completed.returncode,
|
||||
f"STDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}",
|
||||
)
|
||||
|
||||
config_path = self.home_dir / ".local" / "etc" / "ffx.json"
|
||||
self.assertTrue(config_path.exists())
|
||||
|
||||
config_data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(
|
||||
{
|
||||
"databasePath": str(self.home_dir / ".local" / "var" / "ffx" / "ffx.db"),
|
||||
"logDirectory": str(self.home_dir / ".local" / "var" / "log"),
|
||||
"subtitlesDirectory": str(
|
||||
self.home_dir / ".local" / "var" / "sync" / "subtitles"
|
||||
),
|
||||
"defaultIndexSeasonDigits": 2,
|
||||
"defaultIndexEpisodeDigits": 2,
|
||||
"defaultIndicatorSeasonDigits": 2,
|
||||
"defaultIndicatorEpisodeDigits": 2,
|
||||
"metadata": {
|
||||
"signature": {"RECODED_WITH": "FFX"},
|
||||
"remove": [
|
||||
"VERSION-eng",
|
||||
"creation_time",
|
||||
"NAME",
|
||||
],
|
||||
"streams": {
|
||||
"remove": [
|
||||
"BPS",
|
||||
"NUMBER_OF_FRAMES",
|
||||
"NUMBER_OF_BYTES",
|
||||
"_STATISTICS_WRITING_APP",
|
||||
"_STATISTICS_WRITING_DATE_UTC",
|
||||
"_STATISTICS_TAGS",
|
||||
"BPS-eng",
|
||||
"DURATION-eng",
|
||||
"NUMBER_OF_FRAMES-eng",
|
||||
"NUMBER_OF_BYTES-eng",
|
||||
"_STATISTICS_WRITING_APP-eng",
|
||||
"_STATISTICS_WRITING_DATE_UTC-eng",
|
||||
"_STATISTICS_TAGS-eng",
|
||||
]
|
||||
},
|
||||
},
|
||||
},
|
||||
config_data,
|
||||
)
|
||||
|
||||
def test_script_honors_custom_template_override(self):
|
||||
custom_template_path = Path(self.tempdir.name) / "custom-config.j2"
|
||||
custom_template_path.write_text(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
{
|
||||
"databasePath": {{ database_path_json }},
|
||||
"marker": "from-template",
|
||||
"subtitlesDirectory": {{ subtitles_directory_json }}
|
||||
}
|
||||
"""
|
||||
).lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
completed = self.run_script(FFX_CONFIG_TEMPLATE=str(custom_template_path))
|
||||
|
||||
self.assertEqual(
|
||||
0,
|
||||
completed.returncode,
|
||||
f"STDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}",
|
||||
)
|
||||
|
||||
config_path = self.home_dir / ".local" / "etc" / "ffx.json"
|
||||
config_data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual("from-template", config_data["marker"])
|
||||
self.assertEqual(
|
||||
str(self.home_dir / ".local" / "var" / "ffx" / "ffx.db"),
|
||||
config_data["databasePath"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
97
tests/unit/test_show_descriptor_defaults.py
Normal file
97
tests/unit/test_show_descriptor_defaults.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
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.constants import (
|
||||
DEFAULT_SHOW_INDEX_EPISODE_DIGITS,
|
||||
DEFAULT_SHOW_INDEX_SEASON_DIGITS,
|
||||
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||
)
|
||||
from ffx.helper import getEpisodeFileBasename
|
||||
from ffx.show_descriptor import ShowDescriptor
|
||||
|
||||
|
||||
class StaticConfig:
|
||||
def __init__(self, data: dict | None = None):
|
||||
self._data = data or {}
|
||||
|
||||
def getData(self):
|
||||
return self._data
|
||||
|
||||
|
||||
class ShowDescriptorDefaultTests(unittest.TestCase):
|
||||
def make_context(self, config_data: dict | None = None) -> dict:
|
||||
logger = logging.getLogger("ffx-test-show-descriptor-defaults")
|
||||
logger.handlers = []
|
||||
logger.addHandler(logging.NullHandler())
|
||||
return {"config": StaticConfig(config_data), "logger": logger}
|
||||
|
||||
def test_show_descriptor_uses_config_defaults_when_context_is_present(self):
|
||||
descriptor = ShowDescriptor(
|
||||
context=self.make_context(
|
||||
{
|
||||
"defaultIndexSeasonDigits": "1",
|
||||
"defaultIndexEpisodeDigits": "3",
|
||||
"defaultIndicatorSeasonDigits": "3",
|
||||
"defaultIndicatorEpisodeDigits": "4",
|
||||
}
|
||||
),
|
||||
id=1,
|
||||
name="Configured Show",
|
||||
year=2024,
|
||||
)
|
||||
|
||||
self.assertEqual(1, descriptor.getIndexSeasonDigits())
|
||||
self.assertEqual(3, descriptor.getIndexEpisodeDigits())
|
||||
self.assertEqual(3, descriptor.getIndicatorSeasonDigits())
|
||||
self.assertEqual(4, descriptor.getIndicatorEpisodeDigits())
|
||||
|
||||
def test_show_descriptor_without_context_uses_shared_constants(self):
|
||||
descriptor = ShowDescriptor(id=1, name="Default Show", year=2024)
|
||||
|
||||
self.assertEqual(DEFAULT_SHOW_INDEX_SEASON_DIGITS, descriptor.getIndexSeasonDigits())
|
||||
self.assertEqual(DEFAULT_SHOW_INDEX_EPISODE_DIGITS, descriptor.getIndexEpisodeDigits())
|
||||
self.assertEqual(
|
||||
DEFAULT_SHOW_INDICATOR_SEASON_DIGITS,
|
||||
descriptor.getIndicatorSeasonDigits(),
|
||||
)
|
||||
self.assertEqual(
|
||||
DEFAULT_SHOW_INDICATOR_EPISODE_DIGITS,
|
||||
descriptor.getIndicatorEpisodeDigits(),
|
||||
)
|
||||
|
||||
def test_episode_basename_uses_configured_digit_defaults_when_omitted(self):
|
||||
basename = getEpisodeFileBasename(
|
||||
"Configured Show",
|
||||
"Episode Name",
|
||||
2,
|
||||
7,
|
||||
context=self.make_context(
|
||||
{
|
||||
"defaultIndexSeasonDigits": 1,
|
||||
"defaultIndexEpisodeDigits": 3,
|
||||
"defaultIndicatorSeasonDigits": 3,
|
||||
"defaultIndicatorEpisodeDigits": 4,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
"Configured Show - 2007 Episode Name - S002E0007",
|
||||
basename,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user