diff --git a/bin/ffx/iso_language.py b/bin/ffx/iso_language.py new file mode 100644 index 0000000..1b644c2 --- /dev/null +++ b/bin/ffx/iso_language.py @@ -0,0 +1,113 @@ +from enum import Enum +import difflib + +class IsoLanguage(Enum): + + AFRIKAANS = {"name": "Afrikaans", "iso639_1": "af", "iso639_2": "afr"} + ALBANIAN = {"name": "Albanian", "iso639_1": "sq", "iso639_2": "alb"} + ARABIC = {"name": "Arabic", "iso639_1": "ar", "iso639_2": "ara"} + ARMENIAN = {"name": "Armenian", "iso639_1": "hy", "iso639_2": "arm"} + AZERBAIJANI = {"name": "Azerbaijani", "iso639_1": "az", "iso639_2": "aze"} + BASQUE = {"name": "Basque", "iso639_1": "eu", "iso639_2": "baq"} + BELARUSIAN = {"name": "Belarusian", "iso639_1": "be", "iso639_2": "bel"} + BULGARIAN = {"name": "Bulgarian", "iso639_1": "bg", "iso639_2": "bul"} + CATALAN = {"name": "Catalan", "iso639_1": "ca", "iso639_2": "cat"} + CHINESE = {"name": "Chinese", "iso639_1": "zh", "iso639_2": "chi"} + CROATIAN = {"name": "Croatian", "iso639_1": "hr", "iso639_2": "hrv"} + CZECH = {"name": "Czech", "iso639_1": "cs", "iso639_2": "cze"} + DANISH = {"name": "Danish", "iso639_1": "da", "iso639_2": "dan"} + DUTCH = {"name": "Dutch", "iso639_1": "nl", "iso639_2": "dut"} + ENGLISH = {"name": "English", "iso639_1": "en", "iso639_2": "eng"} + ESTONIAN = {"name": "Estonian", "iso639_1": "et", "iso639_2": "est"} + FINNISH = {"name": "Finnish", "iso639_1": "fi", "iso639_2": "fin"} + FRENCH = {"name": "French", "iso639_1": "fr", "iso639_2": "fre"} + GEORGIAN = {"name": "Georgian", "iso639_1": "ka", "iso639_2": "geo"} + GERMAN = {"name": "German", "iso639_1": "de", "iso639_2": "ger"} + GREEK = {"name": "Greek", "iso639_1": "el", "iso639_2": "gre"} + HEBREW = {"name": "Hebrew", "iso639_1": "he", "iso639_2": "heb"} + HINDI = {"name": "Hindi", "iso639_1": "hi", "iso639_2": "hin"} + HUNGARIAN = {"name": "Hungarian", "iso639_1": "hu", "iso639_2": "hun"} + ICELANDIC = {"name": "Icelandic", "iso639_1": "is", "iso639_2": "ice"} + INDONESIAN = {"name": "Indonesian", "iso639_1": "id", "iso639_2": "ind"} + IRISH = {"name": "Irish", "iso639_1": "ga", "iso639_2": "gle"} + ITALIAN = {"name": "Italian", "iso639_1": "it", "iso639_2": "ita"} + JAPANESE = {"name": "Japanese", "iso639_1": "ja", "iso639_2": "jpn"} + KAZAKH = {"name": "Kazakh", "iso639_1": "kk", "iso639_2": "kaz"} + KOREAN = {"name": "Korean", "iso639_1": "ko", "iso639_2": "kor"} + LATIN = {"name": "Latin", "iso639_1": "la", "iso639_2": "lat"} + LATVIAN = {"name": "Latvian", "iso639_1": "lv", "iso639_2": "lav"} + LITHUANIAN = {"name": "Lithuanian", "iso639_1": "lt", "iso639_2": "lit"} + MACEDONIAN = {"name": "Macedonian", "iso639_1": "mk", "iso639_2": "mac"} + MALAY = {"name": "Malay", "iso639_1": "ms", "iso639_2": "may"} + MALTESE = {"name": "Maltese", "iso639_1": "mt", "iso639_2": "mlt"} + NORWEGIAN = {"name": "Norwegian", "iso639_1": "no", "iso639_2": "nor"} + PERSIAN = {"name": "Persian", "iso639_1": "fa", "iso639_2": "per"} + POLISH = {"name": "Polish", "iso639_1": "pl", "iso639_2": "pol"} + PORTUGUESE = {"name": "Portuguese", "iso639_1": "pt", "iso639_2": "por"} + ROMANIAN = {"name": "Romanian", "iso639_1": "ro", "iso639_2": "rum"} + RUSSIAN = {"name": "Russian", "iso639_1": "ru", "iso639_2": "rus"} + NORTHERN_SAMI = {"name": "Northern Sami", "iso639_1": "se", "iso639_2": "sme"} + SAMOAN = {"name": "Samoan", "iso639_1": "sm", "iso639_2": "smo"} + SANGO = {"name": "Sango", "iso639_1": "sg", "iso639_2": "sag"} + SANSKRIT = {"name": "Sanskrit", "iso639_1": "sa", "iso639_2": "san"} + SARDINIAN = {"name": "Sardinian", "iso639_1": "sc", "iso639_2": "srd"} + SERBIAN = {"name": "Serbian", "iso639_1": "sr", "iso639_2": "srp"} + SHONA = {"name": "Shona", "iso639_1": "sn", "iso639_2": "sna"} + SINDHI = {"name": "Sindhi", "iso639_1": "sd", "iso639_2": "snd"} + SINHALA = {"name": "Sinhala", "iso639_1": "si", "iso639_2": "sin"} + SLOVAK = {"name": "Slovak", "iso639_1": "sk", "iso639_2": "slk"} + SLOVENIAN = {"name": "Slovenian", "iso639_1": "sl", "iso639_2": "slv"} + SOMALI = {"name": "Somali", "iso639_1": "so", "iso639_2": "som"} + SOUTHERN_SOTHO = {"name": "Southern Sotho", "iso639_1": "st", "iso639_2": "sot"} + SPANISH = {"name": "Spanish", "iso639_1": "es", "iso639_2": "spa"} + SUNDANESE = {"name": "Sundanese", "iso639_1": "su", "iso639_2": "sun"} + SWAHILI = {"name": "Swahili", "iso639_1": "sw", "iso639_2": "swa"} + SWATI = {"name": "Swati", "iso639_1": "ss", "iso639_2": "ssw"} + SWEDISH = {"name": "Swedish", "iso639_1": "sv", "iso639_2": "swe"} + TAGALOG = {"name": "Tagalog", "iso639_1": "tl", "iso639_2": "tgl"} + TAMIL = {"name": "Tamil", "iso639_1": "ta", "iso639_2": "tam"} + THAI = {"name": "Thai", "iso639_1": "th", "iso639_2": "tha"} + TURKISH = {"name": "Turkish", "iso639_1": "tr", "iso639_2": "tur"} + UKRAINIAN = {"name": "Ukrainian", "iso639_1": "uk", "iso639_2": "ukr"} + URDU = {"name": "Urdu", "iso639_1": "ur", "iso639_2": "urd"} + VIETNAMESE = {"name": "Vietnamese", "iso639_1": "vi", "iso639_2": "vie"} + WELSH = {"name": "Welsh", "iso639_1": "cy", "iso639_2": "wel"} + + + @staticmethod + def find(label : str): + + closestMatches = difflib.get_close_matches(label, [l.value["name"] for l in IsoLanguage], n=1) + + if closestMatches: + foundLangs = [l for l in IsoLanguage if l.value['name'] == closestMatches[0]] + return foundLangs[0] if foundLangs else None + else: + return None + + @staticmethod + def findThreeLetter(theeLetter : str): + foundLangs = [l for l in IsoLanguage if l.value['iso639_2'] == str(theeLetter)] + return foundLangs[0] if foundLangs else None + + +# def get(lang : str): +# +# selectedLangs = [l for l in IsoLanguage if l.value['iso639_2'] == lang] +# +# if selectedLangs: +# return selectedLangs[0] +# else: +# return None + + def label(self): + return str(self.value['name']) + + def twoLetter(self): + return str(self.value['iso639_1']) + + def threeLetter(self): + return str(self.value['iso639_2']) + + + \ No newline at end of file diff --git a/bin/ffx/model/tag.py b/bin/ffx/model/tag.py new file mode 100644 index 0000000..61aaf5d --- /dev/null +++ b/bin/ffx/model/tag.py @@ -0,0 +1,30 @@ +# from typing import List +from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Enum +from sqlalchemy.orm import relationship, declarative_base, sessionmaker + +from .show import Base + +from ffx.track_type import TrackType + + +class Tag(Base): + """ + relationship(argument, opt1, opt2, ...) + argument is string of class or Mapped class of the target entity + backref creates a bi-directional corresponding relationship (back_populates preferred) + back_populates points to the corresponding relationship (the actual class attribute identifier) + + See: https://docs.sqlalchemy.org/en/(14|20)/orm/basic_relationships.html + """ + + __tablename__ = 'tags' + + # v1.x + id = Column(Integer, primary_key=True) + + key = Column(String) + value = Column(String) + + # v1.x + track_id = Column(Integer, ForeignKey('tracks.id', ondelete="CASCADE")) + track = relationship('Track', back_populates='tags') diff --git a/bin/ffx/model/track.py b/bin/ffx/model/track.py index dfb2867..a50b539 100644 --- a/bin/ffx/model/track.py +++ b/bin/ffx/model/track.py @@ -1,11 +1,16 @@ # from typing import List -from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Enum +from sqlalchemy import create_engine, Column, Integer, String, ForeignKey from sqlalchemy.orm import relationship, declarative_base, sessionmaker from .show import Base from ffx.track_type import TrackType +from ffx.iso_language import IsoLanguage + +from ffx.model.tag import Tag + + class Track(Base): """ @@ -20,11 +25,21 @@ class Track(Base): __tablename__ = 'tracks' # v1.x - id = Column(Integer, primary_key=True) + id = Column(Integer, primary_key=True, autoincrement = True) - track_type = Column(Enum[TrackType]) + # P=pattern_id+sub_index+track_type + track_type = Column(Integer) # TrackType + sub_index = Column(Integer) # v1.x pattern_id = Column(Integer, ForeignKey('patterns.id', ondelete="CASCADE")) pattern = relationship('Pattern', back_populates='tracks') + + language = Column(String) # IsoLanguage threeLetter + title = Column(String) + + tags = relationship('Tag', back_populates='track', cascade="all, delete") + + + disposition_flags = Column(Integer) diff --git a/bin/ffx/pattern_details_screen.py b/bin/ffx/pattern_details_screen.py index 53c9ad9..11384ba 100644 --- a/bin/ffx/pattern_details_screen.py +++ b/bin/ffx/pattern_details_screen.py @@ -11,12 +11,16 @@ 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 + + # Screen[dict[int, str, int]] class PatternDetailsScreen(Screen): @@ -71,23 +75,29 @@ class PatternDetailsScreen(Screen): self.__pc = PatternController(context = self.context) self.__sc = ShowController(context = self.context) + self.__tc = TrackController(context = self.context) self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} -# def loadPatterns(self, show_id): -# -# try: -# 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: -# click.ClickException(f"loadPatterns(): {repr(ex)}") -# finally: -# s.close() + 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: + click.ClickException(f"loadPatterns(): {repr(ex)}") + finally: + s.close() def on_mount(self): @@ -97,30 +107,52 @@ class PatternDetailsScreen(Screen): if self.show_obj: self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})") - - # for pattern in self.loadPatterns(int(self.show_obj['id'])): - # row = (pattern['pattern'],) - # self.patternTable.add_row(*map(str, row)) + + if self.pattern_obj: + + trackIds = self.__tc.findAllTracks(self.pattern_obj['id']) - for subIndex in range(3): + for audioTrackId in trackIds['audio']: - row4 = (str(subIndex),str(subIndex),str(subIndex),str(subIndex),) - self.audioStreamsTable.add_row(*map(str, row4)) + ad = self.__tc.getTrackDescriptor(audioTrackId) + dispoList = ad['disposition_list'] - row5 = (str(subIndex),str(subIndex),str(subIndex),str(subIndex),str(subIndex),) - self.subtitleStreamsTable.add_row(*map(str, row5)) + row = (ad['sub_index'], + " ", + ad['language'].label(), + ad['title'], + 'Yes' if TrackDisposition.DEFAULT in dispoList else 'No', + 'Yes' if TrackDisposition.FORCED in dispoList else 'No') + + self.audioStreamsTable.add_row(*map(str, row)) + + for subtitleTrackId in trackIds['subtitle']: + sd = self.__tc.getTrackDescriptor(subtitleTrackId) + dispoList = sd['disposition_list'] + + row = (sd['sub_index'], + " ", + sd['language'].label(), + sd['title'], + 'Yes' if TrackDisposition.DEFAULT in dispoList else 'No', + 'Yes' if TrackDisposition.FORCED in dispoList else 'No') + + self.subtitleStreamsTable.add_row(*map(str, row)) + def compose(self): self.audioStreamsTable = DataTable(classes="five") # Define the columns with headers - self.column_key_audio_subid = self.audioStreamsTable.add_column("Subindex", width=10) - self.column_key_audio_layout = self.audioStreamsTable.add_column("Layout", width=10) - self.column_key_audio_language = self.audioStreamsTable.add_column("Language", width=10) - self.column_key_audio_title = self.audioStreamsTable.add_column("Title", width=10) + 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_subtitle_default = self.audioStreamsTable.add_column("Default", width=10) + self.column_key_subtitle_forced = self.audioStreamsTable.add_column("Forced", width=10) self.audioStreamsTable.cursor_type = 'row' @@ -128,9 +160,10 @@ class PatternDetailsScreen(Screen): self.subtitleStreamsTable = DataTable(classes="five") # Define the columns with headers - self.column_key_subtitle_subid = self.subtitleStreamsTable.add_column("Subindex", width=10) - self.column_key_subtitle_language = self.subtitleStreamsTable.add_column("Language", width=10) - self.column_key_subtitle_title = self.subtitleStreamsTable.add_column("Title", width=10) + 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) @@ -157,9 +190,15 @@ class PatternDetailsScreen(Screen): # 5 yield Static("Audio streams") yield Static(" ") - yield Button("Add", id="button_add_audio_stream") - yield Button("Edit", id="button_edit_audio_stream") - yield Button("Delete", id="button_delete_audio_stream") + + if self.pattern_obj: + 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 @@ -169,9 +208,15 @@ class PatternDetailsScreen(Screen): # 8 yield Static("Subtitle streams") yield Static(" ") - yield Button("Add", id="button_add_subtitle_stream") - yield Button("Edit", id="button_edit_subtitle_stream") - yield Button("Delete", id="button_delete_subtitle_stream") + + if self.pattern_obj: + 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 @@ -211,24 +256,51 @@ class PatternDetailsScreen(Screen): self.app.pop_screen() + # Save pattern when just created before adding streams + if self.pattern_obj: - if event.button.id == "button_add_audio_stream": - self.app.push_screen(TrackDetailsScreen(TrackType.AUDIO), self.handle_add_stream) - if event.button.id == "button_edit_audio_stream": - self.app.push_screen(TrackDetailsScreen(TrackType.AUDIO), self.handle_edit_stream) - if event.button.id == "button_delete_audio_stream": - self.app.push_screen(TrackDeleteScreen(TrackType.AUDIO), self.handle_delete_stream) + if event.button.id == "button_add_audio_stream": + self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.pattern_obj['id'], subIndex = len(self.audioStreamsTable.rows)), self.handle_add_stream) + if event.button.id == "button_edit_audio_stream": + self.app.push_screen(TrackDetailsScreen(trackType = TrackType.AUDIO, patternId = self.pattern_obj['id']), self.handle_edit_stream) + if event.button.id == "button_delete_audio_stream": + self.app.push_screen(TrackDeleteScreen(trackType = TrackType.AUDIO, patternId = self.pattern_obj['id']), self.handle_delete_stream) - if event.button.id == "button_add_subtitle_stream": - self.app.push_screen(TrackDetailsScreen(TrackType.SUBTITLE), self.handle_add_stream) - if event.button.id == "button_edit_subtitle_stream": - self.app.push_screen(TrackDetailsScreen(TrackType.SUBTITLE), self.handle_edit_stream) - if event.button.id == "button_delete_subtitle_stream": - self.app.push_screen(TrackDeleteScreen(TrackType.SUBTITLE), self.handle_delete_stream) + if event.button.id == "button_add_subtitle_stream": + self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.pattern_obj['id'], subIndex = len(self.subtitleStreamsTable.rows)), self.handle_add_stream) + if event.button.id == "button_edit_subtitle_stream": + self.app.push_screen(TrackDetailsScreen(trackType = TrackType.SUBTITLE, patternId = self.pattern_obj['id']), self.handle_edit_stream) + if event.button.id == "button_delete_subtitle_stream": + self.app.push_screen(TrackDeleteScreen(trackType = TrackType.SUBTITLE, patternId = self.pattern_obj['id']), self.handle_delete_stream) + + + def handle_add_stream(self, trackDescriptor): + + dispoList = trackDescriptor['disposition_list'] + + if trackDescriptor['type'] == TrackType.AUDIO: + + row = (trackDescriptor['sub_index'], + " ", + trackDescriptor['language'].label(), + trackDescriptor['title'], + 'Yes' if TrackDisposition.DEFAULT in dispoList else 'No', + 'Yes' if TrackDisposition.FORCED in dispoList else 'No') + + self.audioStreamsTable.add_row(*map(str, row)) + + if trackDescriptor['type'] == TrackType.SUBTITLE: + + row = (trackDescriptor['sub_index'], + " ", + trackDescriptor['language'].label(), + trackDescriptor['title'], + 'Yes' if TrackDisposition.DEFAULT in dispoList else 'No', + 'Yes' if TrackDisposition.FORCED in dispoList else 'No') + + self.subtitleStreamsTable.add_row(*map(str, row)) - def handle_add_stream(self): - pass def handle_edit_stream(self): pass def handle_delete_stream(self): diff --git a/bin/ffx/show_details_screen.py b/bin/ffx/show_details_screen.py index d4a456d..32bb832 100644 --- a/bin/ffx/show_details_screen.py +++ b/bin/ffx/show_details_screen.py @@ -5,6 +5,8 @@ from textual.app import App, ComposeResult from textual.screen import Screen from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input from textual.containers import Grid, Horizontal +from textual.widgets._data_table import CellDoesNotExist + from ffx.model.show import Show from ffx.model.pattern import Pattern @@ -21,9 +23,9 @@ class ShowDetailsScreen(Screen): CSS = """ Grid { - grid-size: 2; - grid-rows: 2 auto; - grid-columns: 30 330; + grid-size: 2 14; + grid-rows: 2 2 2 2 2 2 2 2 2 2 2 6 2 2; + grid-columns: 30 90; height: 100%; width: 100%; padding: 1; @@ -114,22 +116,30 @@ class ShowDetailsScreen(Screen): def getSelectedPattern(self): - # Fetch the currently selected row when 'Enter' is pressed - #selected_row_index = self.table.cursor_row - row_key, col_key = self.patternTable.coordinate_to_cell_key(self.patternTable.cursor_coordinate) - selectedPattern = {} - if row_key is not None: - selected_row_data = self.patternTable.get_row(row_key) - - selectedPattern['pattern'] = str(selected_row_data[0]) - #selectedPattern['pattern'] = selected_row_data[1] + try: + + # Fetch the currently selected row when 'Enter' is pressed + #selected_row_index = self.table.cursor_row + row_key, col_key = self.patternTable.coordinate_to_cell_key(self.patternTable.cursor_coordinate) + + + if row_key is not None: + selected_row_data = self.patternTable.get_row(row_key) + + selectedPattern['pattern'] = str(selected_row_data[0]) + #selectedPattern['pattern'] = selected_row_data[1] + + except CellDoesNotExist: + pass return selectedPattern + + def action_add_pattern(self): if self.show_obj: self.app.push_screen(PatternDetailsScreen(showId = self.show_obj['id']), self.handle_add_pattern) @@ -186,7 +196,7 @@ class ShowDetailsScreen(Screen): self.patternTable = DataTable(classes="two") # Define the columns with headers - self.column_key_id = self.patternTable.add_column("Pattern", width=60) + self.column_key_id = self.patternTable.add_column("Pattern", width=90) #self.column_key_name = self.patternTable.add_column("Name", width=50) #self.column_key_year = self.patternTable.add_column("Year", width=10) @@ -197,36 +207,55 @@ class ShowDetailsScreen(Screen): with Grid(): + # 1 yield Static("Show" if self.show_obj else "New Show", id="toplabel", classes="two") + # 2 yield Static("ID") if self.show_obj: yield Static("", id="id_wdg") else: yield Input(type="integer", id="id_wdg") + # 3 yield Static("Name") yield Input(type="text", id="name_input") + + # 4 yield Static("Year") yield Input(type="integer", id="year_input") + #5 yield Static(" ", classes="two") + #6 yield Static("Index Season Digits") yield Input(type="integer", id="index_season_digits_input") + + #7 yield Static("Index Episode Digits") yield Input(type="integer", id="index_episode_digits_input") + + #8 yield Static("Indicator Season Digits") yield Input(type="integer", id="indicator_season_digits_input") + + #9 yield Static("Indicator Edisode Digits") yield Input(type="integer", id="indicator_episode_digits_input") + # 10 yield Static(" ", classes="two") + # 11 + yield Static("File patterns", classes="two") + # 12 yield self.patternTable + # 13 yield Static(" ", classes="two") + # 14 yield Button("Save", id="save_button") yield Button("Cancel", id="cancel_button") diff --git a/bin/ffx/track_controller.py b/bin/ffx/track_controller.py index bcc9075..4b889e3 100644 --- a/bin/ffx/track_controller.py +++ b/bin/ffx/track_controller.py @@ -2,6 +2,11 @@ import click from ffx.model.track import Track +from .track_type import TrackType + +from .track_disposition import TrackDisposition +from .iso_language import IsoLanguage + class TrackController(): @@ -11,82 +16,151 @@ class TrackController(): self.Session = self.context['database_session'] # convenience -# def updatePattern(self, show_id, pattern): -# -# try: -# s = self.Session() -# q = s.query(Pattern).filter(Pattern.show_id == int(show_id), Pattern.pattern == str(pattern)) -# -# if not q.count(): -# pattern = Pattern(show_id = int(show_id), pattern = str(pattern)) -# s.add(pattern) -# s.commit() -# return True -# -# except Exception as ex: -# raise click.ClickException(f"PatternController.updatePattern(): {repr(ex)}") -# finally: -# s.close() -# -# -# -# def findPattern(self, showId, pattern): -# -# try: -# s = self.Session() -# q = s.query(Pattern).filter(Pattern.show_id == int(showId), Pattern.pattern == str(pattern)) -# -# if q.count(): -# pattern = q.first() -# return int(pattern.id) -# else: -# return None -# -# except Exception as ex: -# raise click.ClickException(f"PatternController.findPattern(): {repr(ex)}") -# finally: -# s.close() -# -# -# def getPatternDescriptor(self, patternId): -# -# try: -# s = self.Session() -# q = s.query(Pattern).filter(Pattern.id == int(patternId)) -# -# patternDescriptor = {} -# if q.count(): -# pattern = q.first() -# -# patternDescriptor['id'] = pattern.id -# patternDescriptor['pattern'] = pattern.pattern -# patternDescriptor['show_id'] = pattern.show_id -# -# return patternDescriptor -# -# except Exception as ex: -# raise click.ClickException(f"PatternController.getPatternDescriptor(): {repr(ex)}") -# finally: -# s.close() -# -# -# def deletePattern(self, patternId): -# try: -# s = self.Session() -# q = s.query(Pattern).filter(Pattern.id == int(patternId)) -# -# if q.count(): -# -# #DAFUQ: https://stackoverflow.com/a/19245058 -# # q.delete() -# pattern = q.first() -# s.delete(pattern) -# -# s.commit() -# return True -# return False -# -# except Exception as ex: -# raise click.ClickException(f"PatternController.deletePattern(): {repr(ex)}") -# finally: -# s.close() + def addTrack(self, trackDescriptor): + + try: + s = self.Session() + + track = Track(pattern_id = int(trackDescriptor['pattern_id']), + + track_type = int(trackDescriptor['type'].value), + + sub_index = int(trackDescriptor['sub_index']), + + language = str(trackDescriptor['language'].threeLetter()), + + title = str(trackDescriptor['title']), + + disposition_flags = int(TrackDisposition.toFlags(trackDescriptor['disposition_list']))) + + s.add(track) + s.commit() + return True + + + except Exception as ex: + raise click.ClickException(f"TrackController.addTrack(): {repr(ex)}") + finally: + s.close() + + +# def updateTrack(self, trackDescriptor): +# +# try: +# s = self.Session() +# q = s.query(Track).filter(Track.id == int(trackId)) +# +# if not q.count(): +# track = Track(pattern_id = int(trackDescriptor['pattern_id']), +# track_type = TrackType(trackDescriptor['type']), +# sub_index = int(trackDescriptor['sub_index']), +# language = IsoLanguage(trackDescriptor['language']), +# title = str(trackDescriptor['title']), +# disposition_flags = TrackDisposition.toFlags(trackDescriptor['disposition_list'])) +# s.add(track) +# s.commit() +# return True +# else: +# return False +# +# except Exception as ex: +# raise click.ClickException(f"TrackController.updateTrack(): {repr(ex)}") +# finally: +# s.close() + + + def findAllTracks(self, patternId): + + try: + s = self.Session() + + trackDescriptors = {} + trackDescriptors['audio'] = [] + trackDescriptors['subtitle'] = [] + + q_audio = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.AUDIO.value) + + for audioTrack in q_audio.all(): + trackDescriptors['audio'].append(audioTrack.id) + + q_subtitle = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == TrackType.SUBTITLE.value) + for subtitleTrack in q_subtitle.all(): + trackDescriptors['subtitle'].append(subtitleTrack.id) + + + return trackDescriptors + + + except Exception as ex: + raise click.ClickException(f"TrackController.findAllTracks(): {repr(ex)}") + finally: + s.close() + + + def findTrack(self, patternId, trackType, subIndex): + + try: + s = self.Session() + q = s.query(Track).filter(Track.pattern_id == int(patternId), Track.track_type == trackType, Track.sub_index == int(subIndex)) + + if q.count(): + track = q.first() + return int(track.id) + else: + return None + + except Exception as ex: + raise click.ClickException(f"TrackController.findTrack(): {repr(ex)}") + finally: + s.close() + + + def getTrackDict(self, track): + trackDescriptor = {} + trackDescriptor['pattern_id'] = int(track.pattern_id) + trackDescriptor['type'] = TrackType(track.track_type) + trackDescriptor['sub_index'] = int(track.sub_index) + trackDescriptor['language'] = IsoLanguage.findThreeLetter(track.language) + trackDescriptor['title'] = str(track.title) + trackDescriptor['disposition_list'] = TrackDisposition.toList(track.disposition_flags) + return trackDescriptor + + + def getTrackDescriptor(self, trackId): + + try: + s = self.Session() + q = s.query(Track).filter(Track.id == int(trackId)) + + if q.count(): + track = q.first() + return self.getTrackDict(track) + else: + return {} + + except Exception as ex: + raise click.ClickException(f"TrackController.getTrackDescriptor(): {repr(ex)}") + finally: + s.close() + + + def deleteTrack(self, trackId): + try: + s = self.Session() + q = s.query(Track).filter(Track.id == int(trackId)) + + if q.count(): + + #DAFUQ: https://stackoverflow.com/a/19245058 + # q.delete() + pattern = q.first() + s.delete(pattern) + + s.commit() + return True + return False + + except Exception as ex: + raise click.ClickException(f"TrackController.deleteTrack(): {repr(ex)}") + finally: + s.close() diff --git a/bin/ffx/track_details_screen.py b/bin/ffx/track_details_screen.py index 85e3c14..8c57117 100644 --- a/bin/ffx/track_details_screen.py +++ b/bin/ffx/track_details_screen.py @@ -1,20 +1,24 @@ -import click +import click, time from textual import events from textual.app import App, ComposeResult from textual.screen import Screen -from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input +from textual.widgets import Header, Footer, Placeholder, Label, ListView, ListItem, Static, DataTable, Button, Input, Checkbox, SelectionList, Select from textual.containers import Grid, Horizontal + from ffx.model.show import Show from ffx.model.pattern import Pattern from .track_controller import TrackController -# from .pattern_controller import PatternController +from .pattern_controller import PatternController # from .show_controller import ShowController from .track_type import TrackType +from .iso_language import IsoLanguage +from .track_disposition import TrackDisposition + # Screen[dict[int, str, int]] class TrackDetailsScreen(Screen): @@ -22,8 +26,8 @@ class TrackDetailsScreen(Screen): CSS = """ Grid { - grid-size: 5 11; - grid-rows: 2 2 2 2 6 2 2 6 2 2 2; + grid-size: 5 18; + grid-rows: 2 2 2 2 2 3 2 2 2 2 2 6 2 2 6 2 2 2; grid-columns: 25 25 25 25 25; height: 100%; width: 100%; @@ -36,7 +40,13 @@ class TrackDetailsScreen(Screen): Button { border: none; } - + SelectionList { + border: none; + min-height: 6; + } + Select { + border: none; + } DataTable { min-height: 6; } @@ -44,7 +54,10 @@ class TrackDetailsScreen(Screen): #toplabel { height: 1; } - + + .two { + column-span: 2; + } .three { column-span: 3; } @@ -62,21 +75,25 @@ class TrackDetailsScreen(Screen): } """ - def __init__(self, trackType : TrackType, streamId = None, patternId = None): + def __init__(self, trackId = None, patternId = None, trackType : TrackType = None, subIndex = None): super().__init__() self.context = self.app.getContext() self.Session = self.context['database_session'] # convenience + + self.__tc = TrackController(context = self.context) + self.__pc = PatternController(context = self.context) + + self.track_obj = self.__tc.getTrackDescriptor(trackId) if trackId is not None else {} #TODO: Overwriting alternative values if set + + if trackType is None: + raise click.ClickException('Track type is required to be set') self.trackType = trackType + self.subIndex = subIndex - self.__tc = TrackController(context = self.context) - #self.__pc = PatternController(context = self.context) - #self.__sc = ShowController(context = self.context) + self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} - # self.pattern_obj = self.__pc.getPatternDescriptor(patternId) if patternId is not None else {} - # self.show_obj = self.__sc.getShowDesciptor(showId) if showId is not None else {} - self.track_obj = {} # def loadPatterns(self, show_id): # @@ -93,9 +110,12 @@ class TrackDetailsScreen(Screen): def on_mount(self): - pass - # if self.pattern_obj: - # self.query_one("#pattern_input", Input).value = str(self.pattern_obj['pattern']) + + if self.pattern_obj: + self.query_one("#patternlabel", Static).update(str(self.pattern_obj['pattern'])) + + if self.subIndex is not None: + self.query_one("#subindexlabel", Static).update(str(self.subIndex)) # if self.show_obj: # self.query_one("#showlabel", Static).update(f"{self.show_obj['id']} - {self.show_obj['name']} ({self.show_obj['year']})") @@ -116,27 +136,19 @@ class TrackDetailsScreen(Screen): def compose(self): -# self.audioStreamsTable = DataTable(classes="five") -# -# # Define the columns with headers -# self.column_key_audio_subid = self.audioStreamsTable.add_column("Subindex", width=10) -# self.column_key_audio_layout = self.audioStreamsTable.add_column("Layout", width=10) -# self.column_key_audio_language = self.audioStreamsTable.add_column("Language", width=10) -# self.column_key_audio_title = self.audioStreamsTable.add_column("Title", 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=10) -# self.column_key_subtitle_language = self.subtitleStreamsTable.add_column("Language", width=10) -# self.column_key_subtitle_title = self.subtitleStreamsTable.add_column("Title", width=10) -# 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' + self.trackTagsTable = DataTable(classes="five") + + # Define the columns with headers + self.column_key_track_tag_key = self.trackTagsTable.add_column("Key", width=10) + self.column_key_track_tag_value = self.trackTagsTable.add_column("Value", width=30) + + self.trackTagsTable.cursor_type = 'row' + + + languages = [l.label() for l in IsoLanguage] + + dispositions = [(d.label(), d.index(), False) for d in TrackDisposition] + yield Header() @@ -144,69 +156,102 @@ class TrackDetailsScreen(Screen): # 1 yield Static(f"Edit {self.trackType.label()} stream" if self.track_obj else f"New {self.trackType.label()} stream", id="toplabel", classes="five") -# yield Input(type="text", id="pattern_input", classes="four") # 2 -# yield Static("from show") -# yield Static("", id="showlabel") -# -# # 3 -# yield Static(" ", classes="five") -# # 4 -# yield Static(" ", classes="five") -# -# # 5 -# yield Static("Audio streams") -# yield Static(" ") -# yield Button("Add", id="button_add_audio_stream") -# yield Button("Edit", id="button_edit_audio_stream") -# yield Button("Delete", id="button_delete_audio_stream") -# # 6 -# yield self.audioStreamsTable -# -# # 7 -# yield Static(" ", classes="five") -# -# # 8 -# yield Static("Subtitle streams") -# yield Static(" ") -# yield Button("Add", id="button_add_subtitle_stream") -# yield Button("Edit", id="button_edit_subtitle_stream") -# yield Button("Delete", id="button_delete_subtitle_stream") -# # 9 -# yield self.subtitleStreamsTable -# + yield Static("for pattern") + yield Static("", id="patternlabel", classes="four") + + # 3 + yield Static("sub index") + yield Static("", id="subindexlabel", classes="four") + + # 4 + yield Static(" ", classes="five") + # 5 + yield Static(" ", classes="five") + + # 6 + yield Static("Language") + yield Select.from_values(languages, classes="four", id="language_select") + # 7 + yield Static(" ", classes="five") + + # 8 + yield Static("Title") + yield Input(id="title_input", classes="four") + + # 9 + yield Static(" ", classes="five") + # 10 yield Static(" ", classes="five") # 11 + yield Static("Stream tags") + yield Static(" ", classes="two") + yield Button("Add", id="button_add_stream_tag") + yield Button("Delete", id="button_delete_stream_tag") + # 12 + yield self.trackTagsTable + + # 13 + yield Static(" ", classes="five") + + # 14 + yield Static("Stream dispositions", classes="five") + + # 15 + yield SelectionList[int]( + *dispositions, + classes="five", + id = "dispositions_selection_list" + ) + + # 16 + yield Static(" ", classes="five") + # 17 + yield Static(" ", classes="five") + + # 18 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 getTrackDescriptorFromInput(self): + + trackDescriptor = {} + + trackDescriptor['pattern_id'] = int(self.pattern_obj['id']) + + trackDescriptor['type'] = TrackType(self.trackType) + trackDescriptor['sub_index'] = self.subIndex + + trackDescriptor['language'] = IsoLanguage.find(str(self.query_one("#language_select", Select).value)) + trackDescriptor['title'] = str(self.query_one("#title_input", Input).value) + + + disposition_flags = sum([2**f for f in self.query_one("#dispositions_selection_list", SelectionList).selected]) + + trackDescriptor['disposition_list'] = TrackDisposition.toList(disposition_flags) + + + return trackDescriptor # Event handler for button press def on_button_pressed(self, event: Button.Pressed) -> None: # Check if the button pressed is the one we are interested in -# if event.button.id == "save_button": -# -# pattern = self.getPatternFromInput() -# -# if self.__pc.updatePattern(self.show_obj['id'], pattern): -# -# screenResult = {} -# screenResult['show_id'] = self.show_obj['id'] -# screenResult['pattern'] = pattern -# -# self.dismiss(screenResult) -# else: -# #TODO: Meldung -# self.app.pop_screen() + if event.button.id == "save_button": + + trackDescriptor = self.getTrackDescriptorFromInput() + + if self.__tc.addTrack(trackDescriptor): #! + self.dismiss(trackDescriptor) + else: + #TODO: Meldung + self.app.pop_screen() if event.button.id == "cancel_button": self.app.pop_screen() diff --git a/bin/ffx/track_disposition.py b/bin/ffx/track_disposition.py new file mode 100644 index 0000000..b26eebe --- /dev/null +++ b/bin/ffx/track_disposition.py @@ -0,0 +1,49 @@ +from enum import Enum +import difflib + +class TrackDisposition(Enum): + + DEFAULT = {"name": "default", "index": 0} + FORCED = {"name": "forced", "index": 1} + + DUB = {"name": "dub", "index": 2} + ORIGINAL = {"name": "original", "index": 3} + COMMENT = {"name": "comment", "index": 4} + LYRICS = {"name": "lyrics", "index": 5} + KARAOKE = {"name": "karaoke", "index": 6} + HEARING_IMPAIRED = {"name": "hearing_impaired", "index": 7} + VISUAL_IMPAIRED = {"name": "visual_impaired", "index": 8} + CLEAN_EFFECTS = {"name": "clean_effects", "index": 9} + ATTACHED_PIC = {"name": "attached_pic", "index": 10} + TIMED_THUMBNAILS = {"name": "timed_thumbnails", "index": 11} + NON_DIEGETICS = {"name": "non_diegetic", "index": 12} + CAPTIONS = {"name": "captions", "index": 13} + DESCRIPTIONS = {"name": "descriptions", "index": 14} + METADATA = {"name": "metadata", "index": 15} + DEPENDENT = {"name": "dependent", "index": 16} + STILL_IMAGE = {"name": "still_image", "index": 17} + + def label(self): + return str(self.value['name']) + + def index(self): + return int(self.value['index']) + + + @staticmethod + def toFlags(dispositionList): + """Flags stored in integer bits (2**index)""" + + flags = 0 + for d in dispositionList: + flags += 2 ** d.index() + return flags + + @staticmethod + def toList(flags): + + dispositionList = [] + for d in TrackDisposition: + if flags & int(2 ** d.index()): + dispositionList += [d] + return dispositionList