diff --git a/plugin/__init__.py b/plugin/__init__.py index 3c602ac..e4fea20 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -13,41 +13,47 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import base64 -import glob -import hashlib -import inspect -import json -import os -import os.path -import random -import re -import string -import time -import unicodedata +from . import api +from . import host +from . import settings -from PyQt5 import QtCore -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QMessageBox +# import api.gui -import anki -import anki.exporting -import anki.storage -import aqt -from anki.cards import Card -from anki.consts import MODEL_CLOZE +# import base64 +# import glob +# import hashlib +# import inspect +# import json +# import os +# import os.path +# import random +# import re +# import string +# import time +# import unicodedata +# +# from PyQt5 import QtCore +# from PyQt5.QtCore import QTimer +# from PyQt5.QtWidgets import QMessageBox +# +# import anki +# import anki.exporting +# import anki.storage +# import aqt +# from anki.cards import Card +# from anki.consts import MODEL_CLOZE +# +# from anki.exporting import AnkiPackageExporter +# from anki.importing import AnkiPackageImporter +# from anki.notes import Note +# # from anki.utils import joinFields, intTime, guid64, fieldChecksum +# +# try: +# from anki.rsbackend import NotFoundError +# except: +# NotFoundError = Exception -from anki.exporting import AnkiPackageExporter -from anki.importing import AnkiPackageImporter -from anki.notes import Note -from anki.utils import joinFields, intTime, guid64, fieldChecksum - -try: - from anki.rsbackend import NotFoundError -except: - NotFoundError = Exception - -from . import web, util +# from . import web, util # @@ -55,1595 +61,1605 @@ from . import web, util # class AnkiConnect: - def __init__(self): - try: - self.server = web.WebServer(self.handler) - self.server.listen() - - self.timer = QTimer() - self.timer.timeout.connect(self.advance) - self.timer.start(util.setting('apiPollInterval')) - except: - QMessageBox.critical( - self.window(), - 'AnkiConnect', - 'Failed to listen on port {}.\nMake sure it is available and is not in use.'.format(util.setting('webBindPort')) - ) - - - def advance(self): - self.server.advance() - - - def handler(self, request): - name = request.get('action', '') - version = request.get('version', 4) - params = request.get('params', {}) - key = request.get('key') - reply = {'result': None, 'error': None} - - try: - if key != util.setting('apiKey') and name != 'requestPermission': - raise Exception('valid api key must be provided') - - method = None - - for methodName, methodInst in inspect.getmembers(self, predicate=inspect.ismethod): - apiVersionLast = 0 - apiNameLast = None - - if getattr(methodInst, 'api', False): - for apiVersion, apiName in getattr(methodInst, 'versions', []): - if apiVersionLast < apiVersion <= version: - apiVersionLast = apiVersion - apiNameLast = apiName - - if apiNameLast is None and apiVersionLast == 0: - apiNameLast = methodName - - if apiNameLast is not None and apiNameLast == name: - method = methodInst - break - - if method is None: - raise Exception('unsupported action') - else: - reply['result'] = methodInst(**params) - - if version <= 4: - reply = reply['result'] - - except Exception as e: - reply['error'] = str(e) - - return reply - - - def window(self): - return aqt.mw - - - def reviewer(self): - reviewer = self.window().reviewer - if reviewer is None: - raise Exception('reviewer is not available') - - return reviewer - - - def collection(self): - collection = self.window().col - if collection is None: - raise Exception('collection is not available') - - return collection - - - def decks(self): - decks = self.collection().decks - if decks is None: - raise Exception('decks are not available') - - return decks - - - def scheduler(self): - scheduler = self.collection().sched - if scheduler is None: - raise Exception('scheduler is not available') - - return scheduler - - - def database(self): - database = self.collection().db - if database is None: - raise Exception('database is not available') - - return database - - - def media(self): - media = self.collection().media - if media is None: - raise Exception('media is not available') - - return media - - - def startEditing(self): - self.window().requireReset() - - - def stopEditing(self): - if self.collection() is not None: - self.window().maybeReset() - - - def createNote(self, note): - collection = self.collection() - - model = collection.models.byName(note['modelName']) - if model is None: - raise Exception('model was not found: {}'.format(note['modelName'])) - - deck = collection.decks.byName(note['deckName']) - if deck is None: - raise Exception('deck was not found: {}'.format(note['deckName'])) - - ankiNote = anki.notes.Note(collection, model) - ankiNote.model()['did'] = deck['id'] - if 'tags' in note: - ankiNote.tags = note['tags'] - - for name, value in note['fields'].items(): - for ankiName in ankiNote.keys(): - if name.lower() == ankiName.lower(): - ankiNote[ankiName] = value - break - - allowDuplicate = False - duplicateScope = None - duplicateScopeDeckName = None - duplicateScopeCheckChildren = False - duplicateScopeCheckAllModels = False - - if 'options' in note: - options = note['options'] - if 'allowDuplicate' in options: - allowDuplicate = options['allowDuplicate'] - if type(allowDuplicate) is not bool: - raise Exception('option parameter "allowDuplicate" must be boolean') - if 'duplicateScope' in options: - duplicateScope = options['duplicateScope'] - if 'duplicateScopeOptions' in options: - duplicateScopeOptions = options['duplicateScopeOptions'] - if 'deckName' in duplicateScopeOptions: - duplicateScopeDeckName = duplicateScopeOptions['deckName'] - if 'checkChildren' in duplicateScopeOptions: - duplicateScopeCheckChildren = duplicateScopeOptions['checkChildren'] - if type(duplicateScopeCheckChildren) is not bool: - raise Exception('option parameter "duplicateScopeOptions.checkChildren" must be boolean') - if 'checkAllModels' in duplicateScopeOptions: - duplicateScopeCheckAllModels = duplicateScopeOptions['checkAllModels'] - if type(duplicateScopeCheckAllModels) is not bool: - raise Exception('option parameter "duplicateScopeOptions.checkAllModels" must be boolean') - - duplicateOrEmpty = self.isNoteDuplicateOrEmptyInScope( - ankiNote, - deck, - collection, - duplicateScope, - duplicateScopeDeckName, - duplicateScopeCheckChildren, - duplicateScopeCheckAllModels - ) - - if duplicateOrEmpty == 1: - raise Exception('cannot create note because it is empty') - elif duplicateOrEmpty == 2: - if allowDuplicate: - return ankiNote - raise Exception('cannot create note because it is a duplicate') - elif duplicateOrEmpty == 0: - return ankiNote - else: - raise Exception('cannot create note for unknown reason') - - - def isNoteDuplicateOrEmptyInScope( - self, - note, - deck, - collection, - duplicateScope, - duplicateScopeDeckName, - duplicateScopeCheckChildren, - duplicateScopeCheckAllModels - ): - # Returns: 1 if first is empty, 2 if first is a duplicate, 0 otherwise. - - # note.dupeOrEmpty returns if a note is a global duplicate with the specific model. - # This is used as the default check, and the rest of this function is manually - # checking if the note is a duplicate with additional options. - if duplicateScope != 'deck' and not duplicateScopeCheckAllModels: - return note.dupeOrEmpty() or 0 - - # Primary field for uniqueness - val = note.fields[0] - if not val.strip(): - return 1 - csum = anki.utils.fieldChecksum(val) - - # Create dictionary of deck ids - dids = None - if duplicateScope == 'deck': - did = deck['id'] - if duplicateScopeDeckName is not None: - deck2 = collection.decks.byName(duplicateScopeDeckName) - if deck2 is None: - # Invalid deck, so cannot be duplicate - return 0 - did = deck2['id'] - - dids = {did: True} - if duplicateScopeCheckChildren: - for kv in collection.decks.children(did): - dids[kv[1]] = True - - # Build query - query = 'select id from notes where csum=?' - queryArgs = [csum] - if note.id: - query += ' and id!=?' - queryArgs.append(note.id) - if not duplicateScopeCheckAllModels: - query += ' and mid=?' - queryArgs.append(note.mid) - - # Search - for noteId in note.col.db.list(query, *queryArgs): - if dids is None: - # Duplicate note exists in the collection - return 2 - # Validate that a card exists in one of the specified decks - for cardDeckId in note.col.db.list('select did from cards where nid=?', noteId): - if cardDeckId in dids: - return 2 - - # Not a duplicate - return 0 - - def getCard(self, card_id: int) -> Card: - try: - return self.collection().getCard(card_id) - except NotFoundError: - raise NotFoundError('Card was not found: {}'.format(card_id)) - - def getNote(self, note_id: int) -> Note: - try: - return self.collection().getNote(note_id) - except NotFoundError: - raise NotFoundError('Note was not found: {}'.format(note_id)) - + pass + # def __init__(self): + # try: + # self.server = web.WebServer(self.handler) + # self.server.listen() # - # Miscellaneous + # self.timer = QTimer() + # self.timer.timeout.connect(self.advance) + # self.timer.start(util.setting('apiPollInterval')) + # except: + # QMessageBox.critical( + # self.window(), + # 'AnkiConnect', + # 'Failed to listen on port {}.\nMake sure it is available and is not in use.'.format(util.setting('webBindPort')) + # ) # - - @util.api() - def version(self): - return util.setting('apiVersion') - - @util.api() - def requestPermission(self, origin, allowed): - if allowed: - return { - "permission": "granted", - "requireApikey": bool(util.setting('apiKey')), - "version": util.setting('apiVersion') - } - - if origin in util.setting('ignoreOriginList') : - return { - "permission": "denied", - } - - msg = QMessageBox(None) - msg.setWindowTitle("A website want to access to Anki") - msg.setText(origin + " request permission to use Anki through AnkiConnect.\nDo you want to give it access ?") - msg.setInformativeText("By giving permission, the website will be able to do actions on anki, including destructives actions like deck deletion.") - msg.setWindowIcon(self.window().windowIcon()) - msg.setIcon(QMessageBox.Question) - msg.setStandardButtons(QMessageBox.Yes|QMessageBox.Ignore|QMessageBox.No) - msg.setDefaultButton(QMessageBox.No) - msg.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) - pressedButton = msg.exec_() - - if pressedButton == QMessageBox.Yes: - config = aqt.mw.addonManager.getConfig(__name__) - config["webCorsOriginList"] = util.setting('webCorsOriginList') - config["webCorsOriginList"].append(origin) - aqt.mw.addonManager.writeConfig(__name__, config) - - if pressedButton == QMessageBox.Ignore: - config = aqt.mw.addonManager.getConfig(__name__) - config["ignoreOriginList"] = util.setting('ignoreOriginList') - config["ignoreOriginList"].append(origin) - aqt.mw.addonManager.writeConfig(__name__, config) - - if pressedButton == QMessageBox.Yes: - results = { - "permission": "granted", - "requireApikey": bool(util.setting('apiKey')), - "version": util.setting('apiVersion') - } - else : - results = { - "permission": "denied", - } - return results - - - @util.api() - def getProfiles(self): - return self.window().pm.profiles() - - - @util.api() - def loadProfile(self, name): - if name not in self.window().pm.profiles(): - return False - - if self.window().isVisible(): - cur_profile = self.window().pm.name - if cur_profile != name: - self.window().unloadProfileAndShowProfileManager() - - def waiter(): - # This function waits until main window is closed - # It's needed cause sync can take quite some time - # And if we call loadProfile until sync is ended things will go wrong - if self.window().isVisible(): - QTimer.singleShot(1000, waiter) - else: - self.loadProfile(name) - - waiter() - else: - self.window().pm.load(name) - self.window().loadProfile() - self.window().profileDiag.closeWithoutQuitting() - - return True - - - @util.api() - def sync(self): - self.window().onSync() - - - @util.api() - def multi(self, actions): - return list(map(self.handler, actions)) - - - @util.api() - def getNumCardsReviewedToday(self): - return self.database().scalar('select count() from revlog where id > ?', (self.scheduler().dayCutoff - 86400) * 1000) - - @util.api() - def getNumCardsReviewedByDay(self): - return self.database().all('select date(id/1000 - ?, "unixepoch", "localtime") as day, count() from revlog group by day order by day desc', - int(time.strftime("%H", time.localtime(self.scheduler().dayCutoff))) * 3600) - - - @util.api() - def getCollectionStatsHTML(self, wholeCollection=True): - stats = self.collection().stats() - stats.wholeCollection = wholeCollection - return stats.report() - - # - # Decks + # def advance(self): + # self.server.advance() # + # + # def handler(self, request): + # name = request.get('action', '') + # version = request.get('version', 4) + # params = request.get('params', {}) + # key = request.get('key') + # reply = {'result': None, 'error': None} + # + # try: + # if key != util.setting('apiKey') and name != 'requestPermission': + # raise Exception('valid api key must be provided') + # + # method = None + # + # for methodName, methodInst in inspect.getmembers(self, predicate=inspect.ismethod): + # apiVersionLast = 0 + # apiNameLast = None + # + # if getattr(methodInst, 'api', False): + # for apiVersion, apiName in getattr(methodInst, 'versions', []): + # if apiVersionLast < apiVersion <= version: + # apiVersionLast = apiVersion + # apiNameLast = apiName + # + # if apiNameLast is None and apiVersionLast == 0: + # apiNameLast = methodName + # + # if apiNameLast is not None and apiNameLast == name: + # method = methodInst + # break + # + # if method is None: + # raise Exception('unsupported action') + # else: + # reply['result'] = methodInst(**params) + # + # if version <= 4: + # reply = reply['result'] + # + # except Exception as e: + # reply['error'] = str(e) + # + # return reply + # + # + # def window(self): + # return aqt.mw + # + # + # def reviewer(self): + # reviewer = self.window().reviewer + # if reviewer is None: + # raise Exception('reviewer is not available') + # + # return reviewer + # + # + # def collection(self): + # collection = self.window().col + # if collection is None: + # raise Exception('collection is not available') + # + # return collection + # + # + # def decks(self): + # decks = self.collection().decks + # if decks is None: + # raise Exception('decks are not available') + # + # return decks + # + # + # def scheduler(self): + # scheduler = self.collection().sched + # if scheduler is None: + # raise Exception('scheduler is not available') + # + # return scheduler + # + # + # def database(self): + # database = self.collection().db + # if database is None: + # raise Exception('database is not available') + # + # return database + # + # + # def media(self): + # media = self.collection().media + # if media is None: + # raise Exception('media is not available') + # + # return media + # + # + # def startEditing(self): + # self.window().requireReset() + # + # + # def stopEditing(self): + # if self.collection() is not None: + # self.window().maybeReset() + # + # + # def createNote(self, note): + # collection = self.collection() + # + # model = collection.models.byName(note['modelName']) + # if model is None: + # raise Exception('model was not found: {}'.format(note['modelName'])) + # + # deck = collection.decks.byName(note['deckName']) + # if deck is None: + # raise Exception('deck was not found: {}'.format(note['deckName'])) + # + # ankiNote = anki.notes.Note(collection, model) + # ankiNote.model()['did'] = deck['id'] + # if 'tags' in note: + # ankiNote.tags = note['tags'] + # + # for name, value in note['fields'].items(): + # for ankiName in ankiNote.keys(): + # if name.lower() == ankiName.lower(): + # ankiNote[ankiName] = value + # break + # + # allowDuplicate = False + # duplicateScope = None + # duplicateScopeDeckName = None + # duplicateScopeCheckChildren = False + # duplicateScopeCheckAllModels = False + # + # if 'options' in note: + # options = note['options'] + # if 'allowDuplicate' in options: + # allowDuplicate = options['allowDuplicate'] + # if type(allowDuplicate) is not bool: + # raise Exception('option parameter "allowDuplicate" must be boolean') + # if 'duplicateScope' in options: + # duplicateScope = options['duplicateScope'] + # if 'duplicateScopeOptions' in options: + # duplicateScopeOptions = options['duplicateScopeOptions'] + # if 'deckName' in duplicateScopeOptions: + # duplicateScopeDeckName = duplicateScopeOptions['deckName'] + # if 'checkChildren' in duplicateScopeOptions: + # duplicateScopeCheckChildren = duplicateScopeOptions['checkChildren'] + # if type(duplicateScopeCheckChildren) is not bool: + # raise Exception('option parameter "duplicateScopeOptions.checkChildren" must be boolean') + # if 'checkAllModels' in duplicateScopeOptions: + # duplicateScopeCheckAllModels = duplicateScopeOptions['checkAllModels'] + # if type(duplicateScopeCheckAllModels) is not bool: + # raise Exception('option parameter "duplicateScopeOptions.checkAllModels" must be boolean') + # + # duplicateOrEmpty = self.isNoteDuplicateOrEmptyInScope( + # ankiNote, + # deck, + # collection, + # duplicateScope, + # duplicateScopeDeckName, + # duplicateScopeCheckChildren, + # duplicateScopeCheckAllModels + # ) + # + # if duplicateOrEmpty == 1: + # raise Exception('cannot create note because it is empty') + # elif duplicateOrEmpty == 2: + # if allowDuplicate: + # return ankiNote + # raise Exception('cannot create note because it is a duplicate') + # elif duplicateOrEmpty == 0: + # return ankiNote + # else: + # raise Exception('cannot create note for unknown reason') + # + # + # def isNoteDuplicateOrEmptyInScope( + # self, + # note, + # deck, + # collection, + # duplicateScope, + # duplicateScopeDeckName, + # duplicateScopeCheckChildren, + # duplicateScopeCheckAllModels + # ): + # # Returns: 1 if first is empty, 2 if first is a duplicate, 0 otherwise. + # + # # note.dupeOrEmpty returns if a note is a global duplicate with the specific model. + # # This is used as the default check, and the rest of this function is manually + # # checking if the note is a duplicate with additional options. + # if duplicateScope != 'deck' and not duplicateScopeCheckAllModels: + # return note.dupeOrEmpty() or 0 + # + # # Primary field for uniqueness + # val = note.fields[0] + # if not val.strip(): + # return 1 + # csum = anki.utils.fieldChecksum(val) + # + # # Create dictionary of deck ids + # dids = None + # if duplicateScope == 'deck': + # did = deck['id'] + # if duplicateScopeDeckName is not None: + # deck2 = collection.decks.byName(duplicateScopeDeckName) + # if deck2 is None: + # # Invalid deck, so cannot be duplicate + # return 0 + # did = deck2['id'] + # + # dids = {did: True} + # if duplicateScopeCheckChildren: + # for kv in collection.decks.children(did): + # dids[kv[1]] = True + # + # # Build query + # query = 'select id from notes where csum=?' + # queryArgs = [csum] + # if note.id: + # query += ' and id!=?' + # queryArgs.append(note.id) + # if not duplicateScopeCheckAllModels: + # query += ' and mid=?' + # queryArgs.append(note.mid) + # + # # Search + # for noteId in note.col.db.list(query, *queryArgs): + # if dids is None: + # # Duplicate note exists in the collection + # return 2 + # # Validate that a card exists in one of the specified decks + # for cardDeckId in note.col.db.list('select did from cards where nid=?', noteId): + # if cardDeckId in dids: + # return 2 + # + # # Not a duplicate + # return 0 + # + # def getCard(self, card_id: int) -> Card: + # try: + # return self.collection().getCard(card_id) + # except NotFoundError: + # raise NotFoundError('Card was not found: {}'.format(card_id)) + # + # def getNote(self, note_id: int) -> Note: + # try: + # return self.collection().getNote(note_id) + # except NotFoundError: + # raise NotFoundError('Note was not found: {}'.format(note_id)) + # + # # + # # Miscellaneous + # # + # + # @util.api() + # def version(self): + # return util.setting('apiVersion') + # + # @util.api() + # def requestPermission(self, origin, allowed): + # if allowed: + # return { + # "permission": "granted", + # "requireApikey": bool(util.setting('apiKey')), + # "version": util.setting('apiVersion') + # } + # + # if origin in util.setting('ignoreOriginList') : + # return { + # "permission": "denied", + # } + # + # msg = QMessageBox(None) + # msg.setWindowTitle("A website want to access to Anki") + # msg.setText(origin + " request permission to use Anki through AnkiConnect.\nDo you want to give it access ?") + # msg.setInformativeText("By giving permission, the website will be able to do actions on anki, including destructives actions like deck deletion.") + # msg.setWindowIcon(self.window().windowIcon()) + # msg.setIcon(QMessageBox.Question) + # msg.setStandardButtons(QMessageBox.Yes|QMessageBox.Ignore|QMessageBox.No) + # msg.setDefaultButton(QMessageBox.No) + # msg.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + # pressedButton = msg.exec_() + # + # if pressedButton == QMessageBox.Yes: + # config = aqt.mw.addonManager.getConfig(__name__) + # config["webCorsOriginList"] = util.setting('webCorsOriginList') + # config["webCorsOriginList"].append(origin) + # aqt.mw.addonManager.writeConfig(__name__, config) + # + # if pressedButton == QMessageBox.Ignore: + # config = aqt.mw.addonManager.getConfig(__name__) + # config["ignoreOriginList"] = util.setting('ignoreOriginList') + # config["ignoreOriginList"].append(origin) + # aqt.mw.addonManager.writeConfig(__name__, config) + # + # if pressedButton == QMessageBox.Yes: + # results = { + # "permission": "granted", + # "requireApikey": bool(util.setting('apiKey')), + # "version": util.setting('apiVersion') + # } + # else : + # results = { + # "permission": "denied", + # } + # return results + # + # + # @util.api() + # def getProfiles(self): + # return self.window().pm.profiles() + # + # + # @util.api() + # def loadProfile(self, name): + # if name not in self.window().pm.profiles(): + # return False + # + # if self.window().isVisible(): + # cur_profile = self.window().pm.name + # if cur_profile != name: + # self.window().unloadProfileAndShowProfileManager() + # + # def waiter(): + # # This function waits until main window is closed + # # It's needed cause sync can take quite some time + # # And if we call loadProfile until sync is ended things will go wrong + # if self.window().isVisible(): + # QTimer.singleShot(1000, waiter) + # else: + # self.loadProfile(name) + # + # waiter() + # else: + # self.window().pm.load(name) + # self.window().loadProfile() + # self.window().profileDiag.closeWithoutQuitting() + # + # return True + # + # + # @util.api() + # def sync(self): + # self.window().onSync() + # + # + # @util.api() + # def multi(self, actions): + # return list(map(self.handler, actions)) + # + # + # @util.api() + # def getNumCardsReviewedToday(self): + # return self.database().scalar('select count() from revlog where id > ?', (self.scheduler().dayCutoff - 86400) * 1000) + # + # @util.api() + # def getNumCardsReviewedByDay(self): + # return self.database().all('select date(id/1000 - ?, "unixepoch", "localtime") as day, count() from revlog group by day order by day desc', + # int(time.strftime("%H", time.localtime(self.scheduler().dayCutoff))) * 3600) + # + # + # @util.api() + # def getCollectionStatsHTML(self, wholeCollection=True): + # stats = self.collection().stats() + # stats.wholeCollection = wholeCollection + # return stats.report() + # + # + # # + # # Decks + # # + # + # @util.api() + # def deckNames(self): + # return self.decks().allNames() + # + # + # @util.api() + # def deckNamesAndIds(self): + # decks = {} + # for deck in self.deckNames(): + # decks[deck] = self.decks().id(deck) + # + # return decks + # + # + # @util.api() + # def getDecks(self, cards): + # decks = {} + # for card in cards: + # did = self.database().scalar('select did from cards where id=?', card) + # deck = self.decks().get(did)['name'] + # if deck in decks: + # decks[deck].append(card) + # else: + # decks[deck] = [card] + # + # return decks + # + # + # @util.api() + # def createDeck(self, deck): + # try: + # self.startEditing() + # did = self.decks().id(deck) + # finally: + # self.stopEditing() + # + # return did + # + # + # @util.api() + # def changeDeck(self, cards, deck): + # self.startEditing() + # + # did = self.collection().decks.id(deck) + # mod = anki.utils.intTime() + # usn = self.collection().usn() + # + # # normal cards + # scids = anki.utils.ids2str(cards) + # # remove any cards from filtered deck first + # self.collection().sched.remFromDyn(cards) + # + # # then move into new deck + # self.collection().db.execute('update cards set usn=?, mod=?, did=? where id in ' + scids, usn, mod, did) + # self.stopEditing() + # + # + # @util.api() + # def deleteDecks(self, decks, cardsToo=False): + # try: + # self.startEditing() + # decks = filter(lambda d: d in self.deckNames(), decks) + # for deck in decks: + # did = self.decks().id(deck) + # self.decks().rem(did, cardsToo) + # finally: + # self.stopEditing() + # + # + # @util.api() + # def getDeckConfig(self, deck): + # if deck not in self.deckNames(): + # return False + # + # collection = self.collection() + # did = collection.decks.id(deck) + # return collection.decks.confForDid(did) + # + # + # @util.api() + # def saveDeckConfig(self, config): + # collection = self.collection() + # + # config['id'] = str(config['id']) + # config['mod'] = anki.utils.intTime() + # config['usn'] = collection.usn() + # if int(config['id']) not in [c['id'] for c in collection.decks.all_config()]: + # return False + # try: + # collection.decks.save(config) + # collection.decks.updateConf(config) + # except: + # return False + # return True + # + # + # @util.api() + # def setDeckConfigId(self, decks, configId): + # configId = int(configId) + # for deck in decks: + # if not deck in self.deckNames(): + # return False + # + # collection = self.collection() + # + # for deck in decks: + # try: + # did = str(collection.decks.id(deck)) + # deck_dict = aqt.mw.col.decks.decks[did] + # deck_dict['conf'] = configId + # collection.decks.save(deck_dict) + # except: + # return False + # + # return True + # + # + # @util.api() + # def cloneDeckConfigId(self, name, cloneFrom='1'): + # configId = int(cloneFrom) + # collection = self.collection() + # if configId not in [c['id'] for c in collection.decks.all_config()]: + # return False + # + # config = collection.decks.getConf(configId) + # return collection.decks.confId(name, config) + # + # + # @util.api() + # def removeDeckConfigId(self, configId): + # collection = self.collection() + # if int(configId) not in [c['id'] for c in collection.decks.all_config()]: + # return False + # + # collection.decks.remConf(configId) + # return True + # + # + # @util.api() + # def storeMediaFile(self, filename, data=None, path=None, url=None, skipHash=None, deleteExisting=True): + # if not (data or path or url): + # raise Exception('You must provide a "data", "path", or "url" field.') + # if deleteExisting: + # self.deleteMediaFile(filename) + # if data: + # mediaData = base64.b64decode(data) + # elif path: + # with open(path, 'rb') as f: + # mediaData = f.read() + # elif url: + # mediaData = util.download(url) + # + # if skipHash is None: + # skip = False + # else: + # m = hashlib.md5() + # m.update(mediaData) + # skip = skipHash == m.hexdigest() + # + # if skip: + # return None + # return self.media().writeData(filename, mediaData) + # + # + # @util.api() + # def retrieveMediaFile(self, filename): + # filename = os.path.basename(filename) + # filename = unicodedata.normalize('NFC', filename) + # filename = self.media().stripIllegal(filename) + # + # path = os.path.join(self.media().dir(), filename) + # if os.path.exists(path): + # with open(path, 'rb') as file: + # return base64.b64encode(file.read()).decode('ascii') + # + # return False + # + # + # @util.api() + # def getMediaFilesNames(self, pattern='*'): + # path = os.path.join(self.media().dir(), pattern) + # return [os.path.basename(p) for p in glob.glob(path)] + # + # + # @util.api() + # def deleteMediaFile(self, filename): + # try: + # self.media().syncDelete(filename) + # except AttributeError: + # self.media().trash_files([filename]) + # + # + # @util.api() + # def addNote(self, note): + # ankiNote = self.createNote(note) + # + # self.addMediaFromNote(ankiNote, note) + # + # collection = self.collection() + # self.startEditing() + # nCardsAdded = collection.addNote(ankiNote) + # if nCardsAdded < 1: + # raise Exception('The field values you have provided would make an empty question on all cards.') + # collection.autosave() + # self.stopEditing() + # + # return ankiNote.id + # + # + # def addMediaFromNote(self, ankiNote, note): + # audioObjectOrList = note.get('audio') + # self.addMedia(ankiNote, audioObjectOrList, util.MediaType.Audio) + # + # videoObjectOrList = note.get('video') + # self.addMedia(ankiNote, videoObjectOrList, util.MediaType.Video) + # + # pictureObjectOrList = note.get('picture') + # self.addMedia(ankiNote, pictureObjectOrList, util.MediaType.Picture) + # + # + # + # def addMedia(self, ankiNote, mediaObjectOrList, mediaType): + # if mediaObjectOrList is None: + # return + # + # if isinstance(mediaObjectOrList, list): + # mediaList = mediaObjectOrList + # else: + # mediaList = [mediaObjectOrList] + # + # for media in mediaList: + # if media is not None and len(media['fields']) > 0: + # try: + # mediaFilename = self.storeMediaFile(media['filename'], + # data=media.get('data'), + # path=media.get('path'), + # url=media.get('url'), + # skipHash=media.get('skipHash')) + # + # if mediaFilename is not None: + # for field in media['fields']: + # if field in ankiNote: + # if mediaType is util.MediaType.Picture: + # ankiNote[field] += u''.format(mediaFilename) + # elif mediaType is util.MediaType.Audio or mediaType is util.MediaType.Video: + # ankiNote[field] += u'[sound:{}]'.format(mediaFilename) + # + # except Exception as e: + # errorMessage = str(e).replace('&', '&').replace('<', '<').replace('>', '>') + # for field in media['fields']: + # if field in ankiNote: + # ankiNote[field] += errorMessage + # + # + # @util.api() + # def canAddNote(self, note): + # try: + # return bool(self.createNote(note)) + # except: + # return False + # + # + # @util.api() + # def updateNoteFields(self, note): + # ankiNote = self.getNote(note['id']) + # + # for name, value in note['fields'].items(): + # if name in ankiNote: + # ankiNote[name] = value + # + # audioObjectOrList = note.get('audio') + # self.addMedia(ankiNote, audioObjectOrList, util.MediaType.Audio) + # + # videoObjectOrList = note.get('video') + # self.addMedia(ankiNote, videoObjectOrList, util.MediaType.Video) + # + # pictureObjectOrList = note.get('picture') + # self.addMedia(ankiNote, pictureObjectOrList, util.MediaType.Picture) + # + # ankiNote.flush() + # + # + # @util.api() + # def addTags(self, notes, tags, add=True): + # self.startEditing() + # self.collection().tags.bulkAdd(notes, tags, add) + # self.stopEditing() + # + # + # @util.api() + # def removeTags(self, notes, tags): + # return self.addTags(notes, tags, False) + # + # + # @util.api() + # def getTags(self): + # return self.collection().tags.all() + # + # + # @util.api() + # def clearUnusedTags(self): + # self.collection().tags.registerNotes() + # + # + # @util.api() + # def replaceTags(self, notes, tag_to_replace, replace_with_tag): + # self.window().progress.start() + # + # for nid in notes: + # try: + # note = self.getNote(nid) + # except NotFoundError: + # continue + # + # if note.hasTag(tag_to_replace): + # note.delTag(tag_to_replace) + # note.addTag(replace_with_tag) + # note.flush() + # + # self.window().requireReset() + # self.window().progress.finish() + # self.window().reset() + # + # + # @util.api() + # def replaceTagsInAllNotes(self, tag_to_replace, replace_with_tag): + # self.window().progress.start() + # + # collection = self.collection() + # for nid in collection.db.list('select id from notes'): + # note = self.getNote(nid) + # if note.hasTag(tag_to_replace): + # note.delTag(tag_to_replace) + # note.addTag(replace_with_tag) + # note.flush() + # + # self.window().requireReset() + # self.window().progress.finish() + # self.window().reset() + # + # + # @util.api() + # def setEaseFactors(self, cards, easeFactors): + # couldSetEaseFactors = [] + # for i, card in enumerate(cards): + # try: + # ankiCard = self.getCard(card) + # except NotFoundError: + # couldSetEaseFactors.append(False) + # continue + # + # couldSetEaseFactors.append(True) + # ankiCard.factor = easeFactors[i] + # ankiCard.flush() + # + # return couldSetEaseFactors + # + # + # @util.api() + # def getEaseFactors(self, cards): + # easeFactors = [] + # for card in cards: + # try: + # ankiCard = self.getCard(card) + # except NotFoundError: + # easeFactors.append(None) + # continue + # + # easeFactors.append(ankiCard.factor) + # + # return easeFactors + # + # + # @util.api() + # def suspend(self, cards, suspend=True): + # for card in cards: + # if self.suspended(card) == suspend: + # cards.remove(card) + # + # if len(cards) == 0: + # return False + # + # scheduler = self.scheduler() + # self.startEditing() + # if suspend: + # scheduler.suspendCards(cards) + # else: + # scheduler.unsuspendCards(cards) + # self.stopEditing() + # + # return True + # + # + # @util.api() + # def unsuspend(self, cards): + # self.suspend(cards, False) + # + # + # @util.api() + # def suspended(self, card): + # card = self.getCard(card) + # return card.queue == -1 + # + # + # @util.api() + # def areSuspended(self, cards): + # suspended = [] + # for card in cards: + # try: + # suspended.append(self.suspended(card)) + # except NotFoundError: + # suspended.append(None) + # + # return suspended + # + # + # @util.api() + # def areDue(self, cards): + # due = [] + # for card in cards: + # if self.findCards('cid:{} is:new'.format(card)): + # due.append(True) + # else: + # date, ivl = self.collection().db.all('select id/1000.0, ivl from revlog where cid = ?', card)[-1] + # if ivl >= -1200: + # due.append(bool(self.findCards('cid:{} is:due'.format(card)))) + # else: + # due.append(date - ivl <= time.time()) + # + # return due + # + # + # @util.api() + # def getIntervals(self, cards, complete=False): + # intervals = [] + # for card in cards: + # if self.findCards('cid:{} is:new'.format(card)): + # intervals.append(0) + # else: + # interval = self.collection().db.list('select ivl from revlog where cid = ?', card) + # if not complete: + # interval = interval[-1] + # intervals.append(interval) + # + # return intervals + # + # + # + # @util.api() + # def modelNames(self): + # return self.collection().models.allNames() + # + # + # @util.api() + # def createModel(self, modelName, inOrderFields, cardTemplates, css = None, isCloze = False): + # # https://github.com/dae/anki/blob/b06b70f7214fb1f2ce33ba06d2b095384b81f874/anki/stdmodels.py + # if len(inOrderFields) == 0: + # raise Exception('Must provide at least one field for inOrderFields') + # if len(cardTemplates) == 0: + # raise Exception('Must provide at least one card for cardTemplates') + # if modelName in self.collection().models.allNames(): + # raise Exception('Model name already exists') + # + # collection = self.collection() + # mm = collection.models + # + # # Generate new Note + # m = mm.new(modelName) + # if isCloze: + # m['type'] = MODEL_CLOZE + # + # # Create fields and add them to Note + # for field in inOrderFields: + # fm = mm.newField(field) + # mm.addField(m, fm) + # + # # Add shared css to model if exists. Use default otherwise + # if (css is not None): + # m['css'] = css + # + # # Generate new card template(s) + # cardCount = 1 + # for card in cardTemplates: + # cardName = 'Card ' + str(cardCount) + # if 'Name' in card: + # cardName = card['Name'] + # + # t = mm.newTemplate(cardName) + # cardCount += 1 + # t['qfmt'] = card['Front'] + # t['afmt'] = card['Back'] + # mm.addTemplate(m, t) + # + # mm.add(m) + # return m + # + # + # @util.api() + # def modelNamesAndIds(self): + # models = {} + # for model in self.modelNames(): + # models[model] = int(self.collection().models.byName(model)['id']) + # + # return models + # + # + # @util.api() + # def modelNameFromId(self, modelId): + # model = self.collection().models.get(modelId) + # if model is None: + # raise Exception('model was not found: {}'.format(modelId)) + # else: + # return model['name'] + # + # + # @util.api() + # def modelFieldNames(self, modelName): + # model = self.collection().models.byName(modelName) + # if model is None: + # raise Exception('model was not found: {}'.format(modelName)) + # else: + # return [field['name'] for field in model['flds']] + # + # + # @util.api() + # def modelFieldsOnTemplates(self, modelName): + # model = self.collection().models.byName(modelName) + # if model is None: + # raise Exception('model was not found: {}'.format(modelName)) + # + # templates = {} + # for template in model['tmpls']: + # fields = [] + # for side in ['qfmt', 'afmt']: + # fieldsForSide = [] + # + # # based on _fieldsOnTemplate from aqt/clayout.py + # matches = re.findall('{{[^#/}]+?}}', template[side]) + # for match in matches: + # # remove braces and modifiers + # match = re.sub(r'[{}]', '', match) + # match = match.split(':')[-1] + # + # # for the answer side, ignore fields present on the question side + the FrontSide field + # if match == 'FrontSide' or side == 'afmt' and match in fields[0]: + # continue + # fieldsForSide.append(match) + # + # fields.append(fieldsForSide) + # + # templates[template['name']] = fields + # + # return templates + # + # + # @util.api() + # def modelTemplates(self, modelName): + # model = self.collection().models.byName(modelName) + # if model is None: + # raise Exception('model was not found: {}'.format(modelName)) + # + # templates = {} + # for template in model['tmpls']: + # templates[template['name']] = {'Front': template['qfmt'], 'Back': template['afmt']} + # + # return templates + # + # + # @util.api() + # def modelStyling(self, modelName): + # model = self.collection().models.byName(modelName) + # if model is None: + # raise Exception('model was not found: {}'.format(modelName)) + # + # return {'css': model['css']} + # + # + # @util.api() + # def updateModelTemplates(self, model): + # models = self.collection().models + # ankiModel = models.byName(model['name']) + # if ankiModel is None: + # raise Exception('model was not found: {}'.format(model['name'])) + # + # templates = model['templates'] + # for ankiTemplate in ankiModel['tmpls']: + # template = templates.get(ankiTemplate['name']) + # if template: + # qfmt = template.get('Front') + # if qfmt: + # ankiTemplate['qfmt'] = qfmt + # + # afmt = template.get('Back') + # if afmt: + # ankiTemplate['afmt'] = afmt + # + # models.save(ankiModel, True) + # models.flush() + # + # + # @util.api() + # def updateModelStyling(self, model): + # models = self.collection().models + # ankiModel = models.byName(model['name']) + # if ankiModel is None: + # raise Exception('model was not found: {}'.format(model['name'])) + # + # ankiModel['css'] = model['css'] + # + # models.save(ankiModel, True) + # models.flush() + # + # + # @util.api() + # def findAndReplaceInModels(self, modelName, findText, replaceText, front=True, back=True, css=True): + # if not modelName: + # ankiModel = self.collection().models.allNames() + # else: + # model = self.collection().models.byName(modelName) + # if model is None: + # raise Exception('model was not found: {}'.format(modelName)) + # ankiModel = [model] + # updatedModels = 0 + # for model in ankiModel: + # model = self.collection().models.byName(model) + # checkForText = False + # if css and findText in model['css']: + # checkForText = True + # model['css'] = model['css'].replace(findText, replaceText) + # for tmpls in model.get('tmpls'): + # if front and findText in tmpls['qfmt']: + # checkForText = True + # tmpls['qfmt'] = tmpls['qfmt'].replace(findText, replaceText) + # if back and findText in tmpls['afmt']: + # checkForText = True + # tmpls['afmt'] = tmpls['afmt'].replace(findText, replaceText) + # self.collection().models.save(model, True) + # self.collection().models.flush() + # if checkForText: + # updatedModels += 1 + # return updatedModels + # + # + # @util.api() + # def deckNameFromId(self, deckId): + # deck = self.collection().decks.get(deckId) + # if deck is None: + # raise Exception('deck was not found: {}'.format(deckId)) + # + # return deck['name'] + # + # + # @util.api() + # def findNotes(self, query=None): + # if query is None: + # return [] + # + # return list(map(int, self.collection().findNotes(query))) + # + # + # @util.api() + # def findCards(self, query=None): + # if query is None: + # return [] + # + # return list(map(int, self.collection().findCards(query))) + # + # + # @util.api() + # def cardsInfo(self, cards): + # result = [] + # for cid in cards: + # try: + # card = self.getCard(cid) + # model = card.model() + # note = card.note() + # fields = {} + # for info in model['flds']: + # order = info['ord'] + # name = info['name'] + # fields[name] = {'value': note.fields[order], 'order': order} + # + # result.append({ + # 'cardId': card.id, + # 'fields': fields, + # 'fieldOrder': card.ord, + # 'question': util.cardQuestion(card), + # 'answer': util.cardAnswer(card), + # 'modelName': model['name'], + # 'ord': card.ord, + # 'deckName': self.deckNameFromId(card.did), + # 'css': model['css'], + # 'factor': card.factor, + # #This factor is 10 times the ease percentage, + # # so an ease of 310% would be reported as 3100 + # 'interval': card.ivl, + # 'note': card.nid, + # 'type': card.type, + # 'queue': card.queue, + # 'due': card.due, + # 'reps': card.reps, + # 'lapses': card.lapses, + # 'left': card.left, + # }) + # except NotFoundError: + # # Anki will give a NotFoundError if the card ID does not exist. + # # Best behavior is probably to add an 'empty card' to the + # # returned result, so that the items of the input and return + # # lists correspond. + # result.append({}) + # + # return result + # + # + # @util.api() + # def forgetCards(self, cards): + # self.startEditing() + # scids = anki.utils.ids2str(cards) + # self.collection().db.execute('update cards set type=0, queue=0, left=0, ivl=0, due=0, odue=0, factor=0 where id in ' + scids) + # self.stopEditing() + # + # + # @util.api() + # def relearnCards(self, cards): + # self.startEditing() + # scids = anki.utils.ids2str(cards) + # self.collection().db.execute('update cards set type=3, queue=1 where id in ' + scids) + # self.stopEditing() + # + # + # @util.api() + # def cardReviews(self, deck, startID): + # return self.database().all( + # 'select id, cid, usn, ease, ivl, lastIvl, factor, time, type from revlog ''where id>? and cid in (select id from cards where did=?)', + # startID, + # self.decks().id(deck) + # ) + # + # + # @util.api() + # def reloadCollection(self): + # self.collection().reset() + # + # + # @util.api() + # def getLatestReviewID(self, deck): + # return self.database().scalar( + # 'select max(id) from revlog where cid in (select id from cards where did=?)', + # self.decks().id(deck) + # ) or 0 + # + # + # @util.api() + # def updateCompleteDeck(self, data): + # self.startEditing() + # did = self.decks().id(data['deck']) + # self.decks().flush() + # model_manager = self.collection().models + # for _, card in data['cards'].items(): + # self.database().execute( + # 'replace into cards (id, nid, did, ord, type, queue, due, ivl, factor, reps, lapses, left, ' + # 'mod, usn, odue, odid, flags, data) ' + # 'values (' + '?,' * (12 + 6 - 1) + '?)', + # card['id'], card['nid'], did, card['ord'], card['type'], card['queue'], card['due'], + # card['ivl'], card['factor'], card['reps'], card['lapses'], card['left'], + # intTime(), -1, 0, 0, 0, 0 + # ) + # note = data['notes'][str(card['nid'])] + # tags = self.collection().tags.join(self.collection().tags.canonify(note['tags'])) + # self.database().execute( + # 'replace into notes(id, mid, tags, flds,' + # 'guid, mod, usn, flags, data, sfld, csum) values (' + '?,' * (4 + 7 - 1) + '?)', + # note['id'], note['mid'], tags, joinFields(note['fields']), + # guid64(), intTime(), -1, 0, 0, '', fieldChecksum(note['fields'][0]) + # ) + # model = data['models'][str(note['mid'])] + # if not model_manager.get(model['id']): + # model_o = model_manager.new(model['name']) + # for field_name in model['fields']: + # field = model_manager.newField(field_name) + # model_manager.addField(model_o, field) + # for template_name in model['templateNames']: + # template = model_manager.newTemplate(template_name) + # model_manager.addTemplate(model_o, template) + # model_o['id'] = model['id'] + # model_manager.update(model_o) + # model_manager.flush() + # + # self.stopEditing() + # + # + # @util.api() + # def insertReviews(self, reviews): + # if len(reviews) > 0: + # sql = 'insert into revlog(id,cid,usn,ease,ivl,lastIvl,factor,time,type) values ' + # for row in reviews: + # sql += '(%s),' % ','.join(map(str, row)) + # sql = sql[:-1] + # self.database().execute(sql) + # + # + # @util.api() + # def notesInfo(self, notes): + # result = [] + # for nid in notes: + # try: + # note = self.getNote(nid) + # model = note.model() + # + # fields = {} + # for info in model['flds']: + # order = info['ord'] + # name = info['name'] + # fields[name] = {'value': note.fields[order], 'order': order} + # + # result.append({ + # 'noteId': note.id, + # 'tags' : note.tags, + # 'fields': fields, + # 'modelName': model['name'], + # 'cards': self.collection().db.list('select id from cards where nid = ? order by ord', note.id) + # }) + # except NotFoundError: + # # Anki will give a NotFoundError if the note ID does not exist. + # # Best behavior is probably to add an 'empty card' to the + # # returned result, so that the items of the input and return + # # lists correspond. + # result.append({}) + # + # return result + # + # + # @util.api() + # def deleteNotes(self, notes): + # try: + # self.collection().remNotes(notes) + # finally: + # self.stopEditing() + # + # + # @util.api() + # def removeEmptyNotes(self): + # for model in self.collection().models.all(): + # if self.collection().models.useCount(model) == 0: + # self.collection().models.rem(model) + # self.window().requireReset() + # + # + # @util.api() + # def cardsToNotes(self, cards): + # return self.collection().db.list('select distinct nid from cards where id in ' + anki.utils.ids2str(cards)) + # + # + # @util.api() + # def guiBrowse(self, query=None): + # browser = aqt.dialogs.open('Browser', self.window()) + # browser.activateWindow() + # + # if query is not None: + # browser.form.searchEdit.lineEdit().setText(query) + # if hasattr(browser, 'onSearch'): + # browser.onSearch() + # else: + # browser.onSearchActivated() + # + # return list(map(int, browser.model.cards)) + # + # + # @util.api() + # def guiAddCards(self, note=None): + # if note is not None: + # collection = self.collection() + # + # deck = collection.decks.byName(note['deckName']) + # if deck is None: + # raise Exception('deck was not found: {}'.format(note['deckName'])) + # + # collection.decks.select(deck['id']) + # savedMid = deck.pop('mid', None) + # + # model = collection.models.byName(note['modelName']) + # if model is None: + # raise Exception('model was not found: {}'.format(note['modelName'])) + # + # collection.models.setCurrent(model) + # collection.models.update(model) + # + # closeAfterAdding = False + # if note is not None and 'options' in note: + # if 'closeAfterAdding' in note['options']: + # closeAfterAdding = note['options']['closeAfterAdding'] + # if type(closeAfterAdding) is not bool: + # raise Exception('option parameter \'closeAfterAdding\' must be boolean') + # + # addCards = None + # + # if closeAfterAdding: + # randomString = ''.join(random.choice(string.ascii_letters) for _ in range(10)) + # windowName = 'AddCardsAndClose' + randomString + # + # class AddCardsAndClose(aqt.addcards.AddCards): + # + # def __init__(self, mw): + # # the window must only reset if + # # * function `onModelChange` has been called prior + # # * window was newly opened + # + # self.modelHasChanged = True + # super().__init__(mw) + # + # self.addButton.setText('Add and Close') + # self.addButton.setShortcut(aqt.qt.QKeySequence('Ctrl+Return')) + # + # def _addCards(self): + # super()._addCards() + # + # # if adding was successful it must mean it was added to the history of the window + # if len(self.history): + # self.reject() + # + # def onModelChange(self): + # if self.isActiveWindow(): + # super().onModelChange() + # self.modelHasChanged = True + # + # def onReset(self, model=None, keep=False): + # if self.isActiveWindow() or self.modelHasChanged: + # super().onReset(model, keep) + # self.modelHasChanged = False + # + # else: + # # modelchoosers text is changed by a reset hook + # # therefore we need to change it back manually + # self.modelChooser.models.setText(self.editor.note.model()['name']) + # self.modelHasChanged = False + # + # def _reject(self): + # savedMarkClosed = aqt.dialogs.markClosed + # aqt.dialogs.markClosed = lambda _: savedMarkClosed(windowName) + # super()._reject() + # aqt.dialogs.markClosed = savedMarkClosed + # + # aqt.dialogs._dialogs[windowName] = [AddCardsAndClose, None] + # addCards = aqt.dialogs.open(windowName, self.window()) + # + # if savedMid: + # deck['mid'] = savedMid + # + # editor = addCards.editor + # ankiNote = editor.note + # + # if 'fields' in note: + # for name, value in note['fields'].items(): + # if name in ankiNote: + # ankiNote[name] = value + # + # self.addMediaFromNote(ankiNote, note) + # editor.loadNote() + # + # if 'tags' in note: + # ankiNote.tags = note['tags'] + # editor.updateTags() + # + # # if Anki does not Focus, the window will not notice that the + # # fields are actually filled + # aqt.dialogs.open(windowName, self.window()) + # addCards.setAndFocusNote(editor.note) + # + # return ankiNote.id + # + # elif note is not None: + # collection = self.collection() + # ankiNote = anki.notes.Note(collection, model) + # + # # fill out card beforehand, so we can be sure of the note id + # if 'fields' in note: + # for name, value in note['fields'].items(): + # if name in ankiNote: + # ankiNote[name] = value + # + # self.addMediaFromNote(ankiNote, note) + # + # if 'tags' in note: + # ankiNote.tags = note['tags'] + # + # def openNewWindow(): + # nonlocal ankiNote + # + # addCards = aqt.dialogs.open('AddCards', self.window()) + # + # if savedMid: + # deck['mid'] = savedMid + # + # addCards.editor.note = ankiNote + # addCards.editor.loadNote() + # addCards.editor.updateTags() + # + # addCards.activateWindow() + # + # aqt.dialogs.open('AddCards', self.window()) + # addCards.setAndFocusNote(addCards.editor.note) + # + # currentWindow = aqt.dialogs._dialogs['AddCards'][1] + # + # if currentWindow is not None: + # currentWindow.closeWithCallback(openNewWindow) + # else: + # openNewWindow() + # + # return ankiNote.id + # + # else: + # addCards = aqt.dialogs.open('AddCards', self.window()) + # addCards.activateWindow() + # + # return addCards.editor.note.id + # + # + # @util.api() + # def guiReviewActive(self): + # return self.reviewer().card is not None and self.window().state == 'review' + # + # + # @util.api() + # def guiCurrentCard(self): + # if not self.guiReviewActive(): + # raise Exception('Gui review is not currently active.') + # + # reviewer = self.reviewer() + # card = reviewer.card + # model = card.model() + # note = card.note() + # + # fields = {} + # for info in model['flds']: + # order = info['ord'] + # name = info['name'] + # fields[name] = {'value': note.fields[order], 'order': order} + # + # buttonList = reviewer._answerButtonList() + # return { + # 'cardId': card.id, + # 'fields': fields, + # 'fieldOrder': card.ord, + # 'question': util.cardQuestion(card), + # 'answer': util.cardAnswer(card), + # 'buttons': [b[0] for b in buttonList], + # 'nextReviews': [reviewer.mw.col.sched.nextIvlStr(reviewer.card, b[0], True) for b in buttonList], + # 'modelName': model['name'], + # 'deckName': self.deckNameFromId(card.did), + # 'css': model['css'], + # 'template': card.template()['name'] + # } + # + # + # @util.api() + # def guiStartCardTimer(self): + # if not self.guiReviewActive(): + # return False + # + # card = self.reviewer().card + # if card is not None: + # card.startTimer() + # return True + # + # return False + # + # + # @util.api() + # def guiShowQuestion(self): + # if self.guiReviewActive(): + # self.reviewer()._showQuestion() + # return True + # + # return False + # + # + # @util.api() + # def guiShowAnswer(self): + # if self.guiReviewActive(): + # self.window().reviewer._showAnswer() + # return True + # + # return False + # + # + # @util.api() + # def guiAnswerCard(self, ease): + # if not self.guiReviewActive(): + # return False + # + # reviewer = self.reviewer() + # if reviewer.state != 'answer': + # return False + # if ease <= 0 or ease > self.scheduler().answerButtons(reviewer.card): + # return False + # + # reviewer._answerCard(ease) + # return True + # + # + # @util.api() + # def guiDeckOverview(self, name): + # collection = self.collection() + # if collection is not None: + # deck = collection.decks.byName(name) + # if deck is not None: + # collection.decks.select(deck['id']) + # self.window().onOverview() + # return True + # + # return False + # + # + # @util.api() + # def guiDeckBrowser(self): + # self.window().moveToState('deckBrowser') + # + # + # @util.api() + # def guiDeckReview(self, name): + # if self.guiDeckOverview(name): + # self.window().moveToState('review') + # return True + # + # return False + # + # + # @util.api() + # def guiExitAnki(self): + # timer = QTimer() + # timer.timeout.connect(self.window().close) + # timer.start(1000) # 1s should be enough to allow the response to be sent. + # + # + # @util.api() + # def guiCheckDatabase(self): + # self.window().onCheckDB() + # return True + # + # + # @util.api() + # def addNotes(self, notes): + # results = [] + # for note in notes: + # try: + # results.append(self.addNote(note)) + # except: + # results.append(None) + # + # return results + # + # + # @util.api() + # def canAddNotes(self, notes): + # results = [] + # for note in notes: + # results.append(self.canAddNote(note)) + # + # return results + # + # + # @util.api() + # def exportPackage(self, deck, path, includeSched=False): + # collection = self.collection() + # if collection is not None: + # deck = collection.decks.byName(deck) + # if deck is not None: + # exporter = AnkiPackageExporter(collection) + # exporter.did = deck['id'] + # exporter.includeSched = includeSched + # exporter.exportInto(path) + # return True + # + # return False + # + # + # @util.api() + # def importPackage(self, path): + # collection = self.collection() + # if collection is not None: + # try: + # self.startEditing() + # importer = AnkiPackageImporter(collection, path) + # importer.run() + # except: + # self.stopEditing() + # raise + # else: + # self.stopEditing() + # return True + # + # return False - @util.api() - def deckNames(self): - return self.decks().allNames() - - - @util.api() - def deckNamesAndIds(self): - decks = {} - for deck in self.deckNames(): - decks[deck] = self.decks().id(deck) - - return decks - - - @util.api() - def getDecks(self, cards): - decks = {} - for card in cards: - did = self.database().scalar('select did from cards where id=?', card) - deck = self.decks().get(did)['name'] - if deck in decks: - decks[deck].append(card) - else: - decks[deck] = [card] - - return decks - - - @util.api() - def createDeck(self, deck): - try: - self.startEditing() - did = self.decks().id(deck) - finally: - self.stopEditing() - - return did - - - @util.api() - def changeDeck(self, cards, deck): - self.startEditing() - - did = self.collection().decks.id(deck) - mod = anki.utils.intTime() - usn = self.collection().usn() - - # normal cards - scids = anki.utils.ids2str(cards) - # remove any cards from filtered deck first - self.collection().sched.remFromDyn(cards) - - # then move into new deck - self.collection().db.execute('update cards set usn=?, mod=?, did=? where id in ' + scids, usn, mod, did) - self.stopEditing() - - - @util.api() - def deleteDecks(self, decks, cardsToo=False): - try: - self.startEditing() - decks = filter(lambda d: d in self.deckNames(), decks) - for deck in decks: - did = self.decks().id(deck) - self.decks().rem(did, cardsToo) - finally: - self.stopEditing() - - - @util.api() - def getDeckConfig(self, deck): - if deck not in self.deckNames(): - return False - - collection = self.collection() - did = collection.decks.id(deck) - return collection.decks.confForDid(did) - - - @util.api() - def saveDeckConfig(self, config): - collection = self.collection() - - config['id'] = str(config['id']) - config['mod'] = anki.utils.intTime() - config['usn'] = collection.usn() - if int(config['id']) not in [c['id'] for c in collection.decks.all_config()]: - return False - try: - collection.decks.save(config) - collection.decks.updateConf(config) - except: - return False - return True - - - @util.api() - def setDeckConfigId(self, decks, configId): - configId = int(configId) - for deck in decks: - if not deck in self.deckNames(): - return False - - collection = self.collection() - - for deck in decks: - try: - did = str(collection.decks.id(deck)) - deck_dict = aqt.mw.col.decks.decks[did] - deck_dict['conf'] = configId - collection.decks.save(deck_dict) - except: - return False - - return True - - - @util.api() - def cloneDeckConfigId(self, name, cloneFrom='1'): - configId = int(cloneFrom) - collection = self.collection() - if configId not in [c['id'] for c in collection.decks.all_config()]: - return False - - config = collection.decks.getConf(configId) - return collection.decks.confId(name, config) - - - @util.api() - def removeDeckConfigId(self, configId): - collection = self.collection() - if int(configId) not in [c['id'] for c in collection.decks.all_config()]: - return False - - collection.decks.remConf(configId) - return True - - - @util.api() - def storeMediaFile(self, filename, data=None, path=None, url=None, skipHash=None, deleteExisting=True): - if not (data or path or url): - raise Exception('You must provide a "data", "path", or "url" field.') - if deleteExisting: - self.deleteMediaFile(filename) - if data: - mediaData = base64.b64decode(data) - elif path: - with open(path, 'rb') as f: - mediaData = f.read() - elif url: - mediaData = util.download(url) - - if skipHash is None: - skip = False - else: - m = hashlib.md5() - m.update(mediaData) - skip = skipHash == m.hexdigest() - - if skip: - return None - return self.media().writeData(filename, mediaData) - - - @util.api() - def retrieveMediaFile(self, filename): - filename = os.path.basename(filename) - filename = unicodedata.normalize('NFC', filename) - filename = self.media().stripIllegal(filename) - - path = os.path.join(self.media().dir(), filename) - if os.path.exists(path): - with open(path, 'rb') as file: - return base64.b64encode(file.read()).decode('ascii') - - return False - - - @util.api() - def getMediaFilesNames(self, pattern='*'): - path = os.path.join(self.media().dir(), pattern) - return [os.path.basename(p) for p in glob.glob(path)] - - - @util.api() - def deleteMediaFile(self, filename): - try: - self.media().syncDelete(filename) - except AttributeError: - self.media().trash_files([filename]) - - - @util.api() - def addNote(self, note): - ankiNote = self.createNote(note) - - self.addMediaFromNote(ankiNote, note) - - collection = self.collection() - self.startEditing() - nCardsAdded = collection.addNote(ankiNote) - if nCardsAdded < 1: - raise Exception('The field values you have provided would make an empty question on all cards.') - collection.autosave() - self.stopEditing() - - return ankiNote.id - - - def addMediaFromNote(self, ankiNote, note): - audioObjectOrList = note.get('audio') - self.addMedia(ankiNote, audioObjectOrList, util.MediaType.Audio) - - videoObjectOrList = note.get('video') - self.addMedia(ankiNote, videoObjectOrList, util.MediaType.Video) - - pictureObjectOrList = note.get('picture') - self.addMedia(ankiNote, pictureObjectOrList, util.MediaType.Picture) - - - - def addMedia(self, ankiNote, mediaObjectOrList, mediaType): - if mediaObjectOrList is None: - return - - if isinstance(mediaObjectOrList, list): - mediaList = mediaObjectOrList - else: - mediaList = [mediaObjectOrList] - - for media in mediaList: - if media is not None and len(media['fields']) > 0: - try: - mediaFilename = self.storeMediaFile(media['filename'], - data=media.get('data'), - path=media.get('path'), - url=media.get('url'), - skipHash=media.get('skipHash')) - - if mediaFilename is not None: - for field in media['fields']: - if field in ankiNote: - if mediaType is util.MediaType.Picture: - ankiNote[field] += u''.format(mediaFilename) - elif mediaType is util.MediaType.Audio or mediaType is util.MediaType.Video: - ankiNote[field] += u'[sound:{}]'.format(mediaFilename) - - except Exception as e: - errorMessage = str(e).replace('&', '&').replace('<', '<').replace('>', '>') - for field in media['fields']: - if field in ankiNote: - ankiNote[field] += errorMessage - - - @util.api() - def canAddNote(self, note): - try: - return bool(self.createNote(note)) - except: - return False - - - @util.api() - def updateNoteFields(self, note): - ankiNote = self.getNote(note['id']) - - for name, value in note['fields'].items(): - if name in ankiNote: - ankiNote[name] = value - - audioObjectOrList = note.get('audio') - self.addMedia(ankiNote, audioObjectOrList, util.MediaType.Audio) - - videoObjectOrList = note.get('video') - self.addMedia(ankiNote, videoObjectOrList, util.MediaType.Video) - - pictureObjectOrList = note.get('picture') - self.addMedia(ankiNote, pictureObjectOrList, util.MediaType.Picture) - - ankiNote.flush() - - - @util.api() - def addTags(self, notes, tags, add=True): - self.startEditing() - self.collection().tags.bulkAdd(notes, tags, add) - self.stopEditing() - - - @util.api() - def removeTags(self, notes, tags): - return self.addTags(notes, tags, False) - - - @util.api() - def getTags(self): - return self.collection().tags.all() - - - @util.api() - def clearUnusedTags(self): - self.collection().tags.registerNotes() - - - @util.api() - def replaceTags(self, notes, tag_to_replace, replace_with_tag): - self.window().progress.start() - - for nid in notes: - try: - note = self.getNote(nid) - except NotFoundError: - continue - - if note.hasTag(tag_to_replace): - note.delTag(tag_to_replace) - note.addTag(replace_with_tag) - note.flush() - - self.window().requireReset() - self.window().progress.finish() - self.window().reset() - - - @util.api() - def replaceTagsInAllNotes(self, tag_to_replace, replace_with_tag): - self.window().progress.start() - - collection = self.collection() - for nid in collection.db.list('select id from notes'): - note = self.getNote(nid) - if note.hasTag(tag_to_replace): - note.delTag(tag_to_replace) - note.addTag(replace_with_tag) - note.flush() - - self.window().requireReset() - self.window().progress.finish() - self.window().reset() - - - @util.api() - def setEaseFactors(self, cards, easeFactors): - couldSetEaseFactors = [] - for i, card in enumerate(cards): - try: - ankiCard = self.getCard(card) - except NotFoundError: - couldSetEaseFactors.append(False) - continue - - couldSetEaseFactors.append(True) - ankiCard.factor = easeFactors[i] - ankiCard.flush() - - return couldSetEaseFactors - - - @util.api() - def getEaseFactors(self, cards): - easeFactors = [] - for card in cards: - try: - ankiCard = self.getCard(card) - except NotFoundError: - easeFactors.append(None) - continue - - easeFactors.append(ankiCard.factor) - - return easeFactors - - - @util.api() - def suspend(self, cards, suspend=True): - for card in cards: - if self.suspended(card) == suspend: - cards.remove(card) - - if len(cards) == 0: - return False - - scheduler = self.scheduler() - self.startEditing() - if suspend: - scheduler.suspendCards(cards) - else: - scheduler.unsuspendCards(cards) - self.stopEditing() - - return True - - - @util.api() - def unsuspend(self, cards): - self.suspend(cards, False) - - - @util.api() - def suspended(self, card): - card = self.getCard(card) - return card.queue == -1 - - - @util.api() - def areSuspended(self, cards): - suspended = [] - for card in cards: - try: - suspended.append(self.suspended(card)) - except NotFoundError: - suspended.append(None) - - return suspended - - - @util.api() - def areDue(self, cards): - due = [] - for card in cards: - if self.findCards('cid:{} is:new'.format(card)): - due.append(True) - else: - date, ivl = self.collection().db.all('select id/1000.0, ivl from revlog where cid = ?', card)[-1] - if ivl >= -1200: - due.append(bool(self.findCards('cid:{} is:due'.format(card)))) - else: - due.append(date - ivl <= time.time()) - - return due - - - @util.api() - def getIntervals(self, cards, complete=False): - intervals = [] - for card in cards: - if self.findCards('cid:{} is:new'.format(card)): - intervals.append(0) - else: - interval = self.collection().db.list('select ivl from revlog where cid = ?', card) - if not complete: - interval = interval[-1] - intervals.append(interval) - - return intervals - - - - @util.api() - def modelNames(self): - return self.collection().models.allNames() - - - @util.api() - def createModel(self, modelName, inOrderFields, cardTemplates, css = None, isCloze = False): - # https://github.com/dae/anki/blob/b06b70f7214fb1f2ce33ba06d2b095384b81f874/anki/stdmodels.py - if len(inOrderFields) == 0: - raise Exception('Must provide at least one field for inOrderFields') - if len(cardTemplates) == 0: - raise Exception('Must provide at least one card for cardTemplates') - if modelName in self.collection().models.allNames(): - raise Exception('Model name already exists') - - collection = self.collection() - mm = collection.models - - # Generate new Note - m = mm.new(modelName) - if isCloze: - m['type'] = MODEL_CLOZE - - # Create fields and add them to Note - for field in inOrderFields: - fm = mm.newField(field) - mm.addField(m, fm) - - # Add shared css to model if exists. Use default otherwise - if (css is not None): - m['css'] = css - - # Generate new card template(s) - cardCount = 1 - for card in cardTemplates: - cardName = 'Card ' + str(cardCount) - if 'Name' in card: - cardName = card['Name'] - - t = mm.newTemplate(cardName) - cardCount += 1 - t['qfmt'] = card['Front'] - t['afmt'] = card['Back'] - mm.addTemplate(m, t) - - mm.add(m) - return m - - - @util.api() - def modelNamesAndIds(self): - models = {} - for model in self.modelNames(): - models[model] = int(self.collection().models.byName(model)['id']) - - return models - - - @util.api() - def modelNameFromId(self, modelId): - model = self.collection().models.get(modelId) - if model is None: - raise Exception('model was not found: {}'.format(modelId)) - else: - return model['name'] - - - @util.api() - def modelFieldNames(self, modelName): - model = self.collection().models.byName(modelName) - if model is None: - raise Exception('model was not found: {}'.format(modelName)) - else: - return [field['name'] for field in model['flds']] - - - @util.api() - def modelFieldsOnTemplates(self, modelName): - model = self.collection().models.byName(modelName) - if model is None: - raise Exception('model was not found: {}'.format(modelName)) - - templates = {} - for template in model['tmpls']: - fields = [] - for side in ['qfmt', 'afmt']: - fieldsForSide = [] - - # based on _fieldsOnTemplate from aqt/clayout.py - matches = re.findall('{{[^#/}]+?}}', template[side]) - for match in matches: - # remove braces and modifiers - match = re.sub(r'[{}]', '', match) - match = match.split(':')[-1] - - # for the answer side, ignore fields present on the question side + the FrontSide field - if match == 'FrontSide' or side == 'afmt' and match in fields[0]: - continue - fieldsForSide.append(match) - - fields.append(fieldsForSide) - - templates[template['name']] = fields - - return templates - - - @util.api() - def modelTemplates(self, modelName): - model = self.collection().models.byName(modelName) - if model is None: - raise Exception('model was not found: {}'.format(modelName)) - - templates = {} - for template in model['tmpls']: - templates[template['name']] = {'Front': template['qfmt'], 'Back': template['afmt']} - - return templates - - - @util.api() - def modelStyling(self, modelName): - model = self.collection().models.byName(modelName) - if model is None: - raise Exception('model was not found: {}'.format(modelName)) - - return {'css': model['css']} - - - @util.api() - def updateModelTemplates(self, model): - models = self.collection().models - ankiModel = models.byName(model['name']) - if ankiModel is None: - raise Exception('model was not found: {}'.format(model['name'])) - - templates = model['templates'] - for ankiTemplate in ankiModel['tmpls']: - template = templates.get(ankiTemplate['name']) - if template: - qfmt = template.get('Front') - if qfmt: - ankiTemplate['qfmt'] = qfmt - - afmt = template.get('Back') - if afmt: - ankiTemplate['afmt'] = afmt - - models.save(ankiModel, True) - models.flush() - - - @util.api() - def updateModelStyling(self, model): - models = self.collection().models - ankiModel = models.byName(model['name']) - if ankiModel is None: - raise Exception('model was not found: {}'.format(model['name'])) - - ankiModel['css'] = model['css'] - - models.save(ankiModel, True) - models.flush() - - - @util.api() - def findAndReplaceInModels(self, modelName, findText, replaceText, front=True, back=True, css=True): - if not modelName: - ankiModel = self.collection().models.allNames() - else: - model = self.collection().models.byName(modelName) - if model is None: - raise Exception('model was not found: {}'.format(modelName)) - ankiModel = [model] - updatedModels = 0 - for model in ankiModel: - model = self.collection().models.byName(model) - checkForText = False - if css and findText in model['css']: - checkForText = True - model['css'] = model['css'].replace(findText, replaceText) - for tmpls in model.get('tmpls'): - if front and findText in tmpls['qfmt']: - checkForText = True - tmpls['qfmt'] = tmpls['qfmt'].replace(findText, replaceText) - if back and findText in tmpls['afmt']: - checkForText = True - tmpls['afmt'] = tmpls['afmt'].replace(findText, replaceText) - self.collection().models.save(model, True) - self.collection().models.flush() - if checkForText: - updatedModels += 1 - return updatedModels - - - @util.api() - def deckNameFromId(self, deckId): - deck = self.collection().decks.get(deckId) - if deck is None: - raise Exception('deck was not found: {}'.format(deckId)) - - return deck['name'] - - - @util.api() - def findNotes(self, query=None): - if query is None: - return [] - - return list(map(int, self.collection().findNotes(query))) - - - @util.api() - def findCards(self, query=None): - if query is None: - return [] - - return list(map(int, self.collection().findCards(query))) - - - @util.api() - def cardsInfo(self, cards): - result = [] - for cid in cards: - try: - card = self.getCard(cid) - model = card.model() - note = card.note() - fields = {} - for info in model['flds']: - order = info['ord'] - name = info['name'] - fields[name] = {'value': note.fields[order], 'order': order} - - result.append({ - 'cardId': card.id, - 'fields': fields, - 'fieldOrder': card.ord, - 'question': util.cardQuestion(card), - 'answer': util.cardAnswer(card), - 'modelName': model['name'], - 'ord': card.ord, - 'deckName': self.deckNameFromId(card.did), - 'css': model['css'], - 'factor': card.factor, - #This factor is 10 times the ease percentage, - # so an ease of 310% would be reported as 3100 - 'interval': card.ivl, - 'note': card.nid, - 'type': card.type, - 'queue': card.queue, - 'due': card.due, - 'reps': card.reps, - 'lapses': card.lapses, - 'left': card.left, - }) - except NotFoundError: - # Anki will give a NotFoundError if the card ID does not exist. - # Best behavior is probably to add an 'empty card' to the - # returned result, so that the items of the input and return - # lists correspond. - result.append({}) - - return result - - - @util.api() - def forgetCards(self, cards): - self.startEditing() - scids = anki.utils.ids2str(cards) - self.collection().db.execute('update cards set type=0, queue=0, left=0, ivl=0, due=0, odue=0, factor=0 where id in ' + scids) - self.stopEditing() - - - @util.api() - def relearnCards(self, cards): - self.startEditing() - scids = anki.utils.ids2str(cards) - self.collection().db.execute('update cards set type=3, queue=1 where id in ' + scids) - self.stopEditing() - - - @util.api() - def cardReviews(self, deck, startID): - return self.database().all( - 'select id, cid, usn, ease, ivl, lastIvl, factor, time, type from revlog ''where id>? and cid in (select id from cards where did=?)', - startID, - self.decks().id(deck) - ) - - - @util.api() - def reloadCollection(self): - self.collection().reset() - - - @util.api() - def getLatestReviewID(self, deck): - return self.database().scalar( - 'select max(id) from revlog where cid in (select id from cards where did=?)', - self.decks().id(deck) - ) or 0 - - - @util.api() - def updateCompleteDeck(self, data): - self.startEditing() - did = self.decks().id(data['deck']) - self.decks().flush() - model_manager = self.collection().models - for _, card in data['cards'].items(): - self.database().execute( - 'replace into cards (id, nid, did, ord, type, queue, due, ivl, factor, reps, lapses, left, ' - 'mod, usn, odue, odid, flags, data) ' - 'values (' + '?,' * (12 + 6 - 1) + '?)', - card['id'], card['nid'], did, card['ord'], card['type'], card['queue'], card['due'], - card['ivl'], card['factor'], card['reps'], card['lapses'], card['left'], - intTime(), -1, 0, 0, 0, 0 - ) - note = data['notes'][str(card['nid'])] - tags = self.collection().tags.join(self.collection().tags.canonify(note['tags'])) - self.database().execute( - 'replace into notes(id, mid, tags, flds,' - 'guid, mod, usn, flags, data, sfld, csum) values (' + '?,' * (4 + 7 - 1) + '?)', - note['id'], note['mid'], tags, joinFields(note['fields']), - guid64(), intTime(), -1, 0, 0, '', fieldChecksum(note['fields'][0]) - ) - model = data['models'][str(note['mid'])] - if not model_manager.get(model['id']): - model_o = model_manager.new(model['name']) - for field_name in model['fields']: - field = model_manager.newField(field_name) - model_manager.addField(model_o, field) - for template_name in model['templateNames']: - template = model_manager.newTemplate(template_name) - model_manager.addTemplate(model_o, template) - model_o['id'] = model['id'] - model_manager.update(model_o) - model_manager.flush() - - self.stopEditing() - - - @util.api() - def insertReviews(self, reviews): - if len(reviews) > 0: - sql = 'insert into revlog(id,cid,usn,ease,ivl,lastIvl,factor,time,type) values ' - for row in reviews: - sql += '(%s),' % ','.join(map(str, row)) - sql = sql[:-1] - self.database().execute(sql) - - - @util.api() - def notesInfo(self, notes): - result = [] - for nid in notes: - try: - note = self.getNote(nid) - model = note.model() - - fields = {} - for info in model['flds']: - order = info['ord'] - name = info['name'] - fields[name] = {'value': note.fields[order], 'order': order} - - result.append({ - 'noteId': note.id, - 'tags' : note.tags, - 'fields': fields, - 'modelName': model['name'], - 'cards': self.collection().db.list('select id from cards where nid = ? order by ord', note.id) - }) - except NotFoundError: - # Anki will give a NotFoundError if the note ID does not exist. - # Best behavior is probably to add an 'empty card' to the - # returned result, so that the items of the input and return - # lists correspond. - result.append({}) - - return result - - - @util.api() - def deleteNotes(self, notes): - try: - self.collection().remNotes(notes) - finally: - self.stopEditing() - - - @util.api() - def removeEmptyNotes(self): - for model in self.collection().models.all(): - if self.collection().models.useCount(model) == 0: - self.collection().models.rem(model) - self.window().requireReset() - - - @util.api() - def cardsToNotes(self, cards): - return self.collection().db.list('select distinct nid from cards where id in ' + anki.utils.ids2str(cards)) - - - @util.api() - def guiBrowse(self, query=None): - browser = aqt.dialogs.open('Browser', self.window()) - browser.activateWindow() - - if query is not None: - browser.form.searchEdit.lineEdit().setText(query) - if hasattr(browser, 'onSearch'): - browser.onSearch() - else: - browser.onSearchActivated() - - return list(map(int, browser.model.cards)) - - - @util.api() - def guiAddCards(self, note=None): - if note is not None: - collection = self.collection() - - deck = collection.decks.byName(note['deckName']) - if deck is None: - raise Exception('deck was not found: {}'.format(note['deckName'])) - - collection.decks.select(deck['id']) - savedMid = deck.pop('mid', None) - - model = collection.models.byName(note['modelName']) - if model is None: - raise Exception('model was not found: {}'.format(note['modelName'])) - - collection.models.setCurrent(model) - collection.models.update(model) - - closeAfterAdding = False - if note is not None and 'options' in note: - if 'closeAfterAdding' in note['options']: - closeAfterAdding = note['options']['closeAfterAdding'] - if type(closeAfterAdding) is not bool: - raise Exception('option parameter \'closeAfterAdding\' must be boolean') - - addCards = None - - if closeAfterAdding: - randomString = ''.join(random.choice(string.ascii_letters) for _ in range(10)) - windowName = 'AddCardsAndClose' + randomString - - class AddCardsAndClose(aqt.addcards.AddCards): - - def __init__(self, mw): - # the window must only reset if - # * function `onModelChange` has been called prior - # * window was newly opened - - self.modelHasChanged = True - super().__init__(mw) - - self.addButton.setText('Add and Close') - self.addButton.setShortcut(aqt.qt.QKeySequence('Ctrl+Return')) - - def _addCards(self): - super()._addCards() - - # if adding was successful it must mean it was added to the history of the window - if len(self.history): - self.reject() - - def onModelChange(self): - if self.isActiveWindow(): - super().onModelChange() - self.modelHasChanged = True - - def onReset(self, model=None, keep=False): - if self.isActiveWindow() or self.modelHasChanged: - super().onReset(model, keep) - self.modelHasChanged = False - - else: - # modelchoosers text is changed by a reset hook - # therefore we need to change it back manually - self.modelChooser.models.setText(self.editor.note.model()['name']) - self.modelHasChanged = False - - def _reject(self): - savedMarkClosed = aqt.dialogs.markClosed - aqt.dialogs.markClosed = lambda _: savedMarkClosed(windowName) - super()._reject() - aqt.dialogs.markClosed = savedMarkClosed - - aqt.dialogs._dialogs[windowName] = [AddCardsAndClose, None] - addCards = aqt.dialogs.open(windowName, self.window()) - - if savedMid: - deck['mid'] = savedMid - - editor = addCards.editor - ankiNote = editor.note - - if 'fields' in note: - for name, value in note['fields'].items(): - if name in ankiNote: - ankiNote[name] = value - - self.addMediaFromNote(ankiNote, note) - editor.loadNote() - - if 'tags' in note: - ankiNote.tags = note['tags'] - editor.updateTags() - - # if Anki does not Focus, the window will not notice that the - # fields are actually filled - aqt.dialogs.open(windowName, self.window()) - addCards.setAndFocusNote(editor.note) - - return ankiNote.id - - elif note is not None: - collection = self.collection() - ankiNote = anki.notes.Note(collection, model) - - # fill out card beforehand, so we can be sure of the note id - if 'fields' in note: - for name, value in note['fields'].items(): - if name in ankiNote: - ankiNote[name] = value - - self.addMediaFromNote(ankiNote, note) - - if 'tags' in note: - ankiNote.tags = note['tags'] - - def openNewWindow(): - nonlocal ankiNote - - addCards = aqt.dialogs.open('AddCards', self.window()) - - if savedMid: - deck['mid'] = savedMid - - addCards.editor.note = ankiNote - addCards.editor.loadNote() - addCards.editor.updateTags() - - addCards.activateWindow() - - aqt.dialogs.open('AddCards', self.window()) - addCards.setAndFocusNote(addCards.editor.note) - - currentWindow = aqt.dialogs._dialogs['AddCards'][1] - - if currentWindow is not None: - currentWindow.closeWithCallback(openNewWindow) - else: - openNewWindow() - - return ankiNote.id - - else: - addCards = aqt.dialogs.open('AddCards', self.window()) - addCards.activateWindow() - - return addCards.editor.note.id - - - @util.api() - def guiReviewActive(self): - return self.reviewer().card is not None and self.window().state == 'review' - - - @util.api() - def guiCurrentCard(self): - if not self.guiReviewActive(): - raise Exception('Gui review is not currently active.') - - reviewer = self.reviewer() - card = reviewer.card - model = card.model() - note = card.note() - - fields = {} - for info in model['flds']: - order = info['ord'] - name = info['name'] - fields[name] = {'value': note.fields[order], 'order': order} - - buttonList = reviewer._answerButtonList() - return { - 'cardId': card.id, - 'fields': fields, - 'fieldOrder': card.ord, - 'question': util.cardQuestion(card), - 'answer': util.cardAnswer(card), - 'buttons': [b[0] for b in buttonList], - 'nextReviews': [reviewer.mw.col.sched.nextIvlStr(reviewer.card, b[0], True) for b in buttonList], - 'modelName': model['name'], - 'deckName': self.deckNameFromId(card.did), - 'css': model['css'], - 'template': card.template()['name'] - } - - - @util.api() - def guiStartCardTimer(self): - if not self.guiReviewActive(): - return False - - card = self.reviewer().card - if card is not None: - card.startTimer() - return True - - return False - - - @util.api() - def guiShowQuestion(self): - if self.guiReviewActive(): - self.reviewer()._showQuestion() - return True - - return False - - - @util.api() - def guiShowAnswer(self): - if self.guiReviewActive(): - self.window().reviewer._showAnswer() - return True - - return False - - - @util.api() - def guiAnswerCard(self, ease): - if not self.guiReviewActive(): - return False - - reviewer = self.reviewer() - if reviewer.state != 'answer': - return False - if ease <= 0 or ease > self.scheduler().answerButtons(reviewer.card): - return False - - reviewer._answerCard(ease) - return True - - - @util.api() - def guiDeckOverview(self, name): - collection = self.collection() - if collection is not None: - deck = collection.decks.byName(name) - if deck is not None: - collection.decks.select(deck['id']) - self.window().onOverview() - return True - - return False - - - @util.api() - def guiDeckBrowser(self): - self.window().moveToState('deckBrowser') - - - @util.api() - def guiDeckReview(self, name): - if self.guiDeckOverview(name): - self.window().moveToState('review') - return True - - return False - - - @util.api() - def guiExitAnki(self): - timer = QTimer() - timer.timeout.connect(self.window().close) - timer.start(1000) # 1s should be enough to allow the response to be sent. - - - @util.api() - def guiCheckDatabase(self): - self.window().onCheckDB() - return True - - - @util.api() - def addNotes(self, notes): - results = [] - for note in notes: - try: - results.append(self.addNote(note)) - except: - results.append(None) - - return results - - - @util.api() - def canAddNotes(self, notes): - results = [] - for note in notes: - results.append(self.canAddNote(note)) - - return results - - - @util.api() - def exportPackage(self, deck, path, includeSched=False): - collection = self.collection() - if collection is not None: - deck = collection.decks.byName(deck) - if deck is not None: - exporter = AnkiPackageExporter(collection) - exporter.did = deck['id'] - exporter.includeSched = includeSched - exporter.exportInto(path) - return True - - return False - - - @util.api() - def importPackage(self, path): - collection = self.collection() - if collection is not None: - try: - self.startEditing() - importer = AnkiPackageImporter(collection, path) - importer.run() - except: - self.stopEditing() - raise - else: - self.stopEditing() - return True - - return False # # Entry # -ac = AnkiConnect() +h = host.ApiHost( + settings.query('webCorsOriginList'), + settings.query('apiKey'), + settings.query('webBindAddress'), + settings.query('webBindPort') +) + +h.register(api.gui) +h.run(settings.query('apiPollInterval')) diff --git a/plugin/api/__init__.py b/plugin/api/__init__.py new file mode 100644 index 0000000..17289c7 --- /dev/null +++ b/plugin/api/__init__.py @@ -0,0 +1 @@ +from . import gui diff --git a/plugin/api/gui.py b/plugin/api/gui.py new file mode 100644 index 0000000..50e5522 --- /dev/null +++ b/plugin/api/gui.py @@ -0,0 +1,33 @@ +# Copyright 2016-2021 Alex Yatskov +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from . import util + + +@util.api +def guiBrowse(self, query=None): + print(query) + # browser = aqt.dialogs.open('Browser', self.window()) + # browser.activateWindow() + # + # if query is not None: + # browser.form.searchEdit.lineEdit().setText(query) + # if hasattr(browser, 'onSearch'): + # browser.onSearch() + # else: + # browser.onSearchActivated() + # + # return list(map(int, browser.model.cards)) + diff --git a/plugin/api/util.py b/plugin/api/util.py new file mode 100644 index 0000000..232779e --- /dev/null +++ b/plugin/api/util.py @@ -0,0 +1,61 @@ +# Copyright 2016-2021 Alex Yatskov +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import aqt + + +def api(method): + setattr(method, 'api', True) + return method + + +def window(self): + return aqt.mw + + +def reviewer(): + return window().reviewer + + +def collection(self): + return window().col + + +def decks(self): + return collection().decks + + +def scheduler(self): + return collection().sched + + +def database(self): + return collection().db + + +def media(self): + return collection().media + + +def deckNames(): + return decks().allNames() + + +class EditScope: + def __enter__(self): + window().requireReset() + + def __exit__(self): + window().maybeReset() diff --git a/plugin/host.py b/plugin/host.py index 3ae5400..f53fc76 100644 --- a/plugin/host.py +++ b/plugin/host.py @@ -13,19 +13,22 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import PyQt5 + from . import web class ApiHost: - def __init__(self, origins, key, interval, address, port): + def __init__(self, origins, key, address, port): self.key = key self.modules = [] - self.server = web.WebServer(self.handler, origins) self.server.bindAndListen(address, port) - self.timer = QTimer() - self.timer.timeout.connect(self.advance) + + def run(self, interval): + self.timer = PyQt5.QtCore.QTimer() + self.timer.timeout.connect(self.server.advance) self.timer.start(interval) @@ -46,7 +49,7 @@ class ApiHost: method = None for module in self.modules: for methodName, methodInstance in inspect.getmembers(module, predicate=inspect.ismethod): - if getattr(methodInstance, 'api', False): + if methodName == action and getattr(methodInstance, 'api', False): method = methodInstance break @@ -57,8 +60,3 @@ class ApiHost: except Exception as e: return {'error': str(e), 'result': None} - - -def api(method): - setattr(method, 'api', True) - return decorator diff --git a/plugin/plugin b/plugin/plugin new file mode 120000 index 0000000..ffc70e2 --- /dev/null +++ b/plugin/plugin @@ -0,0 +1 @@ +/home/alex/projects/anki-connect/plugin \ No newline at end of file diff --git a/plugin/settings.py b/plugin/settings.py new file mode 100644 index 0000000..0d1cb4e --- /dev/null +++ b/plugin/settings.py @@ -0,0 +1,48 @@ +# Copyright 2016-2021 Alex Yatskov +# +# 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 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import aqt + + +def query(key): + defaults = { + 'apiKey': None, + 'apiPollInterval': 25, + 'webBindAddress': os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1'), + 'webBindPort': 8765, + 'webCorsOrigin': os.getenv('ANKICONNECT_CORS_ORIGIN'), + 'webCorsOriginList': [], + 'webTimeout': 10000, + } + + try: + value = aqt.mw.addonManager.getConfig(__name__).get(key, defaults[key]) + except: + raise Exception('setting {} not found'.format(key)) + + if key == 'webCorsOriginList': + originOld = query('webCorsOrigin') + if originOld: + value.append(originOld) + + value += [ + 'http://127.0.0.1:', + 'http://localhost:', + 'chrome-extension://', + 'moz-extension://', + ] + + return value diff --git a/plugin/util.py b/plugin/util.py index 99a6457..a78fb50 100644 --- a/plugin/util.py +++ b/plugin/util.py @@ -83,7 +83,7 @@ def setting(key): try: return aqt.mw.addonManager.getConfig(__name__).get(key, defaults[key]) except: - raise Exception('setting {} not found'.format(key)) + raise Exception(f'setting {key} not found') # diff --git a/plugin/web.py b/plugin/web.py index 70848d6..76397a3 100644 --- a/plugin/web.py +++ b/plugin/web.py @@ -157,15 +157,15 @@ class WebServer: origin = '*' allowed = True else: - origin = request.headers.get('origin') - allowed = origin in self.origins - - if not allowed: - origin = 'http://127.0.0.1' + origin = request.headers.get('origin', 'http://127.0.0.1:') + for prefix in self.origins: + if origin.startswith(prefix): + allowed = True + break try: - call = json.loads(request.body) - if call: + if request.body: + call = json.loads(request.body) call['allowed'] = allowed call['origin'] = origin body = json.dumps(self.handler(call)) @@ -182,7 +182,7 @@ class WebServer: ['Content-Length', len(body.encode('utf-8'))] ] - header = bytes() + header = '' for key, value in headers: header += f'{key}: {value}\r\n'