drop flag

This commit is contained in:
Javanaut
2026-07-08 07:18:03 +02:00
parent 697645be25
commit aa864dfd4d
13 changed files with 299 additions and 9 deletions

View File

@@ -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(

View File

@@ -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

View File

@@ -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()

View File

@@ -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$")

View File

@@ -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)