Adds inspect --shift option
This commit is contained in:
@@ -461,13 +461,47 @@ def upgrade(ctx, branch):
|
|||||||
|
|
||||||
@ffx.command()
|
@ffx.command()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@click.argument('filename', nargs=1)
|
@click.option('--shift', is_flag=True, default=False, help='Print resolved season-shift mapping for each file instead of opening the TUI')
|
||||||
def inspect(ctx, filename):
|
@click.argument('filenames', nargs=-1)
|
||||||
|
def inspect(ctx, shift, filenames):
|
||||||
|
if not filenames:
|
||||||
|
raise click.ClickException("At least one filename is required.")
|
||||||
|
|
||||||
|
if shift:
|
||||||
|
from ffx.file_properties import FileProperties
|
||||||
|
from ffx.shifted_season_controller import ShiftedSeasonController
|
||||||
|
|
||||||
|
shiftedSeasonController = ShiftedSeasonController(ctx.obj)
|
||||||
|
|
||||||
|
for filename in filenames:
|
||||||
|
fileProperties = FileProperties(ctx.obj, filename)
|
||||||
|
season = fileProperties.getSeason()
|
||||||
|
episode = fileProperties.getEpisode()
|
||||||
|
|
||||||
|
if season == -1 or episode == -1:
|
||||||
|
click.echo(f"{filename}: no season/episode recognized")
|
||||||
|
continue
|
||||||
|
|
||||||
|
currentPattern = fileProperties.getPattern()
|
||||||
|
shiftedSeason, shiftedEpisode, sourceLabel = shiftedSeasonController.resolveShiftSeason(
|
||||||
|
fileProperties.getShowId(),
|
||||||
|
season=season,
|
||||||
|
episode=episode,
|
||||||
|
patternId=currentPattern.getId() if currentPattern is not None else None,
|
||||||
|
)
|
||||||
|
click.echo(
|
||||||
|
f"{filename}: {season}/{episode} -> {shiftedSeason}/{shiftedEpisode} from {sourceLabel}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(filenames) != 1:
|
||||||
|
raise click.ClickException("Inspect without --shift requires exactly one filename.")
|
||||||
|
|
||||||
from ffx.ffx_app import FfxApp
|
from ffx.ffx_app import FfxApp
|
||||||
|
|
||||||
ctx.obj['command'] = 'inspect'
|
ctx.obj['command'] = 'inspect'
|
||||||
ctx.obj['arguments'] = {}
|
ctx.obj['arguments'] = {}
|
||||||
ctx.obj['arguments']['filename'] = filename
|
ctx.obj['arguments']['filename'] = filenames[0]
|
||||||
|
|
||||||
app = FfxApp(ctx.obj)
|
app = FfxApp(ctx.obj)
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
@@ -383,10 +383,27 @@ class ShiftedSeasonController:
|
|||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
def shiftSeason(self, showId, season, episode, patternId=None):
|
def shiftSeason(self, showId, season, episode, patternId=None):
|
||||||
|
|
||||||
if season == -1 or episode == -1:
|
if season == -1 or episode == -1:
|
||||||
return season, episode
|
return season, episode
|
||||||
|
|
||||||
|
shiftedSeason, shiftedEpisode, sourceLabel = self.resolveShiftSeason(
|
||||||
|
showId,
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
patternId=patternId,
|
||||||
|
)
|
||||||
|
|
||||||
|
if shiftedSeason != season or shiftedEpisode != episode:
|
||||||
|
self.context['logger'].info(
|
||||||
|
f"Setting season shift {season}/{episode} -> {shiftedSeason}/{shiftedEpisode} from {sourceLabel}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return shiftedSeason, shiftedEpisode
|
||||||
|
|
||||||
|
def resolveShiftSeason(self, showId, season, episode, patternId=None):
|
||||||
|
if season == -1 or episode == -1:
|
||||||
|
return season, episode, "unrecognized"
|
||||||
|
|
||||||
session = None
|
session = None
|
||||||
try:
|
try:
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
@@ -420,12 +437,7 @@ class ShiftedSeasonController:
|
|||||||
if activeShift.getPatternId() is not None
|
if activeShift.getPatternId() is not None
|
||||||
else "show"
|
else "show"
|
||||||
)
|
)
|
||||||
|
return shiftedSeason, shiftedEpisode, sourceLabel
|
||||||
self.context['logger'].info(
|
|
||||||
f"Setting season shift {season}/{episode} -> {shiftedSeason}/{shiftedEpisode} from {sourceLabel}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return shiftedSeason, shiftedEpisode
|
|
||||||
|
|
||||||
except ShiftedSeasonOwnerException as ex:
|
except ShiftedSeasonOwnerException as ex:
|
||||||
raise click.ClickException(str(ex))
|
raise click.ClickException(str(ex))
|
||||||
|
|||||||
135
tests/unit/test_cli_inspect_shift.py
Normal file
135
tests/unit/test_cli_inspect_shift.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class _FakePattern:
|
||||||
|
def __init__(self, pattern_id: int):
|
||||||
|
self._pattern_id = pattern_id
|
||||||
|
|
||||||
|
def getId(self):
|
||||||
|
return self._pattern_id
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeFileProperties:
|
||||||
|
def __init__(self, context, source_path):
|
||||||
|
self.source_path = source_path
|
||||||
|
|
||||||
|
def getShowId(self):
|
||||||
|
return 42 if self.source_path.endswith("mapped.mkv") else -1
|
||||||
|
|
||||||
|
def getSeason(self):
|
||||||
|
if self.source_path.endswith("unknown.mkv"):
|
||||||
|
return -1
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def getEpisode(self):
|
||||||
|
if self.source_path.endswith("unknown.mkv"):
|
||||||
|
return -1
|
||||||
|
return 3
|
||||||
|
|
||||||
|
def getPattern(self):
|
||||||
|
if self.source_path.endswith("mapped.mkv"):
|
||||||
|
return _FakePattern(7)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeShiftedSeasonController:
|
||||||
|
def __init__(self, context):
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
def resolveShiftSeason(self, show_id, season, episode, patternId=None):
|
||||||
|
if patternId is not None:
|
||||||
|
return 2, 1, "pattern"
|
||||||
|
return season, episode, "default"
|
||||||
|
|
||||||
|
|
||||||
|
class InspectShiftCliTests(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"
|
||||||
|
self.source_dir = Path(self.tempdir.name) / "source"
|
||||||
|
self.source_dir.mkdir()
|
||||||
|
self.mapped_path = self.source_dir / "mapped.mkv"
|
||||||
|
self.mapped_path.write_bytes(b"mapped")
|
||||||
|
self.unknown_path = self.source_dir / "unknown.mkv"
|
||||||
|
self.unknown_path.write_bytes(b"unknown")
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.tempdir.cleanup()
|
||||||
|
|
||||||
|
def test_inspect_shift_prints_resolved_mapping_for_each_file(self):
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("ffx.file_properties.FileProperties", _FakeFileProperties),
|
||||||
|
patch(
|
||||||
|
"ffx.shifted_season_controller.ShiftedSeasonController",
|
||||||
|
_FakeShiftedSeasonController,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.ffx,
|
||||||
|
[
|
||||||
|
"--database-file",
|
||||||
|
str(self.database_path),
|
||||||
|
"inspect",
|
||||||
|
"--shift",
|
||||||
|
str(self.mapped_path),
|
||||||
|
str(self.unknown_path),
|
||||||
|
],
|
||||||
|
env={**os.environ, "HOME": str(self.home_dir)},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, result.exit_code, result.output)
|
||||||
|
self.assertIn(
|
||||||
|
f"{self.mapped_path}: 1/3 -> 2/1 from pattern",
|
||||||
|
result.output,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
f"{self.unknown_path}: no season/episode recognized",
|
||||||
|
result.output,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_inspect_without_shift_requires_exactly_one_filename(self):
|
||||||
|
runner = CliRunner()
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
cli.ffx,
|
||||||
|
[
|
||||||
|
"--database-file",
|
||||||
|
str(self.database_path),
|
||||||
|
"inspect",
|
||||||
|
str(self.mapped_path),
|
||||||
|
str(self.unknown_path),
|
||||||
|
],
|
||||||
|
env={**os.environ, "HOME": str(self.home_dir)},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotEqual(0, result.exit_code)
|
||||||
|
self.assertIn(
|
||||||
|
"Inspect without --shift requires exactly one filename.",
|
||||||
|
result.output,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -183,9 +183,7 @@ class ShiftedSeasonControllerTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual((1, 3), (shifted_season, shifted_episode))
|
self.assertEqual((1, 3), (shifted_season, shifted_episode))
|
||||||
mocked_info.assert_called_once_with(
|
mocked_info.assert_not_called()
|
||||||
"Setting season shift 1/3 -> 1/3 from pattern"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_shift_season_falls_back_to_identity_when_no_rule_matches(self):
|
def test_shift_season_falls_back_to_identity_when_no_rule_matches(self):
|
||||||
pattern_id = self.add_pattern(1, r"^demo_(s[0-9]+e[0-9]+)\.mkv$")
|
pattern_id = self.add_pattern(1, r"^demo_(s[0-9]+e[0-9]+)\.mkv$")
|
||||||
@@ -199,9 +197,7 @@ class ShiftedSeasonControllerTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual((4, 20), (shifted_season, shifted_episode))
|
self.assertEqual((4, 20), (shifted_season, shifted_episode))
|
||||||
mocked_info.assert_called_once_with(
|
mocked_info.assert_not_called()
|
||||||
"Setting season shift 4/20 -> 4/20 from default"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user