class Window(QWidget): def __init__(self, *args, **kwargs): super(Window, self).__init__(*args, **kwargs) layout = QVBoxLayout(self) self.progressBar = QProgressBar(self) self.progressBar.setRange(0, 100) layout.addWidget(self.progressBar) layout.addWidget(QPushButton('开启线程', self, clicked=self.onStart)) # 当前线程id print('main id', QThread.currentThread()) # 启动线程更新进度条值 self._thread = QThread(self) self._worker = Worker() self._worker.moveToThread(self._thread) # 移动到线程中执行 self._thread.finished.connect(self._worker.deleteLater) self._thread.started.connect(self._worker.run) self._worker.valueChanged.connect(self.progressBar.setValue) def onStart(self): if not self._thread.isRunning(): print('main id', QThread.currentThread()) self._thread.start() # 启动线程 def closeEvent(self, event): if self._thread.isRunning(): self._thread.requestInterruption() self._thread.quit() self._thread.wait() # 强制 # self._thread.terminate() self._thread.deleteLater() super(Window, self).closeEvent(event)
class Windows(QDialog, mainUI.Ui_Dialog): isRunning = False def __init__(self, parent=None): super(Windows, self).__init__(parent) self.selectedDict = None self.currentConfig = dict() self.localWords = [] self.selectedGroups = [] self.workerThread = QThread(self) self.workerThread.start() self.updateCheckThead = QThread(self) self.updateCheckThead.start() self.audioDownloadThread = QThread(self) self.updateCheckWork = None self.loginWorker = None self.queryWorker = None self.pullWorker = None self.audioDownloadWorker = None self.setupUi(self) self.setWindowTitle(MODEL_NAME) self.setupLogger() self.initCore() self.checkUpdate() # self.__dev() # 以备调试时使用 def __dev(self): def on_dev(): logger.debug('whatever') self.devBtn = QPushButton('Magic Button', self.mainTab) self.devBtn.clicked.connect(on_dev) self.gridLayout_4.addWidget(self.devBtn, 4, 3, 1, 1) def closeEvent(self, event): # 插件关闭时退出所有线程 if self.workerThread.isRunning(): self.workerThread.requestInterruption() self.workerThread.quit() self.workerThread.wait() if self.updateCheckThead.isRunning(): self.updateCheckThead.quit() self.updateCheckThead.wait() if self.audioDownloadThread.isRunning(): self.audioDownloadThread.requestInterruption() self.workerThread.quit() self.workerThread.wait() event.accept() def setupLogger(self): """初始化 Logger """ def onDestroyed(): logger.removeHandler(QtHandler) # 防止 debug 信息写入stdout/stderr 导致 Anki 崩溃 logging.basicConfig( handlers=[logging.FileHandler('dict2anki.log', 'w', 'utf-8')], level=logging.DEBUG, format='[%(asctime)s][%(levelname)8s] -- %(message)s - (%(name)s)') logTextBox = QPlainTextEdit(self) logTextBox.setLineWrapMode(QPlainTextEdit.NoWrap) layout = QVBoxLayout() layout.addWidget(logTextBox) self.logTab.setLayout(layout) QtHandler = Handler(self) logger.addHandler(QtHandler) QtHandler.newRecord.connect(logTextBox.appendPlainText) # 日志Widget销毁时移除 Handlers logTextBox.destroyed.connect(onDestroyed) def setupGUIByConfig(self): config = mw.addonManager.getConfig(__name__) self.deckComboBox.setCurrentText(config['deck']) self.dictionaryComboBox.setCurrentIndex(config['selectedDict']) self.apiComboBox.setCurrentIndex(config['selectedApi']) self.usernameLineEdit.setText( config['credential'][config['selectedDict']]['username']) self.passwordLineEdit.setText( config['credential'][config['selectedDict']]['password']) self.cookieLineEdit.setText( config['credential'][config['selectedDict']]['cookie']) self.definitionCheckBox.setChecked(config['definition']) self.imageCheckBox.setChecked(config['image']) self.sentenceCheckBox.setChecked(config['sentence']) self.phraseCheckBox.setChecked(config['phrase']) self.AmEPhoneticCheckBox.setChecked(config['AmEPhonetic']) self.BrEPhoneticCheckBox.setChecked(config['BrEPhonetic']) self.BrEPronRadioButton.setChecked(config['BrEPron']) self.AmEPronRadioButton.setChecked(config['AmEPron']) self.noPronRadioButton.setChecked(config['noPron']) self.selectedGroups = config['selectedGroup'] def initCore(self): self.dictionaryComboBox.addItems([d.name for d in dictionaries]) self.apiComboBox.addItems([d.name for d in apis]) self.deckComboBox.addItems(getDeckList()) self.setupGUIByConfig() def getAndSaveCurrentConfig(self) -> dict: """获取当前设置""" currentConfig = dict( selectedDict=self.dictionaryComboBox.currentIndex(), selectedApi=self.apiComboBox.currentIndex(), selectedGroup=self.selectedGroups, deck=self.deckComboBox.currentText(), username=self.usernameLineEdit.text(), password=Mask(self.passwordLineEdit.text()), cookie=Mask(self.cookieLineEdit.text()), definition=self.definitionCheckBox.isChecked(), sentence=self.sentenceCheckBox.isChecked(), image=self.imageCheckBox.isChecked(), phrase=self.phraseCheckBox.isChecked(), AmEPhonetic=self.AmEPhoneticCheckBox.isChecked(), BrEPhonetic=self.BrEPhoneticCheckBox.isChecked(), BrEPron=self.BrEPronRadioButton.isChecked(), AmEPron=self.AmEPronRadioButton.isChecked(), noPron=self.noPronRadioButton.isChecked(), ) logger.info(f'当前设置:{currentConfig}') self._saveConfig(currentConfig) self.currentConfig = currentConfig return currentConfig @staticmethod def _saveConfig(config): _config = deepcopy(config) _config['credential'] = [dict(username='', password='', cookie='') ] * len(dictionaries) _config['credential'][_config['selectedDict']] = dict( username=_config.pop('username'), password=str(_config.pop('password')), cookie=str(_config.pop('cookie'))) maskedConfig = deepcopy(_config) maskedCredential = [ dict(username=c['username'], password=Mask(c['password']), cookie=Mask(c['cookie'])) for c in maskedConfig['credential'] ] maskedConfig['credential'] = maskedCredential logger.info(f'保存配置项:{maskedConfig}') mw.addonManager.writeConfig(__name__, _config) def checkUpdate(self): @pyqtSlot(str, str) def on_haveNewVersion(version, changeLog): if askUser(f'有新版本:{version}是否更新?\n\n{changeLog.strip()}'): openLink(RELEASE_URL) self.updateCheckWork = VersionCheckWorker() self.updateCheckWork.moveToThread(self.updateCheckThead) self.updateCheckWork.haveNewVersion.connect(on_haveNewVersion) self.updateCheckWork.finished.connect(self.updateCheckThead.quit) self.updateCheckWork.start.connect(self.updateCheckWork.run) self.updateCheckWork.start.emit() @pyqtSlot(int) def on_dictionaryComboBox_currentIndexChanged(self, index): """词典候选框改变事件""" self.currentDictionaryLabel.setText( f'当前选择词典: {self.dictionaryComboBox.currentText()}') config = mw.addonManager.getConfig(__name__) self.usernameLineEdit.setText(config['credential'][index]['username']) self.passwordLineEdit.setText(config['credential'][index]['password']) self.cookieLineEdit.setText(config['credential'][index]['cookie']) @pyqtSlot() def on_pullRemoteWordsBtn_clicked(self): """获取单词按钮点击事件""" if not self.deckComboBox.currentText(): showInfo('\n请选择或输入要同步的牌组') return self.mainTab.setEnabled(False) self.progressBar.setValue(0) self.progressBar.setMaximum(0) currentConfig = self.getAndSaveCurrentConfig() self.selectedDict = dictionaries[currentConfig['selectedDict']]() # 登陆线程 self.loginWorker = LoginWorker( self.selectedDict.login, str(currentConfig['username']), str(currentConfig['password']), json.loads(str(currentConfig['cookie']) or '{}')) self.loginWorker.moveToThread(self.workerThread) self.loginWorker.logSuccess.connect(self.onLogSuccess) self.loginWorker.start.connect(self.loginWorker.run) self.loginWorker.logFailed.connect(self.onLoginFailed) self.loginWorker.start.emit() @pyqtSlot() def onLoginFailed(self): showCritical('登录失败!') self.tabWidget.setCurrentIndex(1) self.progressBar.setValue(0) self.progressBar.setMaximum(1) self.mainTab.setEnabled(True) self.cookieLineEdit.clear() @pyqtSlot(str) def onLogSuccess(self, cookie): self.cookieLineEdit.setText(cookie) self.selectedDict.getGroups() container = QDialog(self) group = wordGroup.Ui_Dialog() group.setupUi(container) for groupName in [ str(group_name) for group_name, _ in self.selectedDict.groups ]: item = QListWidgetItem() item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setText(groupName) item.setCheckState(Qt.Unchecked) group.wordGroupListWidget.addItem(item) # 恢复上次选择的单词本分组 if self.selectedGroups: for groupName in self.selectedGroups[ self.currentConfig['selectedDict']]: items = group.wordGroupListWidget.findItems( groupName, Qt.MatchExactly) for item in items: item.setCheckState(Qt.Checked) else: self.selectedGroups = [list()] * len(dictionaries) def onAccepted(): """选择单词本弹窗确定事件""" # 清空 listWidget self.newWordListWidget.clear() self.needDeleteWordListWidget.clear() self.mainTab.setEnabled(False) selectedGroups = [ group.wordGroupListWidget.item(index).text() for index in range(group.wordGroupListWidget.count()) if group.wordGroupListWidget.item(index).checkState() == Qt.Checked ] # 保存分组记录 self.selectedGroups[ self.currentConfig['selectedDict']] = selectedGroups self.progressBar.setValue(0) self.progressBar.setMaximum(1) logger.info(f'选中单词本{selectedGroups}') self.getRemoteWordList(selectedGroups) def onRejected(): """选择单词本弹窗取消事件""" self.progressBar.setValue(0) self.progressBar.setMaximum(1) self.mainTab.setEnabled(True) group.buttonBox.accepted.connect(onAccepted) group.buttonBox.rejected.connect(onRejected) container.exec() def getRemoteWordList(self, selected_groups: [str]): """根据选中到分组获取分组下到全部单词,并添加到 newWordListWidget""" group_map = dict(self.selectedDict.groups) self.localWords = getWordsByDeck(self.deckComboBox.currentText()) # 启动单词获取线程 self.pullWorker = RemoteWordFetchingWorker(self.selectedDict, [( group_name, group_map[group_name], ) for group_name in selected_groups]) self.pullWorker.moveToThread(self.workerThread) self.pullWorker.start.connect(self.pullWorker.run) self.pullWorker.tick.connect( lambda: self.progressBar.setValue(self.progressBar.value() + 1)) self.pullWorker.setProgress.connect(self.progressBar.setMaximum) self.pullWorker.doneThisGroup.connect(self.insertWordToListWidget) self.pullWorker.done.connect(self.on_allPullWork_done) self.pullWorker.start.emit() @pyqtSlot(list) def insertWordToListWidget(self, words: list): """一个分组获取完毕事件""" for word in words: wordItem = QListWidgetItem(word, self.newWordListWidget) wordItem.setData(Qt.UserRole, None) self.newWordListWidget.clearSelection() @pyqtSlot() def on_allPullWork_done(self): """全部分组获取完毕事件""" localWordList = set(getWordsByDeck(self.deckComboBox.currentText())) remoteWordList = set([ self.newWordListWidget.item(row).text() for row in range(self.newWordListWidget.count()) ]) newWords = remoteWordList - localWordList # 新单词 needToDeleteWords = localWordList - remoteWordList # 需要删除的单词 logger.info(f'本地: {localWordList}') logger.info(f'远程: {remoteWordList}') logger.info(f'待查: {newWords}') logger.info(f'待删: {needToDeleteWords}') waitIcon = QIcon(':/icons/wait.png') delIcon = QIcon(':/icons/delete.png') self.newWordListWidget.clear() self.needDeleteWordListWidget.clear() for word in needToDeleteWords: item = QListWidgetItem(word) item.setCheckState(Qt.Checked) item.setIcon(delIcon) self.needDeleteWordListWidget.addItem(item) for word in newWords: item = QListWidgetItem(word) item.setIcon(waitIcon) self.newWordListWidget.addItem(item) self.newWordListWidget.clearSelection() self.dictionaryComboBox.setEnabled(True) self.apiComboBox.setEnabled(True) self.deckComboBox.setEnabled(True) self.pullRemoteWordsBtn.setEnabled(True) self.queryBtn.setEnabled(self.newWordListWidget.count() > 0) self.syncBtn.setEnabled(self.newWordListWidget.count() == 0 and self.needDeleteWordListWidget.count() > 0) if self.needDeleteWordListWidget.count( ) == self.newWordListWidget.count() == 0: logger.info('无需同步') tooltip('无需同步') self.mainTab.setEnabled(True) @pyqtSlot() def on_queryBtn_clicked(self): logger.info('点击查询按钮') currentConfig = self.getAndSaveCurrentConfig() self.queryBtn.setEnabled(False) self.pullRemoteWordsBtn.setEnabled(False) self.syncBtn.setEnabled(False) wordList = [] selectedWords = self.newWordListWidget.selectedItems() if selectedWords: # 如果选中单词则只查询选中的单词 for wordItem in selectedWords: wordBundle = dict() row = self.newWordListWidget.row(wordItem) wordBundle['term'] = wordItem.text() for configName in BASIC_OPTION + EXTRA_OPTION: wordBundle[configName] = currentConfig[configName] wordBundle['row'] = row wordList.append(wordBundle) else: # 没有选择则查询全部 for row in range(self.newWordListWidget.count()): wordBundle = dict() wordItem = self.newWordListWidget.item(row) wordBundle['term'] = wordItem.text() for configName in BASIC_OPTION + EXTRA_OPTION: wordBundle[configName] = currentConfig[configName] wordBundle['row'] = row wordList.append(wordBundle) logger.info(f'待查询单词{wordList}') # 查询线程 self.progressBar.setMaximum(len(wordList)) self.queryWorker = QueryWorker(wordList, apis[currentConfig['selectedApi']]) self.queryWorker.moveToThread(self.workerThread) self.queryWorker.thisRowDone.connect(self.on_thisRowDone) self.queryWorker.thisRowFailed.connect(self.on_thisRowFailed) self.queryWorker.tick.connect( lambda: self.progressBar.setValue(self.progressBar.value() + 1)) self.queryWorker.allQueryDone.connect(self.on_allQueryDone) self.queryWorker.start.connect(self.queryWorker.run) self.queryWorker.start.emit() @pyqtSlot(int, dict) def on_thisRowDone(self, row, result): """该行单词查询完毕""" doneIcon = QIcon(':/icons/done.png') wordItem = self.newWordListWidget.item(row) wordItem.setIcon(doneIcon) wordItem.setData(Qt.UserRole, result) @pyqtSlot(int) def on_thisRowFailed(self, row): failedIcon = QIcon(':/icons/failed.png') failedWordItem = self.newWordListWidget.item(row) failedWordItem.setIcon(failedIcon) @pyqtSlot() def on_allQueryDone(self): failed = [] for i in range(self.newWordListWidget.count()): wordItem = self.newWordListWidget.item(i) if not wordItem.data(Qt.UserRole): failed.append(wordItem.text()) if failed: logger.warning(f'查询失败或未查询:{failed}') self.pullRemoteWordsBtn.setEnabled(True) self.queryBtn.setEnabled(True) self.syncBtn.setEnabled(True) @pyqtSlot() def on_syncBtn_clicked(self): failedGenerator = (self.newWordListWidget.item(row).data(Qt.UserRole) is None for row in range(self.newWordListWidget.count())) if any(failedGenerator): if not askUser( '存在未查询或失败的单词,确定要加入单词本吗?\n 你可以选择失败的单词点击 "查询按钮" 来重试。'): return currentConfig = self.getAndSaveCurrentConfig() model = getOrCreateModel(MODEL_NAME) getOrCreateModelCardTemplate(model, 'default') deck = getOrCreateDeck(self.deckComboBox.currentText()) logger.info('同步点击') audiosDownloadTasks = [] newWordCount = self.newWordListWidget.count() # 判断是否需要下载发音 if currentConfig['noPron']: logger.info('不下载发音') whichPron = None else: whichPron = 'AmEPron' if self.AmEPronRadioButton.isChecked( ) else 'BrEPron' logger.info(f'下载发音{whichPron}') added = 0 for row in range(newWordCount): wordItem = self.newWordListWidget.item(row) wordItemData = wordItem.data(Qt.UserRole) if wordItemData: addNoteToDeck(deck, model, currentConfig, wordItemData) added += 1 # 添加发音任务 if whichPron and wordItemData.get(whichPron): audiosDownloadTasks.append(( f"{whichPron}_{wordItemData['term']}.mp3", wordItemData[whichPron], )) mw.reset() logger.info(f'发音下载任务:{audiosDownloadTasks}') if audiosDownloadTasks: self.syncBtn.setEnabled(False) self.progressBar.setValue(0) self.progressBar.setMaximum(len(audiosDownloadTasks)) if self.audioDownloadThread is not None: self.audioDownloadThread.requestInterruption() self.audioDownloadThread.quit() self.audioDownloadThread.wait() self.audioDownloadThread = QThread(self) self.audioDownloadThread.start() self.audioDownloadWorker = AudioDownloadWorker(audiosDownloadTasks) self.audioDownloadWorker.moveToThread(self.audioDownloadThread) self.audioDownloadWorker.tick.connect( lambda: self.progressBar.setValue(self.progressBar.value() + 1 )) self.audioDownloadWorker.start.connect( self.audioDownloadWorker.run) self.audioDownloadWorker.done.connect(lambda: tooltip(f'发音下载完成')) self.audioDownloadWorker.done.connect( self.audioDownloadThread.quit) self.audioDownloadWorker.start.emit() self.newWordListWidget.clear() needToDeleteWordItems = [ self.needDeleteWordListWidget.item(row) for row in range(self.needDeleteWordListWidget.count()) if self.needDeleteWordListWidget.item(row).checkState() == Qt.Checked ] needToDeleteWords = [i.text() for i in needToDeleteWordItems] deleted = 0 if needToDeleteWords and askUser( f'确定要删除这些单词吗:{needToDeleteWords[:3]}...({len(needToDeleteWords)}个)', title='Dict2Anki', parent=self): needToDeleteWordNoteIds = getNotes(needToDeleteWords, currentConfig['deck']) mw.col.remNotes(needToDeleteWordNoteIds) deleted += 1 mw.col.reset() mw.reset() for item in needToDeleteWordItems: self.needDeleteWordListWidget.takeItem( self.needDeleteWordListWidget.row(item)) logger.info('删除完成') logger.info('完成') if not audiosDownloadTasks: tooltip(f'添加{added}个笔记\n删除{deleted}个笔记')
class PointLayerImport: def __init__(self, parent, target_layout): self.parent = parent self.canvas = iface.mapCanvas() self.ui = UI(target_layout) self.__init_ui() def search(self): layer = self.source_layer target_layer_name = self.ui.text_edit_target_layer_name.text() selected_only = bool(self.ui.checkbox_selected_only.checkState()) selected_field_names = self.ui.combobox_fields_select.checkedItems() fields_to_copy = [ field for field in layer.dataProvider().fields() if field.name() in selected_field_names ] features_iterator = layer.getSelectedFeatures( ) if selected_only else layer.getFeatures() count = sum(1 for i in features_iterator) self.source_features_count = count self.__cleanup_before_search() self.worker = PointLayerImportWorker(layer, selected_only, target_layer_name, fields_to_copy) self.thread = QThread() self.worker.moveToThread(self.thread) self.worker.progressed.connect(self.__progressed) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.__handle_finished) self.worker.interrupted.connect(self.__handle_interrupted) self.worker.interrupted.connect(self.thread.quit) self.thread.started.connect(self.worker.search) self.thread.start() self.ui.label_status.setText(f"Trwa wyszukiwanie {count} obiektów...") def __init_ui(self): self.ui.button_start.clicked.connect(self.search) self.ui.button_cancel.clicked.connect(self.__stop) self.__on_layer_changed(self.ui.layer_select.currentLayer()) self.ui.layer_select.layerChanged.connect(self.__on_layer_changed) self.ui.label_status.setText("") self.ui.label_found_count.setText("") self.ui.label_not_found_count.setText("") def __on_layer_changed(self, layer): self.ui.combobox_fields_select.clear() self.ui.button_start.setEnabled(False) if layer: if layer.dataProvider().featureCount() == 0: iface.messageBar().pushCritical( "Wtyczka ULDK", f"Warstwa <b>{layer.sourceName()} nie zawiera żadnych obiektów.</b>" ) return self.source_layer = layer layer.selectionChanged.connect( self.__on_layer_features_selection_changed) layer.updatedFields.connect(self.__fill_combobox_fields_select) self.ui.button_start.setEnabled(True) current_layer_name = layer.sourceName() suggested_target_layer_name = f"{current_layer_name} - Działki ULDK" fields = layer.dataProvider().fields() self.ui.text_edit_target_layer_name.setText( suggested_target_layer_name) self.ui.combobox_fields_select.addItems( map(lambda x: x.name(), fields)) self.ui.button_start.setEnabled(True) else: self.source_layer = None self.ui.text_edit_target_layer_name.setText("") self.ui.checkbox_selected_only.setText( "Tylko zaznaczone obiekty [0]") def __on_layer_features_selection_changed(self, selected_features): if not self.source_layer: selected_features = [] self.ui.checkbox_selected_only.setText( f"Tylko zaznaczone obiekty [{len(selected_features)}]") def __fill_combobox_fields_select(self): self.ui.combobox_fields_select.clear() fields = self.source_layer.dataProvider().fields() self.ui.combobox_fields_select.addItems(map(lambda x: x.name(), fields)) def __progressed(self, found, omitted_count, saved): if saved: self.saved_count += 1 if found: self.found_count += 1 else: self.not_found_count += 1 self.omitted_count += omitted_count progressed_count = self.found_count + self.not_found_count self.ui.progress_bar.setValue(progressed_count / self.source_features_count * 100) self.ui.label_status.setText( f"Przetworzono {progressed_count} z {self.source_features_count} obiektów" ) found_message = f"Znaleziono: {self.saved_count}" if self.omitted_count: found_message += f" (pominięto: {self.omitted_count})" self.ui.label_found_count.setText(found_message) self.ui.label_not_found_count.setText( f"Nie znaleziono: {self.not_found_count}") def __handle_finished(self, layer_found, layer_not_found): self.__cleanup_after_search() if layer_found.dataProvider().featureCount(): QgsProject.instance().addMapLayer(layer_found) if layer_not_found.dataProvider().featureCount(): QgsProject.instance().addMapLayer(layer_not_found) iface.messageBar().pushWidget( QgsMessageBarItem( "Wtyczka ULDK", f"Wyszukiwarka działek z warstwy: zakończono wyszukiwanie. Zapisano {self.saved_count} {get_obiekty_form(self.saved_count)} do warstwy <b>{self.ui.text_edit_target_layer_name.text()}</b>" )) def __handle_interrupted(self, layer_found, layer_not_found): self.__cleanup_after_search() if layer_found.dataProvider().featureCount(): QgsProject.instance().addMapLayer(layer_found) if layer_not_found.dataProvider().featureCount(): QgsProject.instance().addMapLayer(layer_not_found) def __cleanup_after_search(self): self.__set_controls_enabled(True) self.ui.button_cancel.setText("Anuluj") self.ui.button_cancel.setEnabled(False) self.ui.progress_bar.setValue(0) def __cleanup_before_search(self): self.__set_controls_enabled(False) self.ui.button_cancel.setEnabled(True) self.ui.label_status.setText("") self.ui.label_found_count.setText("") self.ui.label_not_found_count.setText("") self.found_count = 0 self.not_found_count = 0 self.omitted_count = 0 self.saved_count = 0 def __set_controls_enabled(self, enabled): self.ui.text_edit_target_layer_name.setEnabled(enabled) self.ui.button_start.setEnabled(enabled) self.ui.layer_select.setEnabled(enabled) def __stop(self): self.thread.requestInterruption() self.ui.button_cancel.setEnabled(False) self.ui.button_cancel.setText("Przerywanie...")
class CSVImport: def __init__(self, parent, target_layout, uldk_api, result_collector_factory, layer_factory): self.parent = parent self.iface = parent.iface self.ui = UI(parent.dockwidget, target_layout) self.uldk_api = uldk_api self.result_collector_factory = result_collector_factory self.layer_factory = layer_factory self.file_path = None self.__init_ui() self.uldk_search = uldk_api.ULDKSearchParcel( "dzialka", ("geom_wkt", "wojewodztwo", "powiat", "gmina", "obreb", "numer", "teryt")) def start_import(self): self.__cleanup_before_search() layer_name = self.ui.text_edit_layer_name.text() layer = self.layer_factory(name=layer_name, custom_properties={"ULDK": layer_name}) self.result_collector = self.result_collector_factory( self.parent, layer) self.uldk_received_rows = [] teryts = [] with open(self.file_path) as f: csv_read = csv.DictReader(f) teryt_column = self.ui.combobox_teryt_column.currentText() for row in csv_read: teryt = row[teryt_column] teryts.append(teryt) self.csv_rows_count = len(teryts) self.worker = self.uldk_api.ULDKSearchWorker(self.uldk_search, teryts) self.thread = QThread() self.worker.moveToThread(self.thread) self.worker.found.connect(self.__handle_found) self.worker.found.connect(self.__progressed) self.worker.not_found.connect(self.__handle_not_found) self.worker.not_found.connect(self.__progressed) self.worker.found.connect(self.__progressed) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.__handle_finished) self.worker.interrupted.connect(self.__handle_interrupted) self.worker.interrupted.connect(self.thread.quit) self.thread.started.connect(self.worker.search) self.thread.start() self.ui.label_status.setText( f"Trwa wyszukiwanie {self.csv_rows_count} obiektów...") def __init_ui(self): self.ui.button_start.clicked.connect(self.start_import) self.ui.label_status.setText("") self.ui.label_found_count.setText("") self.ui.label_not_found_count.setText("") self.ui.button_cancel.clicked.connect(self.__stop) self.ui.file_select.fileChanged.connect(self.__on_file_changed) self.__init_table() def __init_table(self): table = self.ui.table_errors table.setEditTriggers(QTableWidget.NoEditTriggers) table.setColumnCount(2) table.setHorizontalHeaderLabels(("TERYT", "Treść błędu")) header = table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.Interactive) header.setSectionResizeMode(1, QHeaderView.Stretch) teryt_column_size = table.width() / 3 header.resizeSection(0, 200) def __on_file_changed(self, path): suggested_target_layer_name = "" if os.path.exists(path): self.ui.button_start.setEnabled(True) self.file_path = path self.__fill_column_select() suggested_target_layer_name = os.path.splitext( os.path.relpath(path))[0] else: self.file_path = None self.ui.combobox_teryt_column.clear() self.ui.text_edit_layer_name.setText(suggested_target_layer_name) def __fill_column_select(self): self.ui.combobox_teryt_column.clear() with open(self.file_path) as f: csv_read = csv.DictReader(f) columns = csv_read.fieldnames self.ui.combobox_teryt_column.addItems(columns) def __handle_found(self, uldk_response_rows): for row in uldk_response_rows: self.uldk_received_rows.append(row) self.found_count += 1 def __handle_not_found(self, teryt, exception): row = self.ui.table_errors.rowCount() self.ui.table_errors.insertRow(row) self.ui.table_errors.setItem(row, 0, QTableWidgetItem(teryt)) self.ui.table_errors.setItem(row, 1, QTableWidgetItem(str(exception))) self.not_found_count += 1 def __progressed(self): found_count = self.found_count not_found_count = self.not_found_count progressed_count = found_count + not_found_count self.ui.progress_bar.setValue(progressed_count / self.csv_rows_count * 100) self.ui.label_status.setText("Przetworzono {} z {} obiektów".format( progressed_count, self.csv_rows_count)) self.ui.label_found_count.setText("Znaleziono: {}".format(found_count)) self.ui.label_not_found_count.setText( "Nie znaleziono: {}".format(not_found_count)) def __handle_finished(self): self.__collect_received_rows() form = "obiekt" found_count = self.found_count if found_count == 1: pass elif 2 <= found_count <= 4: form = "obiekty" elif 5 <= found_count <= 15: form = "obiektów" else: units = found_count % 10 if units in (2, 3, 4): form = "obiekty" self.iface.messageBar().pushWidget( QgsMessageBarItem( "Wtyczka ULDK", f"Import CSV: zakończono wyszukiwanie. Zapisano {found_count} {form} do warstwy <b>{self.ui.text_edit_layer_name.text()}</b>" )) self.__cleanup_after_search() def __handle_interrupted(self): self.__collect_received_rows() self.__cleanup_after_search() def __collect_received_rows(self): if self.uldk_received_rows: self.result_collector.update(self.uldk_received_rows) def __cleanup_after_search(self): self.__set_controls_enabled(True) self.ui.button_cancel.setText("Anuluj") self.ui.button_cancel.setEnabled(False) self.ui.progress_bar.setValue(0) def __cleanup_before_search(self): self.__set_controls_enabled(False) self.ui.button_cancel.setEnabled(True) self.ui.table_errors.setRowCount(0) self.ui.label_status.setText("") self.ui.label_found_count.setText("") self.ui.label_not_found_count.setText("") self.found_count = 0 self.not_found_count = 0 def __set_controls_enabled(self, enabled): self.ui.text_edit_layer_name.setEnabled(enabled) self.ui.button_start.setEnabled(enabled) self.ui.file_select.setEnabled(enabled) self.ui.combobox_teryt_column.setEnabled(enabled) def __stop(self): self.thread.requestInterruption() self.ui.button_cancel.setEnabled(False) self.ui.button_cancel.setText("Przerywanie...")
class TVLinker(QWidget): def __init__(self, settings: QSettings, parent=None): super(TVLinker, self).__init__(parent) self.firstrun = True self.rows, self.cols = 0, 0 self.parent = parent self.settings = settings self.taskbar = TaskbarProgress(self) self.init_styles() self.init_settings() self.init_icons() if sys.platform.startswith('linux'): notify.init(qApp.applicationName()) layout = QVBoxLayout() layout.setSpacing(0) layout.setContentsMargins(15, 15, 15, 0) form_groupbox = QGroupBox(self, objectName='mainForm') form_groupbox.setLayout(self.init_form()) self.table = TVLinkerTable(0, 4, self) self.table.doubleClicked.connect(self.show_hosters) layout.addWidget(form_groupbox) layout.addWidget(self.table) layout.addLayout(self.init_metabar()) self.setLayout(layout) qApp.setWindowIcon(self.icon_app) self.resize(FixedSettings.windowSize) self.show() self.start_scraping() self.firstrun = False class ProcError(Enum): FAILED_TO_START = 0 CRASHED = 1 TIMED_OUT = 2 READ_ERROR = 3 WRITE_ERROR = 4 UNKNOWN_ERROR = 5 class NotifyIcon(Enum): SUCCESS = ':assets/images/tvlinker.png' DEFAULT = ':assets/images/tvlinker.png' def init_threads(self, threadtype: str = 'scrape') -> None: if threadtype == 'scrape': if hasattr(self, 'scrapeThread'): if not sip.isdeleted( self.scrapeThread) and self.scrapeThread.isRunning(): self.scrapeThread.terminate() del self.scrapeWorker del self.scrapeThread self.scrapeThread = QThread(self) self.scrapeWorker = ScrapeWorker(self.source_url, self.user_agent, self.dl_pagecount) self.scrapeThread.started.connect(self.show_progress) self.scrapeThread.started.connect(self.scrapeWorker.begin) self.scrapeWorker.moveToThread(self.scrapeThread) self.scrapeWorker.addRow.connect(self.add_row) self.scrapeWorker.workFinished.connect(self.scrape_finished) self.scrapeWorker.workFinished.connect( self.scrapeWorker.deleteLater, Qt.DirectConnection) self.scrapeWorker.workFinished.connect(self.scrapeThread.quit, Qt.DirectConnection) self.scrapeThread.finished.connect(self.scrapeThread.deleteLater, Qt.DirectConnection) elif threadtype == 'unrestrict': pass @staticmethod def load_stylesheet(qssfile: str) -> None: if QFileInfo(qssfile).exists(): qss = QFile(qssfile) qss.open(QFile.ReadOnly | QFile.Text) qApp.setStyleSheet(QTextStream(qss).readAll()) def init_styles(self) -> None: if sys.platform == 'darwin': qss_stylesheet = self.get_path('%s_osx.qss' % qApp.applicationName().lower()) else: qss_stylesheet = self.get_path('%s.qss' % qApp.applicationName().lower()) TVLinker.load_stylesheet(qss_stylesheet) QFontDatabase.addApplicationFont(':assets/fonts/opensans.ttf') QFontDatabase.addApplicationFont(':assets/fonts/opensans-bold.ttf') QFontDatabase.addApplicationFont(':assets/fonts/opensans-semibold.ttf') qApp.setFont(QFont('Open Sans', 12 if sys.platform == 'darwin' else 10)) def init_icons(self) -> None: self.icon_app = QIcon( self.get_path('images/%s.png' % qApp.applicationName().lower())) self.icon_faves_off = QIcon(':assets/images/star_off.png') self.icon_faves_on = QIcon(':assets/images/star_on.png') self.icon_refresh = QIcon(':assets/images/refresh.png') self.icon_menu = QIcon(':assets/images/menu.png') self.icon_settings = QIcon(':assets/images/cog.png') self.icon_updates = QIcon(':assets/images/cloud.png') def init_settings(self) -> None: self.provider = 'Scene-RLS' self.select_provider(0) self.user_agent = self.settings.value('user_agent') self.dl_pagecount = self.settings.value('dl_pagecount', 20, int) self.dl_pagelinks = FixedSettings.linksPerPage self.realdebrid_api_token = self.settings.value('realdebrid_apitoken') self.realdebrid_api_proxy = self.settings.value('realdebrid_apiproxy') self.download_manager = self.settings.value('download_manager') self.persepolis_cmd = self.settings.value('persepolis_cmd') self.pyload_host = self.settings.value('pyload_host') self.pyload_username = self.settings.value('pyload_username') self.pyload_password = self.settings.value('pyload_password') self.idm_exe_path = self.settings.value('idm_exe_path') self.kget_cmd = self.settings.value('kget_cmd') self.favorites = self.settings.value('favorites') def init_form(self) -> QHBoxLayout: self.search_field = QLineEdit(self, clearButtonEnabled=True, placeholderText='Enter search criteria') self.search_field.setObjectName('searchInput') self.search_field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.search_field.setFocus() self.search_field.textChanged.connect(self.clear_filters) self.search_field.returnPressed.connect( lambda: self.filter_table(self.search_field.text())) self.favorites_button = QPushButton(parent=self, flat=True, cursor=Qt.PointingHandCursor, objectName='favesButton', toolTip='Favorites', checkable=True, toggled=self.filter_faves, checked=self.settings.value( 'faves_filter', False, bool)) self.refresh_button = QPushButton(parent=self, flat=True, cursor=Qt.PointingHandCursor, objectName='refreshButton', toolTip='Refresh', clicked=self.start_scraping) self.dlpages_field = QComboBox(self, toolTip='Pages', editable=False, cursor=Qt.PointingHandCursor) self.dlpages_field.addItems( ('10', '20', '30', '40', '50', '60', '70', '80')) self.dlpages_field.setCurrentIndex( self.dlpages_field.findText(str(self.dl_pagecount), Qt.MatchFixedString)) self.dlpages_field.currentIndexChanged.connect(self.update_pagecount) self.settings_button = QPushButton(parent=self, flat=True, toolTip='Menu', objectName='menuButton', cursor=Qt.PointingHandCursor) self.settings_button.setMenu(self.settings_menu()) layout = QHBoxLayout(spacing=10) # providerCombo = QComboBox(self, toolTip='Provider', editable=False, cursor=Qt.PointingHandCursor) # providerCombo.setObjectName('providercombo') # providerCombo.addItem(QIcon(':assets/images/provider-scenerls.png'), '') # providerCombo.addItem(QIcon(':assets/images/provider-tvrelease.png'), '') # providerCombo.setIconSize(QSize(146, 36)) # providerCombo.setMinimumSize(QSize(160, 40)) # providerCombo.setStyleSheet(''' # QComboBox, QComboBox::drop-down { background-color: transparent; border: none; margin: 5px; } # QComboBox::down-arrow { image: url(:assets/images/down_arrow.png); } # QComboBox QAbstractItemView { selection-background-color: #DDDDE4; } # ''') # providerCombo.currentIndexChanged.connect(self.select_provider) layout.addWidget( QLabel(pixmap=QPixmap(':assets/images/provider-scenerls.png'))) layout.addWidget(self.search_field) layout.addWidget(self.favorites_button) layout.addWidget(self.refresh_button) layout.addWidget(QLabel('Pages:')) layout.addWidget(self.dlpages_field) layout.addWidget(self.settings_button) return layout @pyqtSlot(int) def select_provider(self, index: int): if index == 0: self.provider = 'Scene-RLS' self.source_url = 'http://scene-rls.net/releases/index.php?p={0}&cat=TV%20Shows' elif index == 1: self.provider = 'TV-Release' self.source_url = 'http://tv-release.pw/?cat=TV' self.setWindowTitle('%s :: %s' % (qApp.applicationName(), self.provider)) def settings_menu(self) -> QMenu: settings_action = QAction(self.icon_settings, 'Settings', self, triggered=self.show_settings) updates_action = QAction(self.icon_updates, 'Check for updates', self, triggered=self.check_update) aboutqt_action = QAction('About Qt', self, triggered=qApp.aboutQt) about_action = QAction('About %s' % qApp.applicationName(), self, triggered=self.about_app) menu = QMenu() menu.addAction(settings_action) menu.addAction(updates_action) menu.addSeparator() menu.addAction(aboutqt_action) menu.addAction(about_action) return menu def init_metabar(self) -> QHBoxLayout: self.meta_template = 'Total number of links retrieved: <b>%i</b> / <b>%i</b>' self.progress = QProgressBar(parent=self, minimum=0, maximum=(self.dl_pagecount * self.dl_pagelinks), visible=False) self.taskbar.setProgress(0.0, True) if sys.platform == 'win32': self.win_taskbar_button = QWinTaskbarButton(self) self.meta_label = QLabel(textFormat=Qt.RichText, alignment=Qt.AlignRight, objectName='totals') self.update_metabar() layout = QHBoxLayout() layout.setContentsMargins(10, 5, 10, 10) layout.addWidget(self.progress, Qt.AlignLeft) layout.addWidget(self.meta_label, Qt.AlignRight) return layout @pyqtSlot() def check_update(self) -> None: QDesktopServices.openUrl(QUrl(FixedSettings.latest_release_url)) @pyqtSlot() def show_settings(self) -> None: settings_win = Settings(self, self.settings) settings_win.exec_() def update_metabar(self) -> bool: rowcount = self.table.rowCount() self.meta_label.setText( self.meta_template % (rowcount, self.dl_pagecount * self.dl_pagelinks)) self.progress.setValue(rowcount) self.taskbar.setProgress(rowcount / self.progress.maximum()) if sys.platform == 'win32': self.win_taskbar_button.progress().setValue(self.progress.value()) return True def start_scraping(self) -> None: self.init_threads('scrape') self.rows = 0 self.table.clearContents() self.table.setRowCount(0) self.table.setSortingEnabled(False) self.update_metabar() self.scrapeThread.start() @pyqtSlot() def about_app(self) -> None: about_html = '''<style> a { color:#441d4e; text-decoration:none; font-weight:bold; } a:hover { text-decoration:underline; } </style> <p style="font-size:24pt; font-weight:bold; color:#6A687D;">%s</p> <p> <span style="font-size:13pt;"><b>Version: %s</b></span> <span style="font-size:10pt;position:relative;left:5px;">( %s )</span> </p> <p style="font-size:13px;"> Copyright © %s <a href="mailto:[email protected]">Pete Alexandrou</a> <br/> Web: <a href="%s">%s</a> </p> <p style="font-size:11px;"> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. </p>''' % (qApp.applicationName(), qApp.applicationVersion(), platform.architecture()[0], datetime.now().year, qApp.organizationDomain(), qApp.organizationDomain()) QMessageBox.about(self, 'About %s' % qApp.applicationName(), about_html) @pyqtSlot(int) def update_pagecount(self, index: int) -> None: self.dl_pagecount = int(self.dlpages_field.itemText(index)) self.scrapeWorker.maxpages = self.dl_pagecount self.progress.setMaximum(self.dl_pagecount * self.dl_pagelinks) self.settings.setValue('dl_pagecount', self.dl_pagecount) if sys.platform == 'win32': self.win_taskbar_button.progress().setMaximum(self.dl_pagecount * self.dl_pagelinks) if self.scrapeThread.isRunning(): self.scrapeThread.requestInterruption() self.start_scraping() @pyqtSlot() def show_progress(self): self.progress.show() self.taskbar.setProgress(0.0, True) if sys.platform == 'win32': self.win_taskbar_button.setWindow(self.windowHandle()) self.win_taskbar_button.progress().setRange( 0, self.dl_pagecount * self.dl_pagelinks) self.win_taskbar_button.progress().setVisible(True) self.win_taskbar_button.progress().setValue(self.progress.value()) @pyqtSlot() def scrape_finished(self) -> None: self.progress.hide() self.taskbar.setProgress(0.0, False) if sys.platform == 'win32': self.win_taskbar_button.progress().setVisible(False) self.table.setSortingEnabled(True) self.filter_table(text='') @pyqtSlot(list) def add_row(self, row: list) -> None: if self.scrapeThread.isInterruptionRequested(): self.scrapeThread.terminate() else: self.cols = 0 self.table.setRowCount(self.rows + 1) if self.table.cursor() != Qt.PointingHandCursor: self.table.setCursor(Qt.PointingHandCursor) for item in row: table_item = QTableWidgetItem(item) table_item.setToolTip( '%s\n\nDouble-click to view hoster links.' % row[1]) table_item.setFont(QFont('Open Sans', weight=QFont.Normal)) if self.cols == 2: if sys.platform == 'win32': table_item.setFont( QFont('Open Sans Semibold', pointSize=10)) elif sys.platform == 'darwin': table_item.setFont( QFont('Open Sans Bold', weight=QFont.Bold)) else: table_item.setFont( QFont('Open Sans', weight=QFont.DemiBold, pointSize=10)) table_item.setText(' ' + table_item.text()) elif self.cols in (0, 3): table_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(self.rows, self.cols, table_item) self.update_metabar() self.cols += 1 self.rows += 1 @pyqtSlot(list) def add_hosters(self, links: list) -> None: self.hosters_win.show_hosters(links) @pyqtSlot(QModelIndex) def show_hosters(self, index: QModelIndex) -> None: qApp.setOverrideCursor(Qt.BusyCursor) self.hosters_win = HosterLinks(self) self.hosters_win.downloadLink.connect(self.download_link) self.hosters_win.copyLink.connect(self.copy_download_link) self.links = HostersThread( self.table.item(self.table.currentRow(), 1).text(), self.user_agent) self.links.setHosters.connect(self.add_hosters) self.links.noLinks.connect(self.no_links) self.links.start() @pyqtSlot() def no_links(self) -> None: self.hosters_win.loading_progress.cancel() self.hosters_win.close() QMessageBox.warning( self, 'No Links Available', 'No links are available yet for the chosen TV show. ' + 'This is most likely due to the files still being uploaded. This is normal if the ' + 'link was published 30-45 mins ago.\n\nPlease check back again in 10-15 minutes.' ) @pyqtSlot(bool) def filter_faves(self, checked: bool) -> None: self.settings.setValue('faves_filter', checked) # if hasattr(self, 'scrapeWorker') and (sip.isdeleted(self.scrapeWorker) or self.scrapeWorker.complete): if not self.firstrun: self.filter_table() @pyqtSlot(str) @pyqtSlot() def filter_table(self, text: str = '') -> None: filters = [] if self.favorites_button.isChecked(): filters = self.favorites self.table.sortItems(2, Qt.AscendingOrder) else: self.table.sortItems(0, Qt.DescendingOrder) if len(text): filters.append(text) if not len(filters) or not hasattr(self, 'valid_rows'): self.valid_rows = [] for search_term in filters: for item in self.table.findItems(search_term, Qt.MatchContains): self.valid_rows.append(item.row()) for row in range(0, self.table.rowCount()): if not len(filters): self.table.showRow(row) else: if row not in self.valid_rows: self.table.hideRow(row) else: self.table.showRow(row) @pyqtSlot() def clear_filters(self): if not len(self.search_field.text()): self.filter_table('') @pyqtSlot(bool) def aria2_confirmation(self, success: bool) -> None: qApp.restoreOverrideCursor() if success: if sys.platform.startswith('linux'): self.notify( title=qApp.applicationName(), msg='Your download link has been unrestricted and now ' + 'queued in Aria2 RPC Daemon', icon=self.NotifyIcon.SUCCESS) else: QMessageBox.information( self, qApp.applicationName(), 'Download link has been queued in Aria2.', QMessageBox.Ok) else: QMessageBox.critical( self, 'Aria2 RPC Daemon', 'Could not connect to Aria2 RPC Daemon. ' + 'Check your %s settings and try again.' % qApp.applicationName(), QMessageBox.Ok) @pyqtSlot(str) def download_link(self, link: str) -> None: if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ and 'rdeb.io' not in link: qApp.setOverrideCursor(Qt.BusyCursor) self.unrestrict_link(link, True) else: if self.download_manager == 'aria2': self.aria2 = Aria2Thread(settings=self.settings, link_url=link) self.aria2.aria2Confirmation.connect(self.aria2_confirmation) self.aria2.start() self.hosters_win.close() elif self.download_manager == 'pyload': self.pyload_conn = PyloadConnection(self.pyload_host, self.pyload_username, self.pyload_password) pid = self.pyload_conn.addPackage(name='TVLinker', links=[link]) qApp.restoreOverrideCursor() self.hosters_win.close() if sys.platform.startswith('linux'): self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) else: QMessageBox.information( self, self.download_manager, 'Your link has been queued in %s.' % self.download_manager, QMessageBox.Ok) # open_pyload = msgbox.addButton('Open pyLoad', QMessageBox.AcceptRole) # open_pyload.clicked.connect(self.open_pyload) elif self.download_manager in ('kget', 'persepolis'): provider = self.kget_cmd if self.download_manager == 'kget' else self.persepolis_cmd cmd = '{0} "{1}"'.format(provider, link) if self.cmdexec(cmd): qApp.restoreOverrideCursor() self.hosters_win.close() if sys.platform.startswith('linux'): self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) else: QMessageBox.information( self, self.download_manager, 'Your link has been queued in %s.' % self.download_manager, QMessageBox.Ok) elif self.download_manager == 'idm': cmd = '"%s" /n /d "%s"' % (self.idm_exe_path, link) if self.cmdexec(cmd): qApp.restoreOverrideCursor() self.hosters_win.close() QMessageBox.information( self, 'Internet Download Manager', 'Your link has been queued in IDM.') else: print('IDM QProcess error = %s' % self.ProcError(self.idm.error()).name) qApp.restoreOverrideCursor() self.hosters_win.close() QMessageBox.critical( self, 'Internet Download Manager', '<p>Could not connect to your local IDM application instance. ' + 'Please check your settings and ensure the IDM executable path is correct ' + 'according to your installation.</p><p>Error Code: %s</p>' % self.ProcError(self.idm.error()).name, QMessageBox.Ok) else: dlpath, _ = QFileDialog.getSaveFileName( self, 'Save File', link.split('/')[-1]) if dlpath != '': self.directdl_win = DirectDownload(parent=self) self.directdl = DownloadThread(link_url=link, dl_path=dlpath) self.directdl.dlComplete.connect( self.directdl_win.download_complete) if sys.platform.startswith('linux'): self.directdl.dlComplete.connect( lambda: self.notify(qApp.applicationName( ), 'Download complete', self.NotifyIcon.SUCCESS)) else: self.directdl.dlComplete.connect( lambda: QMessageBox.information( self, qApp.applicationName(), 'Download complete', QMessageBox.Ok)) self.directdl.dlProgressTxt.connect( self.directdl_win.update_progress_label) self.directdl.dlProgress.connect( self.directdl_win.update_progress) self.directdl_win.cancelDownload.connect( self.cancel_download) self.directdl.start() self.hosters_win.close() def _init_notification_icons(self): for icon in self.NotifyIcon: icon_file = QPixmap(icon.value, 'PNG') icon_file.save( os.path.join(FixedSettings.config_path, os.path.basename(icon.value)), 'PNG', 100) def notify(self, title: str, msg: str = '', icon: Enum = None, urgency: int = 1) -> bool: icon_path = icon.value if icon is not None else self.NotifyIcon.DEFAULT.value icon_path = os.path.join(FixedSettings.config_path, os.path.basename(icon_path)) if not os.path.exists(icon_path): self._init_notification_icons() notification = notify.Notification(title, msg, icon_path) notification.set_urgency(urgency) return notification.show() def cmdexec(self, cmd: str) -> bool: self.proc = QProcess() self.proc.setProcessChannelMode(QProcess.MergedChannels) if hasattr(self.proc, 'errorOccurred'): self.proc.errorOccurred.connect(lambda error: print( 'Process error = %s' % self.ProcError(error).name)) if self.proc.state() == QProcess.NotRunning: self.proc.start(cmd) self.proc.waitForFinished(-1) rc = self.proc.exitStatus( ) == QProcess.NormalExit and self.proc.exitCode() == 0 self.proc.deleteLater() return rc return False @pyqtSlot() def cancel_download(self) -> None: self.directdl.cancel_download = True self.directdl.quit() self.directdl.deleteLater() def open_pyload(self) -> None: QDesktopServices.openUrl(QUrl(self.pyload_config.host)) @pyqtSlot(str) def copy_download_link(self, link: str) -> None: if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ and 'rdeb.io' not in link: qApp.setOverrideCursor(Qt.BusyCursor) self.unrestrict_link(link, False) else: clip = qApp.clipboard() clip.setText(link) self.hosters_win.close() qApp.restoreOverrideCursor() def unrestrict_link(self, link: str, download: bool = True) -> None: caller = inspect.stack()[1].function self.realdebrid = RealDebridThread( settings=self.settings, api_url=FixedSettings.realdebrid_api_url, link_url=link, action=RealDebridThread.RealDebridAction.UNRESTRICT_LINK) self.realdebrid.errorMsg.connect(self.error_handler) if download: self.realdebrid.unrestrictedLink.connect(self.download_link) else: self.realdebrid.unrestrictedLink.connect(self.copy_download_link) self.realdebrid.start() def closeEvent(self, event: QCloseEvent) -> None: if hasattr(self, 'scrapeThread'): if not sip.isdeleted( self.scrapeThread) and self.scrapeThread.isRunning(): self.scrapeThread.requestInterruption() self.scrapeThread.quit() qApp.quit() def error_handler(self, props: list) -> None: qApp.restoreOverrideCursor() QMessageBox.critical(self, props[0], props[1], QMessageBox.Ok) @staticmethod def get_path(path: str = None, override: bool = False) -> str: if override: if getattr(sys, 'frozen', False): return os.path.join(sys._MEIPASS, path) return os.path.join(QFileInfo(__file__).absolutePath(), path) return ':assets/%s' % path @staticmethod def get_version(filename: str = '__init__.py') -> str: with open(TVLinker.get_path(filename, override=True), 'r') as initfile: for line in initfile.readlines(): m = re.match('__version__ *= *[\'](.*)[\']', line) if m: return m.group(1)
class LabJackWidget(QWidget): view_name = "LabJackView.ui" def __init__(self, labjack, labjack_states, parent=None): super().__init__(parent=parent) self.TableView = None self.labjack_device = labjack self.labjack_states = labjack_states self.is_executing = False self.setup_ui() self.connect() def connect(self): self.labjack_device.status_changed.connect( lambda text: self.labjackStatus.setText(text)) def on_executeStatesButton_pressed(self): if self.is_executing: self.interrupt_execution() elif self.labjack_device.available(): logging.debug('Starting execute states.') self.labjack_device.clear() count = int(self.ExecuteStatesLoopCount.value()) delay = float(self.ExecuteStatesLoopDelay.value()) self.thread = QThread() self.worker = execute_states_thread.ExecuteStatesWorker( number_of_times=count, delay=delay, device=self.labjack_device, states=self.labjack_states.states) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.started.connect(self.on_execute_states_started) self.worker.loop_progress.connect(self.update_loop_progress) self.worker.finished.connect(self.on_execute_states_finished) self.worker.interrupted.connect(self.on_execute_states_finished) self.worker.finished.connect(self.thread.quit) self.thread.finished.connect(self.thread.deleteLater) self.worker.finished.connect(self.worker.deleteLater) self.thread.start() def interrupt_execution(self): self.executeStatesButton.setText('Aborting...') self.thread.requestInterruption() def on_execute_states_finished(self): self.is_executing = False self.executeStatesButton.setText('Execute') self.labjack_device.clear() self.set_input_enabled(enabled=True) def on_execute_states_started(self): self.is_executing = True self.executeStatesButton.setText('Interrupt') self.set_input_enabled(enabled=False) self.LoopProgress.setValue(0) def update_loop_progress(self, value): self.LoopProgress.setValue(value * 100) logging.debug(value) def set_input_enabled(self, enabled): logging.debug('Input enabled: %i' % enabled) self.deleteStates.setEnabled(enabled) self.addStateButton.setEnabled(enabled) def setup_ui(self): p = Path(__file__).parents[1] view_location = p.joinpath('views/LabJackView.ui').__str__() uic.loadUi(view_location, self) self.TableView.setModel(self.labjack_states) self.TableView.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) self.TableView.resizeColumnsToContents() self.TableView.setAcceptDrops(True) self.TableView.setDragEnabled(True) self.TableView.setDefaultDropAction(Qt.MoveAction) self.TableView.setDragDropMode(QAbstractItemView.InternalMove) self.TableView.setDragDropOverwriteMode(False) self.TableView.setSelectionBehavior(QAbstractItemView.SelectRows) width = self.TableView.horizontalHeader().sizeHint().width( ) * self.TableView.horizontalHeader().count() self.TableView.setMinimumWidth(width) self.fio_states = [ self.FIO4State, self.FIO5State, self.FIO6State, self.FIO7State ] for fio in self.labjack_device.fios: getattr(self, "fio{}Label".format(fio.number)).setText(fio.label) getattr(self, "DAC0Label").setText(self.labjack_device.DAC0.label) getattr(self, "DAC1Label").setText(self.labjack_device.DAC1.label) def on_addStateButton_pressed(self): self.labjack_states.add_state(self.FIO4State.isChecked(), self.FIO5State.isChecked(), self.FIO6State.isChecked(), self.FIO7State.isChecked(), self.DAC0Input.value(), self.DAC1Input.value(), self.DurationInput.value()) self.clear_new_input_state() def clear_new_input_state(self): [state.setChecked(False) for state in self.fio_states] self.DAC0Input.setValue(0.00) self.DAC1Input.setValue(0.00) self.DurationInput.setValue(0.00) def on_deleteStates_pressed(self): selection = self.TableView.selectionModel() model_indices = selection.selectedRows() [ self.labjack_states.removeRow(model_index.row() - i) for i, model_index in enumerate(model_indices) ] def on_section_moved(self, logical_index, old_visual_index, new_visual_index): self.model.move_state(old_visual_index, new_visual_index)
class CSVImport: def __init__(self, parent, target_layout, result_collector_factory, layer_factory): self.parent = parent self.ui = UI(parent.dockwidget, target_layout) self.result_collector_factory = result_collector_factory self.layer_factory = layer_factory self.file_path = None self.__init_ui() uldk_search = ULDKSearchParcel("dzialka", ("geom_wkt", "wojewodztwo", "powiat", "gmina", "obreb", "numer", "teryt")) self.uldk_search = ULDKSearchLogger(uldk_search) def start_import(self): self.__cleanup_before_search() teryts = [] self.additional_attributes = defaultdict(list) with open(self.file_path) as f: teryt_column = self.ui.combobox_teryt_column.currentText() csv_read = csv.DictReader(f) additional_fields = [ name for name in csv_read.fieldnames if name != teryt_column ] for row in csv_read: teryt = row.pop(teryt_column) teryts.append(teryt) if additional_fields: for value in row.values(): self.additional_attributes[teryt].append(value) layer_name = self.ui.text_edit_layer_name.text() layer = self.layer_factory(name=layer_name, custom_properties={"ULDK": layer_name}, additional_fields=[ QgsField(field, QVariant.String) for field in additional_fields ]) self.result_collector = self.result_collector_factory( self.parent, layer) self.features_found = [] self.csv_rows_count = len(teryts) self.worker = ULDKSearchWorker(self.uldk_search, teryts) self.thread = QThread() self.worker.moveToThread(self.thread) self.worker.found.connect(self.__handle_found) self.worker.found.connect(self.__progressed) self.worker.not_found.connect(self.__handle_not_found) self.worker.not_found.connect(self.__progressed) self.worker.found.connect(self.__progressed) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.__handle_finished) self.worker.interrupted.connect(self.__handle_interrupted) self.worker.interrupted.connect(self.thread.quit) self.thread.started.connect(self.worker.search) self.thread.start() self.ui.label_status.setText( f"Trwa wyszukiwanie {self.csv_rows_count} obiektów...") def __init_ui(self): self.ui.button_start.clicked.connect(self.start_import) self.ui.label_status.setText("") self.ui.label_found_count.setText("") self.ui.label_not_found_count.setText("") self.ui.button_cancel.clicked.connect(self.__stop) self.ui.file_select.fileChanged.connect(self.__on_file_changed) self.ui.button_save_not_found.clicked.connect( self._export_table_errors_to_csv) self.__init_table() def __init_table(self): table = self.ui.table_errors table.setEditTriggers(QTableWidget.NoEditTriggers) table.setColumnCount(2) table.setHorizontalHeaderLabels(("TERYT", "Treść błędu")) header = table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.Interactive) header.setSectionResizeMode(1, QHeaderView.Stretch) teryt_column_size = table.width() / 3 header.resizeSection(0, 200) def __on_file_changed(self, path): suggested_target_layer_name = "" if os.path.exists(path): self.ui.button_start.setEnabled(True) self.file_path = path self.__fill_column_select() suggested_target_layer_name = os.path.splitext( os.path.relpath(path))[0] else: self.file_path = None self.ui.combobox_teryt_column.clear() self.ui.text_edit_layer_name.setText(suggested_target_layer_name) def __fill_column_select(self): self.ui.combobox_teryt_column.clear() with open(self.file_path) as f: csv_read = csv.DictReader(f) columns = csv_read.fieldnames self.ui.combobox_teryt_column.addItems(columns) def __handle_found(self, uldk_response_rows): for row in uldk_response_rows: try: teryt = row.split("|")[6] attributes = self.additional_attributes.get(teryt) feature = self.result_collector.uldk_response_to_qgs_feature( row, attributes) except self.result_collector.BadGeometryException as e: e = self.result_collector.BadGeometryException( e.feature, "Niepoprawna geometria") self._handle_bad_geometry(e.feature, e) return self.features_found.append(feature) self.found_count += 1 def __handle_not_found(self, teryt, exception): self._add_table_errors_row(teryt, str(exception)) self.not_found_count += 1 def _handle_bad_geometry(self, feature, exception): self._add_table_errors_row(feature.attribute("teryt"), str(exception)) self.not_found_count += 1 def __progressed(self): found_count = self.found_count not_found_count = self.not_found_count progressed_count = found_count + not_found_count self.ui.progress_bar.setValue(progressed_count / self.csv_rows_count * 100) self.ui.label_status.setText("Przetworzono {} z {} obiektów".format( progressed_count, self.csv_rows_count)) self.ui.label_found_count.setText("Znaleziono: {}".format(found_count)) self.ui.label_not_found_count.setText( "Nie znaleziono: {}".format(not_found_count)) def __handle_finished(self): self.__collect_received_features() form = "obiekt" found_count = self.found_count if found_count == 1: pass elif 2 <= found_count <= 4: form = "obiekty" elif 5 <= found_count <= 15: form = "obiektów" else: units = found_count % 10 if units in (2, 3, 4): form = "obiekty" else: form = "obiektów" iface.messageBar().pushWidget( QgsMessageBarItem( "Wtyczka ULDK", f"Import CSV: zakończono wyszukiwanie. Zapisano {found_count} {form} do warstwy <b>{self.ui.text_edit_layer_name.text()}</b>" )) if self.not_found_count > 0: self.ui.button_save_not_found.setEnabled(True) self.__cleanup_after_search() def __handle_interrupted(self): self.__collect_received_features() self.__cleanup_after_search() def __collect_received_features(self): if self.features_found: self.result_collector.update_with_features(self.features_found) def _export_table_errors_to_csv(self): count = self.ui.table_errors.rowCount() path, _ = QFileDialog.getSaveFileName(filter='*.csv') if path: with open(path, 'w') as f: writer = csv.writer(f, delimiter=',') writer.writerow([ self.ui.table_errors.horizontalHeaderItem(0).text(), self.ui.table_errors.horizontalHeaderItem(1).text() ]) for row in range(0, count): teryt = self.ui.table_errors.item(row, 0).text() error = self.ui.table_errors.item(row, 1).text() writer.writerow([teryt, error]) iface.messageBar().pushWidget( QgsMessageBarItem( "Wtyczka ULDK", "Pomyślnie wyeksportowano nieznalezione działki.")) def _add_table_errors_row(self, teryt, exception_message): row = self.ui.table_errors.rowCount() self.ui.table_errors.insertRow(row) self.ui.table_errors.setItem(row, 0, QTableWidgetItem(teryt)) self.ui.table_errors.setItem(row, 1, QTableWidgetItem(exception_message)) def __cleanup_after_search(self): self.__set_controls_enabled(True) self.ui.button_cancel.setText("Anuluj") self.ui.button_cancel.setEnabled(False) self.ui.progress_bar.setValue(0) def __cleanup_before_search(self): self.__set_controls_enabled(False) self.ui.button_cancel.setEnabled(True) self.ui.button_save_not_found.setEnabled(False) self.ui.table_errors.setRowCount(0) self.ui.label_status.setText("") self.ui.label_found_count.setText("") self.ui.label_not_found_count.setText("") self.found_count = 0 self.not_found_count = 0 def __set_controls_enabled(self, enabled): self.ui.text_edit_layer_name.setEnabled(enabled) self.ui.button_start.setEnabled(enabled) self.ui.file_select.setEnabled(enabled) self.ui.combobox_teryt_column.setEnabled(enabled) def __stop(self): self.thread.requestInterruption() self.ui.button_cancel.setEnabled(False) self.ui.button_cancel.setText("Przerywanie...")
class GeneratorDialog(QDialog): """Generator Dialog""" def __init__(self, version, iconPath, addonDir, mediaDir): super().__init__() self.mediaDir = mediaDir self.iconPath = iconPath # Paths self.ankiCsvPath = join(addonDir, Constant.ANKI_DECK) # Create Generator GUI self.ui = UiGenerator() self.ui.setupUi(self, version, iconPath) self.ui.cancelBtn.setDisabled(True) # Create Importer Instance self.importer = ImporterDialog(version, iconPath, addonDir, mediaDir) self.importer.keyPressed.connect(self.importer.on_key) # Set Total Input Word count self.ui.inputTxt.textChanged.connect(self.input_text_changed) # Set Completed Output Card count self.ui.outputTxt.textChanged.connect(self.output_text_changed) # Set Failures Word count self.ui.failureTxt.textChanged.connect(self.failure_text_changed) # Handle clicks on Generate button self.ui.generateBtn.clicked.connect(self.btn_generate_clicked) # Handle clicks on Progress bar self.ui.importBtn.clicked.connect(self.btn_importer_clicked) self.get_supported_languages() # Handle user clicks on translation self.ui.source1.clicked.connect(self.get_supported_languages) self.ui.source2.clicked.connect(self.get_supported_languages) self.ui.source3.clicked.connect(self.get_supported_languages) self.ui.source4.clicked.connect(self.get_supported_languages) def close_event(self, event): logging.shutdown() def input_text_changed(self): words = self.ui.inputTxt.toPlainText().split("\n") # Filter words list, only get non-empty words self.words = list(filter(None, words)) self.ui.totalLbl.setText("Total: {}".format(len(self.words))) def output_text_changed(self): cards = self.ui.outputTxt.toPlainText().split("\n") # Filter cards list, only get non-empty cards self.cards = list(filter(None, cards)) self.cardCount = len(self.cards) self.ui.completedLbl.setText("Completed: {}".format(len(self.cards))) def failure_text_changed(self): failures = self.ui.failureTxt.toPlainText().split("\n") # Filter failures list, only get non-empty failures self.failures = list(filter(None, failures)) self.failureCount = len(self.failures) self.ui.failureLbl.setText("Failure: {}".format(len(self.failures))) def btn_generate_clicked(self): # Validate if input text empty? inputText = self.ui.inputTxt.toPlainText() if not inputText: AnkiHelper.message_box("Info", "No input words available for generating.", "Please check your input words!", self.iconPath) return # Increase to 2% as a processing signal to user self.ui.generateProgressBar.setValue(2) self.ui.generateBtn.setDisabled(True) # Clean up output before generating cards self.ui.outputTxt.setPlainText("") self.ui.failureTxt.setPlainText("") # Get translation options source = self.selected_radio(self.ui.sourceBox) target = self.selected_radio(self.ui.translatedToBox) self.translation = Translation(source, target) # Get generating options self.allWordTypes = self.ui.allWordTypes.isChecked() self.isOnline = self.ui.isOnline.isChecked() # Initialize Generator based on translation self.generator = Worker.initialize_generator(self.translation) # Step 2: Create a QThread object self.bgThread = QThread(self) self.bgThread.setTerminationEnabled(True) # Step 3: Create a worker object self.worker = Worker(self.generator, self.words, self.translation, self.mediaDir, self.isOnline, self.allWordTypes, self.ankiCsvPath) # Step 4: Move worker to the thread self.worker.moveToThread(self.bgThread) # Step 5: Connect signals and slots self.bgThread.started.connect(self.worker.generate_cards_background) self.worker.finished.connect(self.bgThread.quit) self.bgThread.finished.connect(self.bgThread.deleteLater) self.worker.finished.connect(self.worker.deleteLater) self.worker.progress.connect(self.report_progress) self.worker.cardStr.connect(self.report_card) self.worker.failureStr.connect(self.report_failure) # Step 6: Start the thread self.bgThread.start() self.ui.cancelBtn.setEnabled(True) self.ui.generateBtn.setDisabled(True) # Handle cancel background task self.isCancelled = False receiversCount = self.ui.cancelBtn.receivers(self.ui.cancelBtn.clicked) if receiversCount > 0: logging.info( "Already connected before...{}".format(receiversCount)) self.ui.cancelBtn.clicked.disconnect() self.ui.cancelBtn.clicked.connect(self.cancel_background_task) # Final resets self.bgThread.finished.connect(self.finished_generation_progress) def cancel_background_task(self): logging.info("Canceling background task...") self.bgThread.requestInterruption() self.bgThread.quit() self.ui.outputTxt.setPlainText("") self.isCancelled = True AnkiHelper.message_box("Info", "Flashcards generation process stopped!", "Restart by clicking Generate button.", self.iconPath) def finished_generation_progress(self): self.ui.cancelBtn.setDisabled(True) self.ui.generateBtn.setEnabled(True) # Return if thread is cancelled if self.isCancelled: self.ui.outputTxt.setPlainText("") return if self.ui.outputTxt.toPlainText(): btnSelected = AnkiHelper.message_box_buttons( "Info", "Finished generating flashcards.\nThanks for using AnkiFlash!", "Do you want to import generated flashcards now?\n\nProgress completed 100%\n- Input: {}\n- Output: {}\n- Failure: {}" .format(len(self.words), self.cardCount, self.failureCount), QMessageBox.No | QMessageBox.Yes, QMessageBox.Yes, self.iconPath) if btnSelected == QMessageBox.Yes: self.btn_importer_clicked() else: AnkiHelper.message_box_buttons( "Info", "Finished generating flashcards.\nThanks for using AnkiFlash!", "No output flashcards available for importing.\n\nProgress completed 100%\n- Input: {}\n- Output: {}\n- Failure: {}" .format(len(self.words), self.cardCount, self.failureCount), QMessageBox.Close, QMessageBox.Close, self.iconPath) def report_progress(self, percent): # Return if thread is interrupted if self.bgThread.isInterruptionRequested(): return self.ui.generateProgressBar.setValue(percent) def report_card(self, cardStr): # Return if thread is interrupted if self.bgThread.isInterruptionRequested(): return currentText = self.ui.outputTxt.toPlainText() if currentText: currentText += "\n" self.ui.outputTxt.setPlainText("{}{}".format(currentText, cardStr)) def report_failure(self, failureStr): # Return if thread is interrupted if self.bgThread.isInterruptionRequested(): return currentText = self.ui.failureTxt.toPlainText() if currentText: currentText += "\n" self.ui.failureTxt.setPlainText("{}{}".format(currentText, failureStr)) def btn_importer_clicked(self): if self.ui.outputTxt.toPlainText(): self.importer.ui.importProgressBar.setValue(0) self.importer.show() else: AnkiHelper.message_box( "Info", "No output flashcards available for importing.", "Please check your input words!", self.iconPath) def get_supported_languages(self): source = self.selected_radio(self.ui.sourceBox) supportTranslations = { Constant.ENGLISH: [ Constant.ENGLISH, Constant.VIETNAMESE, Constant.CHINESE_TD, Constant.CHINESE_SP, Constant.FRENCH, Constant.JAPANESE ], Constant.VIETNAMESE: [ Constant.ENGLISH, Constant.FRENCH, Constant.JAPANESE, Constant.VIETNAMESE ], Constant.FRENCH: [Constant.ENGLISH, Constant.VIETNAMESE], Constant.JAPANESE: [Constant.ENGLISH, Constant.VIETNAMESE] } targetLanguages = supportTranslations.get(source) logging.info("targetLanguages {}".format(targetLanguages)) radioBtns = [ radio for radio in self.ui.translatedToBox.children() if isinstance(radio, QRadioButton) ] for radio in radioBtns: if radio.text() == Constant.ENGLISH: radio.click() if radio.text() in targetLanguages: radio.setEnabled(True) self.change_radio_color(radio, True) else: radio.setEnabled(False) self.change_radio_color(radio, False) def change_radio_color(self, radio: QRadioButton, isEnabled: bool): if isEnabled: radio.setStyleSheet(self.ui.source1.styleSheet()) else: radio.setStyleSheet("color:gray") def selected_radio(self, groupBox: QGroupBox) -> str: # Get all radio buttons radioBtns = [ radio for radio in groupBox.children() if isinstance(radio, QRadioButton) ] # Find choosen radio and return text for radio in radioBtns: if radio.isChecked(): return radio.text()
class _POSIXUserscriptRunner(_BaseUserscriptRunner): """Userscript runner to be used on POSIX. Uses _BlockingFIFOReader. The OS must have support for named pipes and select(). Commands are executed immediately when they arrive in the FIFO. Attributes: _reader: The _BlockingFIFOReader instance. _thread: The QThread where reader runs. """ def __init__(self, parent=None): super().__init__(parent) self._reader = None self._thread = None def run(self, cmd, *args, env=None): rundir = utils.get_standard_dir(QStandardPaths.RuntimeLocation) # tempfile.mktemp is deprecated and discouraged, but we use it here to # create a FIFO since the only other alternative would be to create a # directory and place the FIFO there, which sucks. Since os.kfifo will # raise an exception anyways when the path doesn't exist, it shouldn't # be a big issue. self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir) os.mkfifo(self._filepath) # pylint: disable=no-member self._reader = _BlockingFIFOReader(self._filepath) self._thread = QThread(self) self._reader.moveToThread(self._thread) self._reader.got_line.connect(self.got_cmd) self._thread.started.connect(self._reader.read) self._reader.finished.connect(self.on_reader_finished) self._thread.finished.connect(self.on_thread_finished) self._run_process(cmd, *args, env=env) self._thread.start() def on_proc_finished(self): """Interrupt the reader when the process finished.""" log.procs.debug("proc finished") self._thread.requestInterruption() def on_proc_error(self, error): """Interrupt the reader when the process had an error.""" super().on_proc_error(error) self._thread.requestInterruption() def on_reader_finished(self): """Quit the thread and clean up when the reader finished.""" log.procs.debug("reader finished") self._thread.quit() self._reader.fifo.close() self._reader.deleteLater() super()._cleanup() self.finished.emit() def on_thread_finished(self): """Clean up the QThread object when the thread finished.""" log.procs.debug("thread finished") self._thread.deleteLater()
class _POSIXUserscriptRunner(_BaseUserscriptRunner): """Userscript runner to be used on POSIX. Uses _BlockingFIFOReader. The OS must have support for named pipes and select(). Commands are executed immediately when they arrive in the FIFO. Attributes: _reader: The _BlockingFIFOReader instance. _thread: The QThread where reader runs. """ def __init__(self, win_id, parent=None): super().__init__(win_id, parent) self._reader = None self._thread = None def run(self, cmd, *args, env=None): rundir = standarddir.get(QStandardPaths.RuntimeLocation) try: # tempfile.mktemp is deprecated and discouraged, but we use it here # to create a FIFO since the only other alternative would be to # create a directory and place the FIFO there, which sucks. Since # os.kfifo will raise an exception anyways when the path doesn't # exist, it shouldn't be a big issue. self._filepath = tempfile.mktemp(prefix='userscript-', dir=rundir) os.mkfifo(self._filepath) # pylint: disable=no-member except OSError as e: message.error(self._win_id, "Error while creating FIFO: {}".format( e)) return self._reader = _BlockingFIFOReader(self._filepath) self._thread = QThread(self) self._reader.moveToThread(self._thread) self._reader.got_line.connect(self.got_cmd) self._thread.started.connect(self._reader.read) self._reader.finished.connect(self.on_reader_finished) self._thread.finished.connect(self.on_thread_finished) self._run_process(cmd, *args, env=env) self._thread.start() def on_proc_finished(self): """Interrupt the reader when the process finished.""" log.procs.debug("proc finished") self._thread.requestInterruption() def on_proc_error(self, error): """Interrupt the reader when the process had an error.""" super().on_proc_error(error) self._thread.requestInterruption() def on_reader_finished(self): """Quit the thread and clean up when the reader finished.""" log.procs.debug("reader finished") self._thread.quit() self._reader.fifo.close() self._reader.deleteLater() super()._cleanup() self.finished.emit() def on_thread_finished(self): """Clean up the QThread object when the thread finished.""" log.procs.debug("thread finished") self._thread.deleteLater()