anki-connect/plugin/__init__.py

1396 lines
43 KiB
Python
Raw Normal View History

2020-01-05 23:42:08 +00:00
# Copyright 2016-2020 Alex Yatskov
2016-05-21 22:10:12 +00:00
#
# 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 <http://www.gnu.org/licenses/>.
2017-08-22 07:53:35 +00:00
import base64
2016-07-17 05:01:23 +00:00
import hashlib
import inspect
2016-05-21 22:10:12 +00:00
import json
import os
2017-01-30 01:34:18 +00:00
import os.path
2020-01-05 23:42:08 +00:00
import random
2017-08-21 14:10:57 +00:00
import re
2020-01-05 23:42:08 +00:00
import string
import time
import unicodedata
2017-02-19 20:46:40 +00:00
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QMessageBox
2017-02-19 20:46:40 +00:00
2020-01-05 23:42:08 +00:00
import anki
import anki.exporting
import anki.storage
2020-01-05 23:42:08 +00:00
import aqt
from anki.exporting import AnkiPackageExporter
2020-05-01 16:19:47 +00:00
from anki.importing import AnkiPackageImporter
from anki.utils import joinFields, intTime, guid64, fieldChecksum
2017-02-19 20:46:40 +00:00
2020-01-06 01:41:34 +00:00
from . import web, util
2016-05-21 22:10:12 +00:00
2017-02-19 20:07:10 +00:00
#
2018-05-07 00:52:24 +00:00
# AnkiConnect
2016-05-21 22:10:12 +00:00
#
class AnkiConnect:
def __init__(self):
2018-06-30 18:23:13 +00:00
self.log = None
2020-01-05 23:42:08 +00:00
logPath = util.setting('apiLogPath')
if logPath is not None:
self.log = open(logPath, 'w')
try:
2020-01-05 23:42:08 +00:00
self.server = web.WebServer(self.handler)
self.server.listen()
self.timer = QTimer()
self.timer.timeout.connect(self.advance)
2020-01-05 23:42:08 +00:00
self.timer.start(util.setting('apiPollInterval'))
except:
QMessageBox.critical(
self.window(),
'AnkiConnect',
2020-01-05 23:42:08 +00:00
'Failed to listen on port {}.\nMake sure it is available and is not in use.'.format(util.setting('webBindPort'))
)
2020-01-05 23:42:08 +00:00
def logEvent(self, name, data):
if self.log is not None:
self.log.write('[{}]\n'.format(name))
json.dump(data, self.log, indent=4, sort_keys=True)
self.log.write('\n\n')
2020-01-06 00:24:20 +00:00
self.log.flush()
2020-01-05 23:42:08 +00:00
def advance(self):
self.server.advance()
def handler(self, request):
2020-01-05 23:42:08 +00:00
self.logEvent('request', request)
2018-06-30 18:23:13 +00:00
name = request.get('action', '')
version = request.get('version', 4)
params = request.get('params', {})
2020-01-06 00:24:20 +00:00
key = request.get('key')
reply = {'result': None, 'error': None}
try:
2020-01-06 00:24:20 +00:00
if key != util.setting('apiKey'):
raise Exception('valid api key must be provided')
method = None
2020-01-06 00:24:20 +00:00
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)
2020-01-05 23:42:08 +00:00
self.logEvent('reply', reply)
2018-06-30 18:23:13 +00:00
return reply
def window(self):
return aqt.mw
def reviewer(self):
2018-05-07 00:52:24 +00:00
reviewer = self.window().reviewer
if reviewer is None:
raise Exception('reviewer is not available')
else:
return reviewer
def collection(self):
2018-05-07 00:52:24 +00:00
collection = self.window().col
if collection is None:
raise Exception('collection is not available')
else:
return collection
2018-05-07 05:13:21 +00:00
def decks(self):
decks = self.collection().decks
if decks is None:
raise Exception('decks are not available')
else:
return decks
def scheduler(self):
2018-05-07 00:52:24 +00:00
scheduler = self.collection().sched
if scheduler is None:
raise Exception('scheduler is not available')
else:
return scheduler
2018-05-07 21:18:20 +00:00
def database(self):
database = self.collection().db
if database is None:
raise Exception('database is not available')
else:
return database
def media(self):
2018-05-07 00:52:24 +00:00
media = self.collection().media
if media is None:
raise Exception('media is not available')
else:
return media
2018-05-07 00:52:24 +00:00
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()
2018-05-07 00:52:24 +00:00
model = collection.models.byName(note['modelName'])
if model is None:
2018-05-07 00:52:24 +00:00
raise Exception('model was not found: {}'.format(note['modelName']))
2018-05-07 00:52:24 +00:00
deck = collection.decks.byName(note['deckName'])
if deck is None:
2018-05-07 00:52:24 +00:00
raise Exception('deck was not found: {}'.format(note['deckName']))
2018-05-07 00:52:24 +00:00
ankiNote = anki.notes.Note(collection, model)
ankiNote.model()['did'] = deck['id']
2020-05-02 21:18:31 +00:00
if 'tags' in note:
ankiNote.tags = note['tags']
2018-05-07 00:52:24 +00:00
for name, value in note['fields'].items():
if name in ankiNote:
ankiNote[name] = value
allowDuplicate = False
2020-04-23 23:39:11 +00:00
duplicateScope = None
duplicateScopeDeckName = None
duplicateScopeCheckChildren = False
if 'options' in note:
if 'allowDuplicate' in note['options']:
allowDuplicate = note['options']['allowDuplicate']
if type(allowDuplicate) is not bool:
raise Exception('option parameter \'allowDuplicate\' must be boolean')
2020-04-23 23:39:11 +00:00
if 'duplicateScope' in note['options']:
duplicateScope = note['options']['duplicateScope']
if 'duplicateScopeOptions' in note['options']:
duplicateScopeOptions = note['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')
duplicateOrEmpty = self.isNoteDuplicateOrEmptyInScope(ankiNote, deck, collection, duplicateScope, duplicateScopeDeckName, duplicateScopeCheckChildren)
if duplicateOrEmpty == 1:
2018-05-07 00:52:24 +00:00
raise Exception('cannot create note because it is empty')
elif duplicateOrEmpty == 2:
if not allowDuplicate:
2018-06-20 16:52:46 +00:00
raise Exception('cannot create note because it is a duplicate')
else:
return ankiNote
elif duplicateOrEmpty == 0:
2018-05-07 00:52:24 +00:00
return ankiNote
else:
raise Exception('cannot create note for unknown reason')
def isNoteDuplicateOrEmptyInScope(self, note, deck, collection, duplicateScope, duplicateScopeDeckName, duplicateScopeCheckChildren):
"1 if first is empty; 2 if first is a duplicate, 0 otherwise."
if duplicateScope != 'deck':
result = note.dupeOrEmpty()
if result == False:
return 0
return result
# dupeOrEmpty returns if a note is a global duplicate
# the rest of the function checks to see if the note is a duplicate in the deck
val = note.fields[0]
if not val.strip():
return 1
csum = anki.utils.fieldChecksum(val)
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 = {}
dids[did] = True
if duplicateScopeCheckChildren:
for kv in collection.decks.children(did):
dids[kv[1]] = True
for noteId in note.col.db.list(
"select id from notes where csum = ? and id != ? and mid = ?",
csum,
note.id or 0,
note.mid,
):
for cardDeckId in note.col.db.list(
"select did from cards where nid = ?",
noteId
):
if cardDeckId in dids:
return 2
return 0
2018-05-07 02:11:54 +00:00
2018-05-07 01:45:56 +00:00
#
# Miscellaneous
#
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 01:45:56 +00:00
def version(self):
2020-01-05 23:42:08 +00:00
return util.setting('apiVersion')
2018-05-07 01:45:56 +00:00
2020-05-01 14:24:51 +00:00
@util.api()
def getProfiles(self):
return self.window().pm.profiles()
2018-05-07 01:45:56 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def loadProfile(self, name):
2018-12-02 03:42:56 +00:00
if name not in self.window().pm.profiles():
return False
if not self.window().isVisible():
self.window().pm.load(name)
self.window().loadProfile()
self.window().profileDiag.closeWithoutQuitting()
else:
cur_profile = self.window().pm.name
if cur_profile != name:
self.window().unloadProfileAndShowProfileManager()
self.loadProfile(name)
2018-12-02 03:42:56 +00:00
return True
2019-03-07 18:19:35 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 01:45:56 +00:00
def sync(self):
2018-05-07 18:02:51 +00:00
self.window().onSync()
2018-05-07 01:45:56 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 01:45:56 +00:00
def multi(self, actions):
return list(map(self.handler, actions))
2018-05-07 01:45:56 +00:00
@util.api()
def getNumCardsReviewedToday(self):
return self.database().scalar('select count() from revlog where id > ?', (self.scheduler().dayCutoff - 86400) * 1000)
2020-04-22 01:22:10 +00:00
@util.api()
def getCollectionStatsHTML(self, wholeCollection=True):
stats = self.collection().stats()
stats.wholeCollection = wholeCollection
return stats.report()
2018-05-07 02:11:54 +00:00
#
# Decks
#
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def deckNames(self):
2018-05-07 05:14:18 +00:00
return self.decks().allNames()
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def deckNamesAndIds(self):
decks = {}
for deck in self.deckNames():
2018-05-07 18:02:51 +00:00
decks[deck] = self.decks().id(deck)
2018-05-07 02:11:54 +00:00
return decks
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def getDecks(self, cards):
decks = {}
for card in cards:
2018-05-07 21:18:20 +00:00
did = self.database().scalar('select did from cards where id=?', card)
deck = self.decks().get(did)['name']
2018-05-07 02:11:54 +00:00
if deck in decks:
decks[deck].append(card)
else:
decks[deck] = [card]
return decks
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def createDeck(self, deck):
2018-05-07 21:18:20 +00:00
try:
self.startEditing()
did = self.decks().id(deck)
finally:
self.stopEditing()
2018-05-07 02:11:54 +00:00
2018-05-07 21:18:20 +00:00
return did
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def changeDeck(self, cards, deck):
2018-05-09 01:47:45 +00:00
self.startEditing()
2018-05-07 02:11:54 +00:00
2018-05-09 01:47:45 +00:00
did = self.collection().decks.id(deck)
mod = anki.utils.intTime()
usn = self.collection().usn()
2018-05-07 02:11:54 +00:00
2018-05-09 01:47:45 +00:00
# normal cards
scids = anki.utils.ids2str(cards)
# remove any cards from filtered deck first
self.collection().sched.remFromDyn(cards)
2018-05-07 02:11:54 +00:00
2018-05-09 01:47:45 +00:00
# then move into new deck
self.collection().db.execute('update cards set usn=?, mod=?, did=? where id in ' + scids, usn, mod, did)
self.stopEditing()
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def deleteDecks(self, decks, cardsToo=False):
2018-05-07 21:18:20 +00:00
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()
2018-05-07 02:11:54 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def getDeckConfig(self, deck):
if not deck in self.deckNames():
return False
collection = self.collection()
did = collection.decks.id(deck)
return collection.decks.confForDid(did)
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def saveDeckConfig(self, config):
collection = self.collection()
config['id'] = str(config['id'])
config['mod'] = anki.utils.intTime()
config['usn'] = collection.usn()
if not config['id'] in collection.decks.dconf:
return False
collection.decks.dconf[config['id']] = config
collection.decks.changed = True
return True
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def setDeckConfigId(self, decks, configId):
configId = str(configId)
for deck in decks:
if not deck in self.deckNames():
return False
collection = self.collection()
if not configId in collection.decks.dconf:
return False
for deck in decks:
did = str(collection.decks.id(deck))
aqt.mw.col.decks.decks[did]['conf'] = configId
return True
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def cloneDeckConfigId(self, name, cloneFrom='1'):
configId = str(cloneFrom)
if not configId in self.collection().decks.dconf:
return False
config = self.collection().decks.getConf(configId)
return self.collection().decks.confId(name, config)
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 02:11:54 +00:00
def removeDeckConfigId(self, configId):
configId = str(configId)
collection = self.collection()
if configId == 1 or not configId in collection.decks.dconf:
return False
collection.decks.remConf(configId)
return True
2020-01-05 23:42:08 +00:00
@util.api()
def storeMediaFile(self, filename, data=None, path=None, url=None):
if data:
self.deleteMediaFile(filename)
self.media().writeData(filename, base64.b64decode(data))
2020-12-12 16:41:01 +00:00
elif path:
self.deleteMediaFile(filename)
with open(path, 'rb') as f:
data = f.read()
self.media().writeData(filename, data)
elif url:
self.deleteMediaFile(filename)
downloadedData = util.download(url)
self.media().writeData(filename, downloadedData)
else:
raise Exception('You must either provide a "data" or a "url" field.')
2017-08-22 07:53:35 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-08-22 19:05:12 +00:00
def retrieveMediaFile(self, filename):
filename = os.path.basename(filename)
2020-01-05 23:42:08 +00:00
filename = unicodedata.normalize('NFC', filename)
2017-08-22 19:05:12 +00:00
filename = self.media().stripIllegal(filename)
2017-08-22 07:53:35 +00:00
2017-08-22 19:05:12 +00:00
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')
2017-08-22 07:53:35 +00:00
return False
2020-01-05 23:42:08 +00:00
@util.api()
2017-08-22 19:05:12 +00:00
def deleteMediaFile(self, filename):
try:
self.media().syncDelete(filename)
except AttributeError:
self.media().trash_files([filename])
2017-08-22 07:53:35 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def addNote(self, note):
2018-05-07 00:52:24 +00:00
ankiNote = self.createNote(note)
2016-05-21 22:34:09 +00:00
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)
2018-05-07 00:52:24 +00:00
collection = self.collection()
2016-07-17 16:38:33 +00:00
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.')
2016-05-21 22:34:09 +00:00
collection.autosave()
2016-07-17 16:38:33 +00:00
self.stopEditing()
2016-05-21 22:34:09 +00:00
2018-05-07 00:52:24 +00:00
return ankiNote.id
2016-05-21 22:10:12 +00:00
def addMedia(self, ankiNote, mediaObjectOrList, mediaType):
if mediaObjectOrList is None:
2020-03-06 03:45:13 +00:00
return
if isinstance(mediaObjectOrList, list):
mediaList = mediaObjectOrList
2020-03-06 03:45:13 +00:00
else:
mediaList = [mediaObjectOrList]
2020-03-06 03:45:13 +00:00
for media in mediaList:
if media is not None and len(media['fields']) > 0:
2020-03-06 03:45:13 +00:00
try:
data = util.download(media['url'])
skipHash = media.get('skipHash')
2020-03-06 03:45:13 +00:00
if skipHash is None:
skip = False
else:
m = hashlib.md5()
m.update(data)
skip = skipHash == m.hexdigest()
if not skip:
mediaFilename = self.media().writeData(media['filename'], data)
for field in media['fields']:
if field in ankiNote:
if mediaType is util.MediaType.Picture:
ankiNote[field] += u'<div><img src="{}"><br></div>'.format(mediaFilename)
elif mediaType is util.MediaType.Audio or mediaType is util.MediaType.Video:
ankiNote[field] += u'[sound:{}]'.format(mediaFilename)
2020-03-06 03:45:13 +00:00
except Exception as e:
errorMessage = str(e).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
for field in media['fields']:
2020-03-06 03:45:13 +00:00
if field in ankiNote:
ankiNote[field] += errorMessage
2020-01-05 23:42:08 +00:00
@util.api()
2017-02-19 20:07:10 +00:00
def canAddNote(self, note):
try:
2018-05-07 00:52:24 +00:00
return bool(self.createNote(note))
except:
return False
2016-05-21 22:10:12 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-08 22:13:49 +00:00
def updateNoteFields(self, note):
ankiNote = self.collection().getNote(note['id'])
if ankiNote is None:
raise Exception('note was not found: {}'.format(note['id']))
2018-05-07 00:52:24 +00:00
2018-05-08 22:13:49 +00:00
for name, value in note['fields'].items():
if name in ankiNote:
ankiNote[name] = value
2018-05-07 00:52:24 +00:00
2020-03-06 03:45:13 +00:00
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)
2018-05-08 22:13:49 +00:00
ankiNote.flush()
2016-05-21 22:10:12 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def addTags(self, notes, tags, add=True):
self.startEditing()
self.collection().tags.bulkAdd(notes, tags, add)
self.stopEditing()
2017-08-03 20:07:22 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def removeTags(self, notes, tags):
return self.addTags(notes, tags, False)
2020-01-05 23:42:08 +00:00
@util.api()
2018-01-14 11:26:37 +00:00
def getTags(self):
return self.collection().tags.all()
@util.api()
def setEaseFactors(self, cards, easeFactors):
couldSetEaseFactors = []
ind = 0
for card in cards:
ankiCard = self.collection().getCard(card)
if ankiCard is None:
raise Exception('card was not found: {}'.format(card['id']))
couldSetEaseFactors.append(False)
else:
couldSetEaseFactors.append(True)
ankiCard.factor = easeFactors[ind]
ankiCard.flush()
ind += 1
return couldSetEaseFactors
@util.api()
def getEaseFactors(self, cards):
easeFactors = []
for card in cards:
ankiCard = self.collection().getCard(card)
easeFactors.append(ankiCard.factor)
2018-01-14 11:26:37 +00:00
return easeFactors
2018-01-14 11:26:37 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def suspend(self, cards, suspend=True):
for card in cards:
2018-05-07 00:52:24 +00:00
if self.suspended(card) == suspend:
cards.remove(card)
2018-05-07 00:52:24 +00:00
if len(cards) == 0:
return False
2018-05-07 00:52:24 +00:00
scheduler = self.scheduler()
self.startEditing()
if suspend:
scheduler.suspendCards(cards)
else:
scheduler.unsuspendCards(cards)
self.stopEditing()
return True
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def unsuspend(self, cards):
self.suspend(cards, False)
2020-01-05 23:42:08 +00:00
@util.api()
2018-05-07 00:52:24 +00:00
def suspended(self, card):
2018-03-11 12:27:19 +00:00
card = self.collection().getCard(card)
return card.queue == -1
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def areSuspended(self, cards):
suspended = []
for card in cards:
2018-05-07 00:52:24 +00:00
suspended.append(self.suspended(card))
return suspended
2020-01-05 23:42:08 +00:00
@util.api()
def areDue(self, cards):
due = []
for card in cards:
2018-05-07 00:52:24 +00:00
if self.findCards('cid:{} is:new'.format(card)):
due.append(True)
else:
2018-05-07 00:52:24 +00:00
date, ivl = self.collection().db.all('select id/1000.0, ivl from revlog where cid = ?', card)[-1]
if ivl >= -1200:
2018-06-03 15:26:33 +00:00
due.append(bool(self.findCards('cid:{} is:due'.format(card))))
else:
2020-01-05 23:42:08 +00:00
due.append(date - ivl <= time.time())
return due
2017-08-03 20:07:22 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-08-09 17:40:09 +00:00
def getIntervals(self, cards, complete=False):
intervals = []
for card in cards:
2018-05-07 00:52:24 +00:00
if self.findCards('cid:{} is:new'.format(card)):
intervals.append(0)
2018-05-07 00:52:24 +00:00
else:
interval = self.collection().db.list('select ivl from revlog where cid = ?', card)
if not complete:
interval = interval[-1]
intervals.append(interval)
2017-08-09 17:40:09 +00:00
return intervals
2020-01-05 23:42:08 +00:00
@util.api()
2016-05-21 22:10:12 +00:00
def modelNames(self):
2018-05-07 00:52:24 +00:00
return self.collection().models.allNames()
2016-05-21 22:10:12 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def createModel(self, modelName, inOrderFields, cardTemplates, css = None):
# 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)
# Create fields and add them to Note
for field in inOrderFields:
fm = mm.newField(field)
mm.addField(m, fm)
2019-03-07 18:19:35 +00:00
# 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:
2020-03-13 22:40:27 +00:00
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
2020-01-05 23:42:08 +00:00
@util.api()
2017-08-21 16:15:11 +00:00
def modelNamesAndIds(self):
models = {}
2018-05-07 00:52:24 +00:00
for model in self.modelNames():
models[model] = int(self.collection().models.byName(model)['id'])
2017-08-21 16:15:11 +00:00
return models
2020-01-05 23:42:08 +00:00
@util.api()
2017-07-01 19:49:44 +00:00
def modelNameFromId(self, modelId):
2018-05-07 00:52:24 +00:00
model = self.collection().models.get(modelId)
if model is None:
raise Exception('model was not found: {}'.format(modelId))
else:
return model['name']
2016-05-29 17:31:24 +00:00
2017-07-01 19:41:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-07-01 19:41:27 +00:00
def modelFieldNames(self, modelName):
2018-05-07 00:52:24 +00:00
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']]
2016-05-21 22:10:12 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-08-21 14:10:57 +00:00
def modelFieldsOnTemplates(self, modelName):
model = self.collection().models.byName(modelName)
2018-05-07 00:52:24 +00:00
if model is None:
raise Exception('model was not found: {}'.format(modelName))
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
templates = {}
for template in model['tmpls']:
fields = []
for side in ['qfmt', 'afmt']:
fieldsForSide = []
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
# 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]
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
# 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)
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
fields.append(fieldsForSide)
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
templates[template['name']] = fields
2017-08-21 14:10:57 +00:00
2018-05-07 00:52:24 +00:00
return templates
2017-08-21 14:10:57 +00:00
2020-01-05 23:42:08 +00:00
@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
2020-01-05 23:42:08 +00:00
@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']}
2020-01-05 23:42:08 +00:00
@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()
2020-01-05 23:42:08 +00:00
@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()
2017-08-21 14:10:57 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-07-01 19:49:44 +00:00
def deckNameFromId(self, deckId):
2018-05-07 00:52:24 +00:00
deck = self.collection().decks.get(deckId)
if deck is None:
raise Exception('deck was not found: {}'.format(deckId))
else:
return deck['name']
2017-07-01 19:41:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def findNotes(self, query=None):
2018-05-07 00:52:24 +00:00
if query is None:
return []
2018-05-07 00:52:24 +00:00
else:
return list(map(int, self.collection().findNotes(query)))
2020-01-05 23:42:08 +00:00
@util.api()
def findCards(self, query=None):
2018-05-07 01:45:56 +00:00
if query is None:
return []
2018-05-07 00:52:24 +00:00
else:
return list(map(int, self.collection().findCards(query)))
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def cardsInfo(self, cards):
result = []
for cid in cards:
try:
card = self.collection().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}
2018-03-31 21:40:01 +00:00
result.append({
'cardId': card.id,
'fields': fields,
'fieldOrder': card.ord,
2020-04-04 17:28:37 +00:00
'question': util.getQuestion(card),
'answer': util.getAnswer(card),
'modelName': model['name'],
'ord': card.ord,
'deckName': self.deckNameFromId(card.did),
'css': model['css'],
2018-03-31 21:40:01 +00:00
'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 TypeError as e:
# Anki will give a TypeError if the card ID does not exist.
2018-03-11 22:10:07 +00:00
# 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
2018-03-31 21:40:01 +00:00
@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: return
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)
2020-01-05 23:42:08 +00:00
@util.api()
def notesInfo(self, notes):
result = []
for nid in notes:
try:
note = self.collection().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}
2018-03-31 21:40:01 +00:00
result.append({
'noteId': note.id,
'tags' : note.tags,
'fields': fields,
'modelName': model['name'],
2018-05-07 00:52:24 +00:00
'cards': self.collection().db.list('select id from cards where nid = ? order by ord', note.id)
})
except TypeError as e:
# Anki will give a TypeError if the note ID does not exist.
2018-03-11 22:10:07 +00:00
# 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({})
2018-05-07 00:52:24 +00:00
return result
2020-01-05 23:42:08 +00:00
@util.api()
2019-02-26 17:28:36 +00:00
def deleteNotes(self, notes):
try:
self.collection().remNotes(notes)
finally:
self.stopEditing()
2017-08-13 08:02:59 +00:00
2017-08-13 09:05:56 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-08-09 18:05:00 +00:00
def cardsToNotes(self, cards):
return self.collection().db.list('select distinct nid from cards where id in ' + anki.utils.ids2str(cards))
2017-08-09 18:05:00 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def guiBrowse(self, query=None):
2017-05-28 22:54:28 +00:00
browser = aqt.dialogs.open('Browser', self.window())
2017-05-27 12:14:40 +00:00
browser.activateWindow()
2017-05-28 22:54:28 +00:00
if query is not None:
2017-05-27 12:14:40 +00:00
browser.form.searchEdit.lineEdit().setText(query)
if hasattr(browser, 'onSearch'):
browser.onSearch()
else:
browser.onSearchActivated()
2017-05-28 22:54:28 +00:00
return list(map(int, browser.model.cards))
2017-05-27 12:14:40 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def guiAddCards(self, note=None):
2017-05-27 12:14:40 +00:00
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']))
self.collection().decks.select(deck['id'])
2019-01-31 23:15:32 +00:00
savedMid = deck.pop('mid', None)
model = collection.models.byName(note['modelName'])
if model is None:
raise Exception('model was not found: {}'.format(note['modelName']))
self.collection().models.setCurrent(model)
self.collection().models.update(model)
2019-01-18 02:56:02 +00:00
closeAfterAdding = False
if note is not None and 'options' in note:
2019-01-18 02:56:02 +00:00
if 'closeAfterAdding' in note['options']:
closeAfterAdding = note['options']['closeAfterAdding']
if type(closeAfterAdding) is not bool:
raise Exception('option parameter \'closeAfterAdding\' must be boolean')
2019-01-31 23:15:32 +00:00
addCards = None
2019-01-24 06:07:53 +00:00
2019-01-18 02:56:02 +00:00
if closeAfterAdding:
2020-01-05 23:42:08 +00:00
randomString = ''.join(random.choice(string.ascii_letters) for _ in range(10))
2019-01-24 06:07:53 +00:00
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
2019-01-24 08:04:52 +00:00
self.modelHasChanged = True
super().__init__(mw)
self.addButton.setText("Add and Close")
self.addButton.setShortcut(aqt.qt.QKeySequence("Ctrl+Return"))
2019-05-02 16:33:24 +00:00
def _addCards(self):
super()._addCards()
2019-05-02 16:33:24 +00:00
# if adding was successful it must mean it was added to the history of the window
if len(self.history):
self.reject()
2019-05-02 16:33:24 +00:00
def onModelChange(self):
if self.isActiveWindow():
super().onModelChange()
2019-05-02 16:33:24 +00:00
self.modelHasChanged = True
2019-01-24 08:04:52 +00:00
def onReset(self, model=None, keep=False):
if self.isActiveWindow() or self.modelHasChanged:
super().onReset(model, keep)
self.modelHasChanged = False
2019-01-24 07:36:11 +00:00
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
2019-05-02 16:33:24 +00:00
def _reject(self):
savedMarkClosed = aqt.dialogs.markClosed
aqt.dialogs.markClosed = lambda _: savedMarkClosed(windowName)
super()._reject()
aqt.dialogs.markClosed = savedMarkClosed
2019-01-24 06:07:53 +00:00
aqt.dialogs._dialogs[windowName] = [AddCardsAndClose, None]
addCards = aqt.dialogs.open(windowName, self.window())
2017-05-27 12:14:40 +00:00
2019-01-31 23:15:32 +00:00
if savedMid:
deck['mid'] = savedMid
2019-01-24 06:07:53 +00:00
editor = addCards.editor
ankiNote = editor.note
if 'fields' in note:
for name, value in note['fields'].items():
if name in ankiNote:
ankiNote[name] = value
editor.loadNote()
if 'tags' in note:
ankiNote.tags = note['tags']
editor.updateTags()
2019-01-24 06:07:53 +00:00
# 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)
2019-01-24 06:07:53 +00:00
2019-05-04 21:24:13 +00:00
return ankiNote.id
2019-01-24 06:07:53 +00:00
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
if 'tags' in note:
ankiNote.tags = note['tags']
2019-01-24 06:07:53 +00:00
def openNewWindow():
nonlocal ankiNote
2019-01-24 06:07:53 +00:00
addCards = aqt.dialogs.open('AddCards', self.window())
2019-01-31 23:15:32 +00:00
if savedMid:
deck['mid'] = savedMid
addCards.editor.note = ankiNote
addCards.editor.loadNote()
addCards.editor.updateTags()
2019-01-24 06:07:53 +00:00
addCards.activateWindow()
2019-01-24 07:36:11 +00:00
aqt.dialogs.open('AddCards', self.window())
addCards.setAndFocusNote(addCards.editor.note)
currentWindow = aqt.dialogs._dialogs['AddCards'][1]
2019-01-24 06:07:53 +00:00
if currentWindow is not None:
currentWindow.closeWithCallback(openNewWindow)
2019-01-24 06:07:53 +00:00
else:
openNewWindow()
return ankiNote.id
2019-05-04 21:24:13 +00:00
2019-01-24 06:07:53 +00:00
else:
addCards = aqt.dialogs.open('AddCards', self.window())
addCards.activateWindow()
2017-05-27 12:14:40 +00:00
2019-05-04 21:24:13 +00:00
return addCards.editor.note.id
2020-01-05 23:42:08 +00:00
@util.api()
2017-07-01 19:41:27 +00:00
def guiReviewActive(self):
2017-07-03 00:27:31 +00:00
return self.reviewer().card is not None and self.window().state == 'review'
2017-07-01 19:41:27 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-07-01 19:41:27 +00:00
def guiCurrentCard(self):
2017-07-01 19:49:44 +00:00
if not self.guiReviewActive():
2018-03-11 22:10:07 +00:00
raise Exception('Gui review is not currently active.')
2017-07-01 19:41:27 +00:00
reviewer = self.reviewer()
card = reviewer.card
2017-07-03 00:52:57 +00:00
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}
2017-06-29 04:17:11 +00:00
if card is not None:
2019-05-25 19:14:09 +00:00
buttonList = reviewer._answerButtonList()
return {
2017-07-01 19:41:27 +00:00
'cardId': card.id,
2017-07-03 00:52:57 +00:00
'fields': fields,
'fieldOrder': card.ord,
2020-04-04 17:28:37 +00:00
'question': util.getQuestion(card),
'answer': util.getAnswer(card),
2019-05-25 19:14:09 +00:00
'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']
}
2017-06-29 04:17:11 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
2017-08-16 12:04:05 +00:00
def guiStartCardTimer(self):
if not self.guiReviewActive():
return False
card = self.reviewer().card
if card is not None:
card.startTimer()
return True
else:
return False
2017-08-16 12:04:05 +00:00
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def guiShowQuestion(self):
2017-07-01 19:41:27 +00:00
if self.guiReviewActive():
self.reviewer()._showQuestion()
return True
else:
return False
2020-01-05 23:42:08 +00:00
@util.api()
def guiShowAnswer(self):
2017-07-01 19:41:27 +00:00
if self.guiReviewActive():
self.window().reviewer._showAnswer()
2017-07-01 19:41:27 +00:00
return True
else:
return False
2020-01-05 23:42:08 +00:00
@util.api()
2017-07-01 20:16:51 +00:00
def guiAnswerCard(self, ease):
2017-07-01 19:41:27 +00:00
if not self.guiReviewActive():
2017-06-29 04:17:11 +00:00
return False
2017-07-01 19:41:27 +00:00
reviewer = self.reviewer()
if reviewer.state != 'answer':
2017-06-29 04:17:11 +00:00
return False
2017-07-01 20:16:51 +00:00
if ease <= 0 or ease > self.scheduler().answerButtons(reviewer.card):
2017-06-29 04:17:11 +00:00
return False
2017-07-01 19:41:27 +00:00
reviewer._answerCard(ease)
return True
2017-07-03 00:27:31 +00:00
2020-01-05 23:42:08 +00:00
@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
2017-07-03 00:27:31 +00:00
return False
2017-07-03 00:27:31 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def guiDeckBrowser(self):
2017-07-03 00:27:31 +00:00
self.window().moveToState('deckBrowser')
2020-01-05 23:42:08 +00:00
@util.api()
2017-07-03 00:27:31 +00:00
def guiDeckReview(self, name):
if self.guiDeckOverview(name):
self.window().moveToState('review')
return True
else:
return False
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def guiExitAnki(self):
timer = QTimer()
2020-01-05 23:42:08 +00:00
timer.timeout.connect(self.window().close)
timer.start(1000) # 1s should be enough to allow the response to be sent.
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def addNotes(self, notes):
results = []
for note in notes:
try:
2018-05-06 20:24:40 +00:00
results.append(self.addNote(note))
except Exception:
results.append(None)
2018-03-31 21:40:01 +00:00
return results
2018-03-31 21:40:01 +00:00
2020-01-05 23:42:08 +00:00
@util.api()
def canAddNotes(self, notes):
results = []
for note in notes:
2018-05-06 20:24:40 +00:00
results.append(self.canAddNote(note))
2018-03-31 21:40:01 +00:00
return results
2018-02-21 13:16:52 +00:00
@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)
2020-04-22 01:22:10 +00:00
exporter.did = deck['id']
exporter.includeSched = includeSched
exporter.exportInto(path)
return True
return False
2020-05-01 16:19:47 +00:00
@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
2016-05-21 22:10:12 +00:00
#
2018-05-07 00:52:24 +00:00
# Entry
2016-05-21 22:10:12 +00:00
#
ac = AnkiConnect()