You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ffx/bin/ffx/media_details_screen.py

691 lines
25 KiB
Python

import os, click, re
from textual import events
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, Button, Input, DataTable
from textual.containers import Grid
from ffx.model.show import Show
from ffx.model.pattern import Pattern
from .pattern_controller import PatternController
from .show_controller import ShowController
from .track_controller import TrackController
from .track_details_screen import TrackDetailsScreen
from .track_delete_screen import TrackDeleteScreen
from ffx.track_type import TrackType
from ffx.model.track import Track
from ffx.track_disposition import TrackDisposition
from ffx.track_descriptor import TrackDescriptor
from textual.widgets._data_table import CellDoesNotExist
from ffx.media_descriptor import MediaDescriptor
from ffx.file_properties import FileProperties
# Screen[dict[int, str, int]]
class MediaDetailsScreen(Screen):
CSS = """
Grid {
grid-size: 4 9;
grid-rows: 8 2 2 2 8 2 8 2 8;
grid-columns: 25 125 10 75;
height: 100%;
width: 100%;
padding: 1;
}
Input {
border: none;
}
Button {
border: none;
}
DataTable {
min-height: 40;
}
#toplabel {
height: 1;
}
.three {
column-span: 3;
}
.four {
column-span: 4;
}
.five {
column-span: 5;
}
.triple {
row-span: 3;
}
.box {
height: 100%;
border: solid green;
}
.yellow {
tint: yellow 40%;
}
#differences-table {
row-span: 8;
/* tint: magenta 40%; */
}
/* #pattern_input {
tint: red 40%;
}*/
"""
BINDINGS = [
("n", "new_pattern", "New Pattern"),
("u", "update_pattern", "Update Pattern"),
("e", "edit_pattern", "Update Pattern"),
]
def __init__(self):
super().__init__()
self.context = self.app.getContext()
self.Session = self.context['database']['session'] # convenience
self.__pc = PatternController(context = self.context)
self.__sc = ShowController(context = self.context)
self.__tc = TrackController(context = self.context)
if not 'command' in self.context.keys() or self.context['command'] != 'inspect':
raise click.ClickException(f"MediaDetailsScreen.__init__(): Can only perform command 'inspect'")
if not 'arguments' in self.context.keys() or not 'filename' in self.context['arguments'].keys() or not self.context['arguments']['filename']:
raise click.ClickException(f"MediaDetailsScreen.__init__(): Argument 'filename' is required to be provided for command 'inspect'")
self.__mediaFilename = self.context['arguments']['filename']
if not os.path.isfile(self.__mediaFilename):
raise click.ClickException(f"MediaDetailsScreen.__init__(): Media file {self.__mediaFilename} does not exist")
self.__mediaFileProperties = FileProperties(self.context, self.__mediaFilename)
self.__mediaDescriptor = self.__mediaFileProperties.getMediaDescriptor()
self.__mediaFilenamePattern = self.__mediaFileProperties.getPattern()
self.__storedMediaFilenamePattern = self.__mediaFilenamePattern.getMediaDescriptor() if self.__mediaFilenamePattern is not None else None
# def loadTracks(self, show_id):
#
# try:
#
# tracks = {}
# tracks['audio'] = {}
# tracks['subtitle'] = {}
#
# s = self.Session()
# q = s.query(Pattern).filter(Pattern.show_id == int(show_id))
#
# return [{'id': int(p.id), 'pattern': p.pattern} for p in q.all()]
#
# except Exception as ex:
# raise click.ClickException(f"loadTracks(): {repr(ex)}")
# finally:
# s.close()
#
#
# def updateAudioTracks(self):
#
# self.audioStreamsTable.clear()
#
# if self.__pattern is not None:
#
# audioTracks = self.__tc.findAudioTracks(self.__pattern.getId())
#
# for at in audioTracks:
#
# dispoSet = at.getDispositionSet()
#
# row = (at.getSubIndex(),
# " ",
# at.getLanguage().label(),
# at.getTitle(),
# 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
# 'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
#
# self.audioStreamsTable.add_row(*map(str, row))
#
# def updateSubtitleTracks(self):
#
# self.subtitleStreamsTable.clear()
#
# if self.__pattern is not None:
#
# subtitleTracks = self.__tc.findSubtitleTracks(self.__pattern.getId())
#
# for st in subtitleTracks:
#
# dispoSet = st.getDispositionSet()
#
# row = (st.getSubIndex(),
# " ",
# st.getLanguage().label(),
# st.getTitle(),
# 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
# 'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
#
# self.subtitleStreamsTable.add_row(*map(str, row))
def getRowIndexFromShowId(self, showId : int) -> int:
"""Find the index of the row where the value in the specified column matches the target_value."""
for rowKey, row in self.showsTable.rows.items(): # dict[RowKey, Row]
rowData = self.showsTable.get_row(rowKey)
try:
if showId == int(rowData[0]):
return int(self.showsTable.get_row_index(rowKey))
except:
continue
return None
def on_mount(self):
if self.__mediaFilenamePattern is None:
row = (' ', '<New show>', ' ') # Convert each element to a string before adding
self.showsTable.add_row(*map(str, row))
for show in self.__sc.getAllShows():
row = (int(show.id), show.name, show.year) # Convert each element to a string before adding
self.showsTable.add_row(*map(str, row))
for mediaTagKey, mediaTagValue in self.__mediaDescriptor.getTags().items():
row = (mediaTagKey, mediaTagValue) # Convert each element to a string before adding
self.mediaTagsTable.add_row(*map(str, row))
self.updateAudioTracks(self.__mediaDescriptor.getAudioTracks())
self.updateSubtitleTracks(self.__mediaDescriptor.getSubtitleTracks())
if self.__mediaFilenamePattern is not None:
showIdentifier = self.__mediaFilenamePattern.getShowId()
showRowIndex = self.getRowIndexFromShowId(showIdentifier)
if showRowIndex is not None:
self.showsTable.move_cursor(row=showRowIndex)
self.query_one("#pattern_input", Input).value = self.__mediaFilenamePattern.getPattern()
# Enumerating differences between media descriptor from file vs from stored in database
targetMediaDescriptor = self.__mediaFilenamePattern.getMediaDescriptor()
mediaDifferences = targetMediaDescriptor.compare(self.__mediaDescriptor)
if 'tags' in mediaDifferences.keys():
mediaTags = self.__mediaDescriptor.getTags()
if 'added' in mediaDifferences['tags'].keys():
for addedTagKey in mediaDifferences['tags']['added']:
row = (f"added media tag: key='{addedTagKey}' value='{mediaTags[addedTagKey]}'",)
self.differencesTable.add_row(*map(str, row))
if 'removed' in mediaDifferences['tags'].keys():
for removedTagKey in mediaDifferences['tags']['removed']:
row = (f"removed media tag: key='{removedTagKey}' value='{mediaTags[removedTagKey]}'",)
self.differencesTable.add_row(*map(str, row))
if 'tracks' in mediaDifferences.keys():
currentTracks = self.__mediaDescriptor.getAllTracks() # 0,1,2,3
targetTracks = targetMediaDescriptor.getAllTracks() # 0 <- from DB
if 'added' in mediaDifferences['tracks'].keys():
for addedTrackIndex in mediaDifferences['tracks']['added'].keys():
addedTrack : Track = currentTracks[addedTrackIndex]
row = (f"added {addedTrack.getType().label()} track: index={addedTrackIndex} subIndex={addedTrack.getSubIndex()} lang={addedTrack.getLanguage().threeLetter()}",)
self.differencesTable.add_row(*map(str, row))
if 'removed' in mediaDifferences['tracks'].keys():
for removedTrackIndex in mediaDifferences['tracks']['removed']:
row = (f"removed track: index={removedTrackIndex}",)
self.differencesTable.add_row(*map(str, row))
if 'changed' in mediaDifferences['tracks'].keys():
for changedTrackIndex in mediaDifferences['tracks']['changed'].keys():
changedTrack : Track = targetTracks[changedTrackIndex]
changedTrackDiff : dict = mediaDifferences['tracks']['changed'][changedTrackIndex]
if 'tags' in changedTrackDiff.keys():
if 'added' in changedTrackDiff['tags']:
for addedTagKey in changedTrackDiff['tags']['added']:
addedTagValue = changedTrack.getTags()[addedTagKey]
row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} added key={addedTagKey} value={addedTagValue}",)
self.differencesTable.add_row(*map(str, row))
if 'removed' in changedTrackDiff['tags']:
for removedTagKey in changedTrackDiff['tags']['removed']:
row = (f"changed {changedTrack.getType().label()} track index={changedTrackIndex} removed key={removedTagKey}",)
self.differencesTable.add_row(*map(str, row))
else:
self.query_one("#pattern_input", Input).value = self.__mediaFilename
self.query_one("#pattern_input", Input).styles.background = 'red'
def updateAudioTracks(self, audioTracks):
self.audioStreamsTable.clear()
for at in audioTracks:
dispoSet = at.getDispositionSet()
row = (at.getSubIndex(),
" ",
at.getLanguage().label(),
at.getTitle(),
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.audioStreamsTable.add_row(*map(str, row))
def updateSubtitleTracks(self, subtitleTracks):
self.subtitleStreamsTable.clear()
for st in subtitleTracks:
dispoSet = st.getDispositionSet()
row = (st.getSubIndex(),
" ",
st.getLanguage().label(),
st.getTitle(),
'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
self.subtitleStreamsTable.add_row(*map(str, row))
def compose(self):
# Create the DataTable widget
self.showsTable = DataTable()
# Define the columns with headers
self.column_key_show_id = self.showsTable.add_column("ID", width=10)
self.column_key_show_name = self.showsTable.add_column("Name", width=50)
self.column_key_show_year = self.showsTable.add_column("Year", width=10)
self.showsTable.cursor_type = 'row'
self.mediaTagsTable = DataTable()
# Define the columns with headers
self.column_key_track_tag_key = self.mediaTagsTable.add_column("Key", width=20)
self.column_key_track_tag_value = self.mediaTagsTable.add_column("Value", width=90)
self.mediaTagsTable.cursor_type = 'row'
self.audioStreamsTable = DataTable()
# Define the columns with headers
self.column_key_audio_subid = self.audioStreamsTable.add_column("Subindex", width=20)
self.column_key_audio_layout = self.audioStreamsTable.add_column("Layout", width=20)
self.column_key_audio_language = self.audioStreamsTable.add_column("Language", width=20)
self.column_key_audio_title = self.audioStreamsTable.add_column("Title", width=30)
self.column_key_audio_default = self.audioStreamsTable.add_column("Default", width=10)
self.column_key_audio_forced = self.audioStreamsTable.add_column("Forced", width=10)
self.audioStreamsTable.cursor_type = 'row'
self.subtitleStreamsTable = DataTable()
# Define the columns with headers
self.column_key_subtitle_subid = self.subtitleStreamsTable.add_column("Subindex", width=20)
self.column_key_subtitle_spacer = self.subtitleStreamsTable.add_column(" ", width=20)
self.column_key_subtitle_language = self.subtitleStreamsTable.add_column("Language", width=20)
self.column_key_subtitle_title = self.subtitleStreamsTable.add_column("Title", width=30)
self.column_key_subtitle_default = self.subtitleStreamsTable.add_column("Default", width=10)
self.column_key_subtitle_forced = self.subtitleStreamsTable.add_column("Forced", width=10)
self.subtitleStreamsTable.cursor_type = 'row'
# Create the DataTable widget
self.differencesTable = DataTable(id='differences-table') # classes="triple"
# Define the columns with headers
self.column_key_differences = self.differencesTable.add_column("Differences", width=70)
self.differencesTable.cursor_type = 'row'
yield Header()
with Grid():
# 1
yield Static("Show")
yield self.showsTable
yield Static(" ")
yield self.differencesTable
# 2
yield Static(" ")
yield Button("Substitute")
yield Static(" ")
# 3
yield Static("Pattern")
yield Input(type="text", id='pattern_input')
yield Static(" ")
# 4
yield Static(" ", classes="three")
# 5
yield Static("Media Tags")
yield self.mediaTagsTable
yield Static(" ")
# 6
yield Static(" ", classes="three")
# 7
yield Static("Audio Streams")
yield self.audioStreamsTable
yield Static(" ")
# 8
yield Static(" ", classes="three")
# 9
yield Static("Subtitle Streams")
yield self.subtitleStreamsTable
yield Static(" ")
# 1
# yield Static("Edit filename pattern" if self.__pattern is not None else "New filename pattern", id="toplabel")
# yield Input(type="text", id="pattern_input", classes="four")
#
# # 2
# yield Static("from show")
# yield Static("", id="showlabel", classes="three")
# yield Button("Substitute pattern", id="patternbutton")
#
# # 3
# yield Static(" ", classes="five")
# # 4
# yield Static(" ", classes="five")
#
# # 5
# yield Static("Audio streams")
# yield Static(" ")
#
# if self.__pattern is not None:
# yield Button("Add", id="button_add_audio_stream")
# yield Button("Edit", id="button_edit_audio_stream")
# yield Button("Delete", id="button_delete_audio_stream")
# else:
# yield Static("")
# yield Static("")
# yield Static("")
# # 6
# yield self.audioStreamsTable
#
# # 7
# yield Static(" ", classes="five")
#
# # 8
# yield Static("Subtitle streams")
# yield Static(" ")
#
# if self.__pattern is not None:
# yield Button("Add", id="button_add_subtitle_stream")
# yield Button("Edit", id="button_edit_subtitle_stream")
# yield Button("Delete", id="button_delete_subtitle_stream")
# else:
# yield Static("")
# yield Static("")
# yield Static("")
# # 9
# yield self.subtitleStreamsTable
#
# # 10
# yield Static(" ", classes="five")
#
# # 11
# yield Button("Save", id="save_button")
# yield Button("Cancel", id="cancel_button")
yield Footer()
# def getPatternFromInput(self):
# return str(self.query_one("#pattern_input", Input).value)
# def getSelectedAudioTrackDescriptor(self):
#
# if not self.__pattern:
# return None
#
# try:
#
# # Fetch the currently selected row when 'Enter' is pressed
# #selected_row_index = self.table.cursor_row
# row_key, col_key = self.audioStreamsTable.coordinate_to_cell_key(self.audioStreamsTable.cursor_coordinate)
#
# if row_key is not None:
# selected_track_data = self.audioStreamsTable.get_row(row_key)
#
# subIndex = int(selected_track_data[0])
#
# return self.__tc.findTrack(self.__pattern.getId(), TrackType.AUDIO, subIndex).getDescriptor()
#
# else:
# return None
#
# except CellDoesNotExist:
# return None
#
# def getSelectedSubtitleTrackDescriptor(self) -> TrackDescriptor:
#
# if not self.__pattern is None:
# return None
#
# try:
#
# # Fetch the currently selected row when 'Enter' is pressed
# #selected_row_index = self.table.cursor_row
# row_key, col_key = self.subtitleStreamsTable.coordinate_to_cell_key(self.subtitleStreamsTable.cursor_coordinate)
#
# if row_key is not None:
#
# selected_track_data = self.subtitleStreamsTable.get_row(row_key)
# subIndex = int(selected_track_data[0])
#
# return self.__tc.findTrack(self.__pattern.getId(), TrackType.SUBTITLE, subIndex).getDescriptor()
#
# else:
# return None
#
# except CellDoesNotExist:
# return None
# Event handler for button press
def on_button_pressed(self, event: Button.Pressed) -> None:
pass
# # Check if the button pressed is the one we are interested in
# if event.button.id == "save_button":
#
# patternDescriptor = {}
# patternDescriptor['show_id'] = self.show_obj['id']
# patternDescriptor['pattern'] = self.getPatternFromInput()
#
# if self.__pattern is not None:
#
# if self.__pc.updatePattern(self.__pattern.getId(), patternDescriptor):
# self.dismiss(patternDescriptor)
# else:
# #TODO: Meldung
# self.app.pop_screen()
#
# else:
# if self.__pc.addPattern(patternDescriptor):
# self.dismiss(patternDescriptor)
# else:
# #TODO: Meldung
# self.app.pop_screen()
#
#
#
# if event.button.id == "cancel_button":
# self.app.pop_screen()
#
#
# # Save pattern when just created before adding streams
# if self.__pattern is not None:
#
# if event.button.id == "button_add_audio_stream":
# self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.__pattern.getId(), subIndex = len(self.audioStreamsTable.rows)), self.handle_add_track)
#
# selectedAudioTrack = self.getSelectedAudioTrackDescriptor()
# if selectedAudioTrack is not None:
# if event.button.id == "button_edit_audio_stream":
#
# self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedAudioTrack), self.handle_edit_track)
# if event.button.id == "button_delete_audio_stream":
# self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedAudioTrack), self.handle_delete_track)
#
# if event.button.id == "button_add_subtitle_stream":
# self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.__pattern.getId(), subIndex = len(self.subtitleStreamsTable.rows)), self.handle_add_track)
#
# selectedSubtitleTrack = self.getSelectedSubtitleTrackDescriptor()
# if selectedSubtitleTrack is not None:
# if event.button.id == "button_edit_subtitle_stream":
# self.app.push_screen(TrackDetailsScreen(trackDescriptor = selectedSubtitleTrack), self.handle_edit_track)
# if event.button.id == "button_delete_subtitle_stream":
# self.app.push_screen(TrackDeleteScreen(trackDescriptor = selectedSubtitleTrack), self.handle_delete_track)
#
# if event.button.id == "patternbutton":
#
# INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
#
# pattern = self.query_one("#pattern_input", Input).value
#
# patternMatch = re.search(INDICATOR_PATTERN, pattern)
#
# if patternMatch:
# self.query_one("#pattern_input", Input).value = pattern.replace(patternMatch.group(1), INDICATOR_PATTERN)
# def handle_add_track(self, trackDescriptor):
#
# dispoSet = trackDescriptor.getDispositionSet()
# trackType = trackDescriptor.getType()
# subIndex = trackDescriptor.getSubIndex()
# language = trackDescriptor.getLanguage()
# title = trackDescriptor.getTitle()
#
# if trackType == TrackType.AUDIO:
#
# row = (subIndex,
# " ",
# language.label(),
# title,
# 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
# 'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
#
# self.audioStreamsTable.add_row(*map(str, row))
#
# if trackType == TrackType.SUBTITLE:
#
# row = (subIndex,
# " ",
# language.label(),
# title,
# 'Yes' if TrackDisposition.DEFAULT in dispoSet else 'No',
# 'Yes' if TrackDisposition.FORCED in dispoSet else 'No')
#
# self.subtitleStreamsTable.add_row(*map(str, row))
# def handle_edit_track(self, trackDescriptor : TrackDescriptor):
#
# try:
# if trackDescriptor.getType() == TrackType.AUDIO:
#
# row_key, col_key = self.audioStreamsTable.coordinate_to_cell_key(self.audioStreamsTable.cursor_coordinate)
#
# self.audioStreamsTable.update_cell(row_key, self.column_key_audio_language, trackDescriptor.getLanguage().label())
# self.audioStreamsTable.update_cell(row_key, self.column_key_audio_title, trackDescriptor.getTitle())
# self.audioStreamsTable.update_cell(row_key, self.column_key_audio_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No')
# self.audioStreamsTable.update_cell(row_key, self.column_key_audio_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No')
#
# if trackDescriptor.getType() == TrackType.SUBTITLE:
#
# row_key, col_key = self.subtitleStreamsTable.coordinate_to_cell_key(self.subtitleStreamsTable.cursor_coordinate)
#
# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_language, trackDescriptor.getLanguage().label())
# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_title, trackDescriptor.getTitle())
# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_default, 'Yes' if TrackDisposition.DEFAULT in trackDescriptor.getDispositionSet() else 'No')
# self.subtitleStreamsTable.update_cell(row_key, self.column_key_subtitle_forced, 'Yes' if TrackDisposition.FORCED in trackDescriptor.getDispositionSet() else 'No')
#
# except CellDoesNotExist:
# pass
# def handle_delete_track(self, trackDescriptor : TrackDescriptor):
#
# try:
# if trackDescriptor.getType() == TrackType.AUDIO:
# self.updateAudioTracks()
#
# if trackDescriptor.getType() == TrackType.SUBTITLE:
# self.updateSubtitleTracks()
#
# except CellDoesNotExist:
# pass