def _tokenize_time(timestr): timestr = re.sub(r'\s+', '', timestr) SubAssert(timestr, _('Sync: time spec cannot be empty')) time_args = dict(sign=None, h=0, m=0, s=0, ms=0) if timestr[0] in '+-': time_args['sign'] = int('%s1' % timestr[0]) timestr = timestr[1:] found_units = set() expr = re.compile(r'''(?P<value>\d+)(?P<unit>[a-zA-Z]+)''') parsed_len = 0 for elem in expr.finditer(timestr): val = elem.group('value') unit = elem.group('unit') SubAssert(unit not in found_units, _('Sync: non-unique time units in time spec')) found_units.add(unit) time_args[unit] = int(val) parsed_len += (len(unit) + len(val)) SubAssert(parsed_len == len(timestr), _('Sync: some characters not parsed')) try: return _Time(**time_args) except TypeError: raise SubException(_('Sync: incorrect time spec units'))
def __initWidgets(self): minimalSizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) # List of subtitles subListDelegate = SubListItemDelegate() self._model = QStandardItemModel(0, 3, self) self._model.setHorizontalHeaderLabels([_("Begin"), _("End"), _("Subtitle")]) self._subList = QTableView(self) self._subList.setModel(self._model) self._subList.setItemDelegateForColumn(0, subListDelegate) self._subList.setItemDelegateForColumn(1, subListDelegate) self._subList.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) self._searchBar = SearchBar(self) self._searchBar.hide() # Top toolbar toolbar = QHBoxLayout() toolbar.setAlignment(Qt.AlignLeft) #toolbar.addWidget(someWidget....) toolbar.addStretch(1) # Main layout grid = QGridLayout() grid.setSpacing(10) grid.setContentsMargins(0, 3, 0, 0) grid.addLayout(toolbar, 0, 0, 1, 1) # stretch to the right grid.addWidget(self._subList, 1, 0) grid.addWidget(self._searchBar, 2, 0) self.setLayout(grid)
def saveProperties(self): subProperties = None try: subProperties = self._createSubtitleProperties() except Exception as e: dialog = QMessageBox(self) dialog.setIcon(QMessageBox.Critical) dialog.setWindowTitle(_("Incorrect value")) dialog.setText(_("Could not save SPF file because of incorrect parameters.")); dialog.setDetailedText(str(e)); dialog.exec() return fileDialog = FileDialog( parent = self, caption = _('Save Subtitle Properties'), directory = self._settings.getPropertyFilesPath() ) fileDialog.setAcceptMode(QFileDialog.AcceptSave) fileDialog.setFileMode(QFileDialog.AnyFile) if fileDialog.exec(): filename = fileDialog.selectedFiles()[0] if not filename.endswith(".spf"): filename = "%s%s" % (filename, ".spf") self._settings.setPropertyFilesPath(os.path.dirname(filename)) subProperties.save(filename) self._settings.addPropertyFile(filename) self.close()
def _createEncodingBox(self): groupbox = QGroupBox(_("File Encoding")) layout = QGridLayout() self._autoEncoding = QCheckBox(_("Auto input encoding"), self) self._inputEncoding = QComboBox(self) self._inputEncoding.addItems(ALL_ENCODINGS) self._inputEncoding.setDisabled(self._autoEncoding.isChecked()) inputLabel = QLabel(_("Input encoding")) self._changeEncoding = QCheckBox(_("Change encoding on save"), self) self._outputEncoding = QComboBox(self) self._outputEncoding.addItems(ALL_ENCODINGS) self._outputEncoding.setEnabled(self._changeEncoding.isChecked()) outputLabel = QLabel(_("Output encoding")) layout.addWidget(self._autoEncoding, 0, 0) layout.addWidget(self._inputEncoding, 1, 0) layout.addWidget(inputLabel, 1, 1) layout.addWidget(self._changeEncoding, 2, 0) layout.addWidget(self._outputEncoding, 3, 0) layout.addWidget(outputLabel, 3, 1) groupbox.setLayout(layout) return groupbox
def addPoint(self): rows = self._current.editor.selectedRows() newStart = self._videoWidget.position if len(rows) == 0 or newStart is None: self._err.showMessage(_("Select a subtitle and position in current video first.")) return # row and sub reflect the same subtitle, but we need both for different things row = rows[0] sub = self._current.data.subtitles[row] # Don't add the same subtitle or the same sync time twice if any(row == point.subNo or newStart == point.start for point in _syncPoints(self._current.model)): self._err.showMessage(_("Can't repeat synchronization points")) return if sub.fps != newStart.fps: self._err.showMessage(_("Subtitle and video have different framerates (%(sub)s vs" "%(vid)s") % dict(sub=sub.fps, vid=newStart.fps)) return delta = sub.end - sub.start newEnd = newStart + delta startItem, endItem, textItem = createRow(sub, newStart, newEnd) subNoItem = QStandardItem(str(row)) subNoItem.setEditable(False) textItem.setEditable(False) rmItem = QStandardItem("") self._current.model.appendRow([subNoItem, startItem, endItem, textItem, rmItem]) self._rmButton(self._table, rmItem)
def __sub__(self, other): """Defines FrameTime - FrameTime""" SubAssert(self._fps == other._fps, _("FPS values are not equal")) SubAssert(self._full_seconds >= other._full_seconds, _("Cannot substract higher time from lower")) result = self._full_seconds - other._full_seconds return FrameTime(fps = self._fps, seconds = result)
def detectFpsFromMovie(cls, movieFile, default = 23.976): """Fetch movie FPS from MPlayer output or return given default.""" # initialize with a default FPS value, but not with a movieFile videoInfo = VideoInfo(float(default)) command = ['mplayer', '-really-quiet', '-vo', 'null', '-ao', 'null', '-frames', '0', '-identify', movieFile] try: mpOut, mpErr = Popen(command, stdout=PIPE, stderr=PIPE).communicate() log.debug(mpOut) log.debug(mpErr) # Overwrite default (not fetched from video) values. # If there's any error on changing videoInfo.fps, whole videoInfo won't be changed at # all. videoInfo.fps = float(re.search(r'ID_VIDEO_FPS=([\w/.]+)\s?', str(mpOut)).group(1)) videoInfo.videoPath = movieFile except OSError: log.warning(_("Couldn't run mplayer. It has to be installed and placed in your $PATH " "to detect FPS.")) except AttributeError: log.warning(_("Couldn't get FPS from %(movie)s. Using default value: %(fps)s.") % {"movie": movieFile, "fps": videoInfo.fps}) else: pass log.debug(P_( "Got %(fps)s FPS from '%(movie)s'.", "Got %(fps)s FPS from '%(movie)s'.", int(videoInfo.fps)) % {"fps": videoInfo.fps, "movie": videoInfo.videoPath}) return videoInfo
def openHelp(self): url = "https://github.com/mgoral/subconvert/wiki" if QDesktopServices.openUrl(QUrl(url)) is False: dialog = QMessageBox(self) dialog.setIcon(QMessageBox.Critical) dialog.setWindowTitle(_("Couldn't open URL")) dialog.setText(_("""Failed to open URL: <a href="%(url)s">%(url)s</a>.""") % {"url": url}) dialog.exec()
def linkVideo(self): movieExtensions = "%s%s" % ("*.", ' *.'.join(File.MOVIE_EXTENSIONS)) fileDialog = FileDialog( parent = self, caption = _("Select a video"), directory = self._settings.getLatestDirectory(), filter = _("Video files (%s);;All files (*)") % movieExtensions) fileDialog.setFileMode(QFileDialog.ExistingFile) if fileDialog.exec(): movieFilePath = fileDialog.selectedFiles()[0] self._setVideoLink(movieFilePath)
def offset(self, seconds): data = self.data fps = data.subtitles.fps if fps is None: log.error(_("No FPS for '%s' (empty subtitles)." % self.filePath)) return ft = FrameTime(data.subtitles.fps, seconds=seconds) data.subtitles.offset(ft) command = ChangeData(self.filePath, data, _("Offset by: %s") % ft.toStr()) self._subtitleData.execute(command)
def _createUndoView(self, history): undoGroup = QGroupBox(_("History of changes"), self) undoGroupLayout = QVBoxLayout() undoGroup.setLayout(undoGroupLayout) undoView = QUndoView(history, self) undoView.setEmptyLabel(_("<Original file>")) undoGroupLayout.addWidget(undoView) mainLayout = self.layout() mainLayout.addWidget(undoGroup)
def _chooseSubProperties(self): fileDialog = FileDialog( parent = self, caption = _("Open Subtitle Properties"), directory = self._settings.getPropertyFilesPath(), filter = _("Subtitle Properties (*.spf);;All files (*)") ) fileDialog.setFileMode(QFileDialog.ExistingFile) if fileDialog.exec(): filename = fileDialog.selectedFiles()[0] self._useSubProperties(filename)
def openProperties(self): fileDialog = FileDialog( parent = self, caption = _("Open Subtitle Properties"), directory = self._settings.getPropertyFilesPath(), filter = _("Subtitle Properties (*.spf);;All files (*)") ) fileDialog.setFileMode(QFileDialog.ExistingFile) if fileDialog.exec(): filename = fileDialog.selectedFiles()[0] self._settings.setPropertyFilesPath(os.path.dirname(filename)) subProperties = SubtitleProperties(list(self._formats.values()), filename) self.changeProperties(subProperties)
def _createFormatBox(self): groupbox = QGroupBox(_("Subtitle format")) layout = QGridLayout() displayedFormats = list(self._formats.keys()) displayedFormats.sort() self._outputFormat = QComboBox(self) self._outputFormat.addItems(displayedFormats) formatLabel = QLabel(_("Output format")) layout.addWidget(self._outputFormat, 0, 0) layout.addWidget(formatLabel, 0, 1) groupbox.setLayout(layout) return groupbox
def _createFpsBox(self): groupbox = QGroupBox(_("FPS")) layout = QHBoxLayout() self._autoFps = QCheckBox(_("Auto FPS"), self) self._fps = QComboBox(self) self._fps.addItems(["23.976", "24", "25", "29.97", "30"]) self._fps.setEditable(True) layout.addWidget(self._autoFps) layout.addWidget(self._fps) groupbox.setLayout(layout) return groupbox
def read(self, encoding = None): if encoding is None: encoding = self.detectEncoding() fileInput = [] try: with open(self._filePath, mode='r', encoding=encoding) as file_: fileInput = file_.readlines() except LookupError as msg: raise SubFileError(_("Unknown encoding name: '%s'.") % encoding) except UnicodeDecodeError: vals = {"file": self._filePath, "enc": encoding} raise SubFileError(_("Cannot handle '%(file)s' with '%(enc)s' encoding.") % vals) return fileInput
def _addEncodingsBox(self, row, addAuto): mainLayout = self.layout() encodingLabel = QLabel(_("File encoding:"), self) self._encodingBox = QComboBox(self) if addAuto is True: self._encodingBox.addItem(AUTO_ENCODING_STR) self._encodingBox.addItems(ALL_ENCODINGS) self._encodingBox.setToolTip(_("Change file encoding")) self._encodingBox.setEditable(True) mainLayout.addWidget(encodingLabel, row, 0) mainLayout.addWidget(self._encodingBox, row, 1)
def changeSelectedFilesInputEncoding(self, inputEncoding): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.inputEncoding != inputEncoding: try: data.encode(inputEncoding) except UnicodeDecodeError: # TODO: indicate with something more than log entry log.error(_("Cannot decode subtitles to '%s' encoding.") % inputEncoding) else: command = ChangeData(filePath, data, _("Input encoding: %s") % inputEncoding) self._subtitleData.execute(command)
def _createButtons(self): widget = QWidget(self) layout = QHBoxLayout() self._openButton = QPushButton(_("Open")) self._saveButton = QPushButton(_("Save")) self._closeButton = QPushButton(_("Close")) layout.addWidget(self._openButton) layout.addWidget(self._saveButton) layout.addWidget(self._closeButton) widget.setLayout(layout) return widget
def loadSpf(formats, filePath): try: spf = SubtitleProperties(formats, filePath) except FileNotFoundError: log.critical(_("No such file: '%s'") % filePath) sys.exit(2) return spf
def __init__(self, filePath, oldSubtitle, newSubtitle, index, parent = None): super(ChangeSubtitle, self).__init__(filePath, parent) self.setText(_("Subtitle change (%d: %s)") % (int(index + 1), subExcerpt(newSubtitle))) self._oldSubtitle = oldSubtitle self._newSubtitle = newSubtitle self._subNo = index
def __initContextMenu(self): self._contextMenu = QMenu(self) self.setContextMenuPolicy(Qt.CustomContextMenu) af = ActionFactory(self) insertSub = af.create(title = _("&Insert subtitle"), icon = "list-add", connection = self.insertNewSubtitle) self._contextMenu.addAction(insertSub) insertSub = af.create(title = _("&Add subtitle"), icon = "list-add", connection = self.addNewSubtitle) self._contextMenu.addAction(insertSub) removeSub = af.create(title = _("&Remove subtitles"), icon = "list-remove", connection = self.removeSelectedSubtitles) self._contextMenu.addAction(removeSub)
def openTab(self, filePath, background=False): if self._subtitleData.fileExists(filePath): tabIndex = self.__addTab(filePath) if background is False: self.showTab(tabIndex) else: log.error(_("SubtitleEditor not created for %s!" % filePath))
def changeFps(self, fps): data = self.data if data.fps != fps: data.subtitles.changeFps(fps) data.fps = fps command = ChangeData(self.filePath, data, _("FPS: %s") % fps) self._subtitleData.execute(command)
def sync(self, syncPointList): """Synchronise subtitles using a given list of SyncPoints.""" if len(syncPointList) == 0: return subsCopy = self._subs.clone() syncPointList.sort() SubAssert(syncPointList[0].subNo >= 0) SubAssert(syncPointList[0].subNo < subsCopy.size()) SubAssert(syncPointList[-1].subNo < subsCopy.size()) # Always start from the first subtitle. firstSyncPoint = self._getLowestSyncPoint(syncPointList, subsCopy) if firstSyncPoint != syncPointList[0]: syncPointList.insert(0, firstSyncPoint) for i, syncPoint in enumerate(syncPointList): # Algorithm: # 1. Calculate time deltas between sync points and between subs: # DE_OLD = subTime[secondSyncSubNo] - subTime[firstSyncSubNo] # DE_NEW = secondSyncTime - firstSyncTime # 2. Calculate proportional sub position within DE_OLD: # d = (subTime - subTime[firstSubNo]) / DE_OLD # 3. "d" is constant within deltas, so we can now calculate newSubTime: # newSubTime = DE_NEW * d + firstSyncTime firstSyncPoint = syncPointList[i] secondSyncPoint = self._getSyncPointOrEnd(i + 1, syncPointList, subsCopy) log.debug(_("Syncing times for sync points:")) log.debug(" %s" % firstSyncPoint) log.debug(" %s" % secondSyncPoint) # A case for the last one syncPoint if firstSyncPoint == secondSyncPoint: continue secondSubNo = secondSyncPoint.subNo firstSubNo = firstSyncPoint.subNo firstOldSub = subsCopy[firstSubNo] secondOldSub = subsCopy[secondSubNo] oldStartDelta, oldEndDelta = self._getDeltas(firstOldSub, secondOldSub) newStartDelta, newEndDelta = self._getDeltas(firstSyncPoint, secondSyncPoint) for subNo in range(firstSubNo, secondSubNo + 1): sub = subsCopy[subNo] newStartTime = self._calculateTime(sub.start, firstOldSub.start, firstSyncPoint.start, oldStartDelta, newStartDelta) newEndTime = self._calculateTime(sub.end, firstOldSub.end, firstSyncPoint.end, oldEndDelta, newEndDelta) self._subs.changeSubStart(subNo, newStartTime) self._subs.changeSubEnd(subNo, newEndTime)
def offsetSelectedFiles(self, seconds): if seconds == 0: return items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) fps = data.subtitles.fps if fps is None: log.error(_("No FPS for '%s' (empty subtitles)." % filePath)) continue ft = FrameTime(fps, seconds=seconds) data.subtitles.offset(ft) command = ChangeData(filePath, data, _("Offset by: %s") % ft.toStr()) self._subtitleData.execute(command)
def parseFile(self, subFile, inputEncoding , fps): content = subFile.read(inputEncoding) try: subtitles = self._parser.parse(content, fps) except SubParsingError as msg: log.error(msg) raise SubException(_("Couldn't parse file '%s'" % subFile.path)) return subtitles
def changeSelectedFilesOutputEncoding(self, outputEncoding): items = self.__fileList.selectedItems() for item in items: filePath = item.text(0) data = self._subtitleData.data(filePath) if data.outputEncoding != outputEncoding: data.outputEncoding = outputEncoding command = ChangeData(filePath, data, _("Output encoding: %s") % outputEncoding) self._subtitleData.execute(command)
def openFile(self): sub_extensions = self.__getAllSubExtensions() str_sub_exts = ' '.join(['*.%s' % ext for ext in sub_extensions[1:]]) fileDialog = FileDialog( parent = self, caption = _("Open file"), directory = self._settings.getLatestDirectory(), filter = _("Subtitles (%s);;All files (*)") % str_sub_exts ) fileDialog.addEncodings(True) fileDialog.setFileMode(QFileDialog.ExistingFiles) if fileDialog.exec(): filenames = fileDialog.selectedFiles() encoding = fileDialog.getEncoding() self._settings.setLatestDirectory(os.path.dirname(filenames[0])) self._openFiles(filenames, encoding)
def __init__(self, args, parser): super(MainWindow, self).__init__() log.debug(_("Theme search paths: %s") % QIcon.themeSearchPaths()) log.debug(_("Used theme name: '%s'") % QIcon.themeName()) self.setObjectName("main_window") self._subtitleData = DataController(parser, self) self.__initGui() self.__initActions() self.__initMenuBar() self.__initShortcuts() self.__updateMenuItemsState() self.__connectSignals() self.restoreWidgetState() self.handleArgs(args)