diff --git a/README.md b/README.md index 05b3849..d826c39 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,12 @@ TMDB-backed metadata enrichment requires `TMDB_API_KEY` to be set in the environ ## Version History +### 0.4.2 + +- pattern details now show an inline `Show: ` hint next to the quality field when the pattern itself has no stored quality but the selected show does +- inspect stream tables now show attachment format labels like `TTF` in the codec column and keep attachment language cells blank instead of showing an undefined language +- ffmpeg damaged-MP3 diagnostics now recognize additional corruption lines such as `invalid new backstep`, keeping them grouped under the `warn-corrupt-mpeg-audio` review summary + ### 0.4.1 - `convert` now supports `--copy-video` and `--copy-audio` to keep the selected stream type in copy mode without applying the corresponding reencode flags, filters, or formatting options diff --git a/pyproject.toml b/pyproject.toml index 6b96781..1d50c85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ffx" description = "FFX recoding and metadata managing tool" -version = "0.4.1" +version = "0.4.2" license = {file = "LICENSE.md"} dependencies = [ "requests", diff --git a/src/ffx/cli.py b/src/ffx/cli.py index f1268a3..58e4878 100755 --- a/src/ffx/cli.py +++ b/src/ffx/cli.py @@ -1628,7 +1628,7 @@ def convert(ctx, for summaryLine in iterUnremediedIssueSummaryLines(context): ctx.obj['logger'].warning(summaryLine) else: - ctx.obj['logger'].info("All files converted with no ffmpeg findings requiring review.") + ctx.obj['logger'].info("All files converted with no issues.") if __name__ == '__main__': diff --git a/src/ffx/constants.py b/src/ffx/constants.py index 4a272ce..13ac0f7 100644 --- a/src/ffx/constants.py +++ b/src/ffx/constants.py @@ -1,4 +1,4 @@ -VERSION='0.4.1' +VERSION='0.4.2' DATABASE_VERSION = 3 DEFAULT_QUALITY = 32 diff --git a/src/ffx/diagnostics/warn_corrupt_mpeg_audio.py b/src/ffx/diagnostics/warn_corrupt_mpeg_audio.py index 1184f5d..34e558b 100644 --- a/src/ffx/diagnostics/warn_corrupt_mpeg_audio.py +++ b/src/ffx/diagnostics/warn_corrupt_mpeg_audio.py @@ -9,6 +9,7 @@ class WarnCorruptMpegAudioRemedy(FfmpegRemedy): identifier = "warn-corrupt-mpeg-audio" PATTERNS = ( re.compile(r"\[mp3float @ .*\] invalid block type", re.IGNORECASE), + re.compile(r"\[mp3float @ .*\] invalid new backstep -?\d+", re.IGNORECASE), re.compile(r"\[mp3float @ .*\] Header missing"), re.compile(r"\[mp3float @ .*\] overread, skip ", re.IGNORECASE), re.compile(r"Error while decoding MPEG audio frame\."), diff --git a/src/ffx/media_workflow_screen_base.py b/src/ffx/media_workflow_screen_base.py index 38274d7..7a2b79e 100644 --- a/src/ffx/media_workflow_screen_base.py +++ b/src/ffx/media_workflow_screen_base.py @@ -6,6 +6,7 @@ from textual.screen import Screen from textual.widgets import DataTable from textual.widgets._data_table import CellDoesNotExist +from ffx.attachment_format import AttachmentFormat from ffx.audio_layout import AudioLayout from ffx.file_properties import FileProperties from ffx.helper import DIFF_ADDED_KEY, DIFF_CHANGED_KEY, DIFF_REMOVED_KEY @@ -127,9 +128,17 @@ class MediaWorkflowScreenBase(Screen): def _track_codec_cell_value(self, trackDescriptor: TrackDescriptor) -> str: if trackDescriptor.getType() == TrackType.ATTACHMENT: - return " " + attachmentFormat = trackDescriptor.getAttachmentFormat() + if attachmentFormat == AttachmentFormat.UNKNOWN: + return attachmentFormat.identifier() + return attachmentFormat.label() return trackDescriptor.getFormatDescriptor().label() + def _track_language_cell_value(self, trackDescriptor: TrackDescriptor) -> str: + if trackDescriptor.getType() == TrackType.ATTACHMENT: + return " " + return trackDescriptor.getLanguage().label() + def _track_disposition_cell_value( self, trackDescriptor: TrackDescriptor, @@ -244,7 +253,7 @@ class MediaWorkflowScreenBase(Screen): if trackType == TrackType.AUDIO and audioLayout != AudioLayout.LAYOUT_UNDEFINED else " ", - trackDescriptor.getLanguage().label(), + self._track_language_cell_value(trackDescriptor), trackTitle, self._track_disposition_cell_value( trackDescriptor, diff --git a/src/ffx/pattern_details_screen.py b/src/ffx/pattern_details_screen.py index ffd541a..db4426e 100644 --- a/src/ffx/pattern_details_screen.py +++ b/src/ffx/pattern_details_screen.py @@ -88,6 +88,9 @@ class PatternDetailsScreen(Screen): .three { column-span: 3; } + .two { + column-span: 2; + } .four { column-span: 4; @@ -114,7 +117,7 @@ class PatternDetailsScreen(Screen): } .yellow { - tint: yellow 40%; + color: yellow; } """ @@ -331,6 +334,7 @@ class PatternDetailsScreen(Screen): if not self.__showDescriptor is None: self.query_one("#showlabel", Static).update(f"{self.__showDescriptor.getId()} - {self.__showDescriptor.getName()} ({self.__showDescriptor.getYear()})") + self.updateShowQualityHint() if self.__pattern is not None: @@ -350,6 +354,7 @@ class PatternDetailsScreen(Screen): if not hasattr(self, "tracksTable") or not hasattr(self, "tagsTable"): return + self.updateShowQualityHint() self.updateTags() self.updateTracks() @@ -415,7 +420,9 @@ class PatternDetailsScreen(Screen): # Row 4 yield Static(t("Quality")) yield Input(type="integer", id="quality_input") - yield Static(' ', classes="five") + yield Static(" ") + yield Static("", id="show_quality_hint", classes="two yellow") + yield Static(' ', classes="two") # Row 5 @@ -504,6 +511,23 @@ class PatternDetailsScreen(Screen): def getPatternFromInput(self): return str(self.query_one("#pattern_input", Input).value) + def getShowQualityHintText(self): + if self.__showDescriptor is None: + return "" + + showQuality = int(self.__showDescriptor.getQuality() or 0) + if showQuality <= 0: + return "" + + patternQuality = int(getattr(self.__pattern, "quality", 0) or 0) + if patternQuality > 0: + return "" + + return f"{t('Show')}: {showQuality}" + + def updateShowQualityHint(self): + self.query_one("#show_quality_hint", Static).update(self.getShowQualityHintText()) + def getQualityFromInput(self): try: return int(self.query_one("#quality_input", Input).value) diff --git a/tests/unit/test_cli_convert_diagnostics.py b/tests/unit/test_cli_convert_diagnostics.py index 3267262..c66462a 100644 --- a/tests/unit/test_cli_convert_diagnostics.py +++ b/tests/unit/test_cli_convert_diagnostics.py @@ -202,7 +202,7 @@ class ConvertDiagnosticCliTests(unittest.TestCase): self.assertEqual(0, result.exit_code, result.output) self.assertIn( - "All files converted with no ffmpeg findings requiring review.", + "All files converted with no issues.", result.output, ) diff --git a/tests/unit/test_ffmpeg_diagnostics.py b/tests/unit/test_ffmpeg_diagnostics.py index a2e9a89..ad85ffb 100644 --- a/tests/unit/test_ffmpeg_diagnostics.py +++ b/tests/unit/test_ffmpeg_diagnostics.py @@ -126,6 +126,9 @@ class FfmpegDiagnosticsTests(unittest.TestCase): ["ffmpeg", "-y", "-i", "input.avi", "output.mkv"], ) + self.assertFalse( + monitor.handle_stderr_line("[mp3float @ 0x1] invalid new backstep -1") + ) self.assertFalse(monitor.handle_stderr_line("[mp3float @ 0x1] invalid block type")) self.assertFalse( monitor.handle_stderr_line( diff --git a/tests/unit/test_tag_table_screen_state.py b/tests/unit/test_tag_table_screen_state.py index a7c8660..3bf0b71 100644 --- a/tests/unit/test_tag_table_screen_state.py +++ b/tests/unit/test_tag_table_screen_state.py @@ -548,6 +548,11 @@ class TagTableScreenStateTests(unittest.TestCase): screen.tagsTable = FakeTagTable() screen.shiftedSeasonsTable = FakeTagTable() screen._PatternDetailsScreen__pattern = object() + screen._PatternDetailsScreen__showDescriptor = None + widgets = { + "#show_quality_hint": FakeStaticWidget(), + } + screen.query_one = lambda selector, _type=None: widgets[selector] calls = [] screen.updateTags = lambda: calls.append("updateTags") @@ -561,6 +566,48 @@ class TagTableScreenStateTests(unittest.TestCase): calls, ) + def test_pattern_details_screen_on_mount_shows_show_quality_hint_for_new_pattern(self): + set_current_language("en") + + screen = object.__new__(PatternDetailsScreen) + screen.context = {} + screen._PatternDetailsScreen__showDescriptor = ShowDescriptor( + id=7, + name="Demo", + year=1999, + quality=23, + ) + screen._PatternDetailsScreen__pattern = None + + widgets = { + "#showlabel": FakeStaticWidget(), + "#show_quality_hint": FakeStaticWidget(), + } + screen.query_one = lambda selector, _type=None: widgets[selector] + + screen.on_mount() + + self.assertEqual("7 - Demo (1999)", widgets["#showlabel"].value) + self.assertEqual("Show: 23", widgets["#show_quality_hint"].value) + + def test_pattern_details_screen_show_quality_hint_is_hidden_when_pattern_quality_exists(self): + set_current_language("en") + + screen = object.__new__(PatternDetailsScreen) + screen._PatternDetailsScreen__showDescriptor = ShowDescriptor( + id=7, + name="Demo", + year=1999, + quality=23, + ) + screen._PatternDetailsScreen__pattern = type( + "_Pattern", + (), + {"quality": 19}, + )() + + self.assertEqual("", screen.getShowQualityHintText()) + def test_inspect_details_screen_handle_edit_pattern_refreshes_even_without_result(self): screen = object.__new__(InspectDetailsScreen) @@ -722,7 +769,7 @@ class TagTableScreenStateTests(unittest.TestCase): self.assertIn("English Full", screen.tracksTable.rows["row-0"]) self.assertIs(target_track, screen.getSelectedTrackDescriptor()) - def test_inspect_details_screen_update_tracks_blanks_irrelevant_attachment_fields(self): + def test_inspect_details_screen_update_tracks_shows_attachment_format_and_blanks_language(self): attachment_track = TrackDescriptor( index=4, source_index=4, @@ -745,10 +792,36 @@ class TagTableScreenStateTests(unittest.TestCase): row = screen.tracksTable.rows["row-0"] self.assertEqual("4", row[0]) - self.assertEqual(" ", row[3]) + self.assertEqual("TTF", row[3]) + self.assertEqual(" ", row[5]) self.assertEqual(" ", row[7]) self.assertEqual(" ", row[8]) + def test_inspect_details_screen_update_tracks_shows_unknown_for_unknown_attachment_format(self): + attachment_track = TrackDescriptor( + index=5, + source_index=5, + sub_index=0, + track_type=TrackType.ATTACHMENT, + attachment_format=AttachmentFormat.UNKNOWN, + tags={"filename": "blob.bin", "mimetype": "application/octet-stream"}, + ) + + screen = object.__new__(InspectDetailsScreen) + screen.tracksTable = FakeTagTable() + screen._sourceMediaDescriptor = FakeMediaDescriptor([attachment_track]) + screen._targetMediaDescriptor = None + screen._currentPattern = None + screen._trackRowData = {} + screen._applyNormalization = False + + screen.updateTracks() + + row = screen.tracksTable.rows["row-0"] + + self.assertEqual("unknown", row[3]) + self.assertEqual(" ", row[5]) + def test_inspect_details_screen_maps_target_selection_back_to_source_track(self): source_track = TrackDescriptor( index=3,