Compare commits
5 Commits
2aeeaac204
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9432ec564 | ||
|
|
8f5873fbab | ||
|
|
12e5438d22 | ||
|
|
6f09c80aaf | ||
|
|
f49d6f4838 |
@@ -1,6 +1,11 @@
|
|||||||
Version History
|
Version History
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
0.4.5
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Release version bumped to 0.4.5.
|
||||||
|
|
||||||
0.4.4
|
0.4.4
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ffx"
|
name = "ffx"
|
||||||
description = "FFX recoding and metadata managing tool"
|
description = "FFX recoding and metadata managing tool"
|
||||||
version = "0.4.4"
|
version = "0.4.5"
|
||||||
license = {file = "LICENSE.md"}
|
license = {file = "LICENSE.md"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"requests",
|
"requests",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
VERSION='0.4.4'
|
VERSION='0.4.5'
|
||||||
DATABASE_VERSION = 3
|
DATABASE_VERSION = 3
|
||||||
|
|
||||||
DEFAULT_QUALITY = 32
|
DEFAULT_QUALITY = 32
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
|
||||||
from .i18n import set_current_language, t
|
from .i18n import set_current_language, t
|
||||||
|
from .confirm_screen import ConfirmScreen
|
||||||
from .shows_screen import ShowsScreen
|
from .shows_screen import ShowsScreen
|
||||||
from .inspect_details_screen import InspectDetailsScreen
|
from .inspect_details_screen import InspectDetailsScreen
|
||||||
from .media_edit_screen import MediaEditScreen
|
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
|
from .screen_support import configure_screen_log_handler, set_screen_log_pane_enabled
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +42,20 @@ class FfxApp(App):
|
|||||||
self.push_screen(ShowsScreen())
|
self.push_screen(ShowsScreen())
|
||||||
|
|
||||||
if self.context['command'] == 'inspect':
|
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':
|
if self.context['command'] == 'edit':
|
||||||
self.push_screen(MediaEditScreen())
|
self.push_screen(MediaEditScreen())
|
||||||
@@ -49,3 +64,13 @@ class FfxApp(App):
|
|||||||
def getContext(self):
|
def getContext(self):
|
||||||
"""Data 'output' method"""
|
"""Data 'output' method"""
|
||||||
return self.context
|
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.file_properties import FileProperties
|
||||||
from ffx.helper import DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
|
from ffx.helper import DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY
|
||||||
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet
|
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet
|
||||||
|
from ffx.pattern_controller import InvalidPatternCaptureGroupError
|
||||||
from ffx.show_descriptor import ShowDescriptor
|
from ffx.show_descriptor import ShowDescriptor
|
||||||
from ffx.track_descriptor import TrackDescriptor
|
from ffx.track_descriptor import TrackDescriptor
|
||||||
|
|
||||||
@@ -472,11 +473,15 @@ class InspectDetailsScreen(MediaWorkflowScreenBase):
|
|||||||
):
|
):
|
||||||
mediaTags[tagKey] = tagValue
|
mediaTags[tagKey] = tagValue
|
||||||
|
|
||||||
patternId = self._pc.savePatternSchema(
|
try:
|
||||||
patternObj,
|
patternId = self._pc.savePatternSchema(
|
||||||
trackDescriptors=self._sourceMediaDescriptor.getTrackDescriptors(),
|
patternObj,
|
||||||
mediaTags=mediaTags,
|
trackDescriptors=self._sourceMediaDescriptor.getTrackDescriptors(),
|
||||||
)
|
mediaTags=mediaTags,
|
||||||
|
)
|
||||||
|
except InvalidPatternCaptureGroupError as ex:
|
||||||
|
self.notify(str(ex), severity="error")
|
||||||
|
return
|
||||||
if patternId:
|
if patternId:
|
||||||
self.reloadProperties(reset_draft=True)
|
self.reloadProperties(reset_draft=True)
|
||||||
self.updateMediaTags()
|
self.updateMediaTags()
|
||||||
@@ -498,10 +503,14 @@ class InspectDetailsScreen(MediaWorkflowScreenBase):
|
|||||||
patternObj
|
patternObj
|
||||||
and self._currentPattern.getPattern() != patternObj["pattern"]
|
and self._currentPattern.getPattern() != patternObj["pattern"]
|
||||||
):
|
):
|
||||||
updated = self._pc.updatePattern(
|
try:
|
||||||
self._currentPattern.getId(),
|
updated = self._pc.updatePattern(
|
||||||
patternObj,
|
self._currentPattern.getId(),
|
||||||
)
|
patternObj,
|
||||||
|
)
|
||||||
|
except InvalidPatternCaptureGroupError as ex:
|
||||||
|
self.notify(str(ex), severity="error")
|
||||||
|
return False
|
||||||
if updated:
|
if updated:
|
||||||
self.reloadProperties(reset_draft=True)
|
self.reloadProperties(reset_draft=True)
|
||||||
self.updateMediaTags()
|
self.updateMediaTags()
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ class InvalidPatternSchemaError(click.ClickException):
|
|||||||
pass
|
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:
|
class PatternController:
|
||||||
_compiled_regex_cache: dict[str, re.Pattern] = {}
|
_compiled_regex_cache: dict[str, re.Pattern] = {}
|
||||||
|
|
||||||
@@ -66,6 +81,13 @@ class PatternController:
|
|||||||
cls._compiled_regex_cache[expression_text] = compiled
|
cls._compiled_regex_cache[expression_text] = compiled
|
||||||
return 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):
|
def _coerce_pattern_fields(self, patternObj):
|
||||||
return {
|
return {
|
||||||
"show_id": int(patternObj["show_id"]),
|
"show_id": int(patternObj["show_id"]),
|
||||||
@@ -212,6 +234,10 @@ class PatternController:
|
|||||||
fields = self._coerce_pattern_fields(patternObj)
|
fields = self._coerce_pattern_fields(patternObj)
|
||||||
normalized_tracks = self._normalize_track_descriptors(trackDescriptors)
|
normalized_tracks = self._normalize_track_descriptors(trackDescriptors)
|
||||||
normalized_tags = self._coerce_media_tags(mediaTags)
|
normalized_tags = self._coerce_media_tags(mediaTags)
|
||||||
|
self._validate_pattern_capture_groups(
|
||||||
|
patternId if patternId is not None else -1,
|
||||||
|
fields["pattern"],
|
||||||
|
)
|
||||||
session = None
|
session = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -274,6 +300,7 @@ class PatternController:
|
|||||||
def updatePattern(self, patternId, patternObj):
|
def updatePattern(self, patternId, patternObj):
|
||||||
|
|
||||||
fields = self._coerce_pattern_fields(patternObj)
|
fields = self._coerce_pattern_fields(patternObj)
|
||||||
|
self._validate_pattern_capture_groups(patternId, fields["pattern"])
|
||||||
session = None
|
session = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -409,6 +436,11 @@ class PatternController:
|
|||||||
patternMatch = compiled.search(str(filename))
|
patternMatch = compiled.search(str(filename))
|
||||||
if patternMatch is None:
|
if patternMatch is None:
|
||||||
continue
|
continue
|
||||||
|
if not patternMatch.groups():
|
||||||
|
raise InvalidPatternCaptureGroupError(
|
||||||
|
pattern.getId(),
|
||||||
|
pattern.getPattern(),
|
||||||
|
)
|
||||||
|
|
||||||
self._validate_persisted_pattern(pattern)
|
self._validate_persisted_pattern(pattern)
|
||||||
matches.append({"match": patternMatch, "pattern": pattern})
|
matches.append({"match": patternMatch, "pattern": pattern})
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import click, re
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from textual import events
|
from textual import events
|
||||||
|
from textual.css.query import NoMatches
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Header, Footer, Static, Button, Input, DataTable, TextArea
|
from textual.widgets import Header, Footer, Static, Button, Input, DataTable, TextArea
|
||||||
from textual.containers import Grid
|
from textual.containers import Grid
|
||||||
|
|
||||||
from ffx.model.pattern import Pattern
|
from ffx.model.pattern import Pattern
|
||||||
|
from ffx.pattern_controller import InvalidPatternCaptureGroupError
|
||||||
|
|
||||||
from .track_details_screen import TrackDetailsScreen
|
from .track_details_screen import TrackDetailsScreen
|
||||||
from .track_delete_screen import TrackDeleteScreen
|
|
||||||
from .shifted_season_delete_screen import ShiftedSeasonDeleteScreen
|
from .shifted_season_delete_screen import ShiftedSeasonDeleteScreen
|
||||||
from .shifted_season_details_screen import ShiftedSeasonDetailsScreen
|
from .shifted_season_details_screen import ShiftedSeasonDetailsScreen
|
||||||
|
|
||||||
@@ -154,6 +155,13 @@ class PatternDetailsScreen(Screen):
|
|||||||
|
|
||||||
|
|
||||||
def updateTracks(self):
|
def updateTracks(self):
|
||||||
|
selectedTrack = self.getSelectedTrackDescriptor()
|
||||||
|
selectedTrackIndex = (
|
||||||
|
self._track_row_index(selectedTrack)
|
||||||
|
if selectedTrack is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
selectedCursorColumn = getattr(self.tracksTable.cursor_coordinate, "column", 0)
|
||||||
|
|
||||||
self.tracksTable.clear()
|
self.tracksTable.clear()
|
||||||
self.__trackRowData = {}
|
self.__trackRowData = {}
|
||||||
@@ -161,6 +169,7 @@ class PatternDetailsScreen(Screen):
|
|||||||
tracks = self.getCurrentTrackDescriptors()
|
tracks = self.getCurrentTrackDescriptors()
|
||||||
|
|
||||||
typeCounter = {}
|
typeCounter = {}
|
||||||
|
restoredRowIndex = None
|
||||||
|
|
||||||
td: TrackDescriptor
|
td: TrackDescriptor
|
||||||
for td in tracks:
|
for td in tracks:
|
||||||
@@ -190,9 +199,72 @@ class PatternDetailsScreen(Screen):
|
|||||||
|
|
||||||
row_key = self.tracksTable.add_row(*map(str, row))
|
row_key = self.tracksTable.add_row(*map(str, row))
|
||||||
self.__trackRowData[row_key] = td
|
self.__trackRowData[row_key] = td
|
||||||
|
if (
|
||||||
|
selectedTrack is not None
|
||||||
|
and self._descriptor_refs_same_track(td, selectedTrack)
|
||||||
|
):
|
||||||
|
restoredRowIndex = len(self.__trackRowData) - 1
|
||||||
|
|
||||||
typeCounter[trackType] += 1
|
typeCounter[trackType] += 1
|
||||||
|
|
||||||
|
if restoredRowIndex is None and selectedTrackIndex is not None and self.__trackRowData:
|
||||||
|
restoredRowIndex = min(selectedTrackIndex, len(self.__trackRowData) - 1)
|
||||||
|
|
||||||
|
if restoredRowIndex is not None:
|
||||||
|
self.tracksTable.move_cursor(
|
||||||
|
row=restoredRowIndex,
|
||||||
|
column=selectedCursorColumn,
|
||||||
|
animate=False,
|
||||||
|
scroll=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.updateTrackDropButtonState()
|
||||||
|
|
||||||
|
def _track_row_index(self, trackDescriptor: TrackDescriptor | None):
|
||||||
|
if trackDescriptor is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for rowIndex, descriptor in enumerate(self.__trackRowData.values()):
|
||||||
|
if self._descriptor_refs_same_track(descriptor, trackDescriptor):
|
||||||
|
return rowIndex
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _descriptor_refs_same_track(
|
||||||
|
self,
|
||||||
|
descriptor: TrackDescriptor,
|
||||||
|
otherDescriptor: TrackDescriptor,
|
||||||
|
) -> bool:
|
||||||
|
if descriptor.getId() != -1 and otherDescriptor.getId() != -1:
|
||||||
|
return descriptor.getId() == otherDescriptor.getId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
descriptor.getPatternId() == otherDescriptor.getPatternId()
|
||||||
|
and descriptor.getIndex() == otherDescriptor.getIndex()
|
||||||
|
and descriptor.getSubIndex() == otherDescriptor.getSubIndex()
|
||||||
|
and descriptor.getType() == otherDescriptor.getType()
|
||||||
|
)
|
||||||
|
|
||||||
|
def updateTrackDropButtonState(self):
|
||||||
|
try:
|
||||||
|
button = self.query_one("#button_toggle_track_drop", Button)
|
||||||
|
except (NoMatches, AttributeError):
|
||||||
|
return
|
||||||
|
|
||||||
|
selectedTrack = self.getSelectedTrackDescriptor()
|
||||||
|
button.disabled = selectedTrack is None
|
||||||
|
button.variant = (
|
||||||
|
"warning"
|
||||||
|
if selectedTrack is not None and selectedTrack.isDropped()
|
||||||
|
else "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_data_table_row_highlighted(
|
||||||
|
self,
|
||||||
|
event: DataTable.RowHighlighted,
|
||||||
|
) -> None:
|
||||||
|
if event.data_table.id == "tracks_table":
|
||||||
|
self.updateTrackDropButtonState()
|
||||||
|
|
||||||
|
|
||||||
def getCurrentTrackDescriptors(self) -> List[TrackDescriptor]:
|
def getCurrentTrackDescriptors(self) -> List[TrackDescriptor]:
|
||||||
if self.__pattern is not None:
|
if self.__pattern is not None:
|
||||||
@@ -483,7 +555,7 @@ class PatternDetailsScreen(Screen):
|
|||||||
yield Static(t("Streams"))
|
yield Static(t("Streams"))
|
||||||
yield Button(t("Add"), id="button_add_track")
|
yield Button(t("Add"), id="button_add_track")
|
||||||
yield Button(t("Edit"), id="button_edit_track")
|
yield Button(t("Edit"), id="button_edit_track")
|
||||||
yield Button(t("Delete"), id="button_delete_track")
|
yield Button(t("Drop"), id="button_toggle_track_drop")
|
||||||
|
|
||||||
yield Static(" ")
|
yield Static(" ")
|
||||||
yield Button(t("Up"), id="button_track_up")
|
yield Button(t("Up"), id="button_track_up")
|
||||||
@@ -504,7 +576,7 @@ class PatternDetailsScreen(Screen):
|
|||||||
yield Static(" ", classes="five")
|
yield Static(" ", classes="five")
|
||||||
|
|
||||||
# Row 20
|
# Row 20
|
||||||
yield Static(" ", classes="seven")
|
yield Static(" ", id="messagestatic", classes="seven")
|
||||||
|
|
||||||
yield build_screen_log_pane()
|
yield build_screen_log_pane()
|
||||||
yield Footer()
|
yield Footer()
|
||||||
@@ -539,6 +611,12 @@ class PatternDetailsScreen(Screen):
|
|||||||
def getNotesFromInput(self):
|
def getNotesFromInput(self):
|
||||||
return str(self.query_one("#notes_textarea", TextArea).text)
|
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):
|
def getSelectedTrackDescriptor(self):
|
||||||
|
|
||||||
@@ -586,25 +664,26 @@ class PatternDetailsScreen(Screen):
|
|||||||
patternDescriptor['quality'] = self.getQualityFromInput()
|
patternDescriptor['quality'] = self.getQualityFromInput()
|
||||||
patternDescriptor['notes'] = self.getNotesFromInput()
|
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):
|
if self.__pc.updatePattern(self.__pattern.getId(), patternDescriptor):
|
||||||
self.dismiss(patternDescriptor)
|
self.dismiss(patternDescriptor)
|
||||||
else:
|
else:
|
||||||
#TODO: Meldung
|
self.setMessage(t("Unable to update pattern."))
|
||||||
self.app.pop_screen()
|
|
||||||
|
|
||||||
else:
|
|
||||||
patternId = self.__pc.savePatternSchema(
|
|
||||||
patternDescriptor,
|
|
||||||
trackDescriptors=self.__draftTracks,
|
|
||||||
mediaTags=self.__draftTags,
|
|
||||||
)
|
|
||||||
if patternId:
|
|
||||||
self.dismiss(patternDescriptor)
|
|
||||||
else:
|
else:
|
||||||
#TODO: Meldung
|
patternId = self.__pc.savePatternSchema(
|
||||||
self.app.pop_screen()
|
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":
|
if event.button.id == "cancel_button":
|
||||||
@@ -665,11 +744,8 @@ class PatternDetailsScreen(Screen):
|
|||||||
),
|
),
|
||||||
self.handle_edit_track,
|
self.handle_edit_track,
|
||||||
)
|
)
|
||||||
if event.button.id == "button_delete_track":
|
if event.button.id == "button_toggle_track_drop":
|
||||||
self.app.push_screen(
|
self.toggleSelectedTrackDropped(selectedTrack)
|
||||||
TrackDeleteScreen(trackDescriptor = selectedTrack),
|
|
||||||
self.handle_delete_track,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if event.button.id == "button_add_tag":
|
if event.button.id == "button_add_tag":
|
||||||
@@ -753,6 +829,27 @@ class PatternDetailsScreen(Screen):
|
|||||||
|
|
||||||
self.updateTracks()
|
self.updateTracks()
|
||||||
|
|
||||||
|
def toggleSelectedTrackDropped(self, trackDescriptor: TrackDescriptor):
|
||||||
|
if trackDescriptor is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
updatedTrack = trackDescriptor.clone(context=self.context)
|
||||||
|
updatedTrack.setDropped(not trackDescriptor.isDropped())
|
||||||
|
|
||||||
|
if self.__pattern is not None:
|
||||||
|
if not self.__tc.updateTrack(updatedTrack.getId(), updatedTrack):
|
||||||
|
raise click.ClickException("PatternDetailsScreen.toggleSelectedTrackDropped(): track update failed")
|
||||||
|
else:
|
||||||
|
for currentTrack in self.__draftTracks:
|
||||||
|
if (
|
||||||
|
currentTrack.getIndex() == trackDescriptor.getIndex()
|
||||||
|
and currentTrack.getSubIndex() == trackDescriptor.getSubIndex()
|
||||||
|
):
|
||||||
|
currentTrack.setDropped(updatedTrack.isDropped())
|
||||||
|
break
|
||||||
|
|
||||||
|
self.updateTracks()
|
||||||
|
|
||||||
|
|
||||||
def handle_delete_track(self, trackDescriptor : TrackDescriptor):
|
def handle_delete_track(self, trackDescriptor : TrackDescriptor):
|
||||||
if trackDescriptor is None:
|
if trackDescriptor is None:
|
||||||
|
|||||||
@@ -257,11 +257,7 @@ class TrackDetailsScreen(Screen):
|
|||||||
|
|
||||||
self.query_one("#audio_layout_select", Select).value = self.__audioLayout
|
self.query_one("#audio_layout_select", Select).value = self.__audioLayout
|
||||||
|
|
||||||
# CB
|
|
||||||
self.query_one("#drop_checkbox", Checkbox).value = self.__dropped
|
self.query_one("#drop_checkbox", Checkbox).value = self.__dropped
|
||||||
# self.query_one("#drop_checkbox", SelectionList).add_option(
|
|
||||||
# (t("Yes"), 0, self.__dropped)
|
|
||||||
# )
|
|
||||||
|
|
||||||
for disposition in TrackDisposition:
|
for disposition in TrackDisposition:
|
||||||
|
|
||||||
@@ -292,9 +288,7 @@ class TrackDetailsScreen(Screen):
|
|||||||
self.query_one("#type_select", Select).disabled = True
|
self.query_one("#type_select", Select).disabled = True
|
||||||
self.query_one("#audio_layout_select", Select).disabled = True
|
self.query_one("#audio_layout_select", Select).disabled = True
|
||||||
|
|
||||||
# CB
|
|
||||||
self.query_one("#drop_checkbox", Checkbox).disabled = True
|
self.query_one("#drop_checkbox", Checkbox).disabled = True
|
||||||
# self.query_one("#drop_checkbox", SelectionList).disabled = True
|
|
||||||
|
|
||||||
def on_select_changed(self, event: Select.Changed) -> None:
|
def on_select_changed(self, event: Select.Changed) -> None:
|
||||||
if event.select.id == "language_select":
|
if event.select.id == "language_select":
|
||||||
@@ -358,9 +352,7 @@ class TrackDetailsScreen(Screen):
|
|||||||
# Row 8
|
# Row 8
|
||||||
yield Static(t("Drop"))
|
yield Static(t("Drop"))
|
||||||
|
|
||||||
# CB
|
|
||||||
yield Checkbox(id="drop_checkbox", classes="four")
|
yield Checkbox(id="drop_checkbox", classes="four")
|
||||||
# yield SelectionList[int](id="drop_checkbox", classes="four")
|
|
||||||
|
|
||||||
# Row 9
|
# Row 9
|
||||||
yield Static(" ", classes="five")
|
yield Static(" ", classes="five")
|
||||||
@@ -479,7 +471,7 @@ class TrackDetailsScreen(Screen):
|
|||||||
|
|
||||||
kwargs[TrackDescriptor.TAGS_KEY] = trackTags
|
kwargs[TrackDescriptor.TAGS_KEY] = trackTags
|
||||||
kwargs[TrackDescriptor.DROPPED_KEY] = bool(
|
kwargs[TrackDescriptor.DROPPED_KEY] = bool(
|
||||||
self.query_one("#drop_checkbox", SelectionList).selected
|
self.query_one("#drop_checkbox", Checkbox).value
|
||||||
)
|
)
|
||||||
|
|
||||||
dispositionFlags = sum(
|
dispositionFlags = sum(
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class PatternManagementCliTests(unittest.TestCase):
|
|||||||
[to_track_descriptor(track_descriptors[0])],
|
[to_track_descriptor(track_descriptors[0])],
|
||||||
)
|
)
|
||||||
controller.savePatternSchema(
|
controller.savePatternSchema(
|
||||||
{"show_id": 2, "pattern": r"^dup_.*$"},
|
{"show_id": 2, "pattern": r"^dup_(s[0-9]+e[0-9]+).*$"},
|
||||||
[to_track_descriptor(track_descriptors[0])],
|
[to_track_descriptor(track_descriptors[0])],
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from ffx.file_properties import FileProperties # noqa: E402
|
|||||||
from ffx.model.pattern import Pattern # noqa: E402
|
from ffx.model.pattern import Pattern # noqa: E402
|
||||||
from ffx.pattern_controller import ( # noqa: E402
|
from ffx.pattern_controller import ( # noqa: E402
|
||||||
DuplicatePatternMatchError,
|
DuplicatePatternMatchError,
|
||||||
|
InvalidPatternCaptureGroupError,
|
||||||
InvalidPatternSchemaError,
|
InvalidPatternSchemaError,
|
||||||
PatternController,
|
PatternController,
|
||||||
)
|
)
|
||||||
@@ -144,7 +145,7 @@ class PatternManagementTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_match_filename_raises_for_duplicate_matches_in_same_show(self):
|
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_(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:
|
with self.assertRaises(DuplicatePatternMatchError) as caught:
|
||||||
self.pattern_controller.matchFilename("same_s01e01.mkv")
|
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):
|
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(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:
|
with self.assertRaises(DuplicatePatternMatchError) as caught:
|
||||||
self.pattern_controller.matchFilename("cross_s01e01.mkv")
|
self.pattern_controller.matchFilename("cross_s01e01.mkv")
|
||||||
@@ -181,6 +182,48 @@ class PatternManagementTests(unittest.TestCase):
|
|||||||
match = self.pattern_controller.matchFilename("after_s01e01.mkv")
|
match = self.pattern_controller.matchFilename("after_s01e01.mkv")
|
||||||
self.assertEqual(pattern_id, match["pattern"].getId())
|
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):
|
def test_save_pattern_schema_rejects_zero_track_patterns(self):
|
||||||
self.add_show(1, "Empty Pattern Show")
|
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):
|
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(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 = dict(self.context)
|
||||||
no_pattern_context["use_pattern"] = False
|
no_pattern_context["use_pattern"] = False
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ class FakeTagTable:
|
|||||||
def select_row(self, row_key):
|
def select_row(self, row_key):
|
||||||
self._selected_row_key = row_key
|
self._selected_row_key = row_key
|
||||||
|
|
||||||
|
def move_cursor(self, *, row=None, column=None, animate=False, scroll=True):
|
||||||
|
if row is not None and self._row_order:
|
||||||
|
self._selected_row_key = self._row_order[int(row)]
|
||||||
|
if column is not None:
|
||||||
|
self.cursor_coordinate = (self.cursor_coordinate[0], int(column))
|
||||||
|
|
||||||
def get_row_index(self, row_key):
|
def get_row_index(self, row_key):
|
||||||
return self._row_order.index(row_key)
|
return self._row_order.index(row_key)
|
||||||
|
|
||||||
@@ -110,6 +116,12 @@ class FakeValueWidget:
|
|||||||
self.disabled = False
|
self.disabled = False
|
||||||
|
|
||||||
|
|
||||||
|
class FakeButtonWidget:
|
||||||
|
def __init__(self):
|
||||||
|
self.disabled = False
|
||||||
|
self.variant = "default"
|
||||||
|
|
||||||
|
|
||||||
class FakeInputWidget:
|
class FakeInputWidget:
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
@@ -190,7 +202,7 @@ class TagTableScreenStateTests(unittest.TestCase):
|
|||||||
widgets = {
|
widgets = {
|
||||||
"#type_select": FakeValueWidget(TrackType.AUDIO),
|
"#type_select": FakeValueWidget(TrackType.AUDIO),
|
||||||
"#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_STEREO),
|
"#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_STEREO),
|
||||||
"#drop_checkbox": FakeSelectionListWidget({0}),
|
"#drop_checkbox": FakeValueWidget(True),
|
||||||
"#language_select": FakeValueWidget(IsoLanguage.GERMAN),
|
"#language_select": FakeValueWidget(IsoLanguage.GERMAN),
|
||||||
"#title_input": FakeInputWidget("German Audio"),
|
"#title_input": FakeInputWidget("German Audio"),
|
||||||
"#dispositions_selection_list": FakeSelectionListWidget({0, 6}),
|
"#dispositions_selection_list": FakeSelectionListWidget({0, 6}),
|
||||||
@@ -220,7 +232,7 @@ class TagTableScreenStateTests(unittest.TestCase):
|
|||||||
widgets = {
|
widgets = {
|
||||||
"#type_select": FakeValueWidget(TrackType.ATTACHMENT),
|
"#type_select": FakeValueWidget(TrackType.ATTACHMENT),
|
||||||
"#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_UNDEFINED),
|
"#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_UNDEFINED),
|
||||||
"#drop_checkbox": FakeSelectionListWidget(set()),
|
"#drop_checkbox": FakeValueWidget(False),
|
||||||
"#language_select": FakeValueWidget(Select.NULL),
|
"#language_select": FakeValueWidget(Select.NULL),
|
||||||
"#title_input": FakeInputWidget(""),
|
"#title_input": FakeInputWidget(""),
|
||||||
"#dispositions_selection_list": FakeSelectionListWidget(set()),
|
"#dispositions_selection_list": FakeSelectionListWidget(set()),
|
||||||
@@ -329,7 +341,7 @@ class TagTableScreenStateTests(unittest.TestCase):
|
|||||||
"#pattern_label": FakeStaticWidget(),
|
"#pattern_label": FakeStaticWidget(),
|
||||||
"#type_select": FakeValueWidget(None),
|
"#type_select": FakeValueWidget(None),
|
||||||
"#audio_layout_select": FakeValueWidget(None),
|
"#audio_layout_select": FakeValueWidget(None),
|
||||||
"#drop_checkbox": FakeSelectionListWidget(set()),
|
"#drop_checkbox": FakeValueWidget(False),
|
||||||
"#dispositions_selection_list": FakeSelectionListWidget(set()),
|
"#dispositions_selection_list": FakeSelectionListWidget(set()),
|
||||||
"#language_select": FakeValueWidget(None),
|
"#language_select": FakeValueWidget(None),
|
||||||
"#title_input": FakeInputWidget(""),
|
"#title_input": FakeInputWidget(""),
|
||||||
@@ -376,6 +388,100 @@ class TagTableScreenStateTests(unittest.TestCase):
|
|||||||
self.assertIs(second_track, screen.getSelectedTrackDescriptor())
|
self.assertIs(second_track, screen.getSelectedTrackDescriptor())
|
||||||
self.assertEqual("Ja", screen.tracksTable.rows["row-1"][9])
|
self.assertEqual("Ja", screen.tracksTable.rows["row-1"][9])
|
||||||
|
|
||||||
|
def test_pattern_details_screen_toggles_draft_track_drop_state(self):
|
||||||
|
first_track = make_track_descriptor(0, 0, TrackType.VIDEO)
|
||||||
|
second_track = make_track_descriptor(1, 0, TrackType.SUBTITLE)
|
||||||
|
drop_button = FakeButtonWidget()
|
||||||
|
|
||||||
|
screen = object.__new__(PatternDetailsScreen)
|
||||||
|
screen.context = {"logger": get_ffx_logger()}
|
||||||
|
screen.tracksTable = FakeTagTable()
|
||||||
|
screen._PatternDetailsScreen__draftTracks = [first_track, second_track]
|
||||||
|
screen._PatternDetailsScreen__pattern = None
|
||||||
|
screen._PatternDetailsScreen__trackRowData = {}
|
||||||
|
screen.query_one = lambda selector, _widget_type=None: drop_button
|
||||||
|
|
||||||
|
screen.updateTracks()
|
||||||
|
screen.tracksTable.select_row("row-1")
|
||||||
|
|
||||||
|
screen.toggleSelectedTrackDropped(screen.getSelectedTrackDescriptor())
|
||||||
|
|
||||||
|
self.assertFalse(first_track.isDropped())
|
||||||
|
self.assertTrue(second_track.isDropped())
|
||||||
|
self.assertEqual("Ja", list(screen.tracksTable.rows.values())[1][9])
|
||||||
|
self.assertIs(second_track, screen.getSelectedTrackDescriptor())
|
||||||
|
self.assertEqual("warning", drop_button.variant)
|
||||||
|
|
||||||
|
def test_pattern_details_screen_updates_drop_button_for_selected_track(self):
|
||||||
|
first_track = make_track_descriptor(0, 0, TrackType.VIDEO)
|
||||||
|
second_track = make_track_descriptor(
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
TrackType.SUBTITLE,
|
||||||
|
dropped=True,
|
||||||
|
)
|
||||||
|
drop_button = FakeButtonWidget()
|
||||||
|
|
||||||
|
screen = object.__new__(PatternDetailsScreen)
|
||||||
|
screen.tracksTable = FakeTagTable()
|
||||||
|
screen._PatternDetailsScreen__draftTracks = [first_track, second_track]
|
||||||
|
screen._PatternDetailsScreen__pattern = None
|
||||||
|
screen._PatternDetailsScreen__trackRowData = {}
|
||||||
|
screen.query_one = lambda selector, _widget_type=None: drop_button
|
||||||
|
|
||||||
|
screen.updateTracks()
|
||||||
|
|
||||||
|
self.assertEqual("default", drop_button.variant)
|
||||||
|
|
||||||
|
screen.tracksTable.select_row("row-1")
|
||||||
|
screen.updateTrackDropButtonState()
|
||||||
|
|
||||||
|
self.assertEqual("warning", drop_button.variant)
|
||||||
|
|
||||||
|
screen.tracksTable.select_row("row-0")
|
||||||
|
screen.updateTrackDropButtonState()
|
||||||
|
|
||||||
|
self.assertEqual("default", drop_button.variant)
|
||||||
|
|
||||||
|
def test_pattern_details_screen_toggles_persisted_track_drop_state(self):
|
||||||
|
class _FakePattern:
|
||||||
|
def getId(self):
|
||||||
|
return 5
|
||||||
|
|
||||||
|
class _FakeTrackController:
|
||||||
|
def __init__(self):
|
||||||
|
self.updated = []
|
||||||
|
|
||||||
|
def updateTrack(self, track_id, track_descriptor):
|
||||||
|
self.updated.append((track_id, track_descriptor))
|
||||||
|
return True
|
||||||
|
|
||||||
|
track = TrackDescriptor(
|
||||||
|
id=17,
|
||||||
|
pattern_id=5,
|
||||||
|
index=1,
|
||||||
|
source_index=1,
|
||||||
|
sub_index=0,
|
||||||
|
track_type=TrackType.SUBTITLE,
|
||||||
|
codec_name=TrackCodec.UNKNOWN,
|
||||||
|
dropped=False,
|
||||||
|
)
|
||||||
|
track_controller = _FakeTrackController()
|
||||||
|
|
||||||
|
screen = object.__new__(PatternDetailsScreen)
|
||||||
|
screen.context = {"logger": get_ffx_logger()}
|
||||||
|
screen._PatternDetailsScreen__pattern = _FakePattern()
|
||||||
|
screen._PatternDetailsScreen__tc = track_controller
|
||||||
|
screen.updateTracks = lambda: None
|
||||||
|
|
||||||
|
screen.toggleSelectedTrackDropped(track)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(track_controller.updated))
|
||||||
|
track_id, updated_track = track_controller.updated[0]
|
||||||
|
self.assertEqual(17, track_id)
|
||||||
|
self.assertFalse(track.isDropped())
|
||||||
|
self.assertTrue(updated_track.isDropped())
|
||||||
|
|
||||||
def test_pattern_details_screen_reads_selected_tag_from_raw_row_mapping(self):
|
def test_pattern_details_screen_reads_selected_tag_from_raw_row_mapping(self):
|
||||||
screen = object.__new__(PatternDetailsScreen)
|
screen = object.__new__(PatternDetailsScreen)
|
||||||
screen.tagsTable = FakeTagTable()
|
screen.tagsTable = FakeTagTable()
|
||||||
|
|||||||
Reference in New Issue
Block a user