Splits screen classes
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
from textual.app import App
|
||||
|
||||
from .shows_screen import ShowsScreen
|
||||
from .media_details_screen import MediaDetailsScreen
|
||||
from .inspect_details_screen import InspectDetailsScreen
|
||||
from .media_edit_screen import MediaEditScreen
|
||||
|
||||
|
||||
class FfxApp(App):
|
||||
@@ -28,8 +29,11 @@ class FfxApp(App):
|
||||
if self.context['command'] == 'shows':
|
||||
self.push_screen(ShowsScreen())
|
||||
|
||||
if self.context['command'] in ('inspect', 'edit'):
|
||||
self.push_screen(MediaDetailsScreen())
|
||||
if self.context['command'] == 'inspect':
|
||||
self.push_screen(InspectDetailsScreen())
|
||||
|
||||
if self.context['command'] == 'edit':
|
||||
self.push_screen(MediaEditScreen())
|
||||
|
||||
|
||||
def getContext(self):
|
||||
|
||||
370
src/ffx/inspect_details_screen.py
Normal file
370
src/ffx/inspect_details_screen.py
Normal file
@@ -0,0 +1,370 @@
|
||||
import re
|
||||
|
||||
import click
|
||||
|
||||
from textual.containers import Grid
|
||||
from textual.widgets import Button, Footer, Header, Input, Static
|
||||
from textual.widgets._data_table import CellDoesNotExist
|
||||
|
||||
from ffx.file_properties import FileProperties
|
||||
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet
|
||||
from ffx.show_descriptor import ShowDescriptor
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
|
||||
from .media_workflow_screen_base import MediaWorkflowScreenBase
|
||||
from .pattern_details_screen import PatternDetailsScreen
|
||||
from .screen_support import build_screen_controllers
|
||||
from .show_details_screen import ShowDetailsScreen
|
||||
|
||||
|
||||
class InspectDetailsScreen(MediaWorkflowScreenBase):
|
||||
|
||||
COMMAND_NAME = "inspect"
|
||||
DIFFERENCES_COLUMN_LABEL = "Differences (file->db/output)"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "app.quit", "Quit"),
|
||||
("n", "new_pattern", "New Pattern"),
|
||||
("u", "update_pattern", "Update Pattern"),
|
||||
("e", "edit_pattern", "Edit Pattern"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self._showRowData: dict[object, ShowDescriptor | None] = {}
|
||||
super().__init__()
|
||||
|
||||
controllers = build_screen_controllers(
|
||||
self.context,
|
||||
pattern=True,
|
||||
show=True,
|
||||
track=True,
|
||||
tag=True,
|
||||
)
|
||||
self._pc = controllers["pattern"]
|
||||
self._sc = controllers["show"]
|
||||
self._tc = controllers["track"]
|
||||
self._tac = controllers["tag"]
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
|
||||
def compose(self):
|
||||
self._build_media_tags_table()
|
||||
self._build_tracks_table()
|
||||
self._build_differences_table()
|
||||
|
||||
yield Header()
|
||||
|
||||
with Grid():
|
||||
self.showsTable = self._build_shows_table()
|
||||
|
||||
yield Static("Show")
|
||||
yield self.showsTable
|
||||
yield Static(" ")
|
||||
yield self.differencesTable
|
||||
|
||||
yield Static(" ", classes="four")
|
||||
|
||||
yield Static(" ")
|
||||
yield Button("Substitute", id="pattern_button")
|
||||
yield Static(" ", classes="two")
|
||||
|
||||
yield Static("Pattern")
|
||||
yield Input(type="text", id="pattern_input", classes="two")
|
||||
yield Static(" ")
|
||||
|
||||
yield Static(" ", classes="four")
|
||||
|
||||
yield Static("Media Tags")
|
||||
yield self.mediaTagsTable
|
||||
yield Static(" ")
|
||||
|
||||
yield Static(" ", classes="four")
|
||||
|
||||
yield Static(" ")
|
||||
yield Button("Set Default", id="select_default_button")
|
||||
yield Button("Set Forced", id="select_forced_button")
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Streams")
|
||||
yield self.tracksTable
|
||||
yield Static(" ")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def _build_shows_table(self):
|
||||
from textual.widgets import DataTable
|
||||
|
||||
showsTable = DataTable(classes="two")
|
||||
showsTable.add_column("ID", width=10)
|
||||
showsTable.add_column("Name", width=80)
|
||||
showsTable.add_column("Year", width=10)
|
||||
showsTable.cursor_type = "row"
|
||||
return showsTable
|
||||
|
||||
def on_mount(self):
|
||||
if self._currentPattern is None:
|
||||
self._add_show_row(None)
|
||||
|
||||
for show in self._sc.getAllShows():
|
||||
self._add_show_row(show.getDescriptor(self.context))
|
||||
|
||||
if self._currentPattern is not None:
|
||||
showIdentifier = self._currentPattern.getShowId()
|
||||
showRowIndex = self.getRowIndexFromShowId(showIdentifier)
|
||||
if showRowIndex is not None:
|
||||
self.showsTable.move_cursor(row=showRowIndex)
|
||||
|
||||
self.query_one("#pattern_input", Input).value = self._currentPattern.getPattern()
|
||||
else:
|
||||
self.query_one("#pattern_input", Input).value = self._mediaFilename
|
||||
self.highlightPattern(True)
|
||||
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "pattern_button":
|
||||
pattern = self.query_one("#pattern_input", Input).value
|
||||
patternMatch = re.search(FileProperties.SE_INDICATOR_PATTERN, pattern)
|
||||
if patternMatch:
|
||||
self.query_one("#pattern_input", Input).value = pattern.replace(
|
||||
patternMatch.group(1),
|
||||
FileProperties.SE_INDICATOR_PATTERN,
|
||||
)
|
||||
|
||||
if event.button.id == "select_default_button":
|
||||
if self.setSelectedTrackDefault():
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
|
||||
if event.button.id == "select_forced_button":
|
||||
if self.setSelectedTrackForced():
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
|
||||
def removeShow(self, showId: int = -1):
|
||||
for row_key, show_descriptor in list(self._showRowData.items()):
|
||||
if (
|
||||
(showId == -1 and show_descriptor is None)
|
||||
or (
|
||||
show_descriptor is not None
|
||||
and show_descriptor.getId() == showId
|
||||
)
|
||||
):
|
||||
self.showsTable.remove_row(row_key)
|
||||
self._showRowData.pop(row_key, None)
|
||||
return
|
||||
|
||||
def getRowIndexFromShowId(self, showId: int = -1) -> int | None:
|
||||
for row_key, show_descriptor in self._showRowData.items():
|
||||
if (
|
||||
(showId == -1 and show_descriptor is None)
|
||||
or (
|
||||
show_descriptor is not None
|
||||
and show_descriptor.getId() == showId
|
||||
)
|
||||
):
|
||||
return int(self.showsTable.get_row_index(row_key))
|
||||
|
||||
return None
|
||||
|
||||
def _add_show_row(self, show_descriptor: ShowDescriptor | None):
|
||||
if show_descriptor is None:
|
||||
row_key = self.showsTable.add_row(" ", "<New show>", " ")
|
||||
else:
|
||||
row_key = self.showsTable.add_row(
|
||||
str(show_descriptor.getId()),
|
||||
str(show_descriptor.getName()),
|
||||
str(show_descriptor.getYear()),
|
||||
)
|
||||
|
||||
self._showRowData[row_key] = show_descriptor
|
||||
return row_key
|
||||
|
||||
def highlightPattern(self, state: bool):
|
||||
patternInput = self.query_one("#pattern_input", Input)
|
||||
patternInput.styles.background = "red" if state else None
|
||||
|
||||
def getSelectedShowDescriptor(self) -> ShowDescriptor | None:
|
||||
try:
|
||||
row_key, _ = self.showsTable.coordinate_to_cell_key(
|
||||
self.showsTable.cursor_coordinate
|
||||
)
|
||||
|
||||
if row_key is not None:
|
||||
return self._showRowData.get(row_key)
|
||||
except (CellDoesNotExist, AttributeError):
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def getPatternObjFromInput(self):
|
||||
patternObj = {}
|
||||
try:
|
||||
patternObj["show_id"] = self.getSelectedShowDescriptor().getId()
|
||||
patternObj["pattern"] = str(self.query_one("#pattern_input", Input).value)
|
||||
except Exception:
|
||||
return {}
|
||||
return patternObj
|
||||
|
||||
def handle_new_pattern(self, showDescriptor: ShowDescriptor):
|
||||
if type(showDescriptor) is not ShowDescriptor:
|
||||
raise TypeError(
|
||||
"InspectDetailsScreen.handle_new_pattern(): Argument 'showDescriptor' has to be of type ShowDescriptor"
|
||||
)
|
||||
|
||||
self.removeShow()
|
||||
|
||||
showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId())
|
||||
if showRowIndex is None:
|
||||
self._add_show_row(showDescriptor)
|
||||
|
||||
showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId())
|
||||
if showRowIndex is not None:
|
||||
self.showsTable.move_cursor(row=showRowIndex)
|
||||
|
||||
patternObj = self.getPatternObjFromInput()
|
||||
if patternObj:
|
||||
mediaTags = {}
|
||||
for tagKey, tagValue in self._sourceMediaDescriptor.getTags().items():
|
||||
if (
|
||||
tagKey not in self._ignoreGlobalKeys
|
||||
and tagKey not in self._removeGlobalKeys
|
||||
):
|
||||
mediaTags[tagKey] = tagValue
|
||||
|
||||
patternId = self._pc.savePatternSchema(
|
||||
patternObj,
|
||||
trackDescriptors=self._sourceMediaDescriptor.getTrackDescriptors(),
|
||||
mediaTags=mediaTags,
|
||||
)
|
||||
if patternId:
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
self.highlightPattern(False)
|
||||
|
||||
def action_new_pattern(self):
|
||||
selectedShowDescriptor = self.getSelectedShowDescriptor()
|
||||
if selectedShowDescriptor is None:
|
||||
self.app.push_screen(ShowDetailsScreen(), self.handle_new_pattern)
|
||||
else:
|
||||
self.handle_new_pattern(selectedShowDescriptor)
|
||||
|
||||
def action_update_pattern(self):
|
||||
if self._currentPattern is not None:
|
||||
patternObj = self.getPatternObjFromInput()
|
||||
if (
|
||||
patternObj
|
||||
and self._currentPattern.getPattern() != patternObj["pattern"]
|
||||
):
|
||||
updated = self._pc.updatePattern(
|
||||
self._currentPattern.getId(),
|
||||
patternObj,
|
||||
)
|
||||
if updated:
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
return updated
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
|
||||
tagDifferences = self._mediaChangeSetObj.get(MediaDescriptorChangeSet.TAGS_KEY, {})
|
||||
for addedTagKey in tagDifferences.get(DIFF_ADDED_KEY, {}).keys():
|
||||
self._tac.deleteMediaTagByKey(self._currentPattern.getId(), addedTagKey)
|
||||
|
||||
for removedTagKey in tagDifferences.get(DIFF_REMOVED_KEY, {}).keys():
|
||||
currentTags = self._sourceMediaDescriptor.getTags()
|
||||
self._tac.updateMediaTag(
|
||||
self._currentPattern.getId(),
|
||||
removedTagKey,
|
||||
currentTags[removedTagKey],
|
||||
)
|
||||
|
||||
for changedTagKey in tagDifferences.get(DIFF_CHANGED_KEY, {}).keys():
|
||||
currentTags = self._sourceMediaDescriptor.getTags()
|
||||
self._tac.updateMediaTag(
|
||||
self._currentPattern.getId(),
|
||||
changedTagKey,
|
||||
currentTags[changedTagKey],
|
||||
)
|
||||
|
||||
trackDifferences = self._mediaChangeSetObj.get(MediaDescriptorChangeSet.TRACKS_KEY, {})
|
||||
|
||||
for trackDescriptor in trackDifferences.get(DIFF_ADDED_KEY, {}).values():
|
||||
self._tc.addTrack(trackDescriptor, patternId=self._currentPattern.getId())
|
||||
|
||||
for trackDescriptor in trackDifferences.get(DIFF_REMOVED_KEY, {}).values():
|
||||
self._tc.deleteTrack(trackDescriptor.getId())
|
||||
|
||||
for trackIndex, trackDiff in trackDifferences.get(DIFF_CHANGED_KEY, {}).items():
|
||||
targetTracks = [
|
||||
track
|
||||
for track in self._targetMediaDescriptor.getTrackDescriptors()
|
||||
if track.getIndex() == trackIndex
|
||||
]
|
||||
targetTrackId = targetTracks[0].getId() if targetTracks else None
|
||||
targetTrackIndex = targetTracks[0].getIndex() if targetTracks else None
|
||||
|
||||
tagsDiff = trackDiff.get(TrackDescriptor.TAGS_KEY, {})
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_ADDED_KEY, {}).items():
|
||||
self._tac.updateTrackTag(targetTrackId, tagKey, tagValue)
|
||||
for tagKey in tagsDiff.get(DIFF_REMOVED_KEY, {}).keys():
|
||||
self._tac.deleteTrackTagByKey(targetTrackId, tagKey)
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_CHANGED_KEY, {}).items():
|
||||
self._tac.updateTrackTag(targetTrackId, tagKey, tagValue)
|
||||
|
||||
dispositionDiff = trackDiff.get(TrackDescriptor.DISPOSITION_SET_KEY, {})
|
||||
for changedDisposition in dispositionDiff.get(DIFF_ADDED_KEY, set()):
|
||||
if targetTrackIndex is not None:
|
||||
self._tc.setDispositionState(
|
||||
self._currentPattern.getId(),
|
||||
targetTrackIndex,
|
||||
changedDisposition,
|
||||
True,
|
||||
)
|
||||
for changedDisposition in dispositionDiff.get(DIFF_REMOVED_KEY, set()):
|
||||
if targetTrackIndex is not None:
|
||||
self._tc.setDispositionState(
|
||||
self._currentPattern.getId(),
|
||||
targetTrackIndex,
|
||||
changedDisposition,
|
||||
False,
|
||||
)
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
|
||||
def action_edit_pattern(self):
|
||||
patternObj = self.getPatternObjFromInput()
|
||||
if patternObj.get("pattern"):
|
||||
selectedPatternId = self._pc.findPattern(patternObj)
|
||||
if selectedPatternId is None:
|
||||
raise click.ClickException(
|
||||
"InspectDetailsScreen.action_edit_pattern(): Pattern to edit has no id"
|
||||
)
|
||||
|
||||
self.app.push_screen(
|
||||
PatternDetailsScreen(
|
||||
patternId=selectedPatternId,
|
||||
showId=self.getSelectedShowDescriptor().getId(),
|
||||
),
|
||||
self.handle_edit_pattern,
|
||||
)
|
||||
|
||||
def handle_edit_pattern(self, screenResult):
|
||||
if not screenResult:
|
||||
return
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
if self._currentPattern is not None:
|
||||
self.query_one("#pattern_input", Input).value = self._currentPattern.getPattern()
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
@@ -1,998 +1 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
import click
|
||||
|
||||
from textual.containers import Grid
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, DataTable, Footer, Header, Input, Static
|
||||
from textual.widgets._data_table import CellDoesNotExist
|
||||
|
||||
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
|
||||
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet
|
||||
from ffx.metadata_editor import apply_metadata_edits
|
||||
from ffx.show_descriptor import ShowDescriptor
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
from ffx.track_disposition import TrackDisposition
|
||||
from ffx.track_type import TrackType
|
||||
|
||||
from .confirm_screen import ConfirmScreen
|
||||
from .pattern_details_screen import PatternDetailsScreen
|
||||
from .screen_support import (
|
||||
build_screen_bootstrap,
|
||||
build_screen_controllers,
|
||||
populate_tag_table,
|
||||
)
|
||||
from .show_details_screen import ShowDetailsScreen
|
||||
from .tag_delete_screen import TagDeleteScreen
|
||||
from .tag_details_screen import TagDetailsScreen
|
||||
from .track_details_screen import TrackDetailsScreen
|
||||
|
||||
|
||||
class MediaDetailsScreen(Screen):
|
||||
|
||||
CSS = """
|
||||
|
||||
Grid {
|
||||
grid-size: 5 10;
|
||||
grid-rows: 2 2 2 2 2 8 2 2 8 2;
|
||||
grid-columns: 15 25 90 10 105;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
DataTable .datatable--cursor {
|
||||
background: darkorange;
|
||||
color: black;
|
||||
}
|
||||
|
||||
DataTable .datatable--header {
|
||||
background: steelblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
Input {
|
||||
border: none;
|
||||
}
|
||||
Button {
|
||||
border: none;
|
||||
}
|
||||
|
||||
DataTable {
|
||||
min-height: 24;
|
||||
}
|
||||
|
||||
.two {
|
||||
column-span: 2;
|
||||
}
|
||||
.three {
|
||||
column-span: 3;
|
||||
}
|
||||
.four {
|
||||
column-span: 4;
|
||||
}
|
||||
.five {
|
||||
column-span: 5;
|
||||
}
|
||||
|
||||
#differences-table {
|
||||
row-span: 10;
|
||||
}
|
||||
"""
|
||||
|
||||
TRACKS_TABLE_INDEX_COLUMN_LABEL = "Index"
|
||||
TRACKS_TABLE_TYPE_COLUMN_LABEL = "Type"
|
||||
TRACKS_TABLE_SUB_INDEX_COLUMN_LABEL = "SubIndex"
|
||||
TRACKS_TABLE_CODEC_COLUMN_LABEL = "Codec"
|
||||
TRACKS_TABLE_LAYOUT_COLUMN_LABEL = "Layout"
|
||||
TRACKS_TABLE_LANGUAGE_COLUMN_LABEL = "Language"
|
||||
TRACKS_TABLE_TITLE_COLUMN_LABEL = "Title"
|
||||
TRACKS_TABLE_DEFAULT_COLUMN_LABEL = "Default"
|
||||
TRACKS_TABLE_FORCED_COLUMN_LABEL = "Forced"
|
||||
|
||||
INSPECT_DIFFERENCES_COLUMN_LABEL = "Differences (file->db/output)"
|
||||
EDIT_DIFFERENCES_COLUMN_LABEL = "Planned Changes (file->edited output)"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "quit_screen", "Quit"),
|
||||
("a", "apply_changes", "Apply"),
|
||||
("r", "revert_changes", "Revert"),
|
||||
("n", "new_pattern", "New Pattern"),
|
||||
("u", "update_pattern", "Update Pattern"),
|
||||
("e", "edit_pattern", "Edit Pattern"),
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
bootstrap = build_screen_bootstrap(self.app.getContext())
|
||||
self.context = bootstrap.context
|
||||
|
||||
self.__applyCleanup = bootstrap.apply_cleanup
|
||||
self.__removeGlobalKeys = bootstrap.remove_global_keys
|
||||
self.__ignoreGlobalKeys = bootstrap.ignore_global_keys
|
||||
|
||||
command = self.context.get("command")
|
||||
self.__inspectMode = command == "inspect"
|
||||
self.__editMode = command == "edit"
|
||||
|
||||
if not (self.__inspectMode or self.__editMode):
|
||||
raise click.ClickException(
|
||||
"MediaDetailsScreen.__init__(): Can only perform command 'inspect' or 'edit'"
|
||||
)
|
||||
|
||||
arguments = self.context.get("arguments", {})
|
||||
self.__mediaFilename = arguments.get("filename", "")
|
||||
if not self.__mediaFilename:
|
||||
raise click.ClickException(
|
||||
"MediaDetailsScreen.__init__(): Argument 'filename' is required"
|
||||
)
|
||||
if not os.path.isfile(self.__mediaFilename):
|
||||
raise click.ClickException(
|
||||
f"MediaDetailsScreen.__init__(): Media file {self.__mediaFilename} does not exist"
|
||||
)
|
||||
|
||||
self.__pc = None
|
||||
self.__sc = None
|
||||
self.__tc = None
|
||||
self.__tac = None
|
||||
if self.__inspectMode:
|
||||
controllers = build_screen_controllers(
|
||||
self.context,
|
||||
pattern=True,
|
||||
show=True,
|
||||
track=True,
|
||||
tag=True,
|
||||
)
|
||||
self.__pc = controllers["pattern"]
|
||||
self.__sc = controllers["show"]
|
||||
self.__tc = controllers["track"]
|
||||
self.__tac = controllers["tag"]
|
||||
|
||||
self.__baselineMediaDescriptor = None
|
||||
self.__sourceMediaDescriptor = None
|
||||
self.__targetMediaDescriptor = None
|
||||
self.__currentPattern = None
|
||||
self.__mediaChangeSetObj = {}
|
||||
self.__messageText = ""
|
||||
|
||||
self.__showRowData: dict[object, ShowDescriptor | None] = {}
|
||||
self.__trackRowData: dict[object, TrackDescriptor] = {}
|
||||
self.__sourceMediaTagRowData: dict[object, tuple[str, str]] = {}
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
|
||||
def _build_media_tags_table(self):
|
||||
self.mediaTagsTable = DataTable(classes="two")
|
||||
self.mediaTagsTable.add_column("Key", width=30)
|
||||
self.mediaTagsTable.add_column("Value", width=70)
|
||||
self.mediaTagsTable.cursor_type = "row"
|
||||
|
||||
def _build_tracks_table(self):
|
||||
self.tracksTable = DataTable(classes="two")
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_INDEX_COLUMN_LABEL,
|
||||
width=5,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_TYPE_COLUMN_LABEL,
|
||||
width=10,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_SUB_INDEX_COLUMN_LABEL,
|
||||
width=8,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_CODEC_COLUMN_LABEL,
|
||||
width=10,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_LAYOUT_COLUMN_LABEL,
|
||||
width=10,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_LANGUAGE_COLUMN_LABEL,
|
||||
width=15,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_TITLE_COLUMN_LABEL,
|
||||
width=48,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_DEFAULT_COLUMN_LABEL,
|
||||
width=8,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
MediaDetailsScreen.TRACKS_TABLE_FORCED_COLUMN_LABEL,
|
||||
width=8,
|
||||
)
|
||||
self.tracksTable.cursor_type = "row"
|
||||
|
||||
def _build_differences_table(self):
|
||||
self.differencesTable = DataTable(id="differences-table")
|
||||
self.differencesTable.add_column(
|
||||
(
|
||||
MediaDetailsScreen.INSPECT_DIFFERENCES_COLUMN_LABEL
|
||||
if self.__inspectMode
|
||||
else MediaDetailsScreen.EDIT_DIFFERENCES_COLUMN_LABEL
|
||||
),
|
||||
width=100,
|
||||
)
|
||||
self.differencesTable.cursor_type = "row"
|
||||
|
||||
def compose(self):
|
||||
self._build_media_tags_table()
|
||||
self._build_tracks_table()
|
||||
self._build_differences_table()
|
||||
|
||||
yield Header()
|
||||
|
||||
with Grid():
|
||||
if self.__inspectMode:
|
||||
self.showsTable = DataTable(classes="two")
|
||||
self.showsTable.add_column("ID", width=10)
|
||||
self.showsTable.add_column("Name", width=80)
|
||||
self.showsTable.add_column("Year", width=10)
|
||||
self.showsTable.cursor_type = "row"
|
||||
|
||||
yield Static("Show")
|
||||
yield self.showsTable
|
||||
yield Static(" ")
|
||||
yield self.differencesTable
|
||||
|
||||
yield Static(" ", classes="four")
|
||||
|
||||
yield Static(" ")
|
||||
yield Button("Substitute", id="pattern_button")
|
||||
yield Static(" ", classes="two")
|
||||
|
||||
yield Static("Pattern")
|
||||
yield Input(type="text", id="pattern_input", classes="two")
|
||||
yield Static(" ")
|
||||
|
||||
yield Static(" ", classes="four")
|
||||
|
||||
yield Static("Media Tags")
|
||||
yield self.mediaTagsTable
|
||||
yield Static(" ")
|
||||
|
||||
yield Static(" ", classes="four")
|
||||
|
||||
yield Static(" ")
|
||||
yield Button("Set Default", id="select_default_button")
|
||||
yield Button("Set Forced", id="select_forced_button")
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Streams")
|
||||
yield self.tracksTable
|
||||
yield Static(" ")
|
||||
|
||||
else:
|
||||
yield Static("File")
|
||||
yield Static(self.__mediaFilename, id="file_label", classes="two", markup=False)
|
||||
yield Static(" ")
|
||||
yield self.differencesTable
|
||||
|
||||
yield Static("Cleanup")
|
||||
yield Static(
|
||||
"Enabled" if self.__applyCleanup else "Disabled",
|
||||
id="cleanup_label",
|
||||
classes="two",
|
||||
markup=False,
|
||||
)
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Status")
|
||||
yield Static("", id="message_label", classes="two", markup=False)
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Media Tags")
|
||||
yield Button("Add", id="button_add_tag")
|
||||
yield Button("Edit", id="button_edit_tag")
|
||||
yield Button("Delete", id="button_delete_tag")
|
||||
|
||||
yield Static(" ")
|
||||
yield self.mediaTagsTable
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Streams")
|
||||
yield Button("Edit", id="button_edit_track")
|
||||
yield Button("Set Default", id="select_default_button")
|
||||
yield Button("Set Forced", id="select_forced_button")
|
||||
|
||||
yield Static(" ")
|
||||
yield self.tracksTable
|
||||
yield Static(" ")
|
||||
|
||||
yield Static(" ")
|
||||
yield Button("Apply", id="apply_button")
|
||||
yield Button("Revert", id="revert_button")
|
||||
yield Button("Quit", id="quit_button")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
if self.__inspectMode:
|
||||
if self.__currentPattern is None:
|
||||
self._add_show_row(None)
|
||||
|
||||
for show in self.__sc.getAllShows():
|
||||
self._add_show_row(show.getDescriptor(self.context))
|
||||
|
||||
if self.__currentPattern is not None:
|
||||
showIdentifier = self.__currentPattern.getShowId()
|
||||
showRowIndex = self.getRowIndexFromShowId(showIdentifier)
|
||||
if showRowIndex is not None:
|
||||
self.showsTable.move_cursor(row=showRowIndex)
|
||||
|
||||
self.query_one("#pattern_input", Input).value = self.__currentPattern.getPattern()
|
||||
else:
|
||||
self.query_one("#pattern_input", Input).value = self.__mediaFilename
|
||||
self.highlightPattern(True)
|
||||
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
self.setMessage(self.__messageText)
|
||||
|
||||
def setMessage(self, message: str):
|
||||
self.__messageText = str(message)
|
||||
|
||||
if self.__editMode:
|
||||
try:
|
||||
self.query_one("#message_label", Static).update(self.__messageText)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def reloadProperties(self, reset_draft: bool = True):
|
||||
self.__mediaFileProperties = FileProperties(self.context, self.__mediaFilename)
|
||||
probedMediaDescriptor = self.__mediaFileProperties.getMediaDescriptor()
|
||||
|
||||
if self.__inspectMode:
|
||||
self.__baselineMediaDescriptor = probedMediaDescriptor
|
||||
self.__sourceMediaDescriptor = probedMediaDescriptor
|
||||
self.__currentPattern = self.__mediaFileProperties.getPattern()
|
||||
self.__targetMediaDescriptor = (
|
||||
self.__currentPattern.getMediaDescriptor(self.context)
|
||||
if self.__currentPattern is not None
|
||||
else None
|
||||
)
|
||||
else:
|
||||
self.__baselineMediaDescriptor = probedMediaDescriptor
|
||||
if reset_draft or self.__sourceMediaDescriptor is None:
|
||||
self.__sourceMediaDescriptor = probedMediaDescriptor.clone(context=self.context)
|
||||
self.__currentPattern = None
|
||||
self.__targetMediaDescriptor = self.__sourceMediaDescriptor
|
||||
|
||||
self.rebuildChangeSet()
|
||||
|
||||
def rebuildChangeSet(self):
|
||||
try:
|
||||
if self.__inspectMode:
|
||||
if self.__targetMediaDescriptor is None:
|
||||
self.__mediaChangeSetObj = {}
|
||||
return
|
||||
mdcs = MediaDescriptorChangeSet(
|
||||
self.context,
|
||||
self.__targetMediaDescriptor,
|
||||
self.__sourceMediaDescriptor,
|
||||
)
|
||||
else:
|
||||
mdcs = MediaDescriptorChangeSet(
|
||||
self.context,
|
||||
self.__sourceMediaDescriptor,
|
||||
self.__baselineMediaDescriptor,
|
||||
)
|
||||
|
||||
self.__mediaChangeSetObj = mdcs.getChangeSetObj()
|
||||
except ValueError:
|
||||
self.__mediaChangeSetObj = {}
|
||||
|
||||
def hasPendingChanges(self) -> bool:
|
||||
return bool(self.__mediaChangeSetObj)
|
||||
|
||||
def updateMediaTags(self):
|
||||
currentDescriptor = self.__sourceMediaDescriptor
|
||||
self.__sourceMediaTagRowData = populate_tag_table(
|
||||
self.mediaTagsTable,
|
||||
currentDescriptor.getTags(),
|
||||
ignore_keys=self.__ignoreGlobalKeys,
|
||||
remove_keys=self.__removeGlobalKeys,
|
||||
)
|
||||
|
||||
def updateTracks(self):
|
||||
self.tracksTable.clear()
|
||||
self.__trackRowData = {}
|
||||
|
||||
trackDescriptorList = self.__sourceMediaDescriptor.getTrackDescriptors()
|
||||
typeCounter = {}
|
||||
|
||||
for td in trackDescriptorList:
|
||||
trackType = td.getType()
|
||||
if trackType not in typeCounter:
|
||||
typeCounter[trackType] = 0
|
||||
|
||||
dispositionSet = td.getDispositionSet()
|
||||
audioLayout = td.getAudioLayout()
|
||||
row = (
|
||||
td.getIndex(),
|
||||
trackType.label(),
|
||||
typeCounter[trackType],
|
||||
td.getCodec().label(),
|
||||
audioLayout.label()
|
||||
if trackType == TrackType.AUDIO
|
||||
and audioLayout != AudioLayout.LAYOUT_UNDEFINED
|
||||
else " ",
|
||||
td.getLanguage().label(),
|
||||
td.getTitle(),
|
||||
"Yes" if TrackDisposition.DEFAULT in dispositionSet else "No",
|
||||
"Yes" if TrackDisposition.FORCED in dispositionSet else "No",
|
||||
)
|
||||
|
||||
row_key = self.tracksTable.add_row(*map(str, row))
|
||||
self.__trackRowData[row_key] = td
|
||||
typeCounter[trackType] += 1
|
||||
|
||||
def updateDifferences(self):
|
||||
self.rebuildChangeSet()
|
||||
self.differencesTable.clear()
|
||||
|
||||
if self.__inspectMode and self.__currentPattern is None:
|
||||
return
|
||||
|
||||
targetDescriptor = (
|
||||
self.__targetMediaDescriptor
|
||||
if self.__inspectMode
|
||||
else self.__sourceMediaDescriptor
|
||||
)
|
||||
targetTrackDescriptorsByIndex = {
|
||||
trackDescriptor.getIndex(): trackDescriptor
|
||||
for trackDescriptor in (
|
||||
targetDescriptor.getTrackDescriptors()
|
||||
if targetDescriptor is not None
|
||||
else []
|
||||
)
|
||||
}
|
||||
|
||||
tagDifferences = self.__mediaChangeSetObj.get(MediaDescriptorChangeSet.TAGS_KEY, {})
|
||||
for tagKey, tagValue in tagDifferences.get(DIFF_ADDED_KEY, {}).items():
|
||||
if tagKey not in self.__ignoreGlobalKeys:
|
||||
self.differencesTable.add_row(
|
||||
f"add media tag: key='{tagKey}' value='{tagValue}'"
|
||||
)
|
||||
|
||||
for tagKey, tagValue in tagDifferences.get(DIFF_REMOVED_KEY, {}).items():
|
||||
if tagKey in self.__ignoreGlobalKeys:
|
||||
continue
|
||||
if self.__inspectMode and tagKey in self.__removeGlobalKeys:
|
||||
continue
|
||||
self.differencesTable.add_row(
|
||||
f"remove media tag: key='{tagKey}' value='{tagValue}'"
|
||||
)
|
||||
|
||||
for tagKey, tagValue in tagDifferences.get(DIFF_CHANGED_KEY, {}).items():
|
||||
if tagKey not in self.__ignoreGlobalKeys:
|
||||
self.differencesTable.add_row(
|
||||
f"change media tag: key='{tagKey}' value='{tagValue}'"
|
||||
)
|
||||
|
||||
trackDifferences = self.__mediaChangeSetObj.get(MediaDescriptorChangeSet.TRACKS_KEY, {})
|
||||
|
||||
for trackDescriptor in trackDifferences.get(DIFF_ADDED_KEY, {}).values():
|
||||
self.differencesTable.add_row(
|
||||
f"add {trackDescriptor.getType().label()} track: "
|
||||
+ f"index={trackDescriptor.getIndex()} lang={trackDescriptor.getLanguage().threeLetter()}"
|
||||
)
|
||||
|
||||
for trackIndex, _trackDescriptor in trackDifferences.get(DIFF_REMOVED_KEY, {}).items():
|
||||
self.differencesTable.add_row(f"remove stream #{trackIndex}")
|
||||
|
||||
for trackIndex, trackDiffObj in trackDifferences.get(DIFF_CHANGED_KEY, {}).items():
|
||||
targetTrackDescriptor = targetTrackDescriptorsByIndex.get(trackIndex)
|
||||
if targetTrackDescriptor is None:
|
||||
continue
|
||||
|
||||
tagsDiff = trackDiffObj.get(MediaDescriptorChangeSet.TAGS_KEY, {})
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_REMOVED_KEY, {}).items():
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"remove key={tagKey} value={tagValue}"
|
||||
)
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_ADDED_KEY, {}).items():
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"add key={tagKey} value={tagValue}"
|
||||
)
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_CHANGED_KEY, {}).items():
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"change key={tagKey} value={tagValue}"
|
||||
)
|
||||
|
||||
dispositionDiff = trackDiffObj.get(MediaDescriptorChangeSet.DISPOSITION_SET_KEY, {})
|
||||
for addedDisposition in dispositionDiff.get(DIFF_ADDED_KEY, set()):
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"add disposition={addedDisposition.label()}"
|
||||
)
|
||||
for removedDisposition in dispositionDiff.get(DIFF_REMOVED_KEY, set()):
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"remove disposition={removedDisposition.label()}"
|
||||
)
|
||||
|
||||
def removeShow(self, showId: int = -1):
|
||||
for row_key, show_descriptor in list(self.__showRowData.items()):
|
||||
if (
|
||||
(showId == -1 and show_descriptor is None)
|
||||
or (
|
||||
show_descriptor is not None
|
||||
and show_descriptor.getId() == showId
|
||||
)
|
||||
):
|
||||
self.showsTable.remove_row(row_key)
|
||||
self.__showRowData.pop(row_key, None)
|
||||
return
|
||||
|
||||
def getRowIndexFromShowId(self, showId: int = -1) -> int | None:
|
||||
for row_key, show_descriptor in self.__showRowData.items():
|
||||
if (
|
||||
(showId == -1 and show_descriptor is None)
|
||||
or (
|
||||
show_descriptor is not None
|
||||
and show_descriptor.getId() == showId
|
||||
)
|
||||
):
|
||||
return int(self.showsTable.get_row_index(row_key))
|
||||
|
||||
return None
|
||||
|
||||
def _add_show_row(self, show_descriptor: ShowDescriptor | None):
|
||||
if show_descriptor is None:
|
||||
row_key = self.showsTable.add_row(" ", "<New show>", " ")
|
||||
else:
|
||||
row_key = self.showsTable.add_row(
|
||||
str(show_descriptor.getId()),
|
||||
str(show_descriptor.getName()),
|
||||
str(show_descriptor.getYear()),
|
||||
)
|
||||
|
||||
self.__showRowData[row_key] = show_descriptor
|
||||
return row_key
|
||||
|
||||
def highlightPattern(self, state: bool):
|
||||
if not self.__inspectMode:
|
||||
return
|
||||
|
||||
patternInput = self.query_one("#pattern_input", Input)
|
||||
patternInput.styles.background = "red" if state else None
|
||||
|
||||
def getSelectedMediaTag(self):
|
||||
try:
|
||||
row_key, _ = self.mediaTagsTable.coordinate_to_cell_key(
|
||||
self.mediaTagsTable.cursor_coordinate
|
||||
)
|
||||
if row_key is not None:
|
||||
return self.__sourceMediaTagRowData.get(row_key)
|
||||
return None
|
||||
except CellDoesNotExist:
|
||||
return None
|
||||
|
||||
def getSelectedTrackDescriptor(self):
|
||||
try:
|
||||
row_key, _ = self.tracksTable.coordinate_to_cell_key(
|
||||
self.tracksTable.cursor_coordinate
|
||||
)
|
||||
if row_key is not None:
|
||||
return self.__trackRowData.get(row_key)
|
||||
return None
|
||||
except CellDoesNotExist:
|
||||
return None
|
||||
|
||||
def getSelectedShowDescriptor(self) -> ShowDescriptor | None:
|
||||
try:
|
||||
row_key, _ = self.showsTable.coordinate_to_cell_key(
|
||||
self.showsTable.cursor_coordinate
|
||||
)
|
||||
if row_key is not None:
|
||||
return self.__showRowData.get(row_key)
|
||||
except (CellDoesNotExist, AttributeError):
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def getPatternObjFromInput(self):
|
||||
patternObj = {}
|
||||
try:
|
||||
patternObj["show_id"] = self.getSelectedShowDescriptor().getId()
|
||||
patternObj["pattern"] = str(self.query_one("#pattern_input", Input).value)
|
||||
except Exception:
|
||||
return {}
|
||||
return patternObj
|
||||
|
||||
def refreshAfterDraftChange(self):
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "pattern_button" and self.__inspectMode:
|
||||
pattern = self.query_one("#pattern_input", Input).value
|
||||
patternMatch = re.search(FileProperties.SE_INDICATOR_PATTERN, pattern)
|
||||
if patternMatch:
|
||||
self.query_one("#pattern_input", Input).value = pattern.replace(
|
||||
patternMatch.group(1),
|
||||
FileProperties.SE_INDICATOR_PATTERN,
|
||||
)
|
||||
|
||||
if event.button.id == "select_default_button":
|
||||
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
|
||||
if selectedTrackDescriptor is not None:
|
||||
self.__sourceMediaDescriptor.setDefaultSubTrack(
|
||||
selectedTrackDescriptor.getType(),
|
||||
selectedTrackDescriptor.getSubIndex(),
|
||||
)
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
if event.button.id == "select_forced_button":
|
||||
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
|
||||
if selectedTrackDescriptor is not None:
|
||||
self.__sourceMediaDescriptor.setForcedSubTrack(
|
||||
selectedTrackDescriptor.getType(),
|
||||
selectedTrackDescriptor.getSubIndex(),
|
||||
)
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
if not self.__editMode:
|
||||
return
|
||||
|
||||
if event.button.id == "button_add_tag":
|
||||
self.app.push_screen(TagDetailsScreen(), self.handle_update_media_tag)
|
||||
|
||||
if event.button.id == "button_edit_tag":
|
||||
selectedTag = self.getSelectedMediaTag()
|
||||
if selectedTag is not None:
|
||||
self.app.push_screen(
|
||||
TagDetailsScreen(key=selectedTag[0], value=selectedTag[1]),
|
||||
self.handle_update_media_tag,
|
||||
)
|
||||
|
||||
if event.button.id == "button_delete_tag":
|
||||
selectedTag = self.getSelectedMediaTag()
|
||||
if selectedTag is not None:
|
||||
self.app.push_screen(
|
||||
TagDeleteScreen(key=selectedTag[0], value=selectedTag[1]),
|
||||
self.handle_delete_media_tag,
|
||||
)
|
||||
|
||||
if event.button.id == "button_edit_track":
|
||||
self.action_edit_selected_track()
|
||||
|
||||
if event.button.id == "apply_button":
|
||||
self.action_apply_changes()
|
||||
|
||||
if event.button.id == "revert_button":
|
||||
self.action_revert_changes()
|
||||
|
||||
if event.button.id == "quit_button":
|
||||
self.action_quit_screen()
|
||||
|
||||
def action_edit_selected_track(self):
|
||||
if not self.__editMode:
|
||||
return
|
||||
|
||||
selectedTrack = self.getSelectedTrackDescriptor()
|
||||
if selectedTrack is None:
|
||||
self.setMessage("Select a stream first.")
|
||||
return
|
||||
|
||||
self.app.push_screen(
|
||||
TrackDetailsScreen(
|
||||
trackDescriptor=selectedTrack,
|
||||
patternLabel=os.path.basename(self.__mediaFilename),
|
||||
siblingTrackDescriptors=self.__sourceMediaDescriptor.getTrackDescriptors(),
|
||||
metadata_only=True,
|
||||
),
|
||||
self.handle_edit_track,
|
||||
)
|
||||
|
||||
def handle_update_media_tag(self, tag):
|
||||
if not self.__editMode or tag is None:
|
||||
return
|
||||
|
||||
self.__sourceMediaDescriptor.getTags()[str(tag[0])] = str(tag[1])
|
||||
self.setMessage(f"Updated media tag {tag[0]!r}.")
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
def handle_delete_media_tag(self, tag):
|
||||
if not self.__editMode or tag is None:
|
||||
return
|
||||
|
||||
self.__sourceMediaDescriptor.getTags().pop(str(tag[0]), None)
|
||||
self.setMessage(f"Deleted media tag {tag[0]!r}.")
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
def handle_edit_track(self, trackDescriptor: TrackDescriptor):
|
||||
if not self.__editMode or trackDescriptor is None:
|
||||
return
|
||||
|
||||
updatedTracks = []
|
||||
replaced = False
|
||||
for currentTrack in self.__sourceMediaDescriptor.getTrackDescriptors():
|
||||
if (
|
||||
currentTrack.getIndex() == trackDescriptor.getIndex()
|
||||
and currentTrack.getSubIndex() == trackDescriptor.getSubIndex()
|
||||
):
|
||||
updatedTracks.append(trackDescriptor)
|
||||
replaced = True
|
||||
else:
|
||||
updatedTracks.append(currentTrack)
|
||||
|
||||
if not replaced:
|
||||
self.setMessage("Unable to update selected stream.")
|
||||
return
|
||||
|
||||
self.__sourceMediaDescriptor = self.__sourceMediaDescriptor.clone(context=self.context)
|
||||
self.__sourceMediaDescriptor.getTrackDescriptors().clear()
|
||||
self.__sourceMediaDescriptor.getTrackDescriptors().extend(updatedTracks)
|
||||
self.setMessage(
|
||||
f"Updated stream #{trackDescriptor.getIndex()} ({trackDescriptor.getType().label()})."
|
||||
)
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
def action_apply_changes(self):
|
||||
if not self.__editMode:
|
||||
return
|
||||
|
||||
if not self.hasPendingChanges():
|
||||
self.setMessage("No changes to apply.")
|
||||
return
|
||||
|
||||
try:
|
||||
applyResult = apply_metadata_edits(
|
||||
self.context,
|
||||
self.__mediaFilename,
|
||||
self.__baselineMediaDescriptor,
|
||||
self.__sourceMediaDescriptor,
|
||||
)
|
||||
except Exception as ex:
|
||||
self.context["logger"].exception(
|
||||
"Failed to apply metadata edits for %s",
|
||||
self.__mediaFilename,
|
||||
)
|
||||
self.setMessage(f"Apply failed: {ex}")
|
||||
return
|
||||
|
||||
if applyResult.get("dry_run", False):
|
||||
self.setMessage(
|
||||
f"Dry-run: would rewrite via temporary file {applyResult['target_path']}"
|
||||
)
|
||||
return
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.refreshAfterDraftChange()
|
||||
self.setMessage("Changes applied and file reloaded.")
|
||||
|
||||
def action_revert_changes(self):
|
||||
if not self.__editMode:
|
||||
return
|
||||
|
||||
if not self.hasPendingChanges():
|
||||
self.setMessage("No changes to revert.")
|
||||
return
|
||||
|
||||
self.app.push_screen(
|
||||
ConfirmScreen(
|
||||
"Discard pending metadata changes and reload the file state?",
|
||||
confirm_label="Discard",
|
||||
cancel_label="Keep Editing",
|
||||
),
|
||||
self.handle_revert_confirmation,
|
||||
)
|
||||
|
||||
def handle_revert_confirmation(self, confirmed):
|
||||
if not confirmed:
|
||||
self.setMessage("Keeping pending changes.")
|
||||
return
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.refreshAfterDraftChange()
|
||||
self.setMessage("Reverted pending changes.")
|
||||
|
||||
def action_quit_screen(self):
|
||||
if self.__editMode and self.hasPendingChanges():
|
||||
self.app.push_screen(
|
||||
ConfirmScreen(
|
||||
"Discard pending metadata changes and quit?",
|
||||
confirm_label="Discard",
|
||||
cancel_label="Stay",
|
||||
),
|
||||
self.handle_quit_confirmation,
|
||||
)
|
||||
return
|
||||
|
||||
self.app.exit()
|
||||
|
||||
def handle_quit_confirmation(self, confirmed):
|
||||
if confirmed:
|
||||
self.app.exit()
|
||||
else:
|
||||
self.setMessage("Continuing edit session.")
|
||||
|
||||
def handle_new_pattern(self, showDescriptor: ShowDescriptor):
|
||||
if type(showDescriptor) is not ShowDescriptor:
|
||||
raise TypeError(
|
||||
"MediaDetailsScreen.handle_new_pattern(): Argument 'showDescriptor' has to be of type ShowDescriptor"
|
||||
)
|
||||
|
||||
self.removeShow()
|
||||
|
||||
showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId())
|
||||
if showRowIndex is None:
|
||||
self._add_show_row(showDescriptor)
|
||||
|
||||
showRowIndex = self.getRowIndexFromShowId(showDescriptor.getId())
|
||||
if showRowIndex is not None:
|
||||
self.showsTable.move_cursor(row=showRowIndex)
|
||||
|
||||
patternObj = self.getPatternObjFromInput()
|
||||
if patternObj:
|
||||
mediaTags = {}
|
||||
for tagKey, tagValue in self.__sourceMediaDescriptor.getTags().items():
|
||||
if (
|
||||
tagKey not in self.__ignoreGlobalKeys
|
||||
and tagKey not in self.__removeGlobalKeys
|
||||
):
|
||||
mediaTags[tagKey] = tagValue
|
||||
|
||||
patternId = self.__pc.savePatternSchema(
|
||||
patternObj,
|
||||
trackDescriptors=self.__sourceMediaDescriptor.getTrackDescriptors(),
|
||||
mediaTags=mediaTags,
|
||||
)
|
||||
if patternId:
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
self.highlightPattern(False)
|
||||
|
||||
def action_new_pattern(self):
|
||||
if not self.__inspectMode:
|
||||
return
|
||||
|
||||
selectedShowDescriptor = self.getSelectedShowDescriptor()
|
||||
if selectedShowDescriptor is None:
|
||||
self.app.push_screen(ShowDetailsScreen(), self.handle_new_pattern)
|
||||
else:
|
||||
self.handle_new_pattern(selectedShowDescriptor)
|
||||
|
||||
def action_update_pattern(self):
|
||||
if not self.__inspectMode:
|
||||
return
|
||||
|
||||
if self.__currentPattern is not None:
|
||||
patternObj = self.getPatternObjFromInput()
|
||||
if (
|
||||
patternObj
|
||||
and self.__currentPattern.getPattern() != patternObj["pattern"]
|
||||
):
|
||||
updated = self.__pc.updatePattern(
|
||||
self.__currentPattern.getId(),
|
||||
patternObj,
|
||||
)
|
||||
if updated:
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
return updated
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
|
||||
tagDifferences = self.__mediaChangeSetObj.get(MediaDescriptorChangeSet.TAGS_KEY, {})
|
||||
for addedTagKey in tagDifferences.get(DIFF_ADDED_KEY, {}).keys():
|
||||
self.__tac.deleteMediaTagByKey(self.__currentPattern.getId(), addedTagKey)
|
||||
|
||||
for removedTagKey in tagDifferences.get(DIFF_REMOVED_KEY, {}).keys():
|
||||
currentTags = self.__sourceMediaDescriptor.getTags()
|
||||
self.__tac.updateMediaTag(
|
||||
self.__currentPattern.getId(),
|
||||
removedTagKey,
|
||||
currentTags[removedTagKey],
|
||||
)
|
||||
|
||||
for changedTagKey in tagDifferences.get(DIFF_CHANGED_KEY, {}).keys():
|
||||
currentTags = self.__sourceMediaDescriptor.getTags()
|
||||
self.__tac.updateMediaTag(
|
||||
self.__currentPattern.getId(),
|
||||
changedTagKey,
|
||||
currentTags[changedTagKey],
|
||||
)
|
||||
|
||||
trackDifferences = self.__mediaChangeSetObj.get(MediaDescriptorChangeSet.TRACKS_KEY, {})
|
||||
|
||||
for trackDescriptor in trackDifferences.get(DIFF_ADDED_KEY, {}).values():
|
||||
self.__tc.addTrack(trackDescriptor, patternId=self.__currentPattern.getId())
|
||||
|
||||
for trackDescriptor in trackDifferences.get(DIFF_REMOVED_KEY, {}).values():
|
||||
self.__tc.deleteTrack(trackDescriptor.getId())
|
||||
|
||||
for trackIndex, trackDiff in trackDifferences.get(DIFF_CHANGED_KEY, {}).items():
|
||||
targetTracks = [
|
||||
track
|
||||
for track in self.__targetMediaDescriptor.getTrackDescriptors()
|
||||
if track.getIndex() == trackIndex
|
||||
]
|
||||
targetTrackId = targetTracks[0].getId() if targetTracks else None
|
||||
targetTrackIndex = targetTracks[0].getIndex() if targetTracks else None
|
||||
|
||||
tagsDiff = trackDiff.get(TrackDescriptor.TAGS_KEY, {})
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_ADDED_KEY, {}).items():
|
||||
self.__tac.updateTrackTag(targetTrackId, tagKey, tagValue)
|
||||
for tagKey in tagsDiff.get(DIFF_REMOVED_KEY, {}).keys():
|
||||
self.__tac.deleteTrackTagByKey(targetTrackId, tagKey)
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_CHANGED_KEY, {}).items():
|
||||
self.__tac.updateTrackTag(targetTrackId, tagKey, tagValue)
|
||||
|
||||
dispositionDiff = trackDiff.get(TrackDescriptor.DISPOSITION_SET_KEY, {})
|
||||
for changedDisposition in dispositionDiff.get(DIFF_ADDED_KEY, set()):
|
||||
if targetTrackIndex is not None:
|
||||
self.__tc.setDispositionState(
|
||||
self.__currentPattern.getId(),
|
||||
targetTrackIndex,
|
||||
changedDisposition,
|
||||
True,
|
||||
)
|
||||
for changedDisposition in dispositionDiff.get(DIFF_REMOVED_KEY, set()):
|
||||
if targetTrackIndex is not None:
|
||||
self.__tc.setDispositionState(
|
||||
self.__currentPattern.getId(),
|
||||
targetTrackIndex,
|
||||
changedDisposition,
|
||||
False,
|
||||
)
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
|
||||
def action_edit_pattern(self):
|
||||
if not self.__inspectMode:
|
||||
return
|
||||
|
||||
patternObj = self.getPatternObjFromInput()
|
||||
if patternObj.get("pattern"):
|
||||
selectedPatternId = self.__pc.findPattern(patternObj)
|
||||
if selectedPatternId is None:
|
||||
raise click.ClickException(
|
||||
"MediaDetailsScreen.action_edit_pattern(): Pattern to edit has no id"
|
||||
)
|
||||
|
||||
self.app.push_screen(
|
||||
PatternDetailsScreen(
|
||||
patternId=selectedPatternId,
|
||||
showId=self.getSelectedShowDescriptor().getId(),
|
||||
),
|
||||
self.handle_edit_pattern,
|
||||
)
|
||||
|
||||
def handle_edit_pattern(self, screenResult):
|
||||
if not screenResult:
|
||||
return
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
if self.__currentPattern is not None:
|
||||
self.query_one("#pattern_input", Input).value = self.__currentPattern.getPattern()
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
from .inspect_details_screen import InspectDetailsScreen as MediaDetailsScreen
|
||||
|
||||
269
src/ffx/media_edit_screen.py
Normal file
269
src/ffx/media_edit_screen.py
Normal file
@@ -0,0 +1,269 @@
|
||||
import os
|
||||
|
||||
from textual.containers import Grid
|
||||
from textual.widgets import Button, Footer, Header, Static
|
||||
|
||||
from ffx.metadata_editor import apply_metadata_edits
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
|
||||
from .confirm_screen import ConfirmScreen
|
||||
from .media_workflow_screen_base import MediaWorkflowScreenBase
|
||||
from .tag_delete_screen import TagDeleteScreen
|
||||
from .tag_details_screen import TagDetailsScreen
|
||||
from .track_details_screen import TrackDetailsScreen
|
||||
|
||||
|
||||
class MediaEditScreen(MediaWorkflowScreenBase):
|
||||
|
||||
COMMAND_NAME = "edit"
|
||||
EDIT_MODE = True
|
||||
DIFFERENCES_COLUMN_LABEL = "Planned Changes (file->edited output)"
|
||||
|
||||
BINDINGS = [
|
||||
("q", "quit_screen", "Quit"),
|
||||
("a", "apply_changes", "Apply"),
|
||||
("r", "revert_changes", "Revert"),
|
||||
]
|
||||
|
||||
def compose(self):
|
||||
self._build_media_tags_table()
|
||||
self._build_tracks_table()
|
||||
self._build_differences_table()
|
||||
|
||||
yield Header()
|
||||
|
||||
with Grid():
|
||||
yield Static("File")
|
||||
yield Static(self._mediaFilename, id="file_label", classes="two", markup=False)
|
||||
yield Static(" ")
|
||||
yield self.differencesTable
|
||||
|
||||
yield Static("Cleanup")
|
||||
yield Static(
|
||||
"Enabled" if self._applyCleanup else "Disabled",
|
||||
id="cleanup_label",
|
||||
classes="two",
|
||||
markup=False,
|
||||
)
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Status")
|
||||
yield Static("", id="message_label", classes="two", markup=False)
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Media Tags")
|
||||
yield Button("Add", id="button_add_tag")
|
||||
yield Button("Edit", id="button_edit_tag")
|
||||
yield Button("Delete", id="button_delete_tag")
|
||||
|
||||
yield Static(" ")
|
||||
yield self.mediaTagsTable
|
||||
yield Static(" ")
|
||||
|
||||
yield Static("Streams")
|
||||
yield Button("Edit", id="button_edit_track")
|
||||
yield Button("Set Default", id="select_default_button")
|
||||
yield Button("Set Forced", id="select_forced_button")
|
||||
|
||||
yield Static(" ")
|
||||
yield self.tracksTable
|
||||
yield Static(" ")
|
||||
|
||||
yield Static(" ")
|
||||
yield Button("Apply", id="apply_button")
|
||||
yield Button("Revert", id="revert_button")
|
||||
yield Button("Quit", id="quit_button")
|
||||
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self):
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
self.setMessage(self._messageText)
|
||||
|
||||
def setMessage(self, message: str):
|
||||
self._messageText = str(message)
|
||||
|
||||
try:
|
||||
self.query_one("#message_label", Static).update(self._messageText)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def refreshAfterDraftChange(self):
|
||||
self.updateMediaTags()
|
||||
self.updateTracks()
|
||||
self.updateDifferences()
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "select_default_button":
|
||||
if self.setSelectedTrackDefault():
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
if event.button.id == "select_forced_button":
|
||||
if self.setSelectedTrackForced():
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
if event.button.id == "button_add_tag":
|
||||
self.app.push_screen(TagDetailsScreen(), self.handle_update_media_tag)
|
||||
|
||||
if event.button.id == "button_edit_tag":
|
||||
selectedTag = self.getSelectedMediaTag()
|
||||
if selectedTag is not None:
|
||||
self.app.push_screen(
|
||||
TagDetailsScreen(key=selectedTag[0], value=selectedTag[1]),
|
||||
self.handle_update_media_tag,
|
||||
)
|
||||
|
||||
if event.button.id == "button_delete_tag":
|
||||
selectedTag = self.getSelectedMediaTag()
|
||||
if selectedTag is not None:
|
||||
self.app.push_screen(
|
||||
TagDeleteScreen(key=selectedTag[0], value=selectedTag[1]),
|
||||
self.handle_delete_media_tag,
|
||||
)
|
||||
|
||||
if event.button.id == "button_edit_track":
|
||||
self.action_edit_selected_track()
|
||||
|
||||
if event.button.id == "apply_button":
|
||||
self.action_apply_changes()
|
||||
|
||||
if event.button.id == "revert_button":
|
||||
self.action_revert_changes()
|
||||
|
||||
if event.button.id == "quit_button":
|
||||
self.action_quit_screen()
|
||||
|
||||
def action_edit_selected_track(self):
|
||||
selectedTrack = self.getSelectedTrackDescriptor()
|
||||
if selectedTrack is None:
|
||||
self.setMessage("Select a stream first.")
|
||||
return
|
||||
|
||||
self.app.push_screen(
|
||||
TrackDetailsScreen(
|
||||
trackDescriptor=selectedTrack,
|
||||
patternLabel=os.path.basename(self._mediaFilename),
|
||||
siblingTrackDescriptors=self._sourceMediaDescriptor.getTrackDescriptors(),
|
||||
metadata_only=True,
|
||||
),
|
||||
self.handle_edit_track,
|
||||
)
|
||||
|
||||
def handle_update_media_tag(self, tag):
|
||||
if tag is None:
|
||||
return
|
||||
|
||||
self._sourceMediaDescriptor.getTags()[str(tag[0])] = str(tag[1])
|
||||
self.setMessage(f"Updated media tag {tag[0]!r}.")
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
def handle_delete_media_tag(self, tag):
|
||||
if tag is None:
|
||||
return
|
||||
|
||||
self._sourceMediaDescriptor.getTags().pop(str(tag[0]), None)
|
||||
self.setMessage(f"Deleted media tag {tag[0]!r}.")
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
def handle_edit_track(self, trackDescriptor: TrackDescriptor):
|
||||
if trackDescriptor is None:
|
||||
return
|
||||
|
||||
updatedTracks = []
|
||||
replaced = False
|
||||
for currentTrack in self._sourceMediaDescriptor.getTrackDescriptors():
|
||||
if (
|
||||
currentTrack.getIndex() == trackDescriptor.getIndex()
|
||||
and currentTrack.getSubIndex() == trackDescriptor.getSubIndex()
|
||||
):
|
||||
updatedTracks.append(trackDescriptor)
|
||||
replaced = True
|
||||
else:
|
||||
updatedTracks.append(currentTrack)
|
||||
|
||||
if not replaced:
|
||||
self.setMessage("Unable to update selected stream.")
|
||||
return
|
||||
|
||||
self._sourceMediaDescriptor = self._sourceMediaDescriptor.clone(context=self.context)
|
||||
self._sourceMediaDescriptor.getTrackDescriptors().clear()
|
||||
self._sourceMediaDescriptor.getTrackDescriptors().extend(updatedTracks)
|
||||
self.setMessage(
|
||||
f"Updated stream #{trackDescriptor.getIndex()} ({trackDescriptor.getType().label()})."
|
||||
)
|
||||
self.refreshAfterDraftChange()
|
||||
|
||||
def action_apply_changes(self):
|
||||
if not self.hasPendingChanges():
|
||||
self.setMessage("No changes to apply.")
|
||||
return
|
||||
|
||||
try:
|
||||
applyResult = apply_metadata_edits(
|
||||
self.context,
|
||||
self._mediaFilename,
|
||||
self._baselineMediaDescriptor,
|
||||
self._sourceMediaDescriptor,
|
||||
)
|
||||
except Exception as ex:
|
||||
self.context["logger"].exception(
|
||||
"Failed to apply metadata edits for %s",
|
||||
self._mediaFilename,
|
||||
)
|
||||
self.setMessage(f"Apply failed: {ex}")
|
||||
return
|
||||
|
||||
if applyResult.get("dry_run", False):
|
||||
self.setMessage(
|
||||
f"Dry-run: would rewrite via temporary file {applyResult['target_path']}"
|
||||
)
|
||||
return
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.refreshAfterDraftChange()
|
||||
self.setMessage("Changes applied and file reloaded.")
|
||||
|
||||
def action_revert_changes(self):
|
||||
if not self.hasPendingChanges():
|
||||
self.setMessage("No changes to revert.")
|
||||
return
|
||||
|
||||
self.app.push_screen(
|
||||
ConfirmScreen(
|
||||
"Discard pending metadata changes and reload the file state?",
|
||||
confirm_label="Discard",
|
||||
cancel_label="Keep Editing",
|
||||
),
|
||||
self.handle_revert_confirmation,
|
||||
)
|
||||
|
||||
def handle_revert_confirmation(self, confirmed):
|
||||
if not confirmed:
|
||||
self.setMessage("Keeping pending changes.")
|
||||
return
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
self.refreshAfterDraftChange()
|
||||
self.setMessage("Reverted pending changes.")
|
||||
|
||||
def action_quit_screen(self):
|
||||
if self.hasPendingChanges():
|
||||
self.app.push_screen(
|
||||
ConfirmScreen(
|
||||
"Discard pending metadata changes and quit?",
|
||||
confirm_label="Discard",
|
||||
cancel_label="Stay",
|
||||
),
|
||||
self.handle_quit_confirmation,
|
||||
)
|
||||
return
|
||||
|
||||
self.app.exit()
|
||||
|
||||
def handle_quit_confirmation(self, confirmed):
|
||||
if confirmed:
|
||||
self.app.exit()
|
||||
else:
|
||||
self.setMessage("Continuing edit session.")
|
||||
398
src/ffx/media_workflow_screen_base.py
Normal file
398
src/ffx/media_workflow_screen_base.py
Normal file
@@ -0,0 +1,398 @@
|
||||
import os
|
||||
|
||||
import click
|
||||
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import DataTable
|
||||
from textual.widgets._data_table import CellDoesNotExist
|
||||
|
||||
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
|
||||
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet
|
||||
from ffx.track_descriptor import TrackDescriptor
|
||||
from ffx.track_disposition import TrackDisposition
|
||||
from ffx.track_type import TrackType
|
||||
|
||||
from .screen_support import build_screen_bootstrap, populate_tag_table
|
||||
|
||||
|
||||
class MediaWorkflowScreenBase(Screen):
|
||||
|
||||
CSS = """
|
||||
|
||||
Grid {
|
||||
grid-size: 5 10;
|
||||
grid-rows: 2 2 2 2 8 2 2 2 8 2;
|
||||
grid-columns: 15 25 90 10 105;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
DataTable .datatable--cursor {
|
||||
background: darkorange;
|
||||
color: black;
|
||||
}
|
||||
|
||||
DataTable .datatable--header {
|
||||
background: steelblue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
Input {
|
||||
border: none;
|
||||
}
|
||||
Button {
|
||||
border: none;
|
||||
}
|
||||
|
||||
DataTable {
|
||||
min-height: 24;
|
||||
}
|
||||
|
||||
.two {
|
||||
column-span: 2;
|
||||
}
|
||||
.three {
|
||||
column-span: 3;
|
||||
}
|
||||
.four {
|
||||
column-span: 4;
|
||||
}
|
||||
.five {
|
||||
column-span: 5;
|
||||
}
|
||||
|
||||
#differences-table {
|
||||
row-span: 10;
|
||||
}
|
||||
"""
|
||||
|
||||
TRACKS_TABLE_INDEX_COLUMN_LABEL = "Index"
|
||||
TRACKS_TABLE_TYPE_COLUMN_LABEL = "Type"
|
||||
TRACKS_TABLE_SUB_INDEX_COLUMN_LABEL = "SubIndex"
|
||||
TRACKS_TABLE_CODEC_COLUMN_LABEL = "Codec"
|
||||
TRACKS_TABLE_LAYOUT_COLUMN_LABEL = "Layout"
|
||||
TRACKS_TABLE_LANGUAGE_COLUMN_LABEL = "Language"
|
||||
TRACKS_TABLE_TITLE_COLUMN_LABEL = "Title"
|
||||
TRACKS_TABLE_DEFAULT_COLUMN_LABEL = "Default"
|
||||
TRACKS_TABLE_FORCED_COLUMN_LABEL = "Forced"
|
||||
|
||||
DIFFERENCES_COLUMN_LABEL = "Differences"
|
||||
COMMAND_NAME = ""
|
||||
EDIT_MODE = False
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
bootstrap = build_screen_bootstrap(self.app.getContext())
|
||||
self.context = bootstrap.context
|
||||
|
||||
self._applyCleanup = bootstrap.apply_cleanup
|
||||
self._removeGlobalKeys = bootstrap.remove_global_keys
|
||||
self._ignoreGlobalKeys = bootstrap.ignore_global_keys
|
||||
|
||||
command = self.context.get("command")
|
||||
if command != self.COMMAND_NAME:
|
||||
raise click.ClickException(
|
||||
f"{type(self).__name__}.__init__(): Can only perform command '{self.COMMAND_NAME}'"
|
||||
)
|
||||
|
||||
arguments = self.context.get("arguments", {})
|
||||
self._mediaFilename = arguments.get("filename", "")
|
||||
if not self._mediaFilename:
|
||||
raise click.ClickException(
|
||||
f"{type(self).__name__}.__init__(): Argument 'filename' is required"
|
||||
)
|
||||
if not os.path.isfile(self._mediaFilename):
|
||||
raise click.ClickException(
|
||||
f"{type(self).__name__}.__init__(): Media file {self._mediaFilename} does not exist"
|
||||
)
|
||||
|
||||
self._baselineMediaDescriptor = None
|
||||
self._sourceMediaDescriptor = None
|
||||
self._targetMediaDescriptor = None
|
||||
self._currentPattern = None
|
||||
self._mediaChangeSetObj = {}
|
||||
self._messageText = ""
|
||||
self._trackRowData: dict[object, TrackDescriptor] = {}
|
||||
self._sourceMediaTagRowData: dict[object, tuple[str, str]] = {}
|
||||
|
||||
self.reloadProperties(reset_draft=True)
|
||||
|
||||
def _build_media_tags_table(self):
|
||||
self.mediaTagsTable = DataTable(classes="two")
|
||||
self.mediaTagsTable.add_column("Key", width=30)
|
||||
self.mediaTagsTable.add_column("Value", width=70)
|
||||
self.mediaTagsTable.cursor_type = "row"
|
||||
|
||||
def _build_tracks_table(self):
|
||||
self.tracksTable = DataTable(classes="two")
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_INDEX_COLUMN_LABEL,
|
||||
width=5,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_TYPE_COLUMN_LABEL,
|
||||
width=10,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_SUB_INDEX_COLUMN_LABEL,
|
||||
width=8,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_CODEC_COLUMN_LABEL,
|
||||
width=10,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_LAYOUT_COLUMN_LABEL,
|
||||
width=10,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_LANGUAGE_COLUMN_LABEL,
|
||||
width=15,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_TITLE_COLUMN_LABEL,
|
||||
width=48,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_DEFAULT_COLUMN_LABEL,
|
||||
width=8,
|
||||
)
|
||||
self.tracksTable.add_column(
|
||||
self.TRACKS_TABLE_FORCED_COLUMN_LABEL,
|
||||
width=8,
|
||||
)
|
||||
self.tracksTable.cursor_type = "row"
|
||||
|
||||
def _build_differences_table(self):
|
||||
self.differencesTable = DataTable(id="differences-table")
|
||||
self.differencesTable.add_column(self.DIFFERENCES_COLUMN_LABEL, width=100)
|
||||
self.differencesTable.cursor_type = "row"
|
||||
|
||||
def reloadProperties(self, reset_draft: bool = True):
|
||||
self._mediaFileProperties = FileProperties(self.context, self._mediaFilename)
|
||||
probedMediaDescriptor = self._mediaFileProperties.getMediaDescriptor()
|
||||
|
||||
if self.EDIT_MODE:
|
||||
self._baselineMediaDescriptor = probedMediaDescriptor
|
||||
if reset_draft or self._sourceMediaDescriptor is None:
|
||||
self._sourceMediaDescriptor = probedMediaDescriptor.clone(context=self.context)
|
||||
self._targetMediaDescriptor = self._sourceMediaDescriptor
|
||||
self._currentPattern = None
|
||||
else:
|
||||
self._baselineMediaDescriptor = probedMediaDescriptor
|
||||
self._sourceMediaDescriptor = probedMediaDescriptor
|
||||
self._currentPattern = self._mediaFileProperties.getPattern()
|
||||
self._targetMediaDescriptor = (
|
||||
self._currentPattern.getMediaDescriptor(self.context)
|
||||
if self._currentPattern is not None
|
||||
else None
|
||||
)
|
||||
|
||||
self.rebuildChangeSet()
|
||||
|
||||
def rebuildChangeSet(self):
|
||||
try:
|
||||
if self.EDIT_MODE:
|
||||
mdcs = MediaDescriptorChangeSet(
|
||||
self.context,
|
||||
self._sourceMediaDescriptor,
|
||||
self._baselineMediaDescriptor,
|
||||
)
|
||||
else:
|
||||
if self._targetMediaDescriptor is None:
|
||||
self._mediaChangeSetObj = {}
|
||||
return
|
||||
mdcs = MediaDescriptorChangeSet(
|
||||
self.context,
|
||||
self._targetMediaDescriptor,
|
||||
self._sourceMediaDescriptor,
|
||||
)
|
||||
|
||||
self._mediaChangeSetObj = mdcs.getChangeSetObj()
|
||||
except ValueError:
|
||||
self._mediaChangeSetObj = {}
|
||||
|
||||
def hasPendingChanges(self) -> bool:
|
||||
return bool(self._mediaChangeSetObj)
|
||||
|
||||
def updateMediaTags(self):
|
||||
self._sourceMediaTagRowData = populate_tag_table(
|
||||
self.mediaTagsTable,
|
||||
self._sourceMediaDescriptor.getTags(),
|
||||
ignore_keys=self._ignoreGlobalKeys,
|
||||
remove_keys=self._removeGlobalKeys,
|
||||
)
|
||||
|
||||
def updateTracks(self):
|
||||
self.tracksTable.clear()
|
||||
self._trackRowData = {}
|
||||
|
||||
trackDescriptorList = self._sourceMediaDescriptor.getTrackDescriptors()
|
||||
typeCounter = {}
|
||||
|
||||
for trackDescriptor in trackDescriptorList:
|
||||
trackType = trackDescriptor.getType()
|
||||
if trackType not in typeCounter:
|
||||
typeCounter[trackType] = 0
|
||||
|
||||
dispositionSet = trackDescriptor.getDispositionSet()
|
||||
audioLayout = trackDescriptor.getAudioLayout()
|
||||
row = (
|
||||
trackDescriptor.getIndex(),
|
||||
trackType.label(),
|
||||
typeCounter[trackType],
|
||||
trackDescriptor.getCodec().label(),
|
||||
audioLayout.label()
|
||||
if trackType == TrackType.AUDIO
|
||||
and audioLayout != AudioLayout.LAYOUT_UNDEFINED
|
||||
else " ",
|
||||
trackDescriptor.getLanguage().label(),
|
||||
trackDescriptor.getTitle(),
|
||||
"Yes" if TrackDisposition.DEFAULT in dispositionSet else "No",
|
||||
"Yes" if TrackDisposition.FORCED in dispositionSet else "No",
|
||||
)
|
||||
|
||||
row_key = self.tracksTable.add_row(*map(str, row))
|
||||
self._trackRowData[row_key] = trackDescriptor
|
||||
typeCounter[trackType] += 1
|
||||
|
||||
def updateDifferences(self):
|
||||
self.rebuildChangeSet()
|
||||
self.differencesTable.clear()
|
||||
|
||||
if not self.EDIT_MODE and self._currentPattern is None:
|
||||
return
|
||||
|
||||
targetDescriptor = (
|
||||
self._sourceMediaDescriptor
|
||||
if self.EDIT_MODE
|
||||
else self._targetMediaDescriptor
|
||||
)
|
||||
targetTrackDescriptorsByIndex = {
|
||||
trackDescriptor.getIndex(): trackDescriptor
|
||||
for trackDescriptor in (
|
||||
targetDescriptor.getTrackDescriptors()
|
||||
if targetDescriptor is not None
|
||||
else []
|
||||
)
|
||||
}
|
||||
|
||||
tagDifferences = self._mediaChangeSetObj.get(MediaDescriptorChangeSet.TAGS_KEY, {})
|
||||
for tagKey, tagValue in tagDifferences.get(DIFF_ADDED_KEY, {}).items():
|
||||
if tagKey not in self._ignoreGlobalKeys:
|
||||
self.differencesTable.add_row(
|
||||
f"add media tag: key='{tagKey}' value='{tagValue}'"
|
||||
)
|
||||
|
||||
for tagKey, tagValue in tagDifferences.get(DIFF_REMOVED_KEY, {}).items():
|
||||
if tagKey in self._ignoreGlobalKeys:
|
||||
continue
|
||||
if not self.EDIT_MODE and tagKey in self._removeGlobalKeys:
|
||||
continue
|
||||
self.differencesTable.add_row(
|
||||
f"remove media tag: key='{tagKey}' value='{tagValue}'"
|
||||
)
|
||||
|
||||
for tagKey, tagValue in tagDifferences.get(DIFF_CHANGED_KEY, {}).items():
|
||||
if tagKey not in self._ignoreGlobalKeys:
|
||||
self.differencesTable.add_row(
|
||||
f"change media tag: key='{tagKey}' value='{tagValue}'"
|
||||
)
|
||||
|
||||
trackDifferences = self._mediaChangeSetObj.get(MediaDescriptorChangeSet.TRACKS_KEY, {})
|
||||
|
||||
for trackDescriptor in trackDifferences.get(DIFF_ADDED_KEY, {}).values():
|
||||
self.differencesTable.add_row(
|
||||
f"add {trackDescriptor.getType().label()} track: "
|
||||
+ f"index={trackDescriptor.getIndex()} lang={trackDescriptor.getLanguage().threeLetter()}"
|
||||
)
|
||||
|
||||
for trackIndex in trackDifferences.get(DIFF_REMOVED_KEY, {}).keys():
|
||||
self.differencesTable.add_row(f"remove stream #{trackIndex}")
|
||||
|
||||
for trackIndex, trackDiffObj in trackDifferences.get(DIFF_CHANGED_KEY, {}).items():
|
||||
targetTrackDescriptor = targetTrackDescriptorsByIndex.get(trackIndex)
|
||||
if targetTrackDescriptor is None:
|
||||
continue
|
||||
|
||||
tagsDiff = trackDiffObj.get(MediaDescriptorChangeSet.TAGS_KEY, {})
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_REMOVED_KEY, {}).items():
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"remove key={tagKey} value={tagValue}"
|
||||
)
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_ADDED_KEY, {}).items():
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"add key={tagKey} value={tagValue}"
|
||||
)
|
||||
for tagKey, tagValue in tagsDiff.get(DIFF_CHANGED_KEY, {}).items():
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"change key={tagKey} value={tagValue}"
|
||||
)
|
||||
|
||||
dispositionDiff = trackDiffObj.get(MediaDescriptorChangeSet.DISPOSITION_SET_KEY, {})
|
||||
for addedDisposition in dispositionDiff.get(DIFF_ADDED_KEY, set()):
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"add disposition={addedDisposition.label()}"
|
||||
)
|
||||
for removedDisposition in dispositionDiff.get(DIFF_REMOVED_KEY, set()):
|
||||
self.differencesTable.add_row(
|
||||
f"change stream #{targetTrackDescriptor.getIndex()} "
|
||||
+ f"({targetTrackDescriptor.getType().label()}:{targetTrackDescriptor.getSubIndex()}) "
|
||||
+ f"remove disposition={removedDisposition.label()}"
|
||||
)
|
||||
|
||||
def getSelectedMediaTag(self):
|
||||
try:
|
||||
row_key, _ = self.mediaTagsTable.coordinate_to_cell_key(
|
||||
self.mediaTagsTable.cursor_coordinate
|
||||
)
|
||||
if row_key is not None:
|
||||
return self._sourceMediaTagRowData.get(row_key)
|
||||
return None
|
||||
except CellDoesNotExist:
|
||||
return None
|
||||
|
||||
def getSelectedTrackDescriptor(self):
|
||||
try:
|
||||
row_key, _ = self.tracksTable.coordinate_to_cell_key(
|
||||
self.tracksTable.cursor_coordinate
|
||||
)
|
||||
if row_key is not None:
|
||||
return self._trackRowData.get(row_key)
|
||||
return None
|
||||
except CellDoesNotExist:
|
||||
return None
|
||||
|
||||
def setSelectedTrackDefault(self):
|
||||
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
|
||||
if selectedTrackDescriptor is None:
|
||||
return False
|
||||
|
||||
self._sourceMediaDescriptor.setDefaultSubTrack(
|
||||
selectedTrackDescriptor.getType(),
|
||||
selectedTrackDescriptor.getSubIndex(),
|
||||
)
|
||||
return True
|
||||
|
||||
def setSelectedTrackForced(self):
|
||||
selectedTrackDescriptor = self.getSelectedTrackDescriptor()
|
||||
if selectedTrackDescriptor is None:
|
||||
return False
|
||||
|
||||
self._sourceMediaDescriptor.setForcedSubTrack(
|
||||
selectedTrackDescriptor.getType(),
|
||||
selectedTrackDescriptor.getSubIndex(),
|
||||
)
|
||||
return True
|
||||
@@ -14,7 +14,8 @@ if str(SRC_ROOT) not in sys.path:
|
||||
from ffx.audio_layout import AudioLayout # noqa: E402
|
||||
from ffx.iso_language import IsoLanguage # noqa: E402
|
||||
from ffx.logging_utils import get_ffx_logger # noqa: E402
|
||||
from ffx.media_details_screen import MediaDetailsScreen # noqa: E402
|
||||
from ffx.inspect_details_screen import InspectDetailsScreen # noqa: E402
|
||||
from ffx.media_edit_screen import MediaEditScreen # noqa: E402
|
||||
from ffx.pattern_details_screen import PatternDetailsScreen # noqa: E402
|
||||
from ffx.show_descriptor import ShowDescriptor # noqa: E402
|
||||
from ffx.show_details_screen import ShowDetailsScreen # noqa: E402
|
||||
@@ -198,16 +199,16 @@ class TagTableScreenStateTests(unittest.TestCase):
|
||||
screen.getSelectedTag(),
|
||||
)
|
||||
|
||||
def test_media_details_screen_reads_selected_track_from_row_mapping(self):
|
||||
def test_media_edit_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)
|
||||
|
||||
screen = object.__new__(MediaDetailsScreen)
|
||||
screen = object.__new__(MediaEditScreen)
|
||||
screen.tracksTable = FakeTagTable()
|
||||
screen._MediaDetailsScreen__sourceMediaDescriptor = FakeMediaDescriptor(
|
||||
screen._sourceMediaDescriptor = FakeMediaDescriptor(
|
||||
[first_track, second_track]
|
||||
)
|
||||
screen._MediaDetailsScreen__trackRowData = {}
|
||||
screen._trackRowData = {}
|
||||
|
||||
screen.updateTracks()
|
||||
screen.tracksTable.select_row("row-1")
|
||||
@@ -299,10 +300,10 @@ class TagTableScreenStateTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(4, screen.getSelectedShowId())
|
||||
|
||||
def test_media_details_screen_reads_selected_show_from_row_mapping(self):
|
||||
screen = object.__new__(MediaDetailsScreen)
|
||||
def test_inspect_details_screen_reads_selected_show_from_row_mapping(self):
|
||||
screen = object.__new__(InspectDetailsScreen)
|
||||
screen.showsTable = FakeTagTable()
|
||||
screen._MediaDetailsScreen__showRowData = {}
|
||||
screen._showRowData = {}
|
||||
|
||||
placeholder_key = screen._add_show_row(None)
|
||||
show_key = screen._add_show_row(make_show_descriptor(8, "Real Show", 2020))
|
||||
@@ -317,7 +318,7 @@ class TagTableScreenStateTests(unittest.TestCase):
|
||||
self.assertEqual(1, screen.getRowIndexFromShowId(8))
|
||||
|
||||
screen.removeShow(-1)
|
||||
self.assertNotIn(placeholder_key, screen._MediaDetailsScreen__showRowData)
|
||||
self.assertNotIn(placeholder_key, screen._showRowData)
|
||||
self.assertEqual(0, screen.getRowIndexFromShowId(8))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user