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
Example #5
0
    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)