fix capture group
This commit is contained in:
@@ -73,3 +73,8 @@
|
||||
|
||||
|
||||
## TODO: Review styled ASS separate handling
|
||||
|
||||
|
||||
|
||||
. ~/.bashrc && cd /home/devnut/.local/src/codex/ffx/tests/assets && /home/devnut/.local/src/codex/ffx/.venv/bin/ffx --debug inspect gits_sac_s02e02_r3.mkv
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from textual.app import App
|
||||
|
||||
from .i18n import set_current_language, t
|
||||
from .confirm_screen import ConfirmScreen
|
||||
from .shows_screen import ShowsScreen
|
||||
from .inspect_details_screen import InspectDetailsScreen
|
||||
from .media_edit_screen import MediaEditScreen
|
||||
from .pattern_controller import InvalidPatternCaptureGroupError, PatternController
|
||||
from .screen_support import configure_screen_log_handler, set_screen_log_pane_enabled
|
||||
|
||||
|
||||
@@ -40,7 +42,20 @@ class FfxApp(App):
|
||||
self.push_screen(ShowsScreen())
|
||||
|
||||
if self.context['command'] == 'inspect':
|
||||
self.push_screen(InspectDetailsScreen())
|
||||
try:
|
||||
self.push_screen(InspectDetailsScreen())
|
||||
except InvalidPatternCaptureGroupError as ex:
|
||||
self._invalid_pattern_error = ex
|
||||
self.push_screen(
|
||||
ConfirmScreen(
|
||||
t(
|
||||
"Pattern #{pattern_id} matches the filename but has no capture group for the season/episode indicator. Delete this pattern and quit?"
|
||||
).format(pattern_id=ex.pattern_id),
|
||||
confirm_label=t("Delete"),
|
||||
cancel_label=t("Quit"),
|
||||
),
|
||||
self._handle_invalid_pattern_confirmation,
|
||||
)
|
||||
|
||||
if self.context['command'] == 'edit':
|
||||
self.push_screen(MediaEditScreen())
|
||||
@@ -49,3 +64,13 @@ class FfxApp(App):
|
||||
def getContext(self):
|
||||
"""Data 'output' method"""
|
||||
return self.context
|
||||
|
||||
def _handle_invalid_pattern_confirmation(self, confirmed: bool):
|
||||
invalid_pattern_error = getattr(self, "_invalid_pattern_error", None)
|
||||
|
||||
if confirmed and invalid_pattern_error is not None:
|
||||
PatternController(self.context).deletePattern(
|
||||
invalid_pattern_error.pattern_id
|
||||
)
|
||||
|
||||
self.exit()
|
||||
|
||||
@@ -10,6 +10,7 @@ from textual.widgets._data_table import CellDoesNotExist
|
||||
from ffx.file_properties import FileProperties
|
||||
from ffx.helper import DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
|
||||
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet
|
||||
from ffx.pattern_controller import InvalidPatternCaptureGroupError
|
||||
from ffx.show_descriptor import ShowDescriptor
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
|
||||
@@ -472,11 +473,15 @@ class InspectDetailsScreen(MediaWorkflowScreenBase):
|
||||
):
|
||||
mediaTags[tagKey] = tagValue
|
||||
|
||||
patternId = self._pc.savePatternSchema(
|
||||
patternObj,
|
||||
trackDescriptors=self._sourceMediaDescriptor.getTrackDescriptors(),
|
||||
mediaTags=mediaTags,
|
||||
)
|
||||
try:
|
||||
patternId = self._pc.savePatternSchema(
|
||||
patternObj,
|
||||
trackDescriptors=self._sourceMediaDescriptor.getTrackDescriptors(),
|
||||
mediaTags=mediaTags,
|
||||
)
|
||||
except InvalidPatternCaptureGroupError as ex:
|
||||
self.notify(str(ex), severity="error")
|
||||
return
|
||||
if patternId:
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
@@ -498,10 +503,14 @@ class InspectDetailsScreen(MediaWorkflowScreenBase):
|
||||
patternObj
|
||||
and self._currentPattern.getPattern() != patternObj["pattern"]
|
||||
):
|
||||
updated = self._pc.updatePattern(
|
||||
self._currentPattern.getId(),
|
||||
patternObj,
|
||||
)
|
||||
try:
|
||||
updated = self._pc.updatePattern(
|
||||
self._currentPattern.getId(),
|
||||
patternObj,
|
||||
)
|
||||
except InvalidPatternCaptureGroupError as ex:
|
||||
self.notify(str(ex), severity="error")
|
||||
return False
|
||||
if updated:
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
|
||||
@@ -19,6 +19,21 @@ class InvalidPatternSchemaError(click.ClickException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPatternCaptureGroupError(click.ClickException):
|
||||
def __init__(self, pattern_id: int, pattern_expression: str):
|
||||
self.pattern_id = int(pattern_id)
|
||||
self.pattern_expression = str(pattern_expression)
|
||||
pattern_label = (
|
||||
f"Pattern #{self.pattern_id}"
|
||||
if self.pattern_id >= 0
|
||||
else "Pattern"
|
||||
)
|
||||
super().__init__(
|
||||
f"{pattern_label} must contain at least one capture group "
|
||||
f"for the season/episode indicator: {self.pattern_expression!r}"
|
||||
)
|
||||
|
||||
|
||||
class PatternController:
|
||||
_compiled_regex_cache: dict[str, re.Pattern] = {}
|
||||
|
||||
@@ -66,6 +81,13 @@ class PatternController:
|
||||
cls._compiled_regex_cache[expression_text] = compiled
|
||||
return compiled
|
||||
|
||||
@classmethod
|
||||
def _validate_pattern_capture_groups(cls, pattern_id: int, expression: str):
|
||||
compiled = cls._compile_pattern_expression(pattern_id, expression)
|
||||
if compiled.groups < 1:
|
||||
raise InvalidPatternCaptureGroupError(pattern_id, expression)
|
||||
return compiled
|
||||
|
||||
def _coerce_pattern_fields(self, patternObj):
|
||||
return {
|
||||
"show_id": int(patternObj["show_id"]),
|
||||
@@ -212,6 +234,10 @@ class PatternController:
|
||||
fields = self._coerce_pattern_fields(patternObj)
|
||||
normalized_tracks = self._normalize_track_descriptors(trackDescriptors)
|
||||
normalized_tags = self._coerce_media_tags(mediaTags)
|
||||
self._validate_pattern_capture_groups(
|
||||
patternId if patternId is not None else -1,
|
||||
fields["pattern"],
|
||||
)
|
||||
session = None
|
||||
|
||||
try:
|
||||
@@ -274,6 +300,7 @@ class PatternController:
|
||||
def updatePattern(self, patternId, patternObj):
|
||||
|
||||
fields = self._coerce_pattern_fields(patternObj)
|
||||
self._validate_pattern_capture_groups(patternId, fields["pattern"])
|
||||
session = None
|
||||
|
||||
try:
|
||||
@@ -409,6 +436,11 @@ class PatternController:
|
||||
patternMatch = compiled.search(str(filename))
|
||||
if patternMatch is None:
|
||||
continue
|
||||
if not patternMatch.groups():
|
||||
raise InvalidPatternCaptureGroupError(
|
||||
pattern.getId(),
|
||||
pattern.getPattern(),
|
||||
)
|
||||
|
||||
self._validate_persisted_pattern(pattern)
|
||||
matches.append({"match": patternMatch, "pattern": pattern})
|
||||
|
||||
@@ -8,6 +8,7 @@ from textual.widgets import Header, Footer, Static, Button, Input, DataTable, Te
|
||||
from textual.containers import Grid
|
||||
|
||||
from ffx.model.pattern import Pattern
|
||||
from ffx.pattern_controller import InvalidPatternCaptureGroupError
|
||||
|
||||
from .track_details_screen import TrackDetailsScreen
|
||||
from .shifted_season_delete_screen import ShiftedSeasonDeleteScreen
|
||||
@@ -575,7 +576,7 @@ class PatternDetailsScreen(Screen):
|
||||
yield Static(" ", classes="five")
|
||||
|
||||
# Row 20
|
||||
yield Static(" ", classes="seven")
|
||||
yield Static(" ", id="messagestatic", classes="seven")
|
||||
|
||||
yield build_screen_log_pane()
|
||||
yield Footer()
|
||||
@@ -610,6 +611,12 @@ class PatternDetailsScreen(Screen):
|
||||
def getNotesFromInput(self):
|
||||
return str(self.query_one("#notes_textarea", TextArea).text)
|
||||
|
||||
def setMessage(self, message: str):
|
||||
try:
|
||||
self.query_one("#messagestatic", Static).update(str(message) or " ")
|
||||
except (NoMatches, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
def getSelectedTrackDescriptor(self):
|
||||
|
||||
@@ -657,25 +664,26 @@ class PatternDetailsScreen(Screen):
|
||||
patternDescriptor['quality'] = self.getQualityFromInput()
|
||||
patternDescriptor['notes'] = self.getNotesFromInput()
|
||||
|
||||
if self.__pattern is not None:
|
||||
try:
|
||||
if self.__pattern is not None:
|
||||
|
||||
if self.__pc.updatePattern(self.__pattern.getId(), patternDescriptor):
|
||||
self.dismiss(patternDescriptor)
|
||||
else:
|
||||
#TODO: Meldung
|
||||
self.app.pop_screen()
|
||||
if self.__pc.updatePattern(self.__pattern.getId(), patternDescriptor):
|
||||
self.dismiss(patternDescriptor)
|
||||
else:
|
||||
self.setMessage(t("Unable to update pattern."))
|
||||
|
||||
else:
|
||||
patternId = self.__pc.savePatternSchema(
|
||||
patternDescriptor,
|
||||
trackDescriptors=self.__draftTracks,
|
||||
mediaTags=self.__draftTags,
|
||||
)
|
||||
if patternId:
|
||||
self.dismiss(patternDescriptor)
|
||||
else:
|
||||
#TODO: Meldung
|
||||
self.app.pop_screen()
|
||||
patternId = self.__pc.savePatternSchema(
|
||||
patternDescriptor,
|
||||
trackDescriptors=self.__draftTracks,
|
||||
mediaTags=self.__draftTags,
|
||||
)
|
||||
if patternId:
|
||||
self.dismiss(patternDescriptor)
|
||||
else:
|
||||
self.setMessage(t("Unable to save pattern."))
|
||||
except InvalidPatternCaptureGroupError as ex:
|
||||
self.setMessage(str(ex))
|
||||
|
||||
|
||||
if event.button.id == "cancel_button":
|
||||
|
||||
@@ -21,6 +21,7 @@ from ffx.file_properties import FileProperties # noqa: E402
|
||||
from ffx.model.pattern import Pattern # noqa: E402
|
||||
from ffx.pattern_controller import ( # noqa: E402
|
||||
DuplicatePatternMatchError,
|
||||
InvalidPatternCaptureGroupError,
|
||||
InvalidPatternSchemaError,
|
||||
PatternController,
|
||||
)
|
||||
@@ -144,7 +145,7 @@ class PatternManagementTests(unittest.TestCase):
|
||||
|
||||
def test_match_filename_raises_for_duplicate_matches_in_same_show(self):
|
||||
self.save_pattern(1, r"^same_(s[0-9]+e[0-9]+)\.mkv$")
|
||||
self.save_pattern(1, r"^same_.*$")
|
||||
self.save_pattern(1, r"^same_(.*)$")
|
||||
|
||||
with self.assertRaises(DuplicatePatternMatchError) as caught:
|
||||
self.pattern_controller.matchFilename("same_s01e01.mkv")
|
||||
@@ -154,7 +155,7 @@ class PatternManagementTests(unittest.TestCase):
|
||||
|
||||
def test_match_filename_raises_for_duplicate_matches_across_shows(self):
|
||||
self.save_pattern(1, r"^cross_(s[0-9]+e[0-9]+)\.mkv$")
|
||||
self.save_pattern(2, r"^cross_.*$")
|
||||
self.save_pattern(2, r"^cross_(.*)$")
|
||||
|
||||
with self.assertRaises(DuplicatePatternMatchError) as caught:
|
||||
self.pattern_controller.matchFilename("cross_s01e01.mkv")
|
||||
@@ -181,6 +182,48 @@ class PatternManagementTests(unittest.TestCase):
|
||||
match = self.pattern_controller.matchFilename("after_s01e01.mkv")
|
||||
self.assertEqual(pattern_id, match["pattern"].getId())
|
||||
|
||||
def test_save_pattern_schema_rejects_pattern_without_capture_group(self):
|
||||
self.add_show(1, "No Capture Pattern Show")
|
||||
|
||||
with self.assertRaises(InvalidPatternCaptureGroupError) as caught:
|
||||
self.pattern_controller.savePatternSchema(
|
||||
{
|
||||
"show_id": 1,
|
||||
"pattern": r"^nocapture_s[0-9]+e[0-9]+\.mkv$",
|
||||
},
|
||||
trackDescriptors=[make_track_descriptor(0)],
|
||||
)
|
||||
|
||||
self.assertIn("capture group", str(caught.exception))
|
||||
|
||||
def test_update_pattern_rejects_pattern_without_capture_group(self):
|
||||
pattern_id = self.save_pattern(1, r"^valid_(s[0-9]+e[0-9]+)\.mkv$")
|
||||
|
||||
with self.assertRaises(InvalidPatternCaptureGroupError):
|
||||
self.pattern_controller.updatePattern(
|
||||
pattern_id,
|
||||
{
|
||||
"show_id": 1,
|
||||
"pattern": r"^invalid_s[0-9]+e[0-9]+\.mkv$",
|
||||
"quality": 0,
|
||||
"notes": "",
|
||||
},
|
||||
)
|
||||
|
||||
match = self.pattern_controller.matchFilename("valid_s01e01.mkv")
|
||||
self.assertEqual(pattern_id, match["pattern"].getId())
|
||||
|
||||
def test_match_filename_raises_specific_error_for_legacy_pattern_without_capture_group(self):
|
||||
pattern_id = self.insert_trackless_pattern_row(
|
||||
1,
|
||||
r"^legacy_s[0-9]+e[0-9]+\.mkv$",
|
||||
)
|
||||
|
||||
with self.assertRaises(InvalidPatternCaptureGroupError) as caught:
|
||||
self.pattern_controller.matchFilename("legacy_s01e01.mkv")
|
||||
|
||||
self.assertEqual(pattern_id, caught.exception.pattern_id)
|
||||
|
||||
def test_save_pattern_schema_rejects_zero_track_patterns(self):
|
||||
self.add_show(1, "Empty Pattern Show")
|
||||
|
||||
@@ -276,7 +319,7 @@ class PatternManagementTests(unittest.TestCase):
|
||||
|
||||
def test_file_properties_skips_pattern_matching_when_disabled(self):
|
||||
self.save_pattern(1, r"^nopattern_(s[0-9]+e[0-9]+)\.mkv$")
|
||||
self.save_pattern(2, r"^nopattern_.*$")
|
||||
self.save_pattern(2, r"^nopattern_(.*)$")
|
||||
|
||||
no_pattern_context = dict(self.context)
|
||||
no_pattern_context["use_pattern"] = False
|
||||
|
||||
Reference in New Issue
Block a user