diff --git a/bin/dd.py b/bin/dd.py new file mode 100644 index 0000000..41c541b --- /dev/null +++ b/bin/dd.py @@ -0,0 +1,6 @@ +from ffx.helper import dictDiff + +a = {'name': 'yolo', 'mass': 56} +b = {'name': 'zolo', 'mass': 58} + +print(dictDiff(a, b)) diff --git a/bin/ffx.py b/bin/ffx.py index b17ccea..35ff523 100755 --- a/bin/ffx.py +++ b/bin/ffx.py @@ -6,9 +6,6 @@ from ffx.ffx_app import FfxApp from ffx.database import databaseContext -from ffx.media_descriptor import MediaDescriptor -from ffx.file_properties import FileProperties - VERSION='0.1.0' @@ -251,32 +248,12 @@ def help(): @click.argument('filename', nargs=1) def inspect(ctx, filename): - fp = FileProperties(ctx.obj, filename) - md = fp.getMediaDescriptor() - - click.echo('\nFile properties:\n') - - click.echo(md.getTags()) - - for at in md.getAudioTracks(): - click.echo(f"Audio: {at.getLanguage()} {'|'.join([f"{k}={v}" for (k,v) in at.getTags().items()])}") - - for st in md.getSubtitleTracks(): - click.echo(f"Subtitle: {st.getLanguage()} {'|'.join([f"{k}={v}" for (k,v) in st.getTags().items()])}") - - - click.echo('\nRecognized pattern:\n') + ctx.obj['command'] = 'inspect' + ctx.obj['arguments'] = {} + ctx.obj['arguments']['filename'] = filename - click.echo(f"Show Id: {fp.getShowId()}") - - pattern = fp.getPattern() - click.echo(f"Pattern: {pattern} id={pattern.id} show={pattern.show.name} year={pattern.show.year}") - - db_md = pattern.getMediaDescriptor() - click.echo(f"md from db: {db_md} tags={'|'.join([f"{k}={v}" for (k,v) in db_md.getTags().items()])} a_tracks={db_md.getAudioTracks()} s_tracks={db_md.getSubtitleTracks()}") - - atrack0 = db_md.getAudioTracks()[0] - click.echo(f"a track0: lang={atrack0.getLanguage()} title={atrack0.getTitle()}") + app = FfxApp(ctx.obj) + app.run() @@ -333,6 +310,8 @@ def shows(ctx): # if 'database' not in ctx.obj.keys(): # ctx.obj['database'] = databaseContext() + + ctx.obj['command'] = 'shows' app = FfxApp(ctx.obj) app.run() diff --git a/bin/ffx/ffx_app.py b/bin/ffx/ffx_app.py index 01838f6..77c7969 100644 --- a/bin/ffx/ffx_app.py +++ b/bin/ffx/ffx_app.py @@ -1,6 +1,7 @@ from textual.app import App from .shows_screen import ShowsScreen +from .media_details_screen import MediaDetailsScreen class FfxApp(App): @@ -21,9 +22,17 @@ class FfxApp(App): def on_mount(self) -> None: - self.push_screen(ShowsScreen()) + + if 'command' in self.context.keys(): + + if self.context['command'] == 'shows': + self.push_screen(ShowsScreen()) + + if self.context['command'] == 'inspect': + self.push_screen(MediaDetailsScreen()) def getContext(self): """Data 'output' method""" return self.context + diff --git a/bin/ffx/helper.py b/bin/ffx/helper.py new file mode 100644 index 0000000..3954f47 --- /dev/null +++ b/bin/ffx/helper.py @@ -0,0 +1,36 @@ +def dictDiff(a : dict, b : dict): + + a_keys = set(a.keys()) + b_keys = set(b.keys()) + + a_only = a_keys - b_keys + b_only = b_keys - a_keys + a_b = a_keys & b_keys + + changed = {k for k in a_b if a[k] != b[k]} + + diffResult = {} + + if a_only: + diffResult['removed'] = a_only + if b_only: + diffResult['added'] = b_only + if changed: + diffResult['changed'] = changed + + return diffResult + + +def setDiff(a : set, b : set) -> set: + + a_only = a - b + b_only = b - a + + diffResult = {} + + if a_only: + diffResult['removed'] = a_only + if b_only: + diffResult['added'] = b_only + + return diffResult diff --git a/bin/ffx/media_descriptor.py b/bin/ffx/media_descriptor.py index 8f09d76..9f4af71 100644 --- a/bin/ffx/media_descriptor.py +++ b/bin/ffx/media_descriptor.py @@ -3,6 +3,8 @@ from typing import List from ffx.track_type import TrackType from ffx.track_descriptor import TrackDescriptor +from ffx.helper import dictDiff + class MediaDescriptor(): """This class represents the structural content of a media file including streams and metadata""" @@ -49,8 +51,6 @@ class MediaDescriptor(): @classmethod def fromFfprobe(cls, formatData, streamData): - #trackDescriptors = {} - kwargs = {} if MediaDescriptor.FFPROBE_TAGS_KEY in formatData.keys(): @@ -59,22 +59,9 @@ class MediaDescriptor(): kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] = [] for streamObj in streamData: - - #trackType = TrackType.fromLabel(streamObj[MediaDescriptor.FFPROBE_CODEC_TYPE_KEY]) - -# if trackType != TrackType.UNKNOWN: -# -# if trackType.label() not in trackDescriptors.keys(): -# trackDescriptors[trackType.label()] = [] -# -# trackDescriptors[trackType.label()].append(TrackDescriptor.fromFfprobe(streamObj)) - if TrackType.fromLabel(streamObj[MediaDescriptor.FFPROBE_CODEC_TYPE_KEY]) != TrackType.UNKNOWN: kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(TrackDescriptor.fromFfprobe(streamObj)) - - # kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY] = trackDescriptors - return cls(**kwargs) @@ -82,10 +69,58 @@ class MediaDescriptor(): return self.__mediaTags + def getAllTracks(self) -> List[TrackDescriptor]: + return self.__trackDescriptors + def getAudioTracks(self) -> List[TrackDescriptor]: - #return self.__trackDescriptors[TrackType.AUDIO.label()] if TrackType.AUDIO.label() in self.__trackDescriptors.keys() else [] return [d for d in self.__trackDescriptors if d.getType() == TrackType.AUDIO] def getSubtitleTracks(self) -> List[TrackDescriptor]: - #return self.__trackDescriptors[TrackType.SUBTITLE.label()] if TrackType.SUBTITLE.label() in self.__trackDescriptors.keys() else [] return [d for d in self.__trackDescriptors if d.getType() == TrackType.SUBTITLE] + + + + def compare(self, vsMediaDescriptor): + + mediaTagsResult = dictDiff(vsMediaDescriptor.getTags(), self.getTags()) + + compareResult = {} + + if mediaTagsResult: + compareResult['tags'] = mediaTagsResult + + + vsTracks = vsMediaDescriptor.getAllTracks() + tracks = self.getAllTracks() + + numVsTracks = len(vsTracks) + numTracks = len(tracks) + maxNumOfTracks = max(numVsTracks, numTracks) + + + trackCompareResult = {} + + for trackIndex in range(maxNumOfTracks): + + if trackIndex > numVsTracks - 1: + if 'removed' not in trackCompareResult.keys(): + trackCompareResult['removed'] = set() + trackCompareResult['removed'].add(trackIndex) + continue + + if trackIndex > numTracks - 1: + if 'added' not in trackCompareResult.keys(): + trackCompareResult['added'] = {} + trackCompareResult['added'][trackIndex] = vsTracks[trackIndex] + continue + + trackResult = tracks[trackIndex].compare(vsTracks[trackIndex]) + if trackResult: + if 'changed' not in trackCompareResult.keys(): + trackCompareResult['changed'] = {} + trackCompareResult['changed'][trackIndex] = trackResult + + if trackCompareResult: + compareResult['tracks'] = trackCompareResult + + return compareResult diff --git a/bin/ffx/media_details_screen.py b/bin/ffx/media_details_screen.py new file mode 100644 index 0000000..4184ce3 --- /dev/null +++ b/bin/ffx/media_details_screen.py @@ -0,0 +1,456 @@ +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.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: 5 12; + grid-rows: 2 2 2 2 2 6 2 2 6 2 2 2; + grid-columns: 25 25 25 25 25; + height: 100%; + width: 100%; + padding: 1; + } + + Input { + border: none; + } + Button { + border: none; + } + + DataTable { + min-height: 6; + } + + #toplabel { + height: 1; + } + + .three { + column-span: 3; + } + + .four { + column-span: 4; + } + .five { + column-span: 5; + } + + .box { + height: 100%; + border: solid green; + } + """ + + def __init__(self, patternId = None, showId = None): + super().__init__() + + self.context = self.app.getContext() + self.Session = self.context['database']['session'] # convenience + + 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() + + raise click.ClickException(f"diff {self.__mediaDescriptor.compare(self.__storedMediaFilenamePattern)}") + +# 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 on_mount(self): + pass + +# if self.show_obj: +# self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})") +# +# if self.__pattern is not None: +# +# self.query_one("#pattern_input", Input).value = str(self.__pattern.getPattern()) +# +# self.updateAudioTracks() +# self.updateSubtitleTracks() + + + def compose(self): + +# self.audioStreamsTable = DataTable(classes="five") +# +# # 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(classes="five") +# +# # 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' + + + yield Header() + +# with Grid(): + + # 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 diff --git a/bin/ffx/track_descriptor.py b/bin/ffx/track_descriptor.py index e9d1273..51e63f4 100644 --- a/bin/ffx/track_descriptor.py +++ b/bin/ffx/track_descriptor.py @@ -3,6 +3,8 @@ from .track_type import TrackType from .audio_layout import AudioLayout from .track_disposition import TrackDisposition +from .helper import dictDiff, setDiff + class TrackDescriptor(): @@ -179,3 +181,22 @@ class TrackDescriptor(): def getDispositionSet(self): return self.__dispositionSet + + + + def compare(self, vsTrackDescriptor): + + compareResult = {} + + tagsDiffResult = dictDiff(vsTrackDescriptor.getTags(), self.getTags()) + + if tagsDiffResult: + compareResult['tags'] = tagsDiffResult + + vsDispositions = vsTrackDescriptor.getDispositionSet() + dispositions = self.getDispositionSet() + + dispositionDiffResult = setDiff(vsDispositions, dispositions) + + if dispositionDiffResult: + compareResult['dispositions'] = dispositionDiffResult