def import_dgi(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports IDR frames as single-frame scenes. ''' pattern = re.compile(r'IDR\s\d+\n(\d+):FRM', re.RegexFlag.MULTILINE) for match in pattern.findall(path.read_text()): try: scening_list.add(Frame(match)) except ValueError: out_of_range_count += 1
def import_x264_2pass_log(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports I- and K-frames as single-frame scenes. ''' pattern = re.compile(r'in:(\d+).*type:I|K') for match in pattern.findall(path.read_text()): try: scening_list.add(Frame(int(match))) except ValueError: out_of_range_count += 1
def import_xvid(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports I-frames as single-frame scenes. ''' for i, line in enumerate(path.read_text().splitlines()): if not line.startswith('i'): continue try: scening_list.add(Frame(i - 3)) except ValueError: out_of_range_count += 1
def import_matroska_xml_chapters(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports chapters as scenes. Preserve end time and text if they're present. ''' from xml.etree import ElementTree timestamp_pattern = re.compile(r'(\d{2}):(\d{2}):(\d{2}(?:\.\d{3})?)') try: root = ElementTree.parse(str(path)).getroot() except ElementTree.ParseError as exc: logging.warning( f'Scening import: error occured' ' while parsing \'{path.name}\':') logging.warning(exc.msg) return for chapter in root.iter('ChapterAtom'): start_element = chapter.find('ChapterTimeStart') if start_element is None or start_element.text is None: continue match = timestamp_pattern.match(start_element.text) if match is None: continue start = Frame(Time( hours = int(match[1]), minutes = int(match[2]), seconds = float(match[3]) )) end = None end_element = chapter.find('ChapterTimeEnd') if end_element is not None and end_element.text is not None: match = timestamp_pattern.match(end_element.text) if match is not None: end = Frame(Time( hours = int(match[1]), minutes = int(match[2]), seconds = float(match[3]) )) label = '' label_element = chapter.find('ChapterDisplay/ChapterString') if label_element is not None and label_element.text is not None: label = label_element.text try: scening_list.add(start, end, label) except ValueError: out_of_range_count += 1
def import_matroska_timestamps_v1(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports gaps between timestamps as scenes. ''' pattern = re.compile(r'(\d+),(\d+),(\d+(?:\.\d+)?)') for match in pattern.finditer(path.read_text()): try: scening_list.add(Frame(int(match[1])), Frame(int(match[2])), '{:.3f} fps'.format(float(match[3]))) except ValueError: out_of_range_count += 1
def import_ass(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports lines as scenes. Text is ignored. ''' import pysubs2 subs = pysubs2.load(str(path)) for line in subs: t_start = Time(milliseconds=line.start) t_end = Time(milliseconds=line.end) try: scening_list.add(Frame(t_start), Frame(t_end)) except ValueError: out_of_range_count += 1
def import_matroska_timestamps_v2(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports intervals of constant FPS as scenes. Uses FPS for scene label. ''' timestamps: List[Time] = [] for line in path.read_text().splitlines(): try: timestamps.append(Time(milliseconds=float(line))) except ValueError: continue if len(timestamps) < 2: logging.warning( 'Scening import: timestamps file contains less than' ' 2 timestamps, so there\'s nothing to import.') return deltas = [ timestamps[i] - timestamps[i - 1] for i in range(1, len(timestamps)) ] scene_delta = deltas[0] scene_start = Frame(0) scene_end: Optional[Frame] = None for i in range(1, len(deltas)): if abs(round(float(deltas[i] - scene_delta), 6)) <= 0.000_001: continue # TODO: investigate, why offset by -1 is necessary here scene_end = Frame(i - 1) try: scening_list.add( scene_start, scene_end, '{:.3f} fps'.format(1 / float(scene_delta))) except ValueError: out_of_range_count += 1 scene_start = Frame(i) scene_end = None scene_delta = deltas[i] if scene_end is None: try: scening_list.add( scene_start, Frame(len(timestamps) - 1), '{:.3f} fps'.format(1 / float(scene_delta))) except ValueError: out_of_range_count += 1
def import_tfm(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports TFM's 'OVR HELP INFORMATION'. Single combed frames are put into single-frame scenes. Frame groups are put into regular scenes. Combed probability is used for label. ''' class TFMFrame(Frame): mic: Optional[int] tfm_frame_pattern = re.compile(r'(\d+)\s\((\d+)\)') tfm_group_pattern = re.compile(r'(\d+),(\d+)\s\((\d+(?:\.\d+)%)\)') log = path.read_text() start_pos = log.find('OVR HELP INFORMATION') if start_pos == -1: logging.warning( 'Scening import: TFM log doesn\'t contain' '"OVR Help Information" section.') return log = log[start_pos:] tfm_frames: Set[TFMFrame] = set() for match in tfm_frame_pattern.finditer(log): tfm_frame = TFMFrame(int(match[1])) tfm_frame.mic = int(match[2]) tfm_frames.add(tfm_frame) for match in tfm_group_pattern.finditer(log): try: scene = scening_list.add( Frame(int(match[1])), Frame(int(match[2])), '{} combed'.format(match[3])) except ValueError: out_of_range_count += 1 continue tfm_frames -= set(range(int(scene.start), int(scene.end) + 1)) for tfm_frame in tfm_frames: try: scening_list.add(tfm_frame, label=str(tfm_frame.mic)) except ValueError: out_of_range_count += 1
def import_ses(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports bookmarks as single-frame scenes ''' import pickle with path.open('rb') as f: try: session = pickle.load(f) except pickle.UnpicklingError: logging.warning('Scening import: failed to load .ses file.') return if 'bookmarks' not in session: return for bookmark in session['bookmarks']: scening_list.add(Frame(bookmark[0]))
def import_ogm_chapters(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports chapters as signle-frame scenes. Uses NAME for scene label. ''' pattern = re.compile( r'(CHAPTER\d+)=(\d{2}):(\d{2}):(\d{2}(?:\.\d{3})?)\n\1NAME=(.*)', re.RegexFlag.MULTILINE) for match in pattern.finditer(path.read_text()): time = Time( hours = int(match[2]), minutes = int(match[3]), seconds = float(match[4])) try: scening_list.add(Frame(time), label=match[5]) except ValueError: out_of_range_count += 1
def import_cue(self, path: Path, scening_list: SceningList, out_of_range_count: int) -> None: ''' Imports tracks as scenes. Uses TITLE for scene label. ''' from cueparser import CueSheet def offset_to_time(offset: str) -> Optional[Time]: pattern = re.compile(r'(\d{1,2}):(\d{1,2}):(\d{1,2})') match = pattern.match(offset) if match is None: return None return Time( minutes = int(match[1]), seconds = int(match[2]), milliseconds = int(match[3]) / 75 * 1000) cue_sheet = CueSheet() cue_sheet.setOutputFormat('') cue_sheet.setData(path.read_text()) cue_sheet.parse() for track in cue_sheet.tracks: if track.offset is None: continue offset = offset_to_time(track.offset) if offset is None: logging.warning( f'Scening import: INDEX timestamp \'{track.offset}\'' ' format isn\'t suported.') continue start = Frame(offset) end = None if track.duration is not None: end = Frame(offset + TimeInterval(track.duration)) label = '' if track.title is not None: label = track.title try: scening_list.add(start, end, label) except ValueError: out_of_range_count += 1
def __init__(self, main: AbstractMainWindow) -> None: super().__init__(main) self.main = main self.scening_list = SceningList() self.setWindowTitle('Scening List View') self.setup_ui() self.end_frame_control .valueChanged.connect(self.on_end_frame_changed) self.end_time_control .valueChanged.connect(self.on_end_time_changed) self.label_lineedit .textChanged.connect(self.on_label_changed) self.name_lineedit .textChanged.connect(self.on_name_changed) self.start_frame_control.valueChanged.connect(self.on_start_frame_changed) self.start_time_control .valueChanged.connect(self.on_start_time_changed) self.tableview .doubleClicked.connect(self.on_tableview_clicked) self.delete_button .clicked.connect(self.on_delete_clicked) add_shortcut(Qt.Qt.Key_Delete, self.delete_button.click, self.tableview) set_qobject_names(self)
class SceningListDialog(Qt.QDialog): __slots__ = ( 'main', 'scening_list', 'name_lineedit', 'tableview', 'start_frame_control', 'end_frame_control', 'start_time_control', 'end_time_control', 'label_lineedit', ) def __init__(self, main: AbstractMainWindow) -> None: super().__init__(main) self.main = main self.scening_list = SceningList() self.setWindowTitle('Scening List View') self.setup_ui() self.end_frame_control .valueChanged.connect(self.on_end_frame_changed) self.end_time_control .valueChanged.connect(self.on_end_time_changed) self.label_lineedit .textChanged.connect(self.on_label_changed) self.name_lineedit .textChanged.connect(self.on_name_changed) self.start_frame_control.valueChanged.connect(self.on_start_frame_changed) self.start_time_control .valueChanged.connect(self.on_start_time_changed) self.tableview .doubleClicked.connect(self.on_tableview_clicked) self.delete_button .clicked.connect(self.on_delete_clicked) add_shortcut(Qt.Qt.Key_Delete, self.delete_button.click, self.tableview) set_qobject_names(self) def setup_ui(self) -> None: layout = Qt.QVBoxLayout(self) layout.setObjectName('SceningListDialog.setup_ui.layout') self.name_lineedit = Qt.QLineEdit(self) layout.addWidget(self.name_lineedit) self.tableview = Qt.QTableView(self) self.tableview.setSelectionMode(Qt.QTableView.SingleSelection) self.tableview.setSelectionBehavior(Qt.QTableView.SelectRows) self.tableview.setSizeAdjustPolicy(Qt.QTableView.AdjustToContents) layout.addWidget(self.tableview) scene_layout = Qt.QHBoxLayout() scene_layout.setObjectName('SceningListDialog.setup_ui.scene_layout') layout.addLayout(scene_layout) self.start_frame_control = FrameEdit[Frame](self) scene_layout.addWidget(self.start_frame_control) self.end_frame_control = FrameEdit[Frame](self) scene_layout.addWidget(self.end_frame_control) self.start_time_control = TimeEdit[Time](self) scene_layout.addWidget(self.start_time_control) self.end_time_control = TimeEdit[Time](self) scene_layout.addWidget(self.end_time_control) self.label_lineedit = Qt.QLineEdit(self) self.label_lineedit.setPlaceholderText('Label') scene_layout.addWidget(self.label_lineedit) buttons_layout = Qt.QHBoxLayout() buttons_layout.setObjectName( 'SceningListDialog.setup_ui.buttons_layout') layout.addLayout(buttons_layout) self.delete_button = Qt.QPushButton(self) self.delete_button.setAutoDefault(False) self.delete_button.setText('Delete Selected Scene') self.delete_button.setEnabled(False) buttons_layout.addWidget(self.delete_button) def on_add_clicked(self, checked: Optional[bool] = None) -> None: pass def on_current_frame_changed(self, frame: Frame, time: Time) -> None: if not self.isVisible(): return if self.tableview.selectionModel() is None: return selection = Qt.QItemSelection() for i, scene in enumerate(self.scening_list): if frame in scene: index = self.scening_list.index(i, 0) selection.select(index, index) self.tableview.selectionModel().select( selection, Qt.QItemSelectionModel.SelectionFlags( # type: ignore Qt.QItemSelectionModel.Rows + Qt.QItemSelectionModel.ClearAndSelect)) def on_current_list_changed(self, scening_list: Optional[SceningList] = None) -> None: if scening_list is not None: self.scening_list = scening_list else: self.scening_list = self.main.toolbars.scening.current_list self.scening_list.rowsMoved.connect(self.on_tableview_rows_moved) # type: ignore self.name_lineedit.setText(self.scening_list.name) self.tableview.setModel(self.scening_list) self.tableview.resizeColumnsToContents() self.tableview.selectionModel().selectionChanged.connect( # type: ignore self.on_tableview_selection_changed) self.delete_button.setEnabled(False) def on_current_output_changed(self, index: int, prev_index: int) -> None: self.start_frame_control.setMaximum(self.main.current_output.end_frame) self. end_frame_control.setMaximum(self.main.current_output.end_frame) self. start_time_control.setMaximum(self.main.current_output.end_time) self. end_time_control.setMaximum(self.main.current_output.end_time) def on_delete_clicked(self, checked: Optional[bool] = None) -> None: for model_index in self.tableview.selectionModel().selectedRows(): self.scening_list.remove(model_index.row()) def on_end_frame_changed(self, value: Union[Frame, int]) -> None: frame = Frame(value) index = self.tableview.selectionModel().selectedRows()[0] if not index.isValid(): return index = index.siblingAtColumn(SceningList.END_FRAME_COLUMN) if not index.isValid(): return self.scening_list.setData(index, frame, Qt.Qt.UserRole) def on_end_time_changed(self, time: Time) -> None: index = self.tableview.selectionModel().selectedRows()[0] if not index.isValid(): return index = index.siblingAtColumn(SceningList.END_TIME_COLUMN) if not index.isValid(): return self.scening_list.setData(index, time, Qt.Qt.UserRole) def on_label_changed(self, text: str) -> None: index = self.tableview.selectionModel().selectedRows()[0] if not index.isValid(): return index = self.scening_list.index(index.row(), SceningList.LABEL_COLUMN) if not index.isValid(): return self.scening_list.setData(index, text, Qt.Qt.UserRole) def on_name_changed(self, text: str) -> None: i = self.main.toolbars.scening.lists.index_of(self.scening_list) index = self.main.toolbars.scening.lists.index(i) self.main.toolbars.scening.lists.setData(index, text, Qt.Qt.UserRole) def on_start_frame_changed(self, value: Union[Frame, int]) -> None: frame = Frame(value) index = self.tableview.selectionModel().selectedRows()[0] if not index.isValid(): return index = index.siblingAtColumn(SceningList.START_FRAME_COLUMN) if not index.isValid(): return self.scening_list.setData(index, frame, Qt.Qt.UserRole) def on_start_time_changed(self, time: Time) -> None: index = self.tableview.selectionModel().selectedRows()[0] if not index.isValid(): return index = index.siblingAtColumn(SceningList.START_TIME_COLUMN) if not index.isValid(): return self.scening_list.setData(index, time, Qt.Qt.UserRole) def on_tableview_clicked(self, index: Qt.QModelIndex) -> None: if index.column() in (SceningList.START_FRAME_COLUMN, SceningList.END_FRAME_COLUMN): self.main.current_frame = self.scening_list.data(index) if index.column() in (SceningList.START_TIME_COLUMN, SceningList.END_TIME_COLUMN): self.main.current_time = self.scening_list.data(index) def on_tableview_rows_moved(self, parent_index: Qt.QModelIndex, start_i: int, end_i: int, dest_index: Qt.QModelIndex, dest_i: int) -> None: Qt.QTimer.singleShot(0, lambda: self.tableview.selectRow(dest_i)) def on_tableview_selection_changed(self, selected: Qt.QItemSelection, deselected: Qt.QItemSelection) -> None: if len(selected.indexes()) == 0: self.delete_button.setEnabled(False) return index = selected.indexes()[0] scene = self.scening_list[index.row()] qt_silent_call(self.start_frame_control.setValue, scene.start) qt_silent_call(self. end_frame_control.setValue, scene.end) qt_silent_call(self. start_time_control.setValue, Time(scene.start)) qt_silent_call(self. end_time_control.setValue, Time(scene.end)) qt_silent_call(self. label_lineedit.setText, scene.label) self.delete_button.setEnabled(True)