diff --git a/src/ffx/database.py b/src/ffx/database.py index 3918d29..7898127 100644 --- a/src/ffx/database.py +++ b/src/ffx/database.py @@ -108,12 +108,18 @@ def ensureCurrentSchemaCompatibility(databaseContext): column['name'] for column in inspector.get_columns('shows') } + trackColumns = { + column['name'] + for column in inspector.get_columns('tracks') + } alterStatements = [] if 'quality' not in showColumns: alterStatements.append("ALTER TABLE shows ADD COLUMN quality INTEGER DEFAULT 0") if 'notes' not in showColumns: alterStatements.append("ALTER TABLE shows ADD COLUMN notes TEXT DEFAULT ''") + if 'dropped' not in trackColumns: + alterStatements.append("ALTER TABLE tracks ADD COLUMN dropped INTEGER DEFAULT 0") if not alterStatements: return diff --git a/src/ffx/model/pattern.py b/src/ffx/model/pattern.py index 1aaf771..5703be4 100644 --- a/src/ffx/model/pattern.py +++ b/src/ffx/model/pattern.py @@ -75,13 +75,17 @@ class Pattern(Base): # Set ordered subindices subIndexCounter = {} + outputIndex = 0 for track in self.tracks: trackType = track.getType() - if trackType == TrackType.ATTACHMENT: + if trackType == TrackType.ATTACHMENT or track.isDropped(): continue if not trackType in subIndexCounter.keys(): subIndexCounter[trackType] = 0 - kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(track.getDescriptor(context, subIndex = subIndexCounter[trackType])) + trackDescriptor = track.getDescriptor(context, subIndex = subIndexCounter[trackType]) + trackDescriptor.setIndex(outputIndex) + kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(trackDescriptor) subIndexCounter[trackType] += 1 + outputIndex += 1 return MediaDescriptor(**kwargs) diff --git a/src/ffx/model/track.py b/src/ffx/model/track.py index b4b195f..aa8d04b 100644 --- a/src/ffx/model/track.py +++ b/src/ffx/model/track.py @@ -36,6 +36,7 @@ class Track(Base): index = Column(Integer) source_index = Column(Integer) + dropped = Column(Integer, default=0) # v1.x pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE")) @@ -181,6 +182,9 @@ class Track(Base): def getSourceIndex(self): return int(self.source_index) if self.source_index is not None else -1 + def isDropped(self) -> bool: + return bool(self.dropped) + def getLanguage(self): tags = {t.key:t.value for t in self.track_tags} return IsoLanguage.findThreeLetter(tags['language']) if 'language' in tags.keys() else IsoLanguage.UNDEFINED @@ -221,6 +225,7 @@ class Track(Base): kwargs[TrackDescriptor.INDEX_KEY] = self.getIndex() kwargs[TrackDescriptor.SOURCE_INDEX_KEY] = self.getSourceIndex() + kwargs[TrackDescriptor.DROPPED_KEY] = self.isDropped() if subIndex > -1: kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex diff --git a/src/ffx/pattern_controller.py b/src/ffx/pattern_controller.py index 384bea9..828881c 100644 --- a/src/ffx/pattern_controller.py +++ b/src/ffx/pattern_controller.py @@ -103,6 +103,14 @@ class PatternController: "Patterns must define at least one track before they can be stored." ) + if not any( + not descriptor.isDropped() + for descriptor in normalized_descriptors + ): + raise InvalidPatternSchemaError( + "Patterns must define at least one non-dropped track before they can be stored." + ) + normalized_descriptors = sorted( normalized_descriptors, key=lambda descriptor: descriptor.getIndex() ) @@ -146,6 +154,7 @@ class PatternController: codec_name=str(trackDescriptor.getFormatDescriptor().identifier()), index=int(trackDescriptor.getIndex()), source_index=int(trackDescriptor.getSourceIndex()), + dropped=1 if trackDescriptor.isDropped() else 0, disposition_flags=int( TrackDisposition.toFlags(trackDescriptor.getDispositionSet()) ), @@ -179,10 +188,19 @@ class PatternController: pattern.tracks.append(self._build_track_row(trackDescriptor)) def _validate_persisted_pattern(self, pattern: Pattern): - if not any(track.getType() != TrackType.ATTACHMENT for track in pattern.tracks): + persisted_tracks = [ + track + for track in pattern.tracks + if track.getType() != TrackType.ATTACHMENT + ] + if not persisted_tracks: raise InvalidPatternSchemaError( f"Pattern #{pattern.getId()} ({pattern.getPattern()!r}) is invalid because it has no tracks." ) + if not any(not track.isDropped() for track in persisted_tracks): + raise InvalidPatternSchemaError( + f"Pattern #{pattern.getId()} ({pattern.getPattern()!r}) is invalid because it has no non-dropped tracks." + ) def savePatternSchema( self, diff --git a/src/ffx/pattern_details_screen.py b/src/ffx/pattern_details_screen.py index db4426e..b5a7f07 100644 --- a/src/ffx/pattern_details_screen.py +++ b/src/ffx/pattern_details_screen.py @@ -185,6 +185,7 @@ class PatternDetailsScreen(Screen): td.getTitle(), t('Yes') if TrackDisposition.DEFAULT in dispoSet else t('No'), t('Yes') if TrackDisposition.FORCED in dispoSet else t('No'), + t('Yes') if td.isDropped() else t('No'), td.getSourceIndex()) row_key = self.tracksTable.add_row(*map(str, row)) @@ -384,6 +385,7 @@ class PatternDetailsScreen(Screen): self.column_key_track_title = add_auto_table_column(self.tracksTable, t("Title")) self.column_key_track_default = add_auto_table_column(self.tracksTable, t("Default")) self.column_key_track_forced = add_auto_table_column(self.tracksTable, t("Forced")) + self.column_key_track_drop = add_auto_table_column(self.tracksTable, t("Drop")) self.column_key_track_source_index = add_auto_table_column(self.tracksTable, t("SrcIndex")) self.tracksTable.cursor_type = 'row' diff --git a/src/ffx/track_controller.py b/src/ffx/track_controller.py index 3af9730..e666b7e 100644 --- a/src/ffx/track_controller.py +++ b/src/ffx/track_controller.py @@ -48,6 +48,7 @@ class TrackController(): codec_name = str(trackDescriptor.getFormatDescriptor().identifier()), index = int(trackDescriptor.getIndex()), source_index = int(trackDescriptor.getSourceIndex()), + dropped = 1 if trackDescriptor.isDropped() else 0, disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())), audio_layout = trackDescriptor.getAudioLayout().index()) @@ -83,7 +84,21 @@ class TrackController(): if track is not None: + patternId = int(track.pattern_id) + if trackDescriptor.isDropped() and not track.isDropped(): + activeSiblingCount = ( + s.query(Track) + .filter(Track.pattern_id == patternId, Track.dropped == 0) + .count() + ) + if activeSiblingCount <= 1: + raise click.ClickException( + f"Cannot drop the last active track from pattern #{patternId}. Patterns must define at least one non-dropped track." + ) + track.index = int(trackDescriptor.getIndex()) + track.source_index = int(trackDescriptor.getSourceIndex()) + track.dropped = 1 if trackDescriptor.isDropped() else 0 track.track_type = int(trackDescriptor.getType().index()) track.codec_name = str(trackDescriptor.getFormatDescriptor().identifier()) @@ -255,6 +270,17 @@ class TrackController(): f"Cannot delete the last track from pattern #{patternId}. Patterns must define at least one track." ) + if not track.isDropped(): + activeSiblingTracks = [ + siblingTrack + for siblingTrack in siblingTracks + if not siblingTrack.isDropped() + ] + if len(activeSiblingTracks) <= 1: + raise click.ClickException( + f"Cannot delete the last active track from pattern #{patternId}. Patterns must define at least one non-dropped track." + ) + index = 0 for track in siblingTracks: diff --git a/src/ffx/track_descriptor.py b/src/ffx/track_descriptor.py index 7dd5f3b..d021a26 100644 --- a/src/ffx/track_descriptor.py +++ b/src/ffx/track_descriptor.py @@ -21,6 +21,7 @@ class TrackDescriptor: SUB_INDEX_KEY = "sub_index" PATTERN_ID_KEY = "pattern_id" EXTERNAL_SOURCE_FILE_PATH_KEY = "external_source_file" + DROPPED_KEY = "dropped" DISPOSITION_SET_KEY = "disposition_set" TAGS_KEY = "tags" @@ -77,6 +78,8 @@ class TrackDescriptor: else: self.__externalSourceFilePath = '' + self.__dropped = bool(kwargs.get(TrackDescriptor.DROPPED_KEY, False)) + if TrackDescriptor.INDEX_KEY in kwargs.keys(): if type(kwargs[TrackDescriptor.INDEX_KEY]) is not int: raise TypeError( @@ -378,11 +381,18 @@ class TrackDescriptor: def getExternalSourceFilePath(self): return self.__externalSourceFilePath + def isDropped(self) -> bool: + return self.__dropped + + def setDropped(self, dropped: bool): + self.__dropped = bool(dropped) + def clone(self, context: dict | None = None): kwargs = { TrackDescriptor.ID_KEY: int(self.__trackId), TrackDescriptor.PATTERN_ID_KEY: int(self.__patternId), TrackDescriptor.EXTERNAL_SOURCE_FILE_PATH_KEY: str(self.__externalSourceFilePath), + TrackDescriptor.DROPPED_KEY: bool(self.__dropped), TrackDescriptor.INDEX_KEY: int(self.__index), TrackDescriptor.SOURCE_INDEX_KEY: int(self.__sourceIndex), TrackDescriptor.SUB_INDEX_KEY: int(self.__subIndex), diff --git a/src/ffx/track_details_screen.py b/src/ffx/track_details_screen.py index c68e3fc..5c37ce8 100644 --- a/src/ffx/track_details_screen.py +++ b/src/ffx/track_details_screen.py @@ -1,7 +1,7 @@ import click from textual.screen import Screen -from textual.widgets import Header, Footer, Static, Button, SelectionList, Select, DataTable, Input +from textual.widgets import Header, Footer, Static, Button, SelectionList, Select, DataTable, Input, Checkbox from textual.containers import Grid from textual.widgets._data_table import CellDoesNotExist @@ -146,6 +146,7 @@ class TrackDetailsScreen(Screen): self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED self.__index = index self.__subIndex = subIndex + self.__dropped = False self.__draftTrackTags = {} initial_language = IsoLanguage.UNDEFINED initial_title = "" @@ -156,6 +157,7 @@ class TrackDetailsScreen(Screen): self.__audioLayout = trackDescriptor.getAudioLayout() self.__index = trackDescriptor.getIndex() self.__subIndex = trackDescriptor.getSubIndex() + self.__dropped = trackDescriptor.isDropped() self.__draftTrackTags = { key: value for key, value in trackDescriptor.getTags().items() @@ -254,6 +256,7 @@ class TrackDetailsScreen(Screen): self.query_one("#type_select", Select).value = self.__trackType self.query_one("#audio_layout_select", Select).value = self.__audioLayout + self.query_one("#drop_checkbox", Checkbox).value = self.__dropped for disposition in TrackDisposition: @@ -283,6 +286,7 @@ class TrackDetailsScreen(Screen): if self.__metadataOnly: self.query_one("#type_select", Select).disabled = True self.query_one("#audio_layout_select", Select).disabled = True + self.query_one("#drop_checkbox", Checkbox).disabled = True def on_select_changed(self, event: Select.Changed) -> None: if event.select.id == "language_select": @@ -344,7 +348,8 @@ class TrackDetailsScreen(Screen): ) # Row 8 - yield Static(" ", classes="five") + yield Static(t("Drop")) + yield Checkbox(id="drop_checkbox", classes="four") # Row 9 yield Static(" ", classes="five") @@ -462,6 +467,9 @@ class TrackDetailsScreen(Screen): trackTags["title"] = title kwargs[TrackDescriptor.TAGS_KEY] = trackTags + kwargs[TrackDescriptor.DROPPED_KEY] = bool( + self.query_one("#drop_checkbox", Checkbox).value + ) dispositionFlags = sum( [2 ** flag for flag in self.query_one("#dispositions_selection_list", SelectionList).selected] @@ -504,6 +512,7 @@ class TrackDetailsScreen(Screen): descriptor for descriptor in siblingTrackList if descriptor.getType() == trackDescriptor.getType() + and not descriptor.isDropped() ] numDefaultTracks = len( @@ -527,10 +536,12 @@ class TrackDetailsScreen(Screen): trackDescriptor.setSubIndex(int(self.__subIndex)) if ( - TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() + not trackDescriptor.isDropped() + and TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() and numDefaultTracks ) or ( - TrackDisposition.FORCED in trackDescriptor.getDispositionSet() + not trackDescriptor.isDropped() + and TrackDisposition.FORCED in trackDescriptor.getDispositionSet() and numForcedTracks ): diff --git a/tests/integration/subtrack_mapping/test_cli_bundle.py b/tests/integration/subtrack_mapping/test_cli_bundle.py index b73ba56..27f5e46 100644 --- a/tests/integration/subtrack_mapping/test_cli_bundle.py +++ b/tests/integration/subtrack_mapping/test_cli_bundle.py @@ -144,6 +144,73 @@ class SubtrackMappingBundleTests(unittest.TestCase): self.assertIn("first embedded subtitle", extracted_subtitle) self.assertNotIn("second embedded subtitle", extracted_subtitle) + def test_pattern_dropped_track_is_omitted_like_deleted_track(self): + source_filename = "dropped_s01e01.mkv" + source_path = create_source_fixture( + self.workdir, + source_filename, + [ + SourceTrackSpec(TrackType.VIDEO, identity="video-0"), + SourceTrackSpec(TrackType.AUDIO, identity="audio-1", language="eng"), + SourceTrackSpec( + TrackType.SUBTITLE, + identity="subtitle-2", + language="eng", + subtitle_lines=("kept subtitle payload",), + ), + SourceTrackSpec( + TrackType.SUBTITLE, + identity="subtitle-3", + language="eng", + subtitle_lines=("dropped subtitle payload",), + ), + ], + ) + + prepare_pattern_database( + self.database_path, + r"^dropped_(s[0-9]+e[0-9]+)\.mkv$", + [ + PatternTrackSpec(index=0, source_index=0, track_type=TrackType.VIDEO), + PatternTrackSpec(index=1, source_index=1, track_type=TrackType.AUDIO), + PatternTrackSpec(index=2, source_index=2, track_type=TrackType.SUBTITLE), + PatternTrackSpec( + index=3, + source_index=3, + track_type=TrackType.SUBTITLE, + dropped=True, + ), + ], + ) + + completed = run_ffx_convert( + self.workdir, + self.home_dir, + self.database_path, + "--video-encoder", + "copy", + "--no-tmdb", + "--no-prompt", + "--no-signature", + str(source_path), + ) + self.assertCompleted(completed) + + output_path = expected_output_path(self.workdir, source_filename) + streams = ffprobe_json(output_path)["streams"] + + self.assertEqual( + [stream["codec_type"] for stream in streams], + ["video", "audio", "subtitle"], + ) + self.assertNotIn( + "subtitle-3", + [get_tag(stream, "THIS_IS") for stream in streams], + ) + extracted_subtitle = extract_first_subtitle_text(self.workdir, output_path) + self.assertIn("kept subtitle payload", extracted_subtitle) + self.assertNotIn("dropped subtitle payload", extracted_subtitle) + def test_cli_rearrange_streams_reorders_tracks_without_database_pattern(self): source_filename = "cli_s01e01.mkv" source_path = create_source_fixture( diff --git a/tests/support/ffx_bundle.py b/tests/support/ffx_bundle.py index 800b62e..bb8fe74 100644 --- a/tests/support/ffx_bundle.py +++ b/tests/support/ffx_bundle.py @@ -56,6 +56,7 @@ class PatternTrackSpec: track_type: TrackType tags: Mapping[str, str] = field(default_factory=dict) dispositions: tuple[TrackDisposition, ...] = () + dropped: bool = False audio_layout: AudioLayout = AudioLayout.LAYOUT_STEREO attachment_format: AttachmentFormat = AttachmentFormat.UNKNOWN @@ -298,6 +299,7 @@ def prepare_pattern_database(database_path: Path, filename_pattern: str, track_s TrackDescriptor.TRACK_TYPE_KEY: track.track_type, TrackDescriptor.TAGS_KEY: dict(track.tags), TrackDescriptor.DISPOSITION_SET_KEY: set(track.dispositions), + TrackDescriptor.DROPPED_KEY: track.dropped, } if track.track_type == TrackType.AUDIO: kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = track.audio_layout diff --git a/tests/unit/test_database.py b/tests/unit/test_database.py index a6fef87..64b7de6 100644 --- a/tests/unit/test_database.py +++ b/tests/unit/test_database.py @@ -149,6 +149,49 @@ class DatabaseContextTests(unittest.TestCase): ) cursor.execute("DROP TABLE shifted_seasons_current") + def rewrite_tracks_table_without_dropped(self, cursor): + cursor.execute("ALTER TABLE tracks RENAME TO tracks_current") + cursor.execute( + """ + CREATE TABLE tracks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + track_type INTEGER, + "index" INTEGER, + source_index INTEGER, + pattern_id INTEGER, + disposition_flags INTEGER, + codec_name VARCHAR, + audio_layout INTEGER, + FOREIGN KEY(pattern_id) REFERENCES patterns(id) ON DELETE CASCADE + ) + """ + ) + cursor.execute( + """ + INSERT INTO tracks ( + id, + track_type, + "index", + source_index, + pattern_id, + disposition_flags, + codec_name, + audio_layout + ) + SELECT + id, + track_type, + "index", + source_index, + pattern_id, + disposition_flags, + codec_name, + audio_layout + FROM tracks_current + """ + ) + cursor.execute("DROP TABLE tracks_current") + def test_database_context_bootstraps_new_database_with_current_version(self): with patch("ffx.database.Base.metadata.create_all", wraps=Base.metadata.create_all) as mocked_create_all: context = databaseContext(str(self.database_path)) @@ -321,6 +364,41 @@ class DatabaseContextTests(unittest.TestCase): mocked_confirm.assert_not_called() mocked_echo.assert_not_called() + def test_database_context_repairs_current_track_schema_without_version_bump(self): + context = databaseContext(str(self.database_path)) + context["engine"].dispose() + + connection = sqlite3.connect(self.database_path) + try: + cursor = connection.cursor() + cursor.execute("PRAGMA foreign_keys=OFF") + self.rewrite_tracks_table_without_dropped(cursor) + connection.commit() + finally: + connection.close() + + with patch("ffx.database.click.confirm") as mocked_confirm, patch( + "ffx.database.click.echo" + ) as mocked_echo: + reopened_context = databaseContext(str(self.database_path)) + try: + self.assertEqual(DATABASE_VERSION, getDatabaseVersion(reopened_context)) + + connection = sqlite3.connect(self.database_path) + try: + column_names = { + row[1] + for row in connection.execute("PRAGMA table_info(tracks)").fetchall() + } + self.assertIn("dropped", column_names) + finally: + connection.close() + finally: + reopened_context["engine"].dispose() + + mocked_confirm.assert_not_called() + mocked_echo.assert_not_called() + if __name__ == "__main__": unittest.main() diff --git a/tests/unit/test_pattern_management.py b/tests/unit/test_pattern_management.py index 3a1aadd..11190c1 100644 --- a/tests/unit/test_pattern_management.py +++ b/tests/unit/test_pattern_management.py @@ -65,6 +65,7 @@ def make_track_descriptor( track_type: TrackType = TrackType.VIDEO, title: str = "", dispositions: set[TrackDisposition] | None = None, + dropped: bool = False, ) -> TrackDescriptor: kwargs = { TrackDescriptor.INDEX_KEY: index, @@ -72,6 +73,7 @@ def make_track_descriptor( TrackDescriptor.TRACK_TYPE_KEY: track_type, TrackDescriptor.TAGS_KEY: {"title": title} if title else {}, TrackDescriptor.DISPOSITION_SET_KEY: dispositions or set(), + TrackDescriptor.DROPPED_KEY: dropped, } if track_type == TrackType.AUDIO: kwargs[TrackDescriptor.AUDIO_LAYOUT_KEY] = AudioLayout.LAYOUT_STEREO @@ -193,6 +195,47 @@ class PatternManagementTests(unittest.TestCase): self.assertIn("at least one track", str(caught.exception)) + def test_save_pattern_schema_rejects_all_dropped_patterns(self): + self.add_show(1, "Dropped Pattern Show") + + with self.assertRaises(InvalidPatternSchemaError) as caught: + self.pattern_controller.savePatternSchema( + { + "show_id": 1, + "pattern": r"^dropped_(s[0-9]+e[0-9]+)\.mkv$", + }, + trackDescriptors=[make_track_descriptor(0, dropped=True)], + ) + + self.assertIn("non-dropped track", str(caught.exception)) + + def test_dropped_pattern_track_is_not_in_conversion_media_descriptor(self): + pattern_id = self.save_pattern( + 1, + r"^active_(s[0-9]+e[0-9]+)\.mkv$", + tracks=[ + make_track_descriptor(0, track_type=TrackType.VIDEO), + make_track_descriptor(1, track_type=TrackType.AUDIO, dropped=True), + make_track_descriptor(2, track_type=TrackType.SUBTITLE), + ], + ) + + Session = self.context["database"]["session"] + session = Session() + try: + pattern = session.query(Pattern).filter(Pattern.id == pattern_id).first() + media_descriptor = pattern.getMediaDescriptor(self.context) + tracks = media_descriptor.getTrackDescriptors() + finally: + session.close() + + self.assertEqual( + [TrackType.VIDEO, TrackType.SUBTITLE], + [track.getType() for track in tracks], + ) + self.assertEqual([0, 1], [track.getIndex() for track in tracks]) + self.assertEqual([0, 2], [track.getSourceIndex() for track in tracks]) + def test_save_pattern_schema_does_not_persist_attachment_tracks(self): pattern_id = self.save_pattern( 1, @@ -257,6 +300,17 @@ class PatternManagementTests(unittest.TestCase): self.assertIn("last track", str(caught.exception)) + def test_track_controller_refuses_to_drop_last_active_track(self): + pattern_id = self.save_pattern(1, r"^drop_(s[0-9]+e[0-9]+)\.mkv$") + track = self.track_controller.getTrack(pattern_id, 0) + descriptor = track.getDescriptor() + descriptor.setDropped(True) + + with self.assertRaises(click.ClickException) as caught: + self.track_controller.updateTrack(track.getId(), descriptor) + + self.assertIn("last active track", str(caught.exception)) + def test_exact_duplicate_pattern_definition_is_rejected(self): self.save_pattern(1, r"^unique_(s[0-9]+e[0-9]+)\.mkv$") diff --git a/tests/unit/test_tag_table_screen_state.py b/tests/unit/test_tag_table_screen_state.py index 819a991..0f6c378 100644 --- a/tests/unit/test_tag_table_screen_state.py +++ b/tests/unit/test_tag_table_screen_state.py @@ -131,13 +131,14 @@ class FakeSelectionListWidget: return None -def make_track_descriptor(index, sub_index, track_type): +def make_track_descriptor(index, sub_index, track_type, *, dropped=False): return TrackDescriptor( index=index, sub_index=sub_index, track_type=track_type, codec_name=TrackCodec.UNKNOWN, audio_layout=AudioLayout.LAYOUT_UNDEFINED, + dropped=dropped, ) @@ -188,6 +189,7 @@ class TagTableScreenStateTests(unittest.TestCase): widgets = { "#type_select": FakeValueWidget(TrackType.AUDIO), "#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_STEREO), + "#drop_checkbox": FakeValueWidget(True), "#language_select": FakeValueWidget(IsoLanguage.GERMAN), "#title_input": FakeInputWidget("German Audio"), "#dispositions_selection_list": FakeSelectionListWidget({0, 6}), @@ -201,6 +203,7 @@ class TagTableScreenStateTests(unittest.TestCase): self.assertEqual("deu", descriptor.getTags()["language"]) self.assertEqual("German Audio", descriptor.getTitle()) self.assertEqual("value", descriptor.getTags()["KEEP"]) + self.assertTrue(descriptor.isDropped()) def test_track_details_screen_preserves_attachment_format_for_attachment_tracks(self): screen = object.__new__(TrackDetailsScreen) @@ -216,6 +219,7 @@ class TagTableScreenStateTests(unittest.TestCase): widgets = { "#type_select": FakeValueWidget(TrackType.ATTACHMENT), "#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_UNDEFINED), + "#drop_checkbox": FakeValueWidget(False), "#language_select": FakeValueWidget(Select.NULL), "#title_input": FakeInputWidget(""), "#dispositions_selection_list": FakeSelectionListWidget(set()), @@ -307,6 +311,7 @@ class TagTableScreenStateTests(unittest.TestCase): audio_layout=AudioLayout.LAYOUT_STEREO, tags={"language": "ger"}, ) + screen._TrackDetailsScreen__dropped = False screen._TrackDetailsScreen__metadataOnly = True screen._TrackDetailsScreen__titleAutoManaged = True screen._TrackDetailsScreen__suppressTitleChanged = False @@ -323,6 +328,7 @@ class TagTableScreenStateTests(unittest.TestCase): "#pattern_label": FakeStaticWidget(), "#type_select": FakeValueWidget(None), "#audio_layout_select": FakeValueWidget(None), + "#drop_checkbox": FakeValueWidget(False), "#dispositions_selection_list": FakeSelectionListWidget(set()), "#language_select": FakeValueWidget(None), "#title_input": FakeInputWidget(""), @@ -355,7 +361,7 @@ class TagTableScreenStateTests(unittest.TestCase): 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) + second_track = make_track_descriptor(1, 0, TrackType.SUBTITLE, dropped=True) screen = object.__new__(PatternDetailsScreen) screen.tracksTable = FakeTagTable() @@ -367,6 +373,7 @@ class TagTableScreenStateTests(unittest.TestCase): screen.tracksTable.select_row("row-1") self.assertIs(second_track, screen.getSelectedTrackDescriptor()) + self.assertEqual("Ja", screen.tracksTable.rows["row-1"][9]) def test_pattern_details_screen_reads_selected_tag_from_raw_row_mapping(self): screen = object.__new__(PatternDetailsScreen)