anki-connect/AnkiConnect.py

1279 lines
33 KiB
Python
Raw Normal View History

2016-05-21 22:10:12 +00:00
# Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
# Author: Alex Yatskov <alex@foosoft.net>
#
# 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/>.
import anki
import aqt
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
2017-08-21 14:10:57 +00:00
import re
2016-05-21 22:10:12 +00:00
import select
import socket
2017-07-06 04:29:46 +00:00
import sys
from time import time
2017-08-22 19:05:12 +00:00
from unicodedata import normalize
from operator import itemgetter
2016-05-21 22:10:12 +00:00
2016-05-29 22:49:38 +00:00
#
# Constants
#
2017-08-31 03:47:08 +00:00
API_VERSION = 5
2017-02-19 20:07:10 +00:00
TICK_INTERVAL = 25
2016-07-26 05:54:40 +00:00
URL_TIMEOUT = 10
2017-08-14 18:30:08 +00:00
URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py'
NET_ADDRESS = os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1')
2017-02-19 20:07:10 +00:00
NET_BACKLOG = 5
NET_PORT = 8765
2016-05-29 22:49:38 +00:00
2017-01-29 01:31:33 +00:00
#
# General helpers
#
2017-07-06 04:29:46 +00:00
if sys.version_info[0] < 3:
2017-01-29 01:31:33 +00:00
import urllib2
2017-01-30 01:34:18 +00:00
web = urllib2
2017-01-29 01:31:33 +00:00
2017-02-19 20:46:40 +00:00
from PyQt4.QtCore import QTimer
from PyQt4.QtGui import QMessageBox
2017-07-06 04:29:46 +00:00
else:
unicode = str
from urllib import request
web = request
2017-02-19 20:46:40 +00:00
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QMessageBox
2017-01-29 01:31:33 +00:00
2017-02-19 20:46:40 +00:00
2017-07-04 18:42:47 +00:00
#
# Helpers
#
2017-08-31 03:47:08 +00:00
def webApi(*versions):
2017-08-27 22:32:49 +00:00
def decorator(func):
2017-08-31 03:47:08 +00:00
method = lambda *args, **kwargs: func(*args, **kwargs)
2017-08-27 22:32:49 +00:00
setattr(method, 'versions', versions)
setattr(method, 'api', True)
return method
return decorator
2017-02-19 20:46:40 +00:00
def makeBytes(data):
return data.encode('utf-8')
def makeStr(data):
return data.decode('utf-8')
2017-02-19 20:57:55 +00:00
def download(url):
2017-02-19 20:46:40 +00:00
try:
resp = web.urlopen(url, timeout=URL_TIMEOUT)
except web.URLError:
return None
if resp.code != 200:
return None
return resp.read()
def audioInject(note, fields, filename):
for field in fields:
if field in note:
note[field] += u'[sound:{}]'.format(filename)
def verifyString(string):
t = type(string)
return t == str or t == unicode
def verifyStringList(strings):
for s in strings:
if not verifyString(s):
return False
return True
2017-01-29 01:31:33 +00:00
2016-05-21 22:10:12 +00:00
#
# AjaxRequest
#
class AjaxRequest:
def __init__(self, headers, body):
self.headers = headers
self.body = body
#
# AjaxClient
#
class AjaxClient:
def __init__(self, sock, handler):
self.sock = sock
self.handler = handler
2017-01-29 01:31:33 +00:00
self.readBuff = bytes()
self.writeBuff = bytes()
2016-05-21 22:10:12 +00:00
def advance(self, recvSize=1024):
if self.sock is None:
return False
rlist, wlist = select.select([self.sock], [self.sock], [], 0)[:2]
if rlist:
msg = self.sock.recv(recvSize)
if not msg:
self.close()
return False
self.readBuff += msg
req, length = self.parseRequest(self.readBuff)
if req is not None:
self.readBuff = self.readBuff[length:]
self.writeBuff += self.handler(req)
if wlist and self.writeBuff:
length = self.sock.send(self.writeBuff)
self.writeBuff = self.writeBuff[length:]
if not self.writeBuff:
self.close()
return False
return True
def close(self):
if self.sock is not None:
self.sock.close()
self.sock = None
2017-01-29 01:31:33 +00:00
self.readBuff = bytes()
self.writeBuff = bytes()
2016-05-21 22:10:12 +00:00
def parseRequest(self, data):
2017-01-29 01:31:33 +00:00
parts = data.split(makeBytes('\r\n\r\n'), 1)
2016-05-21 22:10:12 +00:00
if len(parts) == 1:
return None, 0
headers = {}
2017-01-29 01:31:33 +00:00
for line in parts[0].split(makeBytes('\r\n')):
pair = line.split(makeBytes(': '))
headers[pair[0].lower()] = pair[1] if len(pair) > 1 else None
2016-05-21 22:10:12 +00:00
headerLength = len(parts[0]) + 4
bodyLength = int(headers.get(makeBytes('content-length'), 0))
2016-05-21 22:10:12 +00:00
totalLength = headerLength + bodyLength
if totalLength > len(data):
return None, 0
body = data[headerLength : totalLength]
return AjaxRequest(headers, body), totalLength
#
# AjaxServer
#
class AjaxServer:
def __init__(self, handler):
self.handler = handler
self.clients = []
self.sock = None
2017-08-19 22:24:53 +00:00
self.resetHeaders()
2017-08-20 19:32:16 +00:00
def setHeader(self, name, value):
self.extraHeaders[name] = value
2017-08-20 19:32:16 +00:00
2017-08-19 22:24:53 +00:00
def resetHeaders(self):
self.headers = [
['HTTP/1.1 200 OK', None],
['Content-Type', 'text/json']
]
self.extraHeaders = {}
2017-08-20 19:32:16 +00:00
def getHeaders(self):
2017-08-20 21:39:50 +00:00
headers = self.headers[:]
for name in self.extraHeaders:
2017-08-20 21:39:50 +00:00
headers.append([name, self.extraHeaders[name]])
2017-08-20 19:32:16 +00:00
return headers
2016-05-21 22:10:12 +00:00
def advance(self):
if self.sock is not None:
self.acceptClients()
self.advanceClients()
def acceptClients(self):
rlist = select.select([self.sock], [], [], 0)[0]
if not rlist:
return
clientSock = self.sock.accept()[0]
if clientSock is not None:
clientSock.setblocking(False)
self.clients.append(AjaxClient(clientSock, self.handlerWrapper))
def advanceClients(self):
2017-01-29 01:31:33 +00:00
self.clients = list(filter(lambda c: c.advance(), self.clients))
2016-05-21 22:10:12 +00:00
2017-02-19 20:07:10 +00:00
def listen(self):
2016-05-21 22:10:12 +00:00
self.close()
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.setblocking(False)
2017-02-19 20:07:10 +00:00
self.sock.bind((NET_ADDRESS, NET_PORT))
self.sock.listen(NET_BACKLOG)
2016-05-21 22:10:12 +00:00
def handlerWrapper(self, req):
if len(req.body) == 0:
2017-02-19 21:18:07 +00:00
body = makeBytes('AnkiConnect v.{}'.format(API_VERSION))
else:
try:
params = json.loads(makeStr(req.body))
body = makeBytes(json.dumps(self.handler(params)))
except ValueError:
body = makeBytes(json.dumps(None))
2016-05-21 22:10:12 +00:00
resp = bytes()
2017-08-20 19:32:16 +00:00
self.setHeader('Content-Length', str(len(body)))
headers = self.getHeaders()
2016-05-21 22:10:12 +00:00
for key, value in headers:
2016-05-21 22:10:12 +00:00
if value is None:
2017-01-29 01:31:33 +00:00
resp += makeBytes('{}\r\n'.format(key))
2016-05-21 22:10:12 +00:00
else:
2017-01-29 01:31:33 +00:00
resp += makeBytes('{}: {}\r\n'.format(key, value))
2016-05-21 22:10:12 +00:00
2017-01-29 01:31:33 +00:00
resp += makeBytes('\r\n')
2016-05-21 22:10:12 +00:00
resp += body
return resp
def close(self):
if self.sock is not None:
self.sock.close()
self.sock = None
for client in self.clients:
client.close()
self.clients = []
2017-02-19 20:07:10 +00:00
#
# AnkiNoteParams
#
class AnkiNoteParams:
def __init__(self, params):
self.deckName = params.get('deckName')
self.modelName = params.get('modelName')
self.fields = params.get('fields', {})
self.tags = params.get('tags', [])
class Audio:
def __init__(self, params):
self.url = params.get('url')
self.filename = params.get('filename')
self.skipHash = params.get('skipHash')
self.fields = params.get('fields', [])
def validate(self):
return (
verifyString(self.url) and
verifyString(self.filename) and os.path.dirname(self.filename) == '' and
verifyStringList(self.fields) and
(verifyString(self.skipHash) or self.skipHash is None)
)
audio = Audio(params.get('audio', {}))
self.audio = audio if audio.validate() else None
def validate(self):
return (
verifyString(self.deckName) and
verifyString(self.modelName) and
type(self.fields) == dict and verifyStringList(list(self.fields.keys())) and verifyStringList(list(self.fields.values())) and
type(self.tags) == list and verifyStringList(self.tags)
)
2016-05-21 22:10:12 +00:00
#
# AnkiBridge
#
class AnkiBridge:
2017-08-22 19:05:12 +00:00
def storeMediaFile(self, filename, data):
self.deleteMediaFile(filename)
self.media().writeData(filename, base64.b64decode(data))
2017-08-22 07:53:35 +00:00
2017-08-22 19:05:12 +00:00
def retrieveMediaFile(self, filename):
# based on writeData from anki/media.py
filename = os.path.basename(filename)
filename = normalize("NFC", filename)
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
2017-08-22 19:05:12 +00:00
def deleteMediaFile(self, filename):
self.media().syncDelete(filename)
2017-08-22 07:53:35 +00:00
2017-02-19 20:07:10 +00:00
def addNote(self, params):
2016-05-29 17:31:24 +00:00
collection = self.collection()
if collection is None:
return
2017-02-19 20:07:10 +00:00
note = self.createNote(params)
2016-05-21 22:34:09 +00:00
if note is None:
return
2017-02-19 20:07:10 +00:00
if params.audio is not None and len(params.audio.fields) > 0:
2017-02-19 20:57:55 +00:00
data = download(params.audio.url)
2017-01-30 01:34:18 +00:00
if data is not None:
2017-02-19 20:07:10 +00:00
if params.audio.skipHash is None:
2017-01-30 01:34:18 +00:00
skip = False
else:
m = hashlib.md5()
m.update(data)
2017-02-19 20:07:10 +00:00
skip = params.audio.skipHash == m.hexdigest()
2017-01-30 01:34:18 +00:00
if not skip:
2017-02-19 20:07:10 +00:00
audioInject(note, params.audio.fields, params.audio.filename)
self.media().writeData(params.audio.filename, data)
2016-05-21 22:34:09 +00:00
2016-07-17 16:38:33 +00:00
self.startEditing()
2016-05-21 22:34:09 +00:00
collection.addNote(note)
collection.autosave()
2016-07-17 16:38:33 +00:00
self.stopEditing()
2016-05-21 22:34:09 +00:00
return note.id
2016-05-21 22:10:12 +00:00
2017-02-19 20:07:10 +00:00
def canAddNote(self, note):
return bool(self.createNote(note))
2016-05-21 22:10:12 +00:00
2017-02-19 20:07:10 +00:00
def createNote(self, params):
2016-05-29 17:31:24 +00:00
collection = self.collection()
if collection is None:
return
2017-02-19 20:07:10 +00:00
model = collection.models.byName(params.modelName)
2016-05-21 22:10:12 +00:00
if model is None:
2016-05-21 22:34:09 +00:00
return
2016-05-21 22:10:12 +00:00
2017-02-19 20:07:10 +00:00
deck = collection.decks.byName(params.deckName)
2016-05-21 22:10:12 +00:00
if deck is None:
2016-05-21 22:34:09 +00:00
return
2016-05-21 22:10:12 +00:00
2016-05-29 17:31:24 +00:00
note = anki.notes.Note(collection, model)
2016-05-21 22:10:12 +00:00
note.model()['did'] = deck['id']
2017-02-19 20:07:10 +00:00
note.tags = params.tags
2016-05-21 22:10:12 +00:00
2017-02-19 20:07:10 +00:00
for name, value in params.fields.items():
2016-05-21 22:10:12 +00:00
if name in note:
note[name] = value
if not note.dupeOrEmpty():
return note
def updateNoteFields(self, params):
collection = self.collection()
if collection is None:
return
note = collection.getNote(params['id'])
if note is None:
raise Exception("Failed to get note:{}".format(params['id']))
for name, value in params['fields'].items():
if name in note:
note[name] = value
note.flush()
2016-05-21 22:10:12 +00:00
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
def suspend(self, cards, suspend=True):
for card in cards:
isSuspended = self.isSuspended(card)
if suspend and isSuspended:
cards.remove(card)
elif not suspend and not isSuspended:
cards.remove(card)
if cards:
self.startEditing()
if suspend:
self.collection().sched.suspendCards(cards)
else:
self.collection().sched.unsuspendCards(cards)
self.stopEditing()
return True
return False
def areSuspended(self, cards):
suspended = []
for card in cards:
card = self.collection().getCard(card)
if card.queue == -1:
suspended.append(True)
else:
suspended.append(False)
return suspended
def areDue(self, cards):
due = []
for card in cards:
if self.findCards('cid:%s is:new' % card):
due.append(True)
continue
date, ivl = self.collection().db.all('select id/1000.0, ivl from revlog where cid = ?', card)[-1]
if (ivl >= -1200):
if self.findCards('cid:%s is:due' % card):
due.append(True)
else:
due.append(False)
else:
if date - ivl <= time():
due.append(True)
else:
due.append(False)
return due
2017-08-03 20:07:22 +00:00
2017-08-09 17:40:09 +00:00
def getIntervals(self, cards, complete=False):
intervals = []
for card in cards:
if self.findCards('cid:%s is:new' % card):
intervals.append(0)
continue
interval = self.collection().db.list('select ivl from revlog where cid = ?', card)
2017-08-09 17:40:09 +00:00
if not complete:
interval = interval[-1]
intervals.append(interval)
return intervals
2016-05-21 22:10:12 +00:00
def startEditing(self):
self.window().requireReset()
def stopEditing(self):
2016-05-29 17:31:24 +00:00
if self.collection() is not None:
2016-05-21 22:10:12 +00:00
self.window().maybeReset()
def window(self):
return aqt.mw
2017-07-01 19:41:27 +00:00
def reviewer(self):
return self.window().reviewer
2016-05-21 22:10:12 +00:00
def collection(self):
return self.window().col
2017-07-01 19:41:27 +00:00
def scheduler(self):
return self.collection().sched
def multi(self, actions):
response = []
for item in actions:
response.append(AnkiConnect.handler(ac, item))
return response
2016-07-17 16:38:33 +00:00
def media(self):
collection = self.collection()
if collection is not None:
return collection.media
2016-05-21 22:10:12 +00:00
def modelNames(self):
2016-05-29 17:31:24 +00:00
collection = self.collection()
if collection is not None:
return collection.models.allNames()
2016-05-21 22:10:12 +00:00
2017-08-21 16:15:11 +00:00
def modelNamesAndIds(self):
models = {}
modelNames = self.modelNames()
for model in modelNames:
mid = self.collection().models.byName(model)['id']
mid = int(mid) # sometimes Anki stores the ID as a string
models[model] = mid
return models
2017-07-01 19:49:44 +00:00
def modelNameFromId(self, modelId):
2016-05-29 17:31:24 +00:00
collection = self.collection()
2017-07-01 19:41:27 +00:00
if collection is not None:
2017-07-01 19:49:44 +00:00
model = collection.models.get(modelId)
2017-07-01 19:41:27 +00:00
if model is not None:
return model['name']
2016-05-29 17:31:24 +00:00
2017-07-01 19:41:27 +00:00
def modelFieldNames(self, modelName):
collection = self.collection()
if collection is not None:
model = collection.models.byName(modelName)
if model is not None:
return [field['name'] for field in model['flds']]
2016-05-21 22:10:12 +00:00
2017-08-21 14:10:57 +00:00
def modelFieldsOnTemplates(self, modelName):
model = self.collection().models.byName(modelName)
if model is not None:
templates = {}
for template in model['tmpls']:
fields = []
for side in ['qfmt', 'afmt']:
fieldsForSide = []
# based on _fieldsOnTemplate from aqt/clayout.py
matches = re.findall('{{[^#/}]+?}}', template[side])
for match in matches:
# remove braces and modifiers
match = re.sub(r'[{}]', '', match)
match = match.split(":")[-1]
# for the answer side, ignore fields present on the question side + the FrontSide field
if match == 'FrontSide' or side == 'afmt' and match in fields[0]:
continue
fieldsForSide.append(match)
fields.append(fieldsForSide)
templates[template['name']] = fields
return templates
2017-08-18 19:54:32 +00:00
def getDeckConfig(self, deck):
if not deck in self.deckNames():
return False
2017-08-18 19:37:46 +00:00
did = self.collection().decks.id(deck)
return self.collection().decks.confForDid(did)
2017-08-18 20:16:50 +00:00
def saveDeckConfig(self, config):
configId = str(config['id'])
2017-08-18 20:09:28 +00:00
if not configId in self.collection().decks.dconf:
return False
mod = anki.utils.intTime()
usn = self.collection().usn()
2017-08-18 20:16:50 +00:00
config['mod'] = mod
config['usn'] = usn
2017-08-18 20:16:50 +00:00
self.collection().decks.dconf[configId] = config
self.collection().decks.changed = True
return True
2017-08-18 20:09:28 +00:00
def setDeckConfigId(self, decks, configId):
for deck in decks:
if not deck in self.deckNames():
return False
2017-08-18 20:09:28 +00:00
if not str(configId) in self.collection().decks.dconf:
return False
for deck in decks:
did = str(self.collection().decks.id(deck))
2017-08-18 20:09:28 +00:00
aqt.mw.col.decks.decks[did]['conf'] = configId
return True
2017-08-18 19:54:32 +00:00
def cloneDeckConfigId(self, name, cloneFrom=1):
if not str(cloneFrom) in self.collection().decks.dconf:
return False
cloneFrom = self.collection().decks.getConf(cloneFrom)
return self.collection().decks.confId(name, cloneFrom)
2017-08-18 19:54:32 +00:00
def removeDeckConfigId(self, configId):
2017-08-18 19:37:46 +00:00
if configId == 1 or not str(configId) in self.collection().decks.dconf:
return False
2017-08-18 19:37:46 +00:00
self.collection().decks.remConf(configId)
return True
2017-07-27 17:32:34 +00:00
2016-05-21 22:10:12 +00:00
def deckNames(self):
2016-05-29 17:31:24 +00:00
collection = self.collection()
if collection is not None:
return collection.decks.allNames()
2016-05-21 22:10:12 +00:00
2017-08-16 21:26:53 +00:00
def deckNamesAndIds(self):
decks = {}
deckNames = self.deckNames()
for deck in deckNames:
2017-08-18 19:37:46 +00:00
did = self.collection().decks.id(deck)
decks[deck] = did
2017-08-16 21:26:53 +00:00
return decks
2017-07-01 19:49:44 +00:00
def deckNameFromId(self, deckId):
2017-07-01 19:41:27 +00:00
collection = self.collection()
if collection is not None:
2017-07-01 19:49:44 +00:00
deck = collection.decks.get(deckId)
2017-07-01 19:41:27 +00:00
if deck is not None:
return deck['name']
def findNotes(self, query=None):
if query is not None:
return self.collection().findNotes(query)
else:
return []
def findCards(self, query=None):
if query is not None:
return self.collection().findCards(query)
else:
return []
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}
result.append({
'cardId': card.id,
'fields': fields,
'fieldOrder': card.ord,
'question': card._getQA()['q'],
'answer': card._getQA()['a'],
'modelName': model['name'],
'deckName': self.deckNameFromId(card.did),
'css': model['css'],
'factor': card.factor,
#This factor is 10 times the ease percentage,
# so an ease of 310% would be reported as 3100
'interval': card.ivl,
'note': card.nid
})
except TypeError as e:
# Anki will give a TypeError if the card ID does not exist.
# Best behavior is probably to add an "empty card" to the
# returned result, so that the items of the input and return
# lists correspond.
result.append({})
return result
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}
result.append({
'noteId': note.id,
'tags' : note.tags,
'fields': fields,
'modelName': model['name'],
'cards': self.collection().db.list(
"select id from cards where nid = ? order by ord", self.id)
})
except TypeError as e:
# Anki will give a TypeError if the note ID does not exist.
# Best behavior is probably to add an "empty card" to the
# returned result, so that the items of the input and return
# lists correspond.
result.append({})
return result
2017-08-13 08:02:59 +00:00
def getDecks(self, cards):
decks = {}
for card in cards:
did = self.collection().db.scalar('select did from cards where id = ?', card)
deck = self.collection().decks.get(did)['name']
if deck in decks:
decks[deck].append(card)
else:
decks[deck] = [card]
return decks
2017-08-10 17:34:43 +00:00
def changeDeck(self, cards, deck):
self.startEditing()
did = self.collection().decks.id(deck)
2017-08-10 17:34:43 +00:00
mod = anki.utils.intTime()
usn = self.collection().usn()
2017-08-10 17:34:43 +00:00
# normal cards
scids = anki.utils.ids2str(cards)
# remove any cards from filtered deck first
self.collection().sched.remFromDyn(cards)
2017-08-10 17:34:43 +00:00
# then move into new deck
self.collection().db.execute('update cards set usn=?, mod=?, did=? where id in ' + scids, usn, mod, did)
2017-08-10 17:34:43 +00:00
self.stopEditing()
2017-08-13 09:05:56 +00:00
def deleteDecks(self, decks, cardsToo=False):
self.startEditing()
for deck in decks:
2017-08-18 19:37:46 +00:00
did = self.collection().decks.id(deck)
self.collection().decks.rem(did, cardsToo)
2017-08-13 09:05:56 +00:00
self.stopEditing()
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
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
2017-05-27 12:14:40 +00:00
return browser.model.cards
def guiAddCards(self):
2017-05-28 22:54:28 +00:00
addCards = aqt.dialogs.open('AddCards', self.window())
addCards.activateWindow()
2017-05-27 12:14:40 +00:00
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
def guiCurrentCard(self):
2017-07-01 19:49:44 +00:00
if not self.guiReviewActive():
2017-07-03 00:27:31 +00:00
return
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:
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,
'question': card._getQA()['q'],
'answer': card._getQA()['a'],
'buttons': [b[0] for b in reviewer._answerButtonList()],
'modelName': model['name'],
'deckName': self.deckNameFromId(card.did),
'css': model['css']
}
2017-06-29 04:17:11 +00:00
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
def guiShowQuestion(self):
2017-07-01 19:41:27 +00:00
if self.guiReviewActive():
self.reviewer()._showQuestion()
return True
else:
return False
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
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
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
def guiDeckBrowser(self):
2017-07-03 00:27:31 +00:00
self.window().moveToState('deckBrowser')
def guiDeckReview(self, name):
if self.guiDeckOverview(name):
self.window().moveToState('review')
return True
else:
return False
def guiExitAnki(self):
timer = QTimer()
def exitAnki():
timer.stop()
self.window().close()
timer.timeout.connect(exitAnki)
timer.start(1000) # 1s should be enough to allow the response to be sent.
2016-05-21 22:10:12 +00:00
#
# AnkiConnect
#
class AnkiConnect:
2017-02-19 20:07:10 +00:00
def __init__(self):
2016-05-21 22:10:12 +00:00
self.anki = AnkiBridge()
self.server = AjaxServer(self.handler)
2017-02-19 20:46:40 +00:00
try:
self.server.listen()
self.timer = QTimer()
self.timer.timeout.connect(self.advance)
self.timer.start(TICK_INTERVAL)
except:
QMessageBox.critical(
self.anki.window(),
'AnkiConnect',
2017-02-19 20:57:55 +00:00
'Failed to listen on port {}.\nMake sure it is available and is not in use.'.format(NET_PORT)
2017-02-19 20:46:40 +00:00
)
2016-05-21 22:10:12 +00:00
2017-07-04 18:42:08 +00:00
2016-05-21 22:10:12 +00:00
def advance(self):
self.server.advance()
def handler(self, request):
2017-08-31 03:47:08 +00:00
name = request.get('action', '')
version = request.get('version', 4)
params = request.get('params', {})
reply = {'result': None, 'error': None}
2017-07-04 18:29:09 +00:00
2017-08-31 03:47:08 +00:00
try:
method = None
2017-07-04 18:29:09 +00:00
2017-08-31 03:47:08 +00:00
for methodName, methodInst in inspect.getmembers(self, predicate=inspect.ismethod):
apiVersionLast = 0
apiNameLast = None
2016-05-21 22:10:12 +00:00
2017-08-31 03:47:08 +00:00
if getattr(methodInst, 'api', False):
for apiVersion, apiName in getattr(methodInst, 'versions', []):
if apiVersionLast < apiVersion <= version:
apiVersionLast = apiVersion
apiNameLast = apiName
2016-05-21 22:10:12 +00:00
2017-08-31 03:47:08 +00:00
if apiNameLast is None and apiVersionLast == 0:
apiNameLast = methodName
2017-08-27 22:32:49 +00:00
2017-08-31 03:47:08 +00:00
if apiNameLast is not None and apiNameLast == name:
method = methodInst
break
2017-08-27 22:32:49 +00:00
2017-08-31 03:47:08 +00:00
if method is None:
raise Exception('unsupported action')
else:
reply['result'] = methodInst(**params)
except Exception as e:
reply['error'] = str(e)
2017-08-27 22:32:49 +00:00
2017-08-31 03:47:08 +00:00
if version > 4:
return reply
else:
return reply['result']
2017-08-27 22:32:49 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
def multi(self, actions):
return self.anki.multi(actions)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-22 19:05:12 +00:00
def storeMediaFile(self, filename, data):
return self.anki.storeMediaFile(filename, data)
2017-08-22 07:53:35 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-22 19:05:12 +00:00
def retrieveMediaFile(self, filename):
return self.anki.retrieveMediaFile(filename)
2017-08-22 07:53:35 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-22 19:05:12 +00:00
def deleteMediaFile(self, filename):
return self.anki.deleteMediaFile(filename)
2017-08-22 07:53:35 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def deckNames(self):
2016-05-21 22:34:09 +00:00
return self.anki.deckNames()
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-16 21:26:53 +00:00
def deckNamesAndIds(self):
return self.anki.deckNamesAndIds()
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def modelNames(self):
2016-05-21 22:34:09 +00:00
return self.anki.modelNames()
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-21 16:15:11 +00:00
def modelNamesAndIds(self):
return self.anki.modelNamesAndIds()
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def modelFieldNames(self, modelName):
2016-05-21 22:34:09 +00:00
return self.anki.modelFieldNames(modelName)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-21 14:10:57 +00:00
def modelFieldsOnTemplates(self, modelName):
return self.anki.modelFieldsOnTemplates(modelName)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-18 19:54:32 +00:00
def getDeckConfig(self, deck):
return self.anki.getDeckConfig(deck)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-18 20:16:50 +00:00
def saveDeckConfig(self, config):
return self.anki.saveDeckConfig(config)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-18 20:09:28 +00:00
def setDeckConfigId(self, decks, configId):
return self.anki.setDeckConfigId(decks, configId)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-18 19:54:32 +00:00
def cloneDeckConfigId(self, name, cloneFrom=1):
return self.anki.cloneDeckConfigId(name, cloneFrom)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-18 19:54:32 +00:00
def removeDeckConfigId(self, configId):
return self.anki.removeDeckConfigId(configId)
2017-07-27 17:32:34 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def addNote(self, note):
2017-02-19 20:07:10 +00:00
params = AnkiNoteParams(note)
if params.validate():
return self.anki.addNote(params)
2016-05-29 06:23:14 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def addNotes(self, notes):
2017-02-05 20:09:12 +00:00
results = []
for note in notes:
2017-02-19 20:07:10 +00:00
params = AnkiNoteParams(note)
2017-03-12 16:43:47 +00:00
if params.validate():
2017-02-19 20:07:10 +00:00
results.append(self.anki.addNote(params))
else:
results.append(None)
2017-02-05 20:09:12 +00:00
return results
@webApi()
def updateNoteFields(self, note):
return self.anki.updateNoteFields(note)
2017-02-05 20:09:12 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def canAddNotes(self, notes):
2016-05-29 06:23:14 +00:00
results = []
for note in notes:
2017-02-19 20:07:10 +00:00
params = AnkiNoteParams(note)
results.append(params.validate() and self.anki.canAddNote(params))
2016-05-29 06:23:14 +00:00
return results
2017-08-31 03:47:08 +00:00
@webApi()
def addTags(self, notes, tags, add=True):
return self.anki.addTags(notes, tags, add)
2017-08-03 20:07:22 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
def removeTags(self, notes, tags):
return self.anki.addTags(notes, tags, False)
2017-08-03 20:07:22 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
def suspend(self, cards, suspend=True):
return self.anki.suspend(cards, suspend)
2017-08-03 20:07:22 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
def unsuspend(self, cards):
return self.anki.suspend(cards, False)
2017-08-03 20:07:22 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
def areSuspended(self, cards):
return self.anki.areSuspended(cards)
2017-08-31 03:47:08 +00:00
@webApi()
def areDue(self, cards):
return self.anki.areDue(cards)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-09 17:40:09 +00:00
def getIntervals(self, cards, complete=False):
return self.anki.getIntervals(cards, complete)
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def upgrade(self):
2017-02-19 20:57:55 +00:00
response = QMessageBox.question(
self.anki.window(),
'AnkiConnect',
'Upgrade to the latest version?',
QMessageBox.Yes | QMessageBox.No
)
if response == QMessageBox.Yes:
data = download(URL_UPGRADE)
if data is None:
2017-08-14 18:30:08 +00:00
QMessageBox.critical(self.anki.window(), 'AnkiConnect', 'Failed to download latest version.')
2017-02-19 20:57:55 +00:00
else:
2017-02-19 21:18:07 +00:00
path = os.path.splitext(__file__)[0] + '.py'
with open(path, 'w') as fp:
fp.write(makeStr(data))
2017-02-19 23:02:09 +00:00
QMessageBox.information(self.anki.window(), 'AnkiConnect', 'Upgraded to the latest version, please restart Anki.')
return True
2017-02-19 20:57:55 +00:00
2017-02-19 23:02:09 +00:00
return False
2017-02-19 20:57:55 +00:00
2017-07-04 18:42:08 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def version(self):
2016-05-29 22:49:38 +00:00
return API_VERSION
2017-08-31 03:47:08 +00:00
@webApi()
def findNotes(self, query=None):
return self.anki.findNotes(query)
2017-08-31 03:47:08 +00:00
@webApi()
def findCards(self, query=None):
return self.anki.findCards(query)
2017-05-27 12:14:40 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-13 08:02:59 +00:00
def getDecks(self, cards):
return self.anki.getDecks(cards)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-10 17:34:43 +00:00
def changeDeck(self, cards, deck):
return self.anki.changeDeck(cards, deck)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-13 09:05:56 +00:00
def deleteDecks(self, decks, cardsToo=False):
return self.anki.deleteDecks(decks, cardsToo)
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-09 18:05:00 +00:00
def cardsToNotes(self, cards):
return self.anki.cardsToNotes(cards)
2017-08-31 03:47:08 +00:00
@webApi()
def guiBrowse(self, query=None):
return self.anki.guiBrowse(query)
2017-07-21 00:50:12 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiAddCards(self):
return self.anki.guiAddCards()
2017-05-27 12:14:40 +00:00
2017-06-08 14:09:00 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiCurrentCard(self):
2017-07-01 19:41:27 +00:00
return self.anki.guiCurrentCard()
2017-08-31 03:47:08 +00:00
@webApi()
2017-08-16 12:04:05 +00:00
def guiStartCardTimer(self):
return self.anki.guiStartCardTimer()
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiAnswerCard(self, ease):
2017-07-01 20:16:51 +00:00
return self.anki.guiAnswerCard(ease)
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiShowQuestion(self):
return self.anki.guiShowQuestion()
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiShowAnswer(self):
return self.anki.guiShowAnswer()
2017-07-03 00:27:31 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiDeckOverview(self, name):
return self.anki.guiDeckOverview(name)
2017-07-03 00:27:31 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiDeckBrowser(self):
return self.anki.guiDeckBrowser()
2017-06-08 14:09:00 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
2017-07-04 18:42:08 +00:00
def guiDeckReview(self, name):
return self.anki.guiDeckReview(name)
2017-07-03 00:27:31 +00:00
2017-08-31 03:47:08 +00:00
@webApi()
def guiExitAnki(self):
return self.anki.guiExitAnki()
@webApi()
def cardsInfo(self, cards):
return self.anki.cardsInfo(cards)
@webApi()
def notesInfo(self, notes):
return self.anki.notesInfo(notes)
2017-07-03 00:27:31 +00:00
2016-05-21 22:10:12 +00:00
#
# Entry
#
ac = AnkiConnect()