Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12be6e985a | ||
|
|
12310942ae | ||
|
|
f913cb4fe3 | ||
|
|
0a153280e3 | ||
|
|
6ca0cd54b0 |
@@ -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: <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
|
||||
|
||||
- `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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__':
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VERSION='0.4.1'
|
||||
VERSION='0.4.2'
|
||||
DATABASE_VERSION = 3
|
||||
|
||||
DEFAULT_QUALITY = 32
|
||||
|
||||
@@ -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\."),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user