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
|
2017-07-04 19:35:04 +00:00
|
|
|
import inspect
|
2016-05-21 22:10:12 +00:00
|
|
|
import json
|
2017-08-28 20:15:42 +00:00
|
|
|
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
|
|
|
|
2020-01-05 19:42:59 +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
|
2020-03-17 05:29:49 +00:00
|
|
|
import anki.exporting
|
|
|
|
import anki.storage
|
2020-01-05 23:42:08 +00:00
|
|
|
import aqt
|
2020-03-17 05:29:49 +00:00
|
|
|
from anki.exporting import AnkiPackageExporter
|
2020-05-01 16:19:47 +00:00
|
|
|
from anki.importing import AnkiPackageImporter
|
2020-07-12 19:53:31 +00:00
|
|
|
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
|
|
|
#
|
|
|
|
|
2018-05-06 19:59:31 +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')
|
2018-05-06 19:59:31 +00:00
|
|
|
|
|
|
|
try:
|
2020-01-05 23:42:08 +00:00
|
|
|
self.server = web.WebServer(self.handler)
|
2018-05-06 19:59:31 +00:00
|
|
|
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'))
|
2018-05-06 19:59:31 +00:00
|
|
|
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'))
|
2018-05-06 19:59:31 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +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
|
|
|
|
2018-05-06 19:59:31 +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')
|
2018-05-06 19:59:31 +00:00
|
|
|
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')
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
method = None
|
2020-01-06 00:24:20 +00:00
|
|
|
|
2018-05-06 19:59:31 +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)
|
2019-06-01 18:06:08 +00:00
|
|
|
|
|
|
|
if version <= 4:
|
|
|
|
reply = reply['result']
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
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
|
2018-05-06 19:59:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
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
|
2018-05-06 19:59:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
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-06 19:59:31 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
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-06 19:59:31 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
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-06 19:59:31 +00:00
|
|
|
|
|
|
|
|
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):
|
2018-05-06 19:59:31 +00:00
|
|
|
collection = self.collection()
|
|
|
|
|
2018-05-07 00:52:24 +00:00
|
|
|
model = collection.models.byName(note['modelName'])
|
2018-05-06 19:59:31 +00:00
|
|
|
if model is None:
|
2018-05-07 00:52:24 +00:00
|
|
|
raise Exception('model was not found: {}'.format(note['modelName']))
|
2018-05-06 19:59:31 +00:00
|
|
|
|
2018-05-07 00:52:24 +00:00
|
|
|
deck = collection.decks.byName(note['deckName'])
|
2018-05-06 19:59:31 +00:00
|
|
|
if deck is None:
|
2018-05-07 00:52:24 +00:00
|
|
|
raise Exception('deck was not found: {}'.format(note['deckName']))
|
2018-05-06 19:59:31 +00:00
|
|
|
|
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-06 19:59:31 +00:00
|
|
|
|
2018-05-07 00:52:24 +00:00
|
|
|
for name, value in note['fields'].items():
|
|
|
|
if name in ankiNote:
|
|
|
|
ankiNote[name] = value
|
2018-05-06 19:59:31 +00:00
|
|
|
|
2018-11-07 20:05:02 +00:00
|
|
|
allowDuplicate = False
|
2020-04-23 23:39:11 +00:00
|
|
|
duplicateScope = None
|
2020-11-02 00:14:30 +00:00
|
|
|
duplicateScopeDeckName = None
|
|
|
|
duplicateScopeCheckChildren = False
|
2018-11-07 20:05:02 +00:00
|
|
|
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']
|
2020-11-02 00:14:30 +00:00
|
|
|
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)
|
2018-05-06 19:59:31 +00:00
|
|
|
if duplicateOrEmpty == 1:
|
2018-05-07 00:52:24 +00:00
|
|
|
raise Exception('cannot create note because it is empty')
|
2018-05-06 19:59:31 +00:00
|
|
|
elif duplicateOrEmpty == 2:
|
2018-11-07 20:05:02 +00:00
|
|
|
if not allowDuplicate:
|
2018-06-20 16:52:46 +00:00
|
|
|
raise Exception('cannot create note because it is a duplicate')
|
2018-11-07 20:05:02 +00:00
|
|
|
else:
|
|
|
|
return ankiNote
|
2020-12-06 05:23:03 +00:00
|
|
|
elif duplicateOrEmpty == 0:
|
2018-05-07 00:52:24 +00:00
|
|
|
return ankiNote
|
|
|
|
else:
|
|
|
|
raise Exception('cannot create note for unknown reason')
|
2018-05-06 19:59:31 +00:00
|
|
|
|
2020-11-02 00:14:30 +00:00
|
|
|
def isNoteDuplicateOrEmptyInScope(self, note, deck, collection, duplicateScope, duplicateScopeDeckName, duplicateScopeCheckChildren):
|
2020-12-06 05:23:03 +00:00
|
|
|
"1 if first is empty; 2 if first is a duplicate, 0 otherwise."
|
|
|
|
if duplicateScope != 'deck':
|
|
|
|
result = note.dupeOrEmpty()
|
|
|
|
if result == False:
|
|
|
|
return 0
|
2020-02-02 21:45:05 +00:00
|
|
|
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
|
2020-05-09 00:16:05 +00:00
|
|
|
val = note.fields[0]
|
2020-12-06 05:23:03 +00:00
|
|
|
if not val.strip():
|
|
|
|
return 1
|
2020-02-02 21:45:05 +00:00
|
|
|
csum = anki.utils.fieldChecksum(val)
|
|
|
|
|
2020-12-06 05:23:03 +00:00
|
|
|
did = deck['id']
|
2020-11-02 00:14:30 +00:00
|
|
|
if duplicateScopeDeckName is not None:
|
|
|
|
deck2 = collection.decks.byName(duplicateScopeDeckName)
|
|
|
|
if deck2 is None:
|
|
|
|
# Invalid deck, so cannot be duplicate
|
2020-12-06 05:23:03 +00:00
|
|
|
return 0
|
2020-11-02 00:14:30 +00:00
|
|
|
did = deck2['id']
|
|
|
|
|
|
|
|
dids = {}
|
2020-12-06 05:23:03 +00:00
|
|
|
dids[did] = True
|
2020-11-02 00:14:30 +00:00
|
|
|
if duplicateScopeCheckChildren:
|
|
|
|
for kv in collection.decks.children(did):
|
|
|
|
dids[kv[1]] = True
|
|
|
|
|
2020-02-02 21:45:05 +00:00
|
|
|
for noteId in note.col.db.list(
|
|
|
|
"select id from notes where csum = ? and id != ? and mid = ?",
|
|
|
|
csum,
|
|
|
|
note.id or 0,
|
|
|
|
note.mid,
|
|
|
|
):
|
2020-11-02 00:14:30 +00:00
|
|
|
for cardDeckId in note.col.db.list(
|
|
|
|
"select did from cards where nid = ?",
|
|
|
|
noteId
|
2020-02-02 21:45:05 +00:00
|
|
|
):
|
2020-11-02 00:14:30 +00:00
|
|
|
if cardDeckId in dids:
|
|
|
|
return 2
|
2020-12-06 05:23:03 +00:00
|
|
|
return 0
|
2020-02-02 21:45:05 +00:00
|
|
|
|
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()
|
2018-12-01 16:13:38 +00:00
|
|
|
def loadProfile(self, name):
|
2018-12-02 03:42:56 +00:00
|
|
|
if name not in self.window().pm.profiles():
|
|
|
|
return False
|
2018-12-01 16:13:38 +00:00
|
|
|
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
|
|
|
|
2018-12-01 16:13:38 +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):
|
2018-07-29 08:11:50 +00:00
|
|
|
return list(map(self.handler, actions))
|
2018-05-07 01:45:56 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
|
2020-04-19 01:16:52 +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
|
|
|
|
2020-04-19 01:16:52 +00:00
|
|
|
|
2020-05-03 19:06:43 +00:00
|
|
|
@util.api()
|
2020-05-08 01:49:48 +00:00
|
|
|
def getCollectionStatsHTML(self, wholeCollection=True):
|
|
|
|
stats = self.collection().stats()
|
|
|
|
stats.wholeCollection = wholeCollection
|
|
|
|
return stats.report()
|
2020-05-03 19:06:43 +00:00
|
|
|
|
|
|
|
|
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()
|
2020-12-08 02:50:37 +00:00
|
|
|
def storeMediaFile(self, filename, data=None, path=None, url=None):
|
2020-03-13 23:36:17 +00:00
|
|
|
if data:
|
|
|
|
self.deleteMediaFile(filename)
|
|
|
|
self.media().writeData(filename, base64.b64decode(data))
|
2020-12-12 16:41:01 +00:00
|
|
|
elif path:
|
2020-12-08 02:50:37 +00:00
|
|
|
self.deleteMediaFile(filename)
|
|
|
|
with open(path, 'rb') as f:
|
|
|
|
data = f.read()
|
|
|
|
self.media().writeData(filename, data)
|
2020-03-13 23:36:17 +00:00
|
|
|
elif url:
|
2020-04-08 04:18:48 +00:00
|
|
|
self.deleteMediaFile(filename)
|
2020-03-13 23:36:17 +00:00
|
|
|
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):
|
2020-04-08 04:18:48 +00:00
|
|
|
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()
|
2018-05-06 19:59:31 +00:00
|
|
|
def addNote(self, note):
|
2018-05-07 00:52:24 +00:00
|
|
|
ankiNote = self.createNote(note)
|
2016-05-21 22:34:09 +00:00
|
|
|
|
2020-02-23 10:59:19 +00:00
|
|
|
audioObjectOrList = note.get('audio')
|
2020-12-06 05:24:50 +00:00
|
|
|
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()
|
2019-03-05 02:53:57 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2020-12-06 05:24:50 +00:00
|
|
|
def addMedia(self, ankiNote, mediaObjectOrList, mediaType):
|
|
|
|
if mediaObjectOrList is None:
|
2020-03-06 03:45:13 +00:00
|
|
|
return
|
|
|
|
|
2020-12-06 05:24:50 +00:00
|
|
|
if isinstance(mediaObjectOrList, list):
|
|
|
|
mediaList = mediaObjectOrList
|
2020-03-06 03:45:13 +00:00
|
|
|
else:
|
2020-12-06 05:24:50 +00:00
|
|
|
mediaList = [mediaObjectOrList]
|
2020-03-06 03:45:13 +00:00
|
|
|
|
2020-12-06 05:24:50 +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:
|
2020-12-06 05:24:50 +00:00
|
|
|
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:
|
2020-12-06 05:24:50 +00:00
|
|
|
mediaFilename = self.media().writeData(media['filename'], data)
|
|
|
|
for field in media['fields']:
|
2020-02-23 10:59:19 +00:00
|
|
|
if field in ankiNote:
|
2020-12-06 05:24:50 +00:00
|
|
|
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('&', '&').replace('<', '<').replace('>', '>')
|
2020-12-06 05:24:50 +00:00
|
|
|
for field in media['fields']:
|
2020-03-06 03:45:13 +00:00
|
|
|
if field in ankiNote:
|
|
|
|
ankiNote[field] += errorMessage
|
2020-02-23 10:59:19 +00:00
|
|
|
|
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-02-19 20:07:10 +00:00
|
|
|
def canAddNote(self, note):
|
2018-03-16 12:51:48 +00:00
|
|
|
try:
|
2018-05-07 00:52:24 +00:00
|
|
|
return bool(self.createNote(note))
|
2018-03-16 12:51:48 +00:00
|
|
|
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')
|
2020-12-06 05:24:50 +00:00
|
|
|
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)
|
2020-02-23 10:59:19 +00:00
|
|
|
|
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()
|
2017-08-03 21:21:59 +00:00
|
|
|
def addTags(self, notes, tags, add=True):
|
2017-08-05 08:24:03 +00:00
|
|
|
self.startEditing()
|
2017-08-06 01:29:59 +00:00
|
|
|
self.collection().tags.bulkAdd(notes, tags, add)
|
2017-08-05 08:24:03 +00:00
|
|
|
self.stopEditing()
|
2017-08-03 20:07:22 +00:00
|
|
|
|
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2018-05-06 19:59:31 +00:00
|
|
|
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()
|
2020-06-10 23:21:58 +00:00
|
|
|
|
|
|
|
@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
|
|
|
|
2020-06-10 23:21:58 +00:00
|
|
|
return easeFactors
|
2018-01-14 11:26:37 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-08-03 21:21:59 +00:00
|
|
|
def suspend(self, cards, suspend=True):
|
2017-08-06 01:29:59 +00:00
|
|
|
for card in cards:
|
2018-05-07 00:52:24 +00:00
|
|
|
if self.suspended(card) == suspend:
|
2017-08-06 01:29:59 +00:00
|
|
|
cards.remove(card)
|
|
|
|
|
2018-05-07 00:52:24 +00:00
|
|
|
if len(cards) == 0:
|
|
|
|
return False
|
2017-08-06 01:29:59 +00:00
|
|
|
|
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
|
2017-08-06 01:29:59 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2018-05-06 19:59:31 +00:00
|
|
|
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)
|
2018-05-06 19:59:31 +00:00
|
|
|
return card.queue == -1
|
2017-08-06 01:29:59 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-08-12 14:57:28 +00:00
|
|
|
def areSuspended(self, cards):
|
|
|
|
suspended = []
|
|
|
|
for card in cards:
|
2018-05-07 00:52:24 +00:00
|
|
|
suspended.append(self.suspended(card))
|
|
|
|
|
2017-08-12 14:57:28 +00:00
|
|
|
return suspended
|
|
|
|
|
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-08-12 14:57:28 +00:00
|
|
|
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)):
|
2017-08-12 14:57:28 +00:00
|
|
|
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))))
|
2017-08-12 14:57:28 +00:00
|
|
|
else:
|
2020-01-05 23:42:08 +00:00
|
|
|
due.append(date - ivl <= time.time())
|
2017-08-12 14:57:28 +00:00
|
|
|
|
|
|
|
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)):
|
2017-08-12 15:21:04 +00:00
|
|
|
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-12 15:21:04 +00:00
|
|
|
|
2017-08-09 17:40:09 +00:00
|
|
|
return intervals
|
|
|
|
|
|
|
|
|
2017-08-17 12:25:06 +00:00
|
|
|
|
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()
|
2019-03-01 17:38:30 +00:00
|
|
|
def createModel(self, modelName, inOrderFields, cardTemplates, css = None):
|
2019-02-22 18:44:51 +00:00
|
|
|
# https://github.com/dae/anki/blob/b06b70f7214fb1f2ce33ba06d2b095384b81f874/anki/stdmodels.py
|
2019-03-01 17:38:30 +00:00
|
|
|
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')
|
|
|
|
|
2019-02-22 18:44:51 +00:00
|
|
|
collection = self.collection()
|
|
|
|
mm = collection.models
|
|
|
|
|
|
|
|
# Generate new Note
|
2020-06-03 03:27:33 +00:00
|
|
|
m = mm.new(modelName)
|
2019-02-22 18:44:51 +00:00
|
|
|
|
|
|
|
# Create fields and add them to Note
|
|
|
|
for field in inOrderFields:
|
2020-06-03 03:27:33 +00:00
|
|
|
fm = mm.newField(field)
|
2019-02-22 18:44:51 +00:00
|
|
|
mm.addField(m, fm)
|
2019-03-07 18:19:35 +00:00
|
|
|
|
2019-03-01 17:38:30 +00:00
|
|
|
# Add shared css to model if exists. Use default otherwise
|
|
|
|
if (css is not None):
|
|
|
|
m['css'] = css
|
2019-02-22 18:44:51 +00:00
|
|
|
|
|
|
|
# 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']
|
|
|
|
|
2020-06-03 03:27:33 +00:00
|
|
|
t = mm.newTemplate(cardName)
|
2019-02-22 18:44:51 +00:00
|
|
|
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()
|
2019-10-27 20:41:53 +00:00
|
|
|
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()
|
2019-10-27 20:41:53 +00:00
|
|
|
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()
|
2019-10-27 20:41:53 +00:00
|
|
|
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()
|
2019-10-27 20:41:53 +00:00
|
|
|
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()
|
2017-08-03 21:31:47 +00:00
|
|
|
def findNotes(self, query=None):
|
2018-05-07 00:52:24 +00:00
|
|
|
if query is None:
|
2017-08-03 21:31:47 +00:00
|
|
|
return []
|
2018-05-07 00:52:24 +00:00
|
|
|
else:
|
2020-04-30 23:20:55 +00:00
|
|
|
return list(map(int, self.collection().findNotes(query)))
|
2017-08-03 21:31:47 +00:00
|
|
|
|
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-08-03 21:31:47 +00:00
|
|
|
def findCards(self, query=None):
|
2018-05-07 01:45:56 +00:00
|
|
|
if query is None:
|
2017-08-03 21:31:47 +00:00
|
|
|
return []
|
2018-05-07 00:52:24 +00:00
|
|
|
else:
|
2020-04-30 23:20:55 +00:00
|
|
|
return list(map(int, self.collection().findCards(query)))
|
2017-08-03 21:31:47 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2018-05-06 19:59:31 +00:00
|
|
|
def cardsInfo(self, cards):
|
2018-01-06 19:48:25 +00:00
|
|
|
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']
|
2018-01-09 13:09:38 +00:00
|
|
|
fields[name] = {'value': note.fields[order], 'order': order}
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-01-06 19:48:25 +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),
|
2018-01-06 19:48:25 +00:00
|
|
|
'modelName': model['name'],
|
2020-07-12 19:53:31 +00:00
|
|
|
'ord': card.ord,
|
2018-01-06 19:48:25 +00:00
|
|
|
'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,
|
2018-01-06 19:48:25 +00:00
|
|
|
# so an ease of 310% would be reported as 3100
|
|
|
|
'interval': card.ivl,
|
2020-07-12 19:53:31 +00:00
|
|
|
'note': card.nid,
|
|
|
|
'type': card.type,
|
|
|
|
'queue': card.queue,
|
|
|
|
'due': card.due,
|
|
|
|
'reps': card.reps,
|
|
|
|
'lapses': card.lapses,
|
|
|
|
'left': card.left,
|
2018-01-06 19:48:25 +00:00
|
|
|
})
|
|
|
|
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
|
2018-01-06 19:48:25 +00:00
|
|
|
# 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
|
|
|
|
2020-07-12 19:53:31 +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()
|
2018-05-06 19:59:31 +00:00
|
|
|
def notesInfo(self, notes):
|
2018-01-06 19:48:25 +00:00
|
|
|
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']
|
2018-01-09 13:09:38 +00:00
|
|
|
fields[name] = {'value': note.fields[order], 'order': order}
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-01-06 19:48:25 +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)
|
2018-01-06 19:48:25 +00:00
|
|
|
})
|
|
|
|
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
|
2018-01-06 19:48:25 +00:00
|
|
|
# returned result, so that the items of the input and return
|
|
|
|
# lists correspond.
|
|
|
|
result.append({})
|
2018-05-07 00:52:24 +00:00
|
|
|
|
2018-01-06 19:48:25 +00:00
|
|
|
return result
|
|
|
|
|
2017-08-03 21:31:47 +00:00
|
|
|
|
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):
|
2017-08-12 14:57:28 +00:00
|
|
|
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()
|
2017-07-04 19:35:04 +00:00
|
|
|
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
|
|
|
|
2017-07-04 19:35:04 +00:00
|
|
|
if query is not None:
|
2017-05-27 12:14:40 +00:00
|
|
|
browser.form.searchEdit.lineEdit().setText(query)
|
2017-07-04 19:35:04 +00:00
|
|
|
if hasattr(browser, 'onSearch'):
|
|
|
|
browser.onSearch()
|
|
|
|
else:
|
|
|
|
browser.onSearchActivated()
|
2017-05-28 22:54:28 +00:00
|
|
|
|
2020-05-18 15:49:17 +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()
|
2019-01-17 22:08:52 +00:00
|
|
|
def guiAddCards(self, note=None):
|
2017-05-27 12:14:40 +00:00
|
|
|
|
2019-01-17 22:08:52 +00:00
|
|
|
if note is not None:
|
|
|
|
collection = self.collection()
|
|
|
|
|
2019-01-17 23:40:16 +00:00
|
|
|
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)
|
2019-01-17 23:40:16 +00:00
|
|
|
|
2019-01-17 22:08:52 +00:00
|
|
|
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
|
2019-01-17 22:08:52 +00:00
|
|
|
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-17 22:08:52 +00:00
|
|
|
|
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
|
|
|
|
|
2020-01-05 19:42:59 +00:00
|
|
|
class AddCardsAndClose(aqt.addcards.AddCards):
|
2019-01-17 22:08:52 +00:00
|
|
|
|
2020-01-05 19:42:59 +00:00
|
|
|
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
|
|
|
|
2020-01-05 19:42:59 +00:00
|
|
|
self.modelHasChanged = True
|
|
|
|
super().__init__(mw)
|
2019-01-17 22:08:52 +00:00
|
|
|
|
2020-01-05 19:42:59 +00:00
|
|
|
self.addButton.setText("Add and Close")
|
|
|
|
self.addButton.setShortcut(aqt.qt.QKeySequence("Ctrl+Return"))
|
2019-05-02 16:33:24 +00:00
|
|
|
|
2020-01-05 19:42:59 +00:00
|
|
|
def _addCards(self):
|
|
|
|
super()._addCards()
|
2019-05-02 16:33:24 +00:00
|
|
|
|
2020-01-05 19:42:59 +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
|
|
|
|
2020-01-05 19:42:59 +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
|
|
|
|
2020-01-05 19:42:59 +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
|
|
|
|
2020-01-05 19:42:59 +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
|
|
|
|
2020-01-05 19:42:59 +00:00
|
|
|
def _reject(self):
|
|
|
|
savedMarkClosed = aqt.dialogs.markClosed
|
|
|
|
aqt.dialogs.markClosed = lambda _: savedMarkClosed(windowName)
|
|
|
|
super()._reject()
|
|
|
|
aqt.dialogs.markClosed = savedMarkClosed
|
2019-01-17 22:08:52 +00:00
|
|
|
|
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
|
2019-01-17 22:08:52 +00:00
|
|
|
|
2019-01-17 23:40:16 +00:00
|
|
|
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-17 22:08:52 +00:00
|
|
|
|
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())
|
2020-01-05 19:42:59 +00:00
|
|
|
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:
|
2019-05-04 21:24:13 +00:00
|
|
|
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():
|
2019-05-04 21:24:13 +00:00
|
|
|
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
|
|
|
|
|
2019-05-04 21:24:13 +00:00
|
|
|
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())
|
2019-05-04 21:24:13 +00:00
|
|
|
addCards.setAndFocusNote(addCards.editor.note)
|
|
|
|
|
|
|
|
currentWindow = aqt.dialogs._dialogs['AddCards'][1]
|
2019-01-24 06:07:53 +00:00
|
|
|
|
|
|
|
if currentWindow is not None:
|
2020-01-05 19:42:59 +00:00
|
|
|
currentWindow.closeWithCallback(openNewWindow)
|
2019-01-24 06:07:53 +00:00
|
|
|
else:
|
|
|
|
openNewWindow()
|
|
|
|
|
2019-05-04 21:24:13 +00:00
|
|
|
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']:
|
2017-07-05 20:01:13 +00:00
|
|
|
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()
|
2017-06-26 22:57:41 +00:00
|
|
|
return {
|
2017-07-01 19:41:27 +00:00
|
|
|
'cardId': card.id,
|
2017-07-03 00:52:57 +00:00
|
|
|
'fields': fields,
|
2017-07-05 20:01:13 +00:00
|
|
|
'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],
|
2017-07-05 20:01:13 +00:00
|
|
|
'modelName': model['name'],
|
2017-10-01 15:22:04 +00:00
|
|
|
'deckName': self.deckNameFromId(card.did),
|
2018-05-18 12:46:33 +00:00
|
|
|
'css': model['css'],
|
|
|
|
'template': card.template()['name']
|
2017-06-26 22:57:41 +00:00
|
|
|
}
|
2017-06-29 04:17:11 +00:00
|
|
|
|
2017-06-02 12:19:33 +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
|
2017-08-16 12:09:48 +00:00
|
|
|
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()
|
2017-06-16 16:17:53 +00:00
|
|
|
def guiShowQuestion(self):
|
2017-07-01 19:41:27 +00:00
|
|
|
if self.guiReviewActive():
|
|
|
|
self.reviewer()._showQuestion()
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
2017-06-02 12:19:33 +00:00
|
|
|
|
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-06-16 16:17:53 +00:00
|
|
|
def guiShowAnswer(self):
|
2017-07-01 19:41:27 +00:00
|
|
|
if self.guiReviewActive():
|
2017-06-02 12:19:33 +00:00
|
|
|
self.window().reviewer._showAnswer()
|
2017-07-01 19:41:27 +00:00
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
2017-06-02 12:19:33 +00:00
|
|
|
|
|
|
|
|
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-06-02 12:19:33 +00:00
|
|
|
|
2017-07-01 19:41:27 +00:00
|
|
|
reviewer._answerCard(ease)
|
|
|
|
return True
|
2017-06-02 12:19:33 +00:00
|
|
|
|
2017-07-03 00:27:31 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-07-02 17:35:14 +00:00
|
|
|
def guiDeckOverview(self, name):
|
|
|
|
collection = self.collection()
|
|
|
|
if collection is not None:
|
|
|
|
deck = collection.decks.byName(name)
|
2017-07-02 20:38:08 +00:00
|
|
|
if deck is not None:
|
2017-07-02 17:35:14 +00:00
|
|
|
collection.decks.select(deck['id'])
|
|
|
|
self.window().onOverview()
|
|
|
|
return True
|
2017-07-03 00:27:31 +00:00
|
|
|
|
2017-07-02 17:35:14 +00:00
|
|
|
return False
|
|
|
|
|
2017-07-03 00:27:31 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2017-07-02 17:35:14 +00:00
|
|
|
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()
|
2017-08-29 02:24:08 +00:00
|
|
|
def guiExitAnki(self):
|
|
|
|
timer = QTimer()
|
2020-01-05 23:42:08 +00:00
|
|
|
timer.timeout.connect(self.window().close)
|
2017-08-29 02:24:08 +00:00
|
|
|
timer.start(1000) # 1s should be enough to allow the response to be sent.
|
2017-06-02 12:19:33 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2018-05-06 19:59:31 +00:00
|
|
|
def addNotes(self, notes):
|
|
|
|
results = []
|
|
|
|
for note in notes:
|
|
|
|
try:
|
2018-05-06 20:24:40 +00:00
|
|
|
results.append(self.addNote(note))
|
2018-05-06 19:59:31 +00:00
|
|
|
except Exception:
|
|
|
|
results.append(None)
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
return results
|
2018-01-06 19:48:25 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2020-01-05 23:42:08 +00:00
|
|
|
@util.api()
|
2018-05-06 19:59:31 +00:00
|
|
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
return results
|
2018-02-21 13:16:52 +00:00
|
|
|
|
|
|
|
|
2020-03-17 05:29:49 +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']
|
2020-03-17 05:29:49 +00:00
|
|
|
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
|
2020-03-17 05:29:49 +00:00
|
|
|
|
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()
|