Splits screen classes
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
|
||||||
from .shows_screen import ShowsScreen
|
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):
|
class FfxApp(App):
|
||||||
@@ -28,8 +29,11 @@ class FfxApp(App):
|
|||||||
if self.context['command'] == 'shows':
|
if self.context['command'] == 'shows':
|
||||||
self.push_screen(ShowsScreen())
|
self.push_screen(ShowsScreen())
|
||||||
|
|
||||||
if self.context['command'] in ('inspect', 'edit'):
|
if self.context['command'] == 'inspect':
|
||||||
self.push_screen(MediaDetailsScreen())
|
self.push_screen(InspectDetailsScreen())
|
||||||
|
|
||||||
|
if self.context['command'] == 'edit':
|
||||||
|
self.push_screen(MediaEditScreen())
|
||||||
|
|
||||||
|
|
||||||
def getContext(self):
|
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
|
from .inspect_details_screen import InspectDetailsScreen as MediaDetailsScreen
|
||||||
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()
|
|
||||||
|
|||||||
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.audio_layout import AudioLayout # noqa: E402
|
||||||
from ffx.iso_language import IsoLanguage # noqa: E402
|
from ffx.iso_language import IsoLanguage # noqa: E402
|
||||||
from ffx.logging_utils import get_ffx_logger # 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.pattern_details_screen import PatternDetailsScreen # noqa: E402
|
||||||
from ffx.show_descriptor import ShowDescriptor # noqa: E402
|
from ffx.show_descriptor import ShowDescriptor # noqa: E402
|
||||||
from ffx.show_details_screen import ShowDetailsScreen # noqa: E402
|
from ffx.show_details_screen import ShowDetailsScreen # noqa: E402
|
||||||
@@ -198,16 +199,16 @@ class TagTableScreenStateTests(unittest.TestCase):
|
|||||||
screen.getSelectedTag(),
|
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)
|
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)
|
||||||
|
|
||||||
screen = object.__new__(MediaDetailsScreen)
|
screen = object.__new__(MediaEditScreen)
|
||||||
screen.tracksTable = FakeTagTable()
|
screen.tracksTable = FakeTagTable()
|
||||||
screen._MediaDetailsScreen__sourceMediaDescriptor = FakeMediaDescriptor(
|
screen._sourceMediaDescriptor = FakeMediaDescriptor(
|
||||||
[first_track, second_track]
|
[first_track, second_track]
|
||||||
)
|
)
|
||||||
screen._MediaDetailsScreen__trackRowData = {}
|
screen._trackRowData = {}
|
||||||
|
|
||||||
screen.updateTracks()
|
screen.updateTracks()
|
||||||
screen.tracksTable.select_row("row-1")
|
screen.tracksTable.select_row("row-1")
|
||||||
@@ -299,10 +300,10 @@ class TagTableScreenStateTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(4, screen.getSelectedShowId())
|
self.assertEqual(4, screen.getSelectedShowId())
|
||||||
|
|
||||||
def test_media_details_screen_reads_selected_show_from_row_mapping(self):
|
def test_inspect_details_screen_reads_selected_show_from_row_mapping(self):
|
||||||
screen = object.__new__(MediaDetailsScreen)
|
screen = object.__new__(InspectDetailsScreen)
|
||||||
screen.showsTable = FakeTagTable()
|
screen.showsTable = FakeTagTable()
|
||||||
screen._MediaDetailsScreen__showRowData = {}
|
screen._showRowData = {}
|
||||||
|
|
||||||
placeholder_key = screen._add_show_row(None)
|
placeholder_key = screen._add_show_row(None)
|
||||||
show_key = screen._add_show_row(make_show_descriptor(8, "Real Show", 2020))
|
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))
|
self.assertEqual(1, screen.getRowIndexFromShowId(8))
|
||||||
|
|
||||||
screen.removeShow(-1)
|
screen.removeShow(-1)
|
||||||
self.assertNotIn(placeholder_key, screen._MediaDetailsScreen__showRowData)
|
self.assertNotIn(placeholder_key, screen._showRowData)
|
||||||
self.assertEqual(0, screen.getRowIndexFromShowId(8))
|
self.assertEqual(0, screen.getRowIndexFromShowId(8))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user