def getArticles(self): if not self.accessToken: self.accessToken = self._authenticate() if not self.accessToken: showCritical('Authentication failed.') return [] response = post( 'https://getpocket.com/v3/get', json={ 'consumer_key': self.consumerKey, 'access_token': self.accessToken, 'contentType': 'article', 'count': 30, 'detailType': 'complete', 'sort': 'newest', }, headers=self.headers, ) if response.json()['list']: return [{ 'text': a['resolved_title'], 'data': a } for a in response.json()['list'].values()] showInfo('You have no unread articles remaining.') return []
def _windowsSetMimeData(self, clip, mime): import win32clipboard, time from ctypes import windll grabbed = False for i in range(10): try: win32clipboard.OpenClipboard(self.editor.parentWindow.winId()) grabbed = True break except: time.sleep(0.01) continue if not grabbed: hwnd = win32clipboard.GetOpenClipboardWindow() longpid = ctypes.c_ulong() result = windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(longpid)) pid = longpid.value showCritical(_("Unable to access clipboard - locked by process %d" % pid)) return win32clipboard.CloseClipboard() clip.setMimeData(mime)
def test_executable(self) -> None: "Check to see if the TiddlyWiki executable provided can be called from Anki." # pylint: disable=no-member QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) try: args = [self.form.tiddlywikiBinary_.text(), "--version"] proc = subprocess.run(args, check=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, startupinfo=nowin_startupinfo()) except FileNotFoundError: QApplication.restoreOverrideCursor() showCritical( "It doesn't look like that file exists on your computer. " "Try using the full path to 'tiddlywiki'.") except subprocess.CalledProcessError as e: QApplication.restoreOverrideCursor() showCritical( f"It's not quite working yet. Try seeing if you can run TiddlyWiki " f"from the command line and copy your command in here.\n\n" f"{e.output}") except Exception: QApplication.restoreOverrideCursor() raise else: QApplication.restoreOverrideCursor() showInfo( f"Successfully called TiddlyWiki {proc.stdout.decode().strip()}! " f"You're all set.")
def onLoginFailed(self): showCritical('登录失败!') self.tabWidget.setCurrentIndex(1) self.progressBar.setValue(0) self.progressBar.setMaximum(1) self.mainTab.setEnabled(True) self.cookieLineEdit.clear()
def saveConfig(self): if not self.ui.deckComboBox.currentText(): self.log('deck为空') showCritical('Deck不能为空') return False elif not self.ui.username.text() or not self.ui.password.text(): self.log('用户名或密码为空') showCritical('未登录') self.ui.tabWidget.setCurrentIndex(1) return False index = self.ui.dictionaryComboBox.currentIndex() self._config['active'] = index self._config['dictionaries'][index][ 'username'] = self.ui.username.text() self._config['dictionaries'][index][ 'password'] = self.ui.password.text() self._config['deck'] = self.ui.deckComboBox.currentText() self._config['image'] = self.ui.imageCheckBox.isChecked() self._config['sample'] = self.ui.samplesCheckBox.isChecked() self._config['BrEPron'] = self.ui.BrEPronCheckBox.isChecked() self._config['AmEPron'] = self.ui.AmEPronCheckBox.isChecked() self._config['BrEPhonetic'] = self.ui.BrEPhoneticCheckBox.isChecked() self._config['AmEPhonetic'] = self.ui.AmEPhoneticCheckBox.isChecked() guardConfig = deepcopy(self._config) for i in range(len(guardConfig['dictionaries'])): guardConfig['dictionaries'][i]['username'] = '******' guardConfig['dictionaries'][i]['password'] = '******' guardConfig['dictionaries'][i]['cookie'] = '*******' self.log(f'保存配置项:\n{json.dumps(guardConfig, indent=4)}') mw.addonManager.writeConfig(__name__, self._config) return True
def handle_sync_finished(self) -> None: """Handle sync finished. In case of any error - show error message in manual mode and do nothing otherwise. If no error - save the collection and show sync statistics in manual mode. If `self._remove_obsolete_on_sync` is True - remove all notes that is not added or updated in current sync. """ assert self.notes_manager # mypy assert self.collection # mypy self._alive_workers -= 1 if self._alive_workers: return self.notion_menu.setTitle('Notion') # Show errors if manual sync if self._sync_errors: if not self._is_auto_sync: error_msg = '\n'.join(self._sync_errors) showCritical(error_msg, title='Loading from Notion failed') # If no errors - save collection and refresh Anki window else: if self._remove_obsolete_on_sync: ids_to_remove = self.existing_note_ids - self.synced_note_ids if ids_to_remove: msg = ( f'Will delete {len(ids_to_remove)} obsolete note(s), ' f'continue?') do_delete = QMessageBox.question( mw, 'Confirm deletion', msg, QMessageBox.Yes | QMessageBox.No, ) if do_delete == QMessageBox.Yes: self.notes_manager.remove_notes(ids_to_remove) self._deleted += len(ids_to_remove) self.collection.save(trx=False) mw.maybeReset() # type: ignore[union-attr] mw.deckBrowser.refresh() # type: ignore[union-attr] stats = (f'Processed: {self._processed}\n' f'Created: {self._created}\n' f'Updated: {self._updated}\n' f'Deleted: {self._deleted}') if not self._is_auto_sync: showInfo( f'Successfully loaded:\n{stats}', title='Loading from Notion', ) self.logger.info( 'Sync finished, processed=%s, created=%s, updated=%s, deleted=%s', self._processed, self._created, self._updated, self._deleted, ) self._reset_stats()
def onLoginFailed(self): showCritical('第一次登录或cookie失效!请重新登录') self.progressBar.setValue(0) self.progressBar.setMaximum(1) self.mainTab.setEnabled(True) self.loginDialog = LoginDialog( loginUrl=self.api.loginUrl, loginCheckCallbackFn=self.api.loginCheckCallbackFn, parent=self) self.loginDialog.loginSucceed.connect(self.onLoginSuccess) self.loginDialog.show()
def reload_config(self, new_config: Dict[str, Any]) -> None: """Reload configuration. :param new_config: new configuration """ try: self._validate_config(new_config) except ValidationError as exc: self.logger.error('Config update error', exc_info=exc) showCritical(str(exc), title='Notion loader config update error') else: self.config = new_config
def timesUp(): global death_toll death_toll -= 1 if not death_toll: showCritical("Death Toll Was Immeasurable...", title="Goodbye Cruel World!") if ANKI21: mw.unloadProfileAndExit() else: sys.exit() #freezes on A21 did = mw.col.decks.selected() conf = mw.col.decks.confForDid(did) hp_recover = conf.get('maxLife', 120) // 2 runHook('LifeDrain.recover', True, hp_recover)
def get_valid_config(self, config: Dict[str, Any]) -> Dict[str, Any]: """Get valid configuration. :param config: configuration :returns: either configuration provided (if it's valid) or default config """ try: self._validate_config(config) except ValidationError as exc: showCritical(str(exc), title='Notion loader config load error') assert mw # mypy default_config = mw.addonManager.addonConfigDefaults(str(BASE_DIR)) return cast(Dict[str, Any], default_config) else: return config
def getWKAvailableSubjectIds(type): url = WK_ASSIGNMENT_URL.format(type) retVal = set() while url: assignments = callWaniKani(url) if not assignments: showCritical( "No response from WaniKani, the deck is partially filled.") break retVal = retVal | set(x[u'data'][u'subject_id'] for x in assignments[u'data'] if x[u'data'][u'srs_stage'] > 0) url = assignments[u'pages'][u'next_url'] return retVal
def on_run_ocr(browser: Browser): selected_nids = browser.selectedNotes() num_notes = len(selected_nids) config = mw.addonManager.getConfig(__name__) if num_notes == 0: showInfo("No cards selected.") return elif askUser( f"Are you sure you wish to run OCR processing on {num_notes} notes?" ) is False: return if config.get("tesseract_install_valid") is not True and config.get( "text_output_location") == "new_field": showInfo( f"Note that because this addon changes the note template, you will see a warning about changing the database and uploading to AnkiWeb. \n" f"This is normal, and will be shown each time you modify a note template.\n" f"This message will be only be shown once.") mw.addonManager.writeConfig(__name__, config) config[ "tesseract_install_valid"] = True # Stop the above msg appearing multiple times progress = mw.progress ocr = OCR(col=mw.col, progress=progress, languages=config["languages"]) progress.start(immediate=True, min=0, max=num_notes) try: ocr.run_ocr_on_notes(note_ids=selected_nids, overwrite_existing=config["overwrite_existing"]) progress.finish() showInfo(f"Processed OCR for {num_notes} cards") except pytesseract.TesseractNotFoundError: progress.finish() showCritical( text=f"Could not find a valid Tesseract-OCR installation! \n" f"Please visit the addon page in at https://ankiweb.net/shared/info/450181164 for" f" install instructions") except Exception as errmsg: progress.finish() showCritical( f"Error encountered during processing, attempting to stop AnkiOCR gracefully. Error below:\n" f"{errmsg}") finally: browser.model.reset() mw.requireReset()
def updateWKKanjiDeck(): # select deck deckId = mw.col.decks.id(WK_CONF['WK Kanji Deck Name']) mw.col.decks.select(deckId) # anki defaults to the last note type used in the selected deck model = mw.col.models.byName(WK_CONF['WK Kanji Model Name']) deck = mw.col.decks.get(deckId) deck['mid'] = model['id'] mw.col.decks.save(deck) # set the last deck used by this note type -» puts this note in the last used deck model['did'] = deckId mw.col.models.save(model) NumONotes = len( mw.col.findNotes("'note:{}'".format(WK_CONF['WK Kanji Model Name']))) # let import the notes page by page # just the available levels # be careful the WK_KANJI_URL contains the type parameter so the levels is added to by & availableKanjiIds = getWKAvailableSubjectIds("kanji") url = WK_KANJI_URL + "&" + WKH_LEVELS_KEY + "=" + ",".join( str(x) for x in list(range(1, getWKMaxLevel() + 1))) while url: kanjiJson = callWaniKani(url) if not kanjiJson: showCritical( "No response from WaniKani, the deck is partially filled.") break wki = WKKanjiImporter(mw.col, kanjiJson) # ignore if first field matches existing note (0 update - default, 2 import) wki.importMode = 1 wki.availableIds = availableKanjiIds wki.initMapping() wki.run() url = kanjiJson[u'pages'][u'next_url'] mw.app.processEvents() mw.deckBrowser.show() showInfo( "WaniKani Kanji Syncroniser added {} new kanji(s) to Anki.".format( len( mw.col.findNotes("'note:{}'".format( WK_CONF['WK Kanji Model Name']))) - NumONotes))
def check_note_type(): mm = aqt.mw.col.models m = mm.byName(Textograph_MODEL_NAME) model_versioned_name = f"{Textograph_MODEL_NAME} v{TG_MODEL_VERSION['major']}" if not m: model_CreateOrRename(mm, model_versioned_name) check_note_type() else: try: v_major, v_minor = m['ver'].split(".") ver_dif = int(TG_MODEL_VERSION['major']) - int(v_major) if ver_dif == 0: if int(v_minor) < int(TG_MODEL_VERSION['minor']): # update current model t = m['tmpls'][0] m["css"] += template.get_css() t["qfmt"] = template.create_frontside() t["afmt"] = template.create_backside() m['ver'] = TG_STR_VERSION mm.save() else: # if there is another model with same version? # swap model names in this way: # Textograph --> Textograph v1, Textograph v2 ---> Textograph m['name'] = f"{Textograph_MODEL_NAME} v{v_major}" mm.save(m, updateReqs=False) model_CreateOrRename(mm, model_versioned_name) check_note_type( ) # call again to check for minor version compatibility or other problems if ver_dif > 0: showInfo( f"Textograph Note Type changed to version: {TG_STR_VERSION}" f", Previous version was {m['ver']}") else: showCritical( "Current Textograph model works with new Textograph Add-on version. " "So Please! update your addon, You can continue to review them but creating new cards " "from them is not possible. Although you can create new notes ant it works " "fine with them.") except (ValueError, NameError, KeyError): showCritical( "there was an error when updating note type. renaming these note types may solve the problem:" f"{Textograph_MODEL_NAME} or {model_versioned_name} ")
def get_audio(editor): if 'front' == default_text_source.lower(): source_text = editor.note.fields[0].lower() editor.web.eval("focusField(%d);" % 0) #set focus to field where media will be added else: source_text = editor.note.fields[1].lower() editor.web.eval("focusField(%d);" % 1) #set focus to field where media will be added source_text = source_text.replace(' ', ' ') sot = SoundOfText() result = sot.order_conversion(source_text, default_audio_language) if result['success']: editor.mw.progress.start(label=_("Downloading..."), immediate=True) audio_id = result['id'] audio_url = None try: audio_url = sot.get_audio_url(audio_id) except Exception as e: editor.mw.progress.finish() showCritical(str(e), title='Text2Audio download error') else: #download MP3 file download_result = sot.download_file(audio_url) #rename file before copy old_file_name = download_result[0] media_folder = os.path.join(editor.mw.pm.profileFolder(), "collection.media") new_file_name = create_new_file_name(old_file_name, source_text, media_folder) if new_file_name: os.rename(old_file_name, new_file_name) #save to media folder, set audio in note and remove original file editor.addMedia(new_file_name, canDelete=True) else: #remove downloaded file remove_uploaded_file(old_file_name) editor.mw.progress.finish() else: showCritical(result['message'], title='Text2Audio download error')
def importWebpage(self, url=None, priority=None, silent=False): if not url: url, accepted = getText('Enter URL:', title='Import Webpage') else: accepted = True if not url or not accepted: return if not urlsplit(url).scheme: url = 'http://' + url elif urlsplit(url).scheme not in ['http', 'https']: showCritical('Only HTTP requests are supported.') return try: webpage = self._fetchWebpage(url) except HTTPError as error: showWarning( 'The remote server has returned an error: ' 'HTTP Error {} ({})'.format(error.code, error.reason) ) return except ConnectionError as error: showWarning('There was a problem connecting to the website.') return body = '\n'.join(map(str, webpage.find('body').children)) source = self.settings['sourceFormat'].format( date=date.today(), url='<a href="%s">%s</a>' % (url, url) ) if self.settings['prioEnabled'] and not priority: priority = self._getPriority(webpage.title.string) deck = self._createNote(webpage.title.string, body, source, priority) if not silent: tooltip('Added to deck: {}'.format(deck)) return deck
def eventFilter(self, obj, evt): if not(evt.type() == QEvent.Paint and self.save_png): return super().eventFilter(obj, evt) filename, oldsize = self.save_png self.save_png = () size = self._page.contentsSize().toSize() image = QImage(size, QImage.Format_ARGB32) painter = QPainter(image) self.render(painter) painter.end() success = image.save(filename, "png") self.resize(oldsize) mw.progress.finish() if success: showInfo("Image saved to %s!" % os.path.abspath(filename)) else: showCritical("Failed to save the image.") return super().eventFilter(obj, evt)
def eventFilter(self, obj, evt): if not (evt.type() == QEvent.Paint and self.save_png): return super().eventFilter(obj, evt) filename, oldsize = self.save_png self.save_png = () size = self._page.contentsSize().toSize() image = QImage(size, QImage.Format_ARGB32) painter = QPainter(image) self.render(painter) painter.end() success = image.save(filename, "png") self.resize(oldsize) mw.progress.finish() if success: showInfo("Image saved to %s!" % os.path.abspath(filename)) else: showCritical("Failed to save the image.") return super().eventFilter(obj, evt)
def addCards(self): self.editor.saveNow() # grab data note = self.editor.note # sanity check if note.dupeOrEmpty(): showCritical("Note is a dupe or empty; not adding.") return # check for cloze sanity in case of potential cloze-y notes if len(note.fields) == 2: # find the highest existing cloze highest = note.highestCloze() if highest > 0 and not note.isCloze(): # clozes used, but wrong model, so switch it to cloze self.editor.changeToModel("Cloze") note = self.editor.note elif note.isCloze() and highest == 0: # no clozes, switch to basic self.editor.changeToModel("Basic") note = self.editor.note # send data to TCP server in Anki data = { "model": note.model["name"], "deck": self.deckChooser.currentDeck(), "fields": note.fields, "tags": note.tags, } ret = sendToAnki("addNote", data) if ret == True: self.mw.reset() # stop anything playing clearAudioQueue() self.onReset(keep=True) else: showCritical("Failed to add card. Is Anki ok?")
def importWebpage(self, url=None, priority=None, silent=False): if not url: url, accepted = getText('Enter URL:', title='Import Webpage') else: accepted = True if not url or not accepted: return if not urlsplit(url).scheme: url = 'http://' + url elif urlsplit(url).scheme not in ['http', 'https']: showCritical('Only HTTP requests are supported.') return try: webpage = self._goose(url) body = re.sub("(.+)", r"<p>\1</p>", webpage.cleaned_text) title = webpage.title except Exception as e: showWarning(f'Failed to goose {e}. Falling back to normal processing') return source = self.settings['sourceFormat'].format( date=date.today(), url='<a href="%s">%s</a>' % (url, url) ) if self.settings['prioEnabled'] and not priority: priority = self._getPriority(title) deck = self._createNote(title, body, source, priority) if not silent: tooltip('Added to deck: {}'.format(deck)) return deck
def errorMsg( msg ): showCritical( msg ) printf( msg )
def errorMsg(msg): showCritical(msg) printf(msg)
def on_run_ocr(browser: Browser): time_start = time.time() selected_nids = browser.selectedNotes() config = mw.addonManager.getConfig(__name__) num_notes = len(selected_nids) num_batches = ceil(num_notes / config["batch_size"]) if num_notes == 0: showInfo("No cards selected.") return elif askUser(f"Are you sure you wish to run OCR processing on {num_notes} notes?") is False: return if config.get("tesseract_install_valid") is not True and config.get("text_output_location") == "new_field": showInfo( f"Note that because this addon changes the note template, you will see a warning about changing the " f"database and uploading to AnkiWeb. \n " f"This is normal, and will be shown each time you modify a note template.\n" f"This message will be only be shown once.") config["tesseract_install_valid"] = True # Stop the above msg appearing multiple times mw.addonManager.writeConfig(__name__, config) try: progress = mw.progress progress.start(immediate=True, min=0, max=num_batches) progress.update(value=0, max=num_batches, label="Starting OCR processing...") except TypeError: # old version of Qt/Anki progress = None ocr = OCR(col=mw.col, progress=progress, languages=config["languages"], text_output_location=config["text_output_location"], tesseract_exec_pth=config["tesseract_exec_path"] if config["override_tesseract_exec"] else None, batch_size=config["batch_size"], num_threads=config["num_threads"], use_batching=config["use_batching"], use_multithreading=config["use_multithreading"]) try: ocr.run_ocr_on_notes(note_ids=selected_nids) if progress: progress.finish() time_taken = time.time() - time_start log_messages = logger.handlers[0].flush() showInfo( f"Processed OCR for {num_notes} notes in {round(time_taken, 1)}s ({round(time_taken / num_notes, 1)}s per note)\n" f"{log_messages}") except pytesseract.TesseractNotFoundError: if progress: progress.finish() showCritical(text=f"Could not find a valid Tesseract-OCR installation! \n" f"Please visit the addon page in at https://ankiweb.net/shared/info/450181164 for" f" install instructions") except (RuntimeError, Exception) as exc: from . import __version__ as anki_ocr_version from anki.buildinfo import version as anki_version import sys import platform if progress: progress.finish() msg = f"Error encountered during processing. Debug info: \n" \ f"Anki Version: {anki_version} , AnkiOCR Version: {anki_ocr_version}\n" \ f"Platform: {platform.system()} , Python Version: {sys.version}" log_messages = logger.handlers[0].flush() if len(log_messages) > 0: msg += f"Logging message generated during processing:\n{log_messages}" exception_str: List[str] = traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__) msg += "".join(exception_str) showInfo(msg) finally: browser.model.reset() mw.requireReset()
def create_new_cloze(reviewer, the_card, ease): if ease != 1 and \ len(the_card.sub_questions) != 0 and \ len(the_card.sub_answers) != 0: m = the_card.model() try: v_major, v_minor = m['ver'].split(".") ver_dif = int(TG_MODEL_VERSION['major']) - int(v_major) if ver_dif < 0: showCritical("""this note works with newer Textograph Version. so we can not create new Card.update Textograph Add-on first then try again """) return except (ValueError, NameError, KeyError): showCritical( "there was an error with this note type. if it happens with newly created notes " "please contact add-on author") cur_shown_leafs = "" cloz_fld_name = TG_FIELDS['cloze'] txt_cloze_field = the_card.note()[cloz_fld_name] match_str = r"(sub_answer\[" + the_card.cloze_id + r"\]\s=\s\[(.*?))\];" match = re.search(match_str, txt_cloze_field, re.MULTILINE | re.DOTALL) if match: cur_shown_leafs = match.group(2) sub_cloze = match.group(1) else: sub_cloze = "sub_answer[1] = [" txt_cloze_field = f"""<script id="main"> {sub_cloze}]; </script> {{{{c1::<script>delete sub_answer[1]</script>}}}}""" # remove difficult leafs from current card # sub_question is a temp card_obj property that introduced by me for saving interactions arr_index, clz_index = get_indices(txt_cloze_field) # change this card repl_str = sub_cloze + ", ".join(the_card.sub_questions) + "," txt_cloze_field = txt_cloze_field.replace(sub_cloze, repl_str) # add new card match_str = r"<script id=\"main\">(.*?)<\/script>" repl_str = r'<script id="main">\1sub_answer[{id}] = [{values},];\n</script>' repl_str = repl_str.format(id=arr_index, values=cur_shown_leafs + ", ".join(the_card.sub_answers)) new_cloze = "\n{{{{c{id}::<script>delete sub_answer[{id}]</script>}}}}" txt_cloze_field = "{0}{1}".format( re.sub(match_str, repl_str, txt_cloze_field, count=0, flags=re.MULTILINE | re.DOTALL), new_cloze.format(id=clz_index)) the_card.note()[cloz_fld_name] = txt_cloze_field the_card.note().flush()