diff --git a/src/ffx/media_details_screen.py b/src/ffx/media_details_screen.py index 5a3c3c3..33006f1 100644 --- a/src/ffx/media_details_screen.py +++ b/src/ffx/media_details_screen.py @@ -8,10 +8,9 @@ from ffx.audio_layout import AudioLayout from .show_details_screen import ShowDetailsScreen from .pattern_details_screen import PatternDetailsScreen -from .screen_support import build_screen_bootstrap, build_screen_controllers +from .screen_support import build_screen_bootstrap, build_screen_controllers, populate_tag_table from ffx.track_type import TrackType -from ffx.track_codec import TrackCodec from ffx.model.track import Track from ffx.track_disposition import TrackDisposition @@ -25,7 +24,7 @@ from ffx.file_properties import FileProperties from ffx.media_descriptor_change_set import MediaDescriptorChangeSet -from ffx.helper import formatRichColor, DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY, DIFF_UNCHANGED_KEY +from ffx.helper import DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY, DIFF_UNCHANGED_KEY # Screen[dict[int, str, int]] @@ -156,6 +155,9 @@ class MediaDetailsScreen(Screen): raise click.ClickException(f"MediaDetailsScreen.__init__(): Argument 'filename' is required to be provided for command 'inspect'") self.__mediaFilename = self.context['arguments']['filename'] + self.__showRowData: dict[object, ShowDescriptor | None] = {} + self.__trackRowData: dict[object, TrackDescriptor] = {} + self.__sourceMediaTagRowData: dict[object, tuple[str, str]] = {} if not os.path.isfile(self.__mediaFilename): raise click.ClickException(f"MediaDetailsScreen.__init__(): Media file {self.__mediaFilename} does not exist") @@ -167,37 +169,49 @@ class MediaDetailsScreen(Screen): """Remove show entry from DataTable. Removes the entry if showId is not set""" - for rowKey, row in self.showsTable.rows.items(): # dict[RowKey, Row] - - rowData = self.showsTable.get_row(rowKey) - - try: - if (showId == -1 and rowData[0] == ' ' - or showId == int(rowData[0])): - self.showsTable.remove_row(rowKey) - return - except: - continue + for row_key, show_descriptor in list(self.__showRowData.items()): + if ( + (showId == -1 and show_descriptor is None) + or ( + show_descriptor is not None + and show_descriptor.getId() == showId + ) + ): + self.showsTable.remove_row(row_key) + self.__showRowData.pop(row_key, None) + return def getRowIndexFromShowId(self, showId : int = -1) -> int: """Find the index of the row where the value in the specified column matches the target_value.""" - for rowKey, row in self.showsTable.rows.items(): # dict[RowKey, Row] - - rowData = self.showsTable.get_row(rowKey) - - try: - if ((showId == -1 and rowData[0] == ' ') - or showId == int(rowData[0])): - return int(self.showsTable.get_row_index(rowKey)) - except: - continue + for row_key, show_descriptor in self.__showRowData.items(): + if ( + (showId == -1 and show_descriptor is None) + or ( + show_descriptor is not None + and show_descriptor.getId() == showId + ) + ): + return int(self.showsTable.get_row_index(row_key)) return None + def _add_show_row(self, show_descriptor: ShowDescriptor | None): + if show_descriptor is None: + row_key = self.showsTable.add_row(' ', '', ' ') + else: + row_key = self.showsTable.add_row( + str(show_descriptor.getId()), + str(show_descriptor.getName()), + str(show_descriptor.getYear()), + ) + self.__showRowData[row_key] = show_descriptor + return row_key + + def loadProperties(self): self.__mediaFileProperties = FileProperties(self.context, self.__mediaFilename) @@ -314,23 +328,17 @@ class MediaDetailsScreen(Screen): def on_mount(self): if self.__currentPattern is None: - row = (' ', '', ' ') # Convert each element to a string before adding - self.showsTable.add_row(*map(str, row)) + self._add_show_row(None) for show in self.__sc.getAllShows(): - row = (int(show.id), show.name, show.year) # Convert each element to a string before adding - self.showsTable.add_row(*map(str, row)) + self._add_show_row(show.getDescriptor(self.context)) - for mediaTagKey, mediaTagValue in self.__sourceMediaDescriptor.getTags().items(): - - textColor = None - if mediaTagKey in self.__ignoreGlobalKeys: - textColor = 'blue' - if mediaTagKey in self.__removeGlobalKeys: - textColor = 'red' - - row = (formatRichColor(mediaTagKey, textColor), formatRichColor(mediaTagValue, textColor)) # Convert each element to a string before adding - self.mediaTagsTable.add_row(*map(str, row)) + self.__sourceMediaTagRowData = populate_tag_table( + self.mediaTagsTable, + self.__sourceMediaDescriptor.getTags(), + ignore_keys=self.__ignoreGlobalKeys, + remove_keys=self.__removeGlobalKeys, + ) self.updateTracks() @@ -362,6 +370,7 @@ class MediaDetailsScreen(Screen): def updateTracks(self): self.tracksTable.clear() + self.__trackRowData = {} # trackDescriptorList = self.__sourceMediaDescriptor.getAllTrackDescriptors() trackDescriptorList = self.__sourceMediaDescriptor.getTrackDescriptors() @@ -387,7 +396,8 @@ class MediaDetailsScreen(Screen): 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No', 'Yes' if TrackDisposition.FORCED in dispoSet else 'No') - self.tracksTable.add_row(*map(str, row)) + row_key = self.tracksTable.add_row(*map(str, row)) + self.__trackRowData[row_key] = td typeCounter[trackType] += 1 @@ -529,17 +539,7 @@ class MediaDetailsScreen(Screen): row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate) if row_key is not None: - selected_track_data = self.tracksTable.get_row(row_key) - - kwargs = {} - kwargs[TrackDescriptor.CONTEXT_KEY] = self.context - kwargs[TrackDescriptor.INDEX_KEY] = int(selected_track_data[0]) - kwargs[TrackDescriptor.TRACK_TYPE_KEY] = TrackType.fromLabel(selected_track_data[1]) - kwargs[TrackDescriptor.SUB_INDEX_KEY] = int(selected_track_data[2]) - kwargs[TrackDescriptor.CODEC_KEY] = TrackCodec.fromLabel(selected_track_data[3]) - kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.fromLabel(selected_track_data[4]) - - return TrackDescriptor(**kwargs) + return self.__trackRowData.get(row_key) else: return None @@ -554,20 +554,7 @@ class MediaDetailsScreen(Screen): row_key, col_key = self.showsTable.coordinate_to_cell_key(self.showsTable.cursor_coordinate) if row_key is not None: - selected_row_data = self.showsTable.get_row(row_key) - - try: - kwargs = {} - - kwargs[ShowDescriptor.CONTEXT_KEY] = self.context - kwargs[ShowDescriptor.ID_KEY] = int(selected_row_data[0]) - kwargs[ShowDescriptor.NAME_KEY] = str(selected_row_data[1]) - kwargs[ShowDescriptor.YEAR_KEY] = int(selected_row_data[2]) - - return ShowDescriptor(**kwargs) - - except ValueError: - return None + return self.__showRowData.get(row_key) except CellDoesNotExist: return None @@ -583,8 +570,7 @@ class MediaDetailsScreen(Screen): showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId()) if showRowIndex is None: - show = (showDescriptor.getId(), showDescriptor.getName(), showDescriptor.getYear()) - self.showsTable.add_row(*map(str, show)) + self._add_show_row(showDescriptor) showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId()) if showRowIndex is not None: diff --git a/src/ffx/pattern_details_screen.py b/src/ffx/pattern_details_screen.py index 85e9989..1ea1b11 100644 --- a/src/ffx/pattern_details_screen.py +++ b/src/ffx/pattern_details_screen.py @@ -14,7 +14,11 @@ from .shifted_season_details_screen import ShiftedSeasonDetailsScreen from .tag_details_screen import TagDetailsScreen from .tag_delete_screen import TagDeleteScreen -from .screen_support import build_screen_bootstrap, build_screen_controllers +from .screen_support import ( + build_screen_bootstrap, + build_screen_controllers, + populate_tag_table, +) from ffx.track_type import TrackType @@ -28,8 +32,6 @@ from ffx.iso_language import IsoLanguage from ffx.audio_layout import AudioLayout from ffx.model.shifted_season import ShiftedSeason -from ffx.helper import formatRichColor, removeRichColor - # Screen[dict[int, str, int]] class PatternDetailsScreen(Screen): @@ -130,11 +132,15 @@ class PatternDetailsScreen(Screen): self.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else None self.__draftTracks : List[TrackDescriptor] = [] self.__draftTags : dict[str, str] = {} + self.__trackRowData: dict[object, TrackDescriptor] = {} + self.__tagRowData: dict[object, tuple[str, str]] = {} + self.__shiftedSeasonRowData: dict[object, dict[str, int | None]] = {} def updateTracks(self): self.tracksTable.clear() + self.__trackRowData = {} tracks = self.getCurrentTrackDescriptors() @@ -165,7 +171,8 @@ class PatternDetailsScreen(Screen): 'Yes' if TrackDisposition.FORCED in dispoSet else 'No', td.getSourceIndex()) - self.tracksTable.add_row(*map(str, row)) + row_key = self.tracksTable.add_row(*map(str, row)) + self.__trackRowData[row_key] = td typeCounter[trackType] += 1 @@ -243,29 +250,23 @@ class PatternDetailsScreen(Screen): def updateTags(self): - - self.tagsTable.clear() - tags = ( self.__tac.findAllMediaTags(self.__pattern.getId()) if self.__pattern is not None else self.__draftTags ) - for tagKey, tagValue in tags.items(): - - textColor = None - if tagKey in self.__ignoreGlobalKeys: - textColor = 'blue' - if tagKey in self.__removeGlobalKeys: - textColor = 'red' - - row = (formatRichColor(tagKey, textColor), formatRichColor(tagValue, textColor)) - self.tagsTable.add_row(*map(str, row)) + self.__tagRowData = populate_tag_table( + self.tagsTable, + tags, + ignore_keys=self.__ignoreGlobalKeys, + remove_keys=self.__removeGlobalKeys, + ) def updateShiftedSeasons(self): self.shiftedSeasonsTable.clear() + self.__shiftedSeasonRowData = {} if self.__pattern is None: return @@ -273,6 +274,7 @@ class PatternDetailsScreen(Screen): shiftedSeason: ShiftedSeason for shiftedSeason in self.__ssc.getShiftedSeasonSiblings(patternId=self.__pattern.getId()): shiftedSeasonObj = shiftedSeason.getObj() + shiftedSeasonObj['id'] = shiftedSeason.getId() firstEpisode = shiftedSeasonObj['first_episode'] firstEpisodeStr = str(firstEpisode) if firstEpisode != -1 else '' @@ -288,7 +290,8 @@ class PatternDetailsScreen(Screen): shiftedSeasonObj['episode_offset'], ) - self.shiftedSeasonsTable.add_row(*map(str, row)) + row_key = self.shiftedSeasonsTable.add_row(*map(str, row)) + self.__shiftedSeasonRowData[row_key] = shiftedSeasonObj def getSelectedShiftedSeasonObjFromInput(self): @@ -300,29 +303,7 @@ class PatternDetailsScreen(Screen): ) if row_key is not None: - selected_row_data = self.shiftedSeasonsTable.get_row(row_key) - - def parse_int_or_default(value: str, default: int) -> int: - try: - return int(value) - except (TypeError, ValueError): - return default - - shiftedSeasonObj['original_season'] = int(selected_row_data[0]) - shiftedSeasonObj['first_episode'] = parse_int_or_default(selected_row_data[1], -1) - shiftedSeasonObj['last_episode'] = parse_int_or_default(selected_row_data[2], -1) - shiftedSeasonObj['season_offset'] = parse_int_or_default(selected_row_data[3], 0) - shiftedSeasonObj['episode_offset'] = parse_int_or_default(selected_row_data[4], 0) - - if self.__pattern is not None: - shiftedSeasonId = self.__ssc.findShiftedSeason( - patternId=self.__pattern.getId(), - originalSeason=shiftedSeasonObj['original_season'], - firstEpisode=shiftedSeasonObj['first_episode'], - lastEpisode=shiftedSeasonObj['last_episode'], - ) - if shiftedSeasonId is not None: - shiftedSeasonObj['id'] = shiftedSeasonId + shiftedSeasonObj = dict(self.__shiftedSeasonRowData.get(row_key, {})) except CellDoesNotExist: pass @@ -513,15 +494,7 @@ class PatternDetailsScreen(Screen): row_key, col_key = self.tracksTable.coordinate_to_cell_key(self.tracksTable.cursor_coordinate) if row_key is not None: - selected_track_data = self.tracksTable.get_row(row_key) - - trackIndex = int(selected_track_data[0]) - trackSubIndex = int(selected_track_data[2]) - - for trackDescriptor in self.getCurrentTrackDescriptors(): - if (trackDescriptor.getIndex() == trackIndex - and trackDescriptor.getSubIndex() == trackSubIndex): - return trackDescriptor + return self.__trackRowData.get(row_key) return None @@ -539,12 +512,7 @@ class PatternDetailsScreen(Screen): row_key, col_key = self.tagsTable.coordinate_to_cell_key(self.tagsTable.cursor_coordinate) if row_key is not None: - selected_tag_data = self.tagsTable.get_row(row_key) - - tagKey = removeRichColor(selected_tag_data[0]) - tagValue = removeRichColor(selected_tag_data[1]) - - return tagKey, tagValue + return self.__tagRowData.get(row_key) else: return None diff --git a/src/ffx/screen_support.py b/src/ffx/screen_support.py index a7e24b6..cb654d6 100644 --- a/src/ffx/screen_support.py +++ b/src/ffx/screen_support.py @@ -1,7 +1,9 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass +from .helper import formatRichColor from .pattern_controller import PatternController from .show_controller import ShowController from .shifted_season_controller import ShiftedSeasonController @@ -63,3 +65,34 @@ def build_screen_controllers( controllers['shifted_season'] = ShiftedSeasonController(context=context) return controllers + + +def populate_tag_table( + table, + tags: Mapping[str, object], + *, + ignore_keys: list[str], + remove_keys: list[str], +) -> dict[object, tuple[str, str]]: + """Render display rows while keeping raw tag data addressable by row key.""" + + table.clear() + + row_data: dict[object, tuple[str, str]] = {} + for tag_key, tag_value in tags.items(): + raw_key = str(tag_key) + raw_value = str(tag_value) + + text_color = None + if raw_key in ignore_keys: + text_color = "blue" + if raw_key in remove_keys: + text_color = "red" + + row_key = table.add_row( + str(formatRichColor(raw_key, text_color)), + str(formatRichColor(raw_value, text_color)), + ) + row_data[row_key] = (raw_key, raw_value) + + return row_data diff --git a/src/ffx/show_details_screen.py b/src/ffx/show_details_screen.py index eec0a0d..d174deb 100644 --- a/src/ffx/show_details_screen.py +++ b/src/ffx/show_details_screen.py @@ -108,12 +108,45 @@ class ShowDetailsScreen(Screen): self.__ssc = controllers['shifted_season'] self.__showDescriptor = self.__sc.getShowDescriptor(showId) if showId is not None else None + self.__patternRowData: dict[object, dict[str, object]] = {} + self.__shiftedSeasonRowData: dict[object, dict[str, int | None]] = {} + + + def _add_pattern_row(self, *, pattern_id: int | None, pattern_text: str): + row_key = self.patternTable.add_row(str(pattern_text)) + self.__patternRowData[row_key] = { + 'id': pattern_id, + 'show_id': self.__showDescriptor.getId() if self.__showDescriptor is not None else None, + 'pattern': str(pattern_text), + } + return row_key + + + def _add_shifted_season_row(self, shifted_season_obj: dict[str, int | None]): + firstEpisode = shifted_season_obj['first_episode'] + firstEpisodeStr = str(firstEpisode) if firstEpisode != -1 else '' + + lastEpisode = shifted_season_obj['last_episode'] + lastEpisodeStr = str(lastEpisode) if lastEpisode != -1 else '' + + row = ( + shifted_season_obj['original_season'], + firstEpisodeStr, + lastEpisodeStr, + shifted_season_obj['season_offset'], + shifted_season_obj['episode_offset'], + ) + + row_key = self.shiftedSeasonsTable.add_row(*map(str, row)) + self.__shiftedSeasonRowData[row_key] = dict(shifted_season_obj) + return row_key def updateShiftedSeasons(self): self.shiftedSeasonsTable.clear() + self.__shiftedSeasonRowData = {} if not self.__showDescriptor is None: @@ -123,20 +156,8 @@ class ShowDetailsScreen(Screen): for shiftedSeason in self.__ssc.getShiftedSeasonSiblings(showId=showId): shiftedSeasonObj = shiftedSeason.getObj() - - firstEpisode = shiftedSeasonObj['first_episode'] - firstEpisodeStr = str(firstEpisode) if firstEpisode != -1 else '' - - lastEpisode = shiftedSeasonObj['last_episode'] - lastEpisodeStr = str(lastEpisode) if lastEpisode != -1 else '' - - row = (shiftedSeasonObj['original_season'], - firstEpisodeStr, - lastEpisodeStr, - shiftedSeasonObj['season_offset'], - shiftedSeasonObj['episode_offset']) - - self.shiftedSeasonsTable.add_row(*map(str, row)) + shiftedSeasonObj['id'] = shiftedSeason.getId() + self._add_shifted_season_row(shiftedSeasonObj) @@ -162,8 +183,10 @@ class ShowDetailsScreen(Screen): #raise click.ClickException(f"show_id {showId}") for pattern in self.__pc.getPatternsForShow(showId): - row = (pattern.getPattern(),) - self.patternTable.add_row(*map(str, row)) + self._add_pattern_row( + pattern_id=pattern.getId(), + pattern_text=pattern.getPattern(), + ) self.updateShiftedSeasons() @@ -195,10 +218,7 @@ class ShowDetailsScreen(Screen): row_key, col_key = self.patternTable.coordinate_to_cell_key(self.patternTable.cursor_coordinate) if row_key is not None: - selected_row_data = self.patternTable.get_row(row_key) - - selectedPattern['show_id'] = self.__showDescriptor.getId() - selectedPattern['pattern'] = str(selected_row_data[0]) + selectedPattern = dict(self.__patternRowData.get(row_key, {})) except CellDoesNotExist: pass @@ -217,31 +237,7 @@ class ShowDetailsScreen(Screen): row_key, col_key = self.shiftedSeasonsTable.coordinate_to_cell_key(self.shiftedSeasonsTable.cursor_coordinate) if row_key is not None: - selected_row_data = self.shiftedSeasonsTable.get_row(row_key) - - def parse_int_or_default(value: str, default: int) -> int: - try: - return int(value) - except (TypeError, ValueError): - return default - - shiftedSeasonObj['original_season'] = int(selected_row_data[0]) - shiftedSeasonObj['first_episode'] = parse_int_or_default(selected_row_data[1], -1) - shiftedSeasonObj['last_episode'] = parse_int_or_default(selected_row_data[2], -1) - shiftedSeasonObj['season_offset'] = parse_int_or_default(selected_row_data[3], 0) - shiftedSeasonObj['episode_offset'] = parse_int_or_default(selected_row_data[4], 0) - - - if self.__showDescriptor is not None: - - showId = int(self.__showDescriptor.getId()) - - shiftedSeasonId = self.__ssc.findShiftedSeason(showId, - originalSeason=shiftedSeasonObj['original_season'], - firstEpisode=shiftedSeasonObj['first_episode'], - lastEpisode=shiftedSeasonObj['last_episode']) - if shiftedSeasonId is not None: - shiftedSeasonObj['id'] = shiftedSeasonId + shiftedSeasonObj = dict(self.__shiftedSeasonRowData.get(row_key, {})) except CellDoesNotExist: pass @@ -255,9 +251,14 @@ class ShowDetailsScreen(Screen): def handle_add_pattern(self, screenResult): + if screenResult is None: + return - pattern = (screenResult['pattern'],) - self.patternTable.add_row(*map(str, pattern)) + pattern_id = self.__pc.findPattern(screenResult) + self._add_pattern_row( + pattern_id=pattern_id, + pattern_text=screenResult['pattern'], + ) def action_edit_pattern(self): @@ -265,8 +266,7 @@ class ShowDetailsScreen(Screen): selectedPatternDescriptor = self.getSelectedPatternDescriptor() if selectedPatternDescriptor: - - selectedPatternId = self.__pc.findPattern(selectedPatternDescriptor) + selectedPatternId = selectedPatternDescriptor.get('id') if selectedPatternId is None: raise click.ClickException(f"ShowDetailsScreen.action_edit_pattern(): Pattern to edit has no id") @@ -280,6 +280,8 @@ class ShowDetailsScreen(Screen): row_key, col_key = self.patternTable.coordinate_to_cell_key(self.patternTable.cursor_coordinate) self.patternTable.update_cell(row_key, self.column_key_pattern, screenResult['pattern']) + if row_key in self.__patternRowData: + self.__patternRowData[row_key]['pattern'] = str(screenResult['pattern']) except CellDoesNotExist: pass @@ -291,7 +293,7 @@ class ShowDetailsScreen(Screen): if selectedPatternDescriptor: - selectedPatternId = self.__pc.findPattern(selectedPatternDescriptor) + selectedPatternId = selectedPatternDescriptor.get('id') if selectedPatternId is None: raise click.ClickException(f"ShowDetailsScreen.action_remove_pattern(): Pattern to remove has no id") @@ -304,6 +306,7 @@ class ShowDetailsScreen(Screen): try: row_key, col_key = self.patternTable.coordinate_to_cell_key(self.patternTable.cursor_coordinate) self.patternTable.remove_row(row_key) + self.__patternRowData.pop(row_key, None) except CellDoesNotExist: pass diff --git a/src/ffx/shows_screen.py b/src/ffx/shows_screen.py index a76c921..0853d94 100644 --- a/src/ffx/shows_screen.py +++ b/src/ffx/shows_screen.py @@ -66,6 +66,17 @@ class ShowsScreen(Screen): self.Session = self.context['database']['session'] # convenience self.__sc = ShowController(context = self.context) + self.__showRowData: dict[object, ShowDescriptor] = {} + + + def _add_show_row(self, show_descriptor: ShowDescriptor): + row_key = self.table.add_row( + str(show_descriptor.getId()), + str(show_descriptor.getName()), + str(show_descriptor.getYear()), + ) + self.__showRowData[row_key] = show_descriptor + return row_key def getSelectedShowId(self): @@ -76,9 +87,8 @@ class ShowsScreen(Screen): row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate) if row_key is not None: - selected_row_data = self.table.get_row(row_key) - - return selected_row_data[0] + selected_show = self.__showRowData.get(row_key) + return selected_show.getId() if selected_show is not None else None except CellDoesNotExist: return None @@ -90,9 +100,8 @@ class ShowsScreen(Screen): self.app.push_screen(ShowDetailsScreen(), self.handle_new_screen) def handle_new_screen(self, screenResult): - - show = (screenResult['id'], screenResult['name'], screenResult['year']) - self.table.add_row(*map(str, show)) + if isinstance(screenResult, ShowDescriptor): + self._add_show_row(screenResult) def action_edit_show(self): @@ -110,7 +119,8 @@ class ShowsScreen(Screen): row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate) self.table.update_cell(row_key, self.column_key_name, showDescriptor.getName()) - self.table.update_cell(row_key, self.column_key_year, showDescriptor.getYear()) + self.table.update_cell(row_key, self.column_key_year, showDescriptor.getYear()) + self.__showRowData[row_key] = showDescriptor except CellDoesNotExist: pass @@ -131,6 +141,7 @@ class ShowsScreen(Screen): try: row_key, col_key = self.table.coordinate_to_cell_key(self.table.cursor_coordinate) self.table.remove_row(row_key) + self.__showRowData.pop(row_key, None) except CellDoesNotExist: pass @@ -138,8 +149,7 @@ class ShowsScreen(Screen): def on_mount(self) -> None: for show in self.__sc.getAllShows(): - row = (int(show.id), show.name, show.year) # Convert each element to a string before adding - self.table.add_row(*map(str, row)) + self._add_show_row(show.getDescriptor(self.context)) def compose(self): diff --git a/src/ffx/track_details_screen.py b/src/ffx/track_details_screen.py index f0d1c15..bacbd7a 100644 --- a/src/ffx/track_details_screen.py +++ b/src/ffx/track_details_screen.py @@ -13,8 +13,7 @@ from .track_codec import TrackCodec from .track_descriptor import TrackDescriptor from .track_disposition import TrackDisposition from .track_type import TrackType - -from ffx.helper import formatRichColor, removeRichColor +from .screen_support import build_screen_bootstrap, populate_tag_table class TrackDetailsScreen(Screen): @@ -98,28 +97,12 @@ class TrackDetailsScreen(Screen): ): super().__init__() - self.context = self.app.getContext() + bootstrap = build_screen_bootstrap(self.app.getContext()) + self.context = bootstrap.context - self.__configurationData = self.context["config"].getData() - - metadataConfiguration = ( - self.__configurationData["metadata"] - if "metadata" in self.__configurationData.keys() - else {} - ) - - self.__removeTrackKeys = ( - metadataConfiguration["streams"]["remove"] - if "streams" in metadataConfiguration.keys() - and "remove" in metadataConfiguration["streams"].keys() - else [] - ) - self.__ignoreTrackKeys = ( - metadataConfiguration["streams"]["ignore"] - if "streams" in metadataConfiguration.keys() - and "ignore" in metadataConfiguration["streams"].keys() - else [] - ) + self.__removeTrackKeys = bootstrap.remove_track_keys + self.__ignoreTrackKeys = bootstrap.ignore_track_keys + self.__tagRowData: dict[object, tuple[str, str]] = {} self.__isNew = trackDescriptor is None self.__trackDescriptor = trackDescriptor @@ -166,18 +149,12 @@ class TrackDetailsScreen(Screen): ) def updateTags(self): - - self.trackTagsTable.clear() - - for key, value in self.__draftTrackTags.items(): - textColor = None - if key in self.__ignoreTrackKeys: - textColor = "blue" - if key in self.__removeTrackKeys: - textColor = "red" - - row = (formatRichColor(key, textColor), formatRichColor(value, textColor)) - self.trackTagsTable.add_row(*map(str, row)) + self.__tagRowData = populate_tag_table( + self.trackTagsTable, + self.__draftTrackTags, + ignore_keys=self.__ignoreTrackKeys, + remove_keys=self.__removeTrackKeys, + ) def on_mount(self): @@ -190,9 +167,9 @@ class TrackDetailsScreen(Screen): self.query_one("#pattern_label", Static).update(self.__patternLabel) if self.__trackType is not None: - self.query_one("#type_select", Select).value = self.__trackType.label() + self.query_one("#type_select", Select).value = self.__trackType - self.query_one("#audio_layout_select", Select).value = self.__audioLayout.label() + self.query_one("#audio_layout_select", Select).value = self.__audioLayout for disposition in TrackDisposition: @@ -211,9 +188,7 @@ class TrackDetailsScreen(Screen): ) if self.__trackDescriptor is not None: - self.query_one("#language_select", Select).value = ( - self.__trackDescriptor.getLanguage().label() - ) + self.query_one("#language_select", Select).value = self.__trackDescriptor.getLanguage() self.query_one("#title_input", Input).value = self.__trackDescriptor.getTitle() self.updateTags() @@ -226,8 +201,6 @@ class TrackDetailsScreen(Screen): self.trackTagsTable.cursor_type = "row" - languages = [language.label() for language in IsoLanguage] - yield Header() with Grid(): @@ -250,15 +223,15 @@ class TrackDetailsScreen(Screen): yield Static(" ", classes="five") yield Static("Type") - yield Select.from_values( - [trackType.label() for trackType in TrackType], + yield Select( + [(trackType.label(), trackType) for trackType in TrackType], classes="four", id="type_select", ) yield Static("Audio Layout") - yield Select.from_values( - [layout.label() for layout in AudioLayout], + yield Select( + [(layout.label(), layout) for layout in AudioLayout], classes="four", id="audio_layout_select", ) @@ -268,7 +241,11 @@ class TrackDetailsScreen(Screen): yield Static(" ", classes="five") yield Static("Language") - yield Select.from_values(languages, classes="four", id="language_select") + yield Select( + [(language.label(), language) for language in IsoLanguage], + classes="four", + id="language_select", + ) yield Static(" ", classes="five") @@ -328,15 +305,18 @@ class TrackDetailsScreen(Screen): if self.__subIndex is not None and int(self.__subIndex) >= 0: kwargs[TrackDescriptor.SUB_INDEX_KEY] = int(self.__subIndex) - selectedTrackType = TrackType.fromLabel( - self.query_one("#type_select", Select).value - ) + selectedTrackType = self.query_one("#type_select", Select).value + if not isinstance(selectedTrackType, TrackType): + selectedTrackType = TrackType.UNKNOWN kwargs[TrackDescriptor.TRACK_TYPE_KEY] = selectedTrackType kwargs[TrackDescriptor.CODEC_KEY] = self.__trackCodec if selectedTrackType == TrackType.AUDIO: - kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.fromLabel( - self.query_one("#audio_layout_select", Select).value + selectedAudioLayout = self.query_one("#audio_layout_select", Select).value + kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = ( + selectedAudioLayout + if isinstance(selectedAudioLayout, AudioLayout) + else AudioLayout.LAYOUT_UNDEFINED ) else: kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.LAYOUT_UNDEFINED @@ -344,8 +324,8 @@ class TrackDetailsScreen(Screen): trackTags = dict(self.__draftTrackTags) language = self.query_one("#language_select", Select).value - if language: - trackTags["language"] = IsoLanguage.find(language).threeLetter() + if isinstance(language, IsoLanguage): + trackTags["language"] = language.threeLetter() title = self.query_one("#title_input", Input).value if title: @@ -370,12 +350,7 @@ class TrackDetailsScreen(Screen): ) if row_key is not None: - selected_tag_data = self.trackTagsTable.get_row(row_key) - - tagKey = removeRichColor(selected_tag_data[0]) - tagValue = removeRichColor(selected_tag_data[1]) - - return tagKey, tagValue + return self.__tagRowData.get(row_key) return None diff --git a/tests/unit/test_screen_support.py b/tests/unit/test_screen_support.py index 5bc8b3e..980da48 100644 --- a/tests/unit/test_screen_support.py +++ b/tests/unit/test_screen_support.py @@ -23,6 +23,21 @@ class StaticConfig: return self._data +class FakeTagTable: + def __init__(self): + self.rows = {} + self._next_index = 0 + + def clear(self): + self.rows.clear() + + def add_row(self, *values): + row_key = f"row-{self._next_index}" + self._next_index += 1 + self.rows[row_key] = tuple(values) + return row_key + + class ScreenSupportTests(unittest.TestCase): def make_context(self): return { @@ -81,6 +96,32 @@ class ScreenSupportTests(unittest.TestCase): controllers, ) + def test_populate_tag_table_keeps_raw_values_outside_display_labels(self): + table = FakeTagTable() + + row_data = screen_support.populate_tag_table( + table, + {"BPS": 4835, "KEEP": "plain"}, + ignore_keys=["KEEP"], + remove_keys=["BPS"], + ) + + self.assertEqual( + { + "row-0": ("BPS", "4835"), + "row-1": ("KEEP", "plain"), + }, + row_data, + ) + self.assertEqual( + ("[red]BPS[/red]", "[red]4835[/red]"), + table.rows["row-0"], + ) + self.assertEqual( + ("[blue]KEEP[/blue]", "[blue]plain[/blue]"), + table.rows["row-1"], + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_tag_table_screen_state.py b/tests/unit/test_tag_table_screen_state.py new file mode 100644 index 0000000..70fc278 --- /dev/null +++ b/tests/unit/test_tag_table_screen_state.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +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.audio_layout import AudioLayout # noqa: E402 +from ffx.iso_language import IsoLanguage # noqa: E402 +from ffx.logging_utils import get_ffx_logger # noqa: E402 +from ffx.media_details_screen import MediaDetailsScreen # noqa: E402 +from ffx.pattern_details_screen import PatternDetailsScreen # noqa: E402 +from ffx.show_descriptor import ShowDescriptor # noqa: E402 +from ffx.show_details_screen import ShowDetailsScreen # noqa: E402 +from ffx.shows_screen import ShowsScreen # noqa: E402 +from ffx.track_codec import TrackCodec # noqa: E402 +from ffx.track_descriptor import TrackDescriptor # noqa: E402 +from ffx.track_details_screen import TrackDetailsScreen # noqa: E402 +from ffx.track_type import TrackType # noqa: E402 + + +class FakeTagTable: + def __init__(self): + self.rows = {} + self.cursor_coordinate = (0, 0) + self._selected_row_key = None + self._next_index = 0 + self._row_order = [] + + def clear(self): + self.rows.clear() + self._selected_row_key = None + self._row_order.clear() + + def add_row(self, *values): + row_key = f"row-{self._next_index}" + self._next_index += 1 + self.rows[row_key] = tuple(values) + self._row_order.append(row_key) + if self._selected_row_key is None: + self._selected_row_key = row_key + return row_key + + def coordinate_to_cell_key(self, _coordinate): + return self._selected_row_key, None + + def select_row(self, row_key): + self._selected_row_key = row_key + + def get_row_index(self, row_key): + return self._row_order.index(row_key) + + def remove_row(self, row_key): + self.rows.pop(row_key, None) + if row_key in self._row_order: + self._row_order.remove(row_key) + if self._selected_row_key == row_key: + self._selected_row_key = self._row_order[0] if self._row_order else None + + def update_cell(self, row_key, column_key, value): + row = list(self.rows[row_key]) + row[int(column_key)] = value + self.rows[row_key] = tuple(row) + + +class FakeMediaDescriptor: + def __init__(self, track_descriptors): + self._track_descriptors = list(track_descriptors) + + def getTrackDescriptors(self): + return list(self._track_descriptors) + + +class FakeValueWidget: + def __init__(self, value): + self.value = value + + +class FakeInputWidget: + def __init__(self, value): + self.value = value + + +class FakeSelectionListWidget: + def __init__(self, selected): + self.selected = selected + + +def make_track_descriptor(index, sub_index, track_type): + return TrackDescriptor( + index=index, + sub_index=sub_index, + track_type=track_type, + codec_name=TrackCodec.UNKNOWN, + audio_layout=AudioLayout.LAYOUT_UNDEFINED, + ) + + +def make_show_descriptor(show_id, name="Show", year=2000): + return ShowDescriptor( + id=show_id, + name=name, + year=year, + ) + + +class TagTableScreenStateTests(unittest.TestCase): + def test_track_details_screen_reads_selected_tag_from_raw_row_mapping(self): + screen = object.__new__(TrackDetailsScreen) + screen.trackTagsTable = FakeTagTable() + screen._TrackDetailsScreen__draftTrackTags = { + "BPS": "4835", + "KEEP_ME": "plain", + } + screen._TrackDetailsScreen__ignoreTrackKeys = ["KEEP_ME"] + screen._TrackDetailsScreen__removeTrackKeys = ["BPS"] + screen._TrackDetailsScreen__tagRowData = {} + + screen.updateTags() + + self.assertEqual( + ("[red]BPS[/red]", "[red]4835[/red]"), + screen.trackTagsTable.rows["row-0"], + ) + self.assertEqual( + ("BPS", "4835"), + screen.getSelectedTag(), + ) + + def test_track_details_screen_reads_select_values_from_widget_state(self): + screen = object.__new__(TrackDetailsScreen) + screen.context = {"logger": get_ffx_logger()} + screen._TrackDetailsScreen__trackDescriptor = None + screen._TrackDetailsScreen__patternId = 5 + screen._TrackDetailsScreen__index = 2 + screen._TrackDetailsScreen__subIndex = 0 + screen._TrackDetailsScreen__trackCodec = TrackCodec.UNKNOWN + screen._TrackDetailsScreen__draftTrackTags = {"KEEP": "value"} + + widgets = { + "#type_select": FakeValueWidget(TrackType.AUDIO), + "#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_STEREO), + "#language_select": FakeValueWidget(IsoLanguage.GERMAN), + "#title_input": FakeInputWidget("German Audio"), + "#dispositions_selection_list": FakeSelectionListWidget({0, 6}), + } + screen.query_one = lambda selector, _widget_type=None: widgets[selector] + + descriptor = screen.getTrackDescriptorFromInput() + + self.assertEqual(TrackType.AUDIO, descriptor.getType()) + self.assertEqual(AudioLayout.LAYOUT_STEREO, descriptor.getAudioLayout()) + self.assertEqual("deu", descriptor.getTags()["language"]) + self.assertEqual("German Audio", descriptor.getTitle()) + self.assertEqual("value", descriptor.getTags()["KEEP"]) + + def test_pattern_details_screen_reads_selected_track_from_row_mapping(self): + first_track = make_track_descriptor(0, 0, TrackType.VIDEO) + second_track = make_track_descriptor(1, 0, TrackType.SUBTITLE) + + screen = object.__new__(PatternDetailsScreen) + screen.tracksTable = FakeTagTable() + screen._PatternDetailsScreen__draftTracks = [first_track, second_track] + screen._PatternDetailsScreen__pattern = None + screen._PatternDetailsScreen__trackRowData = {} + + screen.updateTracks() + screen.tracksTable.select_row("row-1") + + self.assertIs(second_track, screen.getSelectedTrackDescriptor()) + + def test_pattern_details_screen_reads_selected_tag_from_raw_row_mapping(self): + screen = object.__new__(PatternDetailsScreen) + screen.tagsTable = FakeTagTable() + screen._PatternDetailsScreen__pattern = None + screen._PatternDetailsScreen__draftTags = { + "BPS": "4835", + "TITLE": "Deutsch [FN]", + } + screen._PatternDetailsScreen__ignoreGlobalKeys = ["TITLE"] + screen._PatternDetailsScreen__removeGlobalKeys = ["BPS"] + screen._PatternDetailsScreen__tagRowData = {} + + screen.updateTags() + + self.assertEqual( + ("[red]BPS[/red]", "[red]4835[/red]"), + screen.tagsTable.rows["row-0"], + ) + self.assertEqual( + ("BPS", "4835"), + screen.getSelectedTag(), + ) + + def test_media_details_screen_reads_selected_track_from_row_mapping(self): + first_track = make_track_descriptor(0, 0, TrackType.VIDEO) + second_track = make_track_descriptor(1, 0, TrackType.SUBTITLE) + + screen = object.__new__(MediaDetailsScreen) + screen.tracksTable = FakeTagTable() + screen._MediaDetailsScreen__sourceMediaDescriptor = FakeMediaDescriptor( + [first_track, second_track] + ) + screen._MediaDetailsScreen__trackRowData = {} + + screen.updateTracks() + screen.tracksTable.select_row("row-1") + + self.assertIs(second_track, screen.getSelectedTrackDescriptor()) + + def test_pattern_details_screen_reads_selected_shifted_season_from_row_mapping(self): + screen = object.__new__(PatternDetailsScreen) + screen.shiftedSeasonsTable = FakeTagTable() + screen._PatternDetailsScreen__pattern = object() + screen._PatternDetailsScreen__shiftedSeasonRowData = {} + + row_key = screen.shiftedSeasonsTable.add_row("9", "1", "3", "1", "0") + screen._PatternDetailsScreen__shiftedSeasonRowData[row_key] = { + "id": 44, + "original_season": 9, + "first_episode": 1, + "last_episode": 3, + "season_offset": 1, + "episode_offset": 0, + } + screen.shiftedSeasonsTable.rows[row_key] = ("broken", "ui", "values", "!", "?") + + self.assertEqual( + { + "id": 44, + "original_season": 9, + "first_episode": 1, + "last_episode": 3, + "season_offset": 1, + "episode_offset": 0, + }, + screen.getSelectedShiftedSeasonObjFromInput(), + ) + + def test_show_details_screen_reads_selected_pattern_from_row_mapping(self): + screen = object.__new__(ShowDetailsScreen) + screen.patternTable = FakeTagTable() + screen._ShowDetailsScreen__showDescriptor = make_show_descriptor(7, "Demo", 1999) + screen._ShowDetailsScreen__patternRowData = {} + + row_key = screen._add_pattern_row(pattern_id=11, pattern_text=r"^demo_(s[0-9]+e[0-9]+)\.mkv$") + screen.patternTable.rows[row_key] = ("display text changed",) + + self.assertEqual( + { + "id": 11, + "show_id": 7, + "pattern": r"^demo_(s[0-9]+e[0-9]+)\.mkv$", + }, + screen.getSelectedPatternDescriptor(), + ) + + def test_show_details_screen_reads_selected_shifted_season_from_row_mapping(self): + screen = object.__new__(ShowDetailsScreen) + screen.shiftedSeasonsTable = FakeTagTable() + screen._ShowDetailsScreen__shiftedSeasonRowData = {} + + row_key = screen.shiftedSeasonsTable.add_row("1", "", "", "0", "0") + screen._ShowDetailsScreen__shiftedSeasonRowData[row_key] = { + "id": 3, + "original_season": 1, + "first_episode": -1, + "last_episode": -1, + "season_offset": 0, + "episode_offset": 0, + } + screen.shiftedSeasonsTable.rows[row_key] = ("bad", "visible", "data", "x", "y") + + self.assertEqual( + { + "id": 3, + "original_season": 1, + "first_episode": -1, + "last_episode": -1, + "season_offset": 0, + "episode_offset": 0, + }, + screen.getSelectedShiftedSeasonObjFromInput(), + ) + + def test_shows_screen_reads_selected_show_id_from_row_mapping(self): + screen = object.__new__(ShowsScreen) + screen.table = FakeTagTable() + screen._ShowsScreen__showRowData = {} + + row_key = screen._add_show_row(make_show_descriptor(4, "Mapped", 2011)) + screen.table.rows[row_key] = ("999", "Visible", "2099") + + self.assertEqual(4, screen.getSelectedShowId()) + + def test_media_details_screen_reads_selected_show_from_row_mapping(self): + screen = object.__new__(MediaDetailsScreen) + screen.showsTable = FakeTagTable() + screen._MediaDetailsScreen__showRowData = {} + + placeholder_key = screen._add_show_row(None) + show_key = screen._add_show_row(make_show_descriptor(8, "Real Show", 2020)) + screen.showsTable.select_row(show_key) + screen.showsTable.rows[show_key] = ("oops", "display", "changed") + + selected_show = screen.getSelectedShowDescriptor() + + self.assertIsInstance(selected_show, ShowDescriptor) + self.assertEqual(8, selected_show.getId()) + self.assertEqual(0, screen.getRowIndexFromShowId(-1)) + self.assertEqual(1, screen.getRowIndexFromShowId(8)) + + screen.removeShow(-1) + self.assertNotIn(placeholder_key, screen._MediaDetailsScreen__showRowData) + self.assertEqual(0, screen.getRowIndexFromShowId(8)) + + +if __name__ == "__main__": + unittest.main()