drop flag
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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$")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user