6 Commits

Author SHA1 Message Date
Javanaut
2e2c94f539 Release v0.4.2 2026-04-24 13:40:37 +02:00
Javanaut
12be6e985a v0.4.2 2026-04-24 13:39:57 +02:00
Javanaut
12310942ae Fix inspect attachment subtracks 2026-04-24 10:34:43 +02:00
Javanaut
f913cb4fe3 ff 2026-04-24 08:49:48 +02:00
Javanaut
0a153280e3 ff 2026-04-24 08:49:30 +02:00
Javanaut
6ca0cd54b0 addendum 2026-04-23 22:16:03 +02:00
10 changed files with 126 additions and 10 deletions

View File

@@ -99,6 +99,12 @@ TMDB-backed metadata enrichment requires `TMDB_API_KEY` to be set in the environ
## Version History ## Version History
### 0.4.2
- pattern details now show an inline `Show: <quality>` 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 ### 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 - `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

View File

@@ -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.1" version = "0.4.2"
license = {file = "LICENSE.md"} license = {file = "LICENSE.md"}
dependencies = [ dependencies = [
"requests", "requests",

View File

@@ -1628,7 +1628,7 @@ def convert(ctx,
for summaryLine in iterUnremediedIssueSummaryLines(context): for summaryLine in iterUnremediedIssueSummaryLines(context):
ctx.obj['logger'].warning(summaryLine) ctx.obj['logger'].warning(summaryLine)
else: 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__': if __name__ == '__main__':

View File

@@ -1,4 +1,4 @@
VERSION='0.4.1' VERSION='0.4.2'
DATABASE_VERSION = 3 DATABASE_VERSION = 3
DEFAULT_QUALITY = 32 DEFAULT_QUALITY = 32

View File

@@ -9,6 +9,7 @@ class WarnCorruptMpegAudioRemedy(FfmpegRemedy):
identifier = "warn-corrupt-mpeg-audio" identifier = "warn-corrupt-mpeg-audio"
PATTERNS = ( PATTERNS = (
re.compile(r"\[mp3float @ .*\] invalid block type", re.IGNORECASE), 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 @ .*\] Header missing"),
re.compile(r"\[mp3float @ .*\] overread, skip ", re.IGNORECASE), re.compile(r"\[mp3float @ .*\] overread, skip ", re.IGNORECASE),
re.compile(r"Error while decoding MPEG audio frame\."), re.compile(r"Error while decoding MPEG audio frame\."),

View File

@@ -6,6 +6,7 @@ from textual.screen import Screen
from textual.widgets import DataTable from textual.widgets import DataTable
from textual.widgets._data_table import CellDoesNotExist from textual.widgets._data_table import CellDoesNotExist
from ffx.attachment_format import AttachmentFormat
from ffx.audio_layout import AudioLayout from ffx.audio_layout import AudioLayout
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
@@ -127,9 +128,17 @@ class MediaWorkflowScreenBase(Screen):
def _track_codec_cell_value(self, trackDescriptor: TrackDescriptor) -> str: def _track_codec_cell_value(self, trackDescriptor: TrackDescriptor) -> str:
if trackDescriptor.getType() == TrackType.ATTACHMENT: if trackDescriptor.getType() == TrackType.ATTACHMENT:
return " " attachmentFormat = trackDescriptor.getAttachmentFormat()
if attachmentFormat == AttachmentFormat.UNKNOWN:
return attachmentFormat.identifier()
return attachmentFormat.label()
return trackDescriptor.getFormatDescriptor().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( def _track_disposition_cell_value(
self, self,
trackDescriptor: TrackDescriptor, trackDescriptor: TrackDescriptor,
@@ -244,7 +253,7 @@ class MediaWorkflowScreenBase(Screen):
if trackType == TrackType.AUDIO if trackType == TrackType.AUDIO
and audioLayout != AudioLayout.LAYOUT_UNDEFINED and audioLayout != AudioLayout.LAYOUT_UNDEFINED
else " ", else " ",
trackDescriptor.getLanguage().label(), self._track_language_cell_value(trackDescriptor),
trackTitle, trackTitle,
self._track_disposition_cell_value( self._track_disposition_cell_value(
trackDescriptor, trackDescriptor,

View File

@@ -88,6 +88,9 @@ class PatternDetailsScreen(Screen):
.three { .three {
column-span: 3; column-span: 3;
} }
.two {
column-span: 2;
}
.four { .four {
column-span: 4; column-span: 4;
@@ -114,7 +117,7 @@ class PatternDetailsScreen(Screen):
} }
.yellow { .yellow {
tint: yellow 40%; color: yellow;
} }
""" """
@@ -331,6 +334,7 @@ class PatternDetailsScreen(Screen):
if not self.__showDescriptor is None: if not self.__showDescriptor is None:
self.query_one("#showlabel", Static).update(f"{self.__showDescriptor.getId()} - {self.__showDescriptor.getName()} ({self.__showDescriptor.getYear()})") self.query_one("#showlabel", Static).update(f"{self.__showDescriptor.getId()} - {self.__showDescriptor.getName()} ({self.__showDescriptor.getYear()})")
self.updateShowQualityHint()
if self.__pattern is not None: if self.__pattern is not None:
@@ -350,6 +354,7 @@ class PatternDetailsScreen(Screen):
if not hasattr(self, "tracksTable") or not hasattr(self, "tagsTable"): if not hasattr(self, "tracksTable") or not hasattr(self, "tagsTable"):
return return
self.updateShowQualityHint()
self.updateTags() self.updateTags()
self.updateTracks() self.updateTracks()
@@ -415,7 +420,9 @@ class PatternDetailsScreen(Screen):
# Row 4 # Row 4
yield Static(t("Quality")) yield Static(t("Quality"))
yield Input(type="integer", id="quality_input") 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 # Row 5
@@ -504,6 +511,23 @@ class PatternDetailsScreen(Screen):
def getPatternFromInput(self): def getPatternFromInput(self):
return str(self.query_one("#pattern_input", Input).value) 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): def getQualityFromInput(self):
try: try:
return int(self.query_one("#quality_input", Input).value) return int(self.query_one("#quality_input", Input).value)

View File

@@ -202,7 +202,7 @@ class ConvertDiagnosticCliTests(unittest.TestCase):
self.assertEqual(0, result.exit_code, result.output) self.assertEqual(0, result.exit_code, result.output)
self.assertIn( self.assertIn(
"All files converted with no ffmpeg findings requiring review.", "All files converted with no issues.",
result.output, result.output,
) )

View File

@@ -126,6 +126,9 @@ class FfmpegDiagnosticsTests(unittest.TestCase):
["ffmpeg", "-y", "-i", "input.avi", "output.mkv"], ["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("[mp3float @ 0x1] invalid block type"))
self.assertFalse( self.assertFalse(
monitor.handle_stderr_line( monitor.handle_stderr_line(

View File

@@ -548,6 +548,11 @@ class TagTableScreenStateTests(unittest.TestCase):
screen.tagsTable = FakeTagTable() screen.tagsTable = FakeTagTable()
screen.shiftedSeasonsTable = FakeTagTable() screen.shiftedSeasonsTable = FakeTagTable()
screen._PatternDetailsScreen__pattern = object() screen._PatternDetailsScreen__pattern = object()
screen._PatternDetailsScreen__showDescriptor = None
widgets = {
"#show_quality_hint": FakeStaticWidget(),
}
screen.query_one = lambda selector, _type=None: widgets[selector]
calls = [] calls = []
screen.updateTags = lambda: calls.append("updateTags") screen.updateTags = lambda: calls.append("updateTags")
@@ -561,6 +566,48 @@ class TagTableScreenStateTests(unittest.TestCase):
calls, 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): def test_inspect_details_screen_handle_edit_pattern_refreshes_even_without_result(self):
screen = object.__new__(InspectDetailsScreen) screen = object.__new__(InspectDetailsScreen)
@@ -722,7 +769,7 @@ class TagTableScreenStateTests(unittest.TestCase):
self.assertIn("English Full", screen.tracksTable.rows["row-0"]) self.assertIn("English Full", screen.tracksTable.rows["row-0"])
self.assertIs(target_track, screen.getSelectedTrackDescriptor()) 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( attachment_track = TrackDescriptor(
index=4, index=4,
source_index=4, source_index=4,
@@ -745,10 +792,36 @@ class TagTableScreenStateTests(unittest.TestCase):
row = screen.tracksTable.rows["row-0"] row = screen.tracksTable.rows["row-0"]
self.assertEqual("4", 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[7])
self.assertEqual(" ", row[8]) 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): def test_inspect_details_screen_maps_target_selection_back_to_source_track(self):
source_track = TrackDescriptor( source_track = TrackDescriptor(
index=3, index=3,