Adds inspect --shift option

This commit is contained in:
Javanaut
2026-04-12 20:34:33 +02:00
parent 8a8c43ecdf
commit d6e885517d
4 changed files with 193 additions and 16 deletions

View File

@@ -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()

View File

@@ -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))

View 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()

View File

@@ -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__":