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
|
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
|
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
|
2018-05-06 19:59:31 +00:00
|
|
|
|
|
|
|
from operator import itemgetter
|
2017-08-12 14:57:28 +00:00
|
|
|
from time import time
|
2017-08-22 19:05:12 +00:00
|
|
|
from unicodedata import normalize
|
2016-05-21 22:10:12 +00:00
|
|
|
|
|
|
|
|
2016-05-29 22:49:38 +00:00
|
|
|
#
|
|
|
|
# Constants
|
|
|
|
#
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
API_VERSION = 5
|
|
|
|
NET_ADDRESS = os.getenv('ANKICONNECT_BIND_ADDRESS', '127.0.0.1')
|
|
|
|
NET_BACKLOG = 5
|
|
|
|
NET_PORT = 8765
|
2017-02-19 20:07:10 +00:00
|
|
|
TICK_INTERVAL = 25
|
2018-05-06 19:59:31 +00:00
|
|
|
URL_TIMEOUT = 10
|
|
|
|
URL_UPGRADE = 'https://raw.githubusercontent.com/FooSoft/anki-connect/master/AnkiConnect.py'
|
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)
|
2018-03-10 01:04:08 +00:00
|
|
|
except web.URLError as e:
|
2018-03-11 22:10:07 +00:00
|
|
|
raise Exception('A urlError has occurred for url ' + url + '. Error messages was: ' + e.message)
|
2017-02-19 20:46:40 +00:00
|
|
|
|
|
|
|
if resp.code != 200:
|
2018-03-11 22:10:07 +00:00
|
|
|
raise Exception('Return code for url request' + url + 'was not 200. Error code: ' + resp.code)
|
2017-02-19 20:46:40 +00:00
|
|
|
|
|
|
|
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
|
|
|
#
|
2018-05-06 19:59:31 +00:00
|
|
|
# WebRequest
|
2016-05-21 22:10:12 +00:00
|
|
|
#
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
class WebRequest:
|
2016-05-21 22:10:12 +00:00
|
|
|
def __init__(self, headers, body):
|
|
|
|
self.headers = headers
|
|
|
|
self.body = body
|
|
|
|
|
|
|
|
|
|
|
|
#
|
2018-05-06 19:59:31 +00:00
|
|
|
# WebClient
|
2016-05-21 22:10:12 +00:00
|
|
|
#
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
class WebClient:
|
2016-05-21 22:10:12 +00:00
|
|
|
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(': '))
|
2017-08-28 20:49:58 +00:00
|
|
|
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
|
2017-08-28 20:49:58 +00:00
|
|
|
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]
|
2018-05-06 19:59:31 +00:00
|
|
|
return WebRequest(headers, body), totalLength
|
2016-05-21 22:10:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
#
|
2018-05-06 19:59:31 +00:00
|
|
|
# WebServer
|
2016-05-21 22:10:12 +00:00
|
|
|
#
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
class WebServer:
|
2016-05-21 22:10:12 +00:00
|
|
|
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):
|
2017-08-20 21:29:52 +00:00
|
|
|
self.extraHeaders[name] = value
|
2017-08-20 19:32:16 +00:00
|
|
|
|
|
|
|
|
2017-08-19 22:24:53 +00:00
|
|
|
def resetHeaders(self):
|
2017-08-20 21:29:52 +00:00
|
|
|
self.headers = [
|
|
|
|
['HTTP/1.1 200 OK', None],
|
2017-12-22 22:53:41 +00:00
|
|
|
['Content-Type', 'text/json'],
|
|
|
|
['Access-Control-Allow-Origin', '*']
|
2017-08-20 21:29:52 +00:00
|
|
|
]
|
|
|
|
self.extraHeaders = {}
|
2017-08-20 19:32:16 +00:00
|
|
|
|
|
|
|
|
|
|
|
def getHeaders(self):
|
2017-08-20 21:39:50 +00:00
|
|
|
headers = self.headers[:]
|
2017-08-20 21:29:52 +00:00
|
|
|
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)
|
2018-05-06 19:59:31 +00:00
|
|
|
self.clients.append(WebClient(clientSock, self.handlerWrapper))
|
2016-05-21 22:10:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
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):
|
2017-02-19 20:22:44 +00:00
|
|
|
if len(req.body) == 0:
|
2017-02-19 21:18:07 +00:00
|
|
|
body = makeBytes('AnkiConnect v.{}'.format(API_VERSION))
|
2017-02-19 20:22:44 +00:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
params = json.loads(makeStr(req.body))
|
|
|
|
body = makeBytes(json.dumps(self.handler(params)))
|
|
|
|
except ValueError:
|
2017-09-16 05:17:52 +00:00
|
|
|
body = makeBytes(json.dumps(None))
|
2016-05-21 22:10:12 +00:00
|
|
|
|
2017-02-19 20:22:44 +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
|
|
|
|
2017-02-19 20:22:44 +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)
|
|
|
|
)
|
2018-03-31 21:40:01 +00:00
|
|
|
|
|
|
|
|
2018-03-11 11:55:47 +00:00
|
|
|
def __str__(self):
|
2018-03-11 22:10:07 +00:00
|
|
|
return 'DeckName: ' + self.deckName + '. ModelName: ' + self.modelName + '. Fields: ' + str(self.fields) + '. Tags: ' + str(self.tags) + '.'
|
2017-02-19 20:07:10 +00:00
|
|
|
|
2016-05-21 22:10:12 +00:00
|
|
|
#
|
|
|
|
# AnkiBridge
|
|
|
|
#
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
class AnkiConnect:
|
|
|
|
def __init__(self):
|
|
|
|
self.server = WebServer(self.handler)
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.server.listen()
|
|
|
|
|
|
|
|
self.timer = QTimer()
|
|
|
|
self.timer.timeout.connect(self.advance)
|
|
|
|
self.timer.start(TICK_INTERVAL)
|
|
|
|
except:
|
|
|
|
QMessageBox.critical(
|
|
|
|
self.window(),
|
|
|
|
'AnkiConnect',
|
|
|
|
'Failed to listen on port {}.\nMake sure it is available and is not in use.'.format(NET_PORT)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def advance(self):
|
|
|
|
self.server.advance()
|
|
|
|
|
|
|
|
|
|
|
|
def handler(self, request):
|
|
|
|
name = request.get('action', '')
|
|
|
|
version = request.get('version', 4)
|
|
|
|
params = request.get('params', {})
|
|
|
|
reply = {'result': None, 'error': None}
|
|
|
|
|
|
|
|
try:
|
|
|
|
method = None
|
|
|
|
|
|
|
|
for methodName, methodInst in inspect.getmembers(self, predicate=inspect.ismethod):
|
|
|
|
apiVersionLast = 0
|
|
|
|
apiNameLast = None
|
|
|
|
|
|
|
|
if getattr(methodInst, 'api', False):
|
|
|
|
for apiVersion, apiName in getattr(methodInst, 'versions', []):
|
|
|
|
if apiVersionLast < apiVersion <= version:
|
|
|
|
apiVersionLast = apiVersion
|
|
|
|
apiNameLast = apiName
|
|
|
|
|
|
|
|
if apiNameLast is None and apiVersionLast == 0:
|
|
|
|
apiNameLast = methodName
|
|
|
|
|
|
|
|
if apiNameLast is not None and apiNameLast == name:
|
|
|
|
method = methodInst
|
|
|
|
break
|
|
|
|
|
|
|
|
if method is None:
|
|
|
|
raise Exception('unsupported action')
|
|
|
|
else:
|
|
|
|
reply['result'] = methodInst(**params)
|
|
|
|
except Exception as e:
|
|
|
|
reply['error'] = str(e)
|
|
|
|
|
|
|
|
if version > 4:
|
|
|
|
return reply
|
|
|
|
else:
|
|
|
|
return reply['result']
|
|
|
|
|
|
|
|
|
|
|
|
def startEditing(self):
|
|
|
|
self.window().requireReset()
|
|
|
|
|
|
|
|
|
|
|
|
def stopEditing(self):
|
|
|
|
if self.collection() is not None:
|
|
|
|
self.window().maybeReset()
|
|
|
|
|
|
|
|
|
|
|
|
def window(self):
|
|
|
|
return aqt.mw
|
|
|
|
|
|
|
|
|
|
|
|
def reviewer(self):
|
|
|
|
return self.window().reviewer
|
|
|
|
|
|
|
|
|
|
|
|
def collection(self):
|
|
|
|
return self.window().col
|
|
|
|
|
|
|
|
|
|
|
|
def scheduler(self):
|
|
|
|
return self.collection().sched
|
|
|
|
|
|
|
|
|
|
|
|
def media(self):
|
|
|
|
collection = self.collection()
|
|
|
|
if collection is not None:
|
|
|
|
return collection.media
|
|
|
|
|
|
|
|
|
|
|
|
def createNote(self, params):
|
|
|
|
collection = self.collection()
|
|
|
|
if collection is None:
|
|
|
|
raise Exception('Collection was not found.')
|
|
|
|
|
|
|
|
model = collection.models.byName(params.modelName)
|
|
|
|
if model is None:
|
|
|
|
raise Exception('Model was not found for model: ' + params.modelName)
|
|
|
|
|
|
|
|
deck = collection.decks.byName(params.deckName)
|
|
|
|
if deck is None:
|
|
|
|
raise Exception('Deck was not found for deck: ' + params.deckName)
|
|
|
|
|
|
|
|
note = anki.notes.Note(collection, model)
|
|
|
|
note.model()['did'] = deck['id']
|
|
|
|
note.tags = params.tags
|
|
|
|
|
|
|
|
for name, value in params.fields.items():
|
|
|
|
if name in note:
|
|
|
|
note[name] = value
|
|
|
|
|
|
|
|
# Returns 1 if empty. 2 if duplicate. Otherwise returns False
|
|
|
|
duplicateOrEmpty = note.dupeOrEmpty()
|
|
|
|
if duplicateOrEmpty == 1:
|
|
|
|
raise Exception('Note was empty. Param were: ' + str(params))
|
|
|
|
elif duplicateOrEmpty == 2:
|
|
|
|
raise Exception('Note is duplicate of existing note. Params were: ' + str(params))
|
|
|
|
elif duplicateOrEmpty == False:
|
|
|
|
return note
|
|
|
|
|
|
|
|
|
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-22 19:05:12 +00:00
|
|
|
def retrieveMediaFile(self, filename):
|
|
|
|
# based on writeData from anki/media.py
|
|
|
|
filename = os.path.basename(filename)
|
2018-03-11 22:10:07 +00:00
|
|
|
filename = 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
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-22 19:05:12 +00:00
|
|
|
def deleteMediaFile(self, filename):
|
|
|
|
self.media().syncDelete(filename)
|
2017-08-22 07:53:35 +00:00
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
|
|
|
def addNote(self, note):
|
|
|
|
params = AnkiNoteParams(note)
|
|
|
|
if not params.validate():
|
|
|
|
raise Exception('Invalid note parameters')
|
|
|
|
|
2016-05-29 17:31:24 +00:00
|
|
|
collection = self.collection()
|
|
|
|
if collection is None:
|
2018-03-11 22:10:07 +00:00
|
|
|
raise Exception('Collection was not found.')
|
2016-05-29 17:31:24 +00:00
|
|
|
|
2017-02-19 20:07:10 +00:00
|
|
|
note = self.createNote(params)
|
2016-05-21 22:34:09 +00:00
|
|
|
if note is None:
|
2018-03-11 22:10:07 +00:00
|
|
|
raise Exception('Failed to create note from params: ' + str(params))
|
2016-05-21 22:34:09 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-02-19 20:07:10 +00:00
|
|
|
def canAddNote(self, note):
|
2018-05-06 19:59:31 +00:00
|
|
|
params = AnkiNoteParams(note)
|
|
|
|
if not params.validate():
|
|
|
|
return False
|
|
|
|
|
2018-03-16 12:51:48 +00:00
|
|
|
try:
|
2018-05-06 20:24:40 +00:00
|
|
|
return bool(self.createNote(params))
|
2018-03-16 12:51:48 +00:00
|
|
|
except:
|
|
|
|
return False
|
2016-05-21 22:10:12 +00:00
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2018-01-01 02:34:38 +00:00
|
|
|
def updateNoteFields(self, params):
|
|
|
|
collection = self.collection()
|
|
|
|
if collection is None:
|
2018-03-11 22:10:07 +00:00
|
|
|
raise Exception('Collection was not found.')
|
2018-01-01 02:34:38 +00:00
|
|
|
|
|
|
|
note = collection.getNote(params['id'])
|
|
|
|
if note is None:
|
2018-03-11 22:10:07 +00:00
|
|
|
raise Exception('Failed to get note:{}'.format(params['id']))
|
2018-01-01 02:34:38 +00:00
|
|
|
for name, value in params['fields'].items():
|
|
|
|
if name in note:
|
|
|
|
note[name] = value
|
|
|
|
note.flush()
|
2016-05-21 22:10:12 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
|
|
|
def removeTags(self, notes, tags):
|
|
|
|
return self.addTags(notes, tags, False)
|
|
|
|
|
|
|
|
|
|
|
|
@webApi()
|
2018-01-14 11:26:37 +00:00
|
|
|
def getTags(self):
|
|
|
|
return self.collection().tags.all()
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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:
|
|
|
|
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
|
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
|
|
|
def unsuspend(self, cards):
|
|
|
|
self.suspend(cards, False)
|
|
|
|
|
|
|
|
|
|
|
|
@webApi()
|
2018-03-11 12:27:19 +00:00
|
|
|
def isSuspended(self, card):
|
|
|
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-12 14:57:28 +00:00
|
|
|
def areSuspended(self, cards):
|
|
|
|
suspended = []
|
|
|
|
for card in cards:
|
2018-03-11 12:27:19 +00:00
|
|
|
suspended.append(self.isSuspended(card))
|
2017-08-12 14:57:28 +00:00
|
|
|
return suspended
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-12 14:57:28 +00:00
|
|
|
def areDue(self, cards):
|
|
|
|
due = []
|
|
|
|
for card in cards:
|
|
|
|
if self.findCards('cid:%s is:new' % card):
|
|
|
|
due.append(True)
|
|
|
|
continue
|
|
|
|
|
2017-08-12 15:21:04 +00:00
|
|
|
date, ivl = self.collection().db.all('select id/1000.0, ivl from revlog where cid = ?', card)[-1]
|
2017-08-12 14:57:28 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-09 17:40:09 +00:00
|
|
|
def getIntervals(self, cards, complete=False):
|
|
|
|
intervals = []
|
|
|
|
for card in cards:
|
2017-08-12 15:21:04 +00:00
|
|
|
if self.findCards('cid:%s is:new' % card):
|
|
|
|
intervals.append(0)
|
|
|
|
continue
|
|
|
|
|
2017-08-12 14:57:28 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-17 12:25:06 +00:00
|
|
|
def multi(self, actions):
|
|
|
|
response = []
|
|
|
|
for item in actions:
|
2018-05-06 19:59:31 +00:00
|
|
|
response.append(self.handler(item))
|
2017-08-17 12:25:06 +00:00
|
|
|
return response
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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)
|
2018-03-11 22:10:07 +00:00
|
|
|
match = match.split(':')[-1]
|
2017-08-21 14:10:57 +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)
|
|
|
|
|
|
|
|
|
|
|
|
fields.append(fieldsForSide)
|
|
|
|
|
|
|
|
templates[template['name']] = fields
|
|
|
|
|
|
|
|
return templates
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-18 19:54:32 +00:00
|
|
|
def getDeckConfig(self, deck):
|
2017-08-17 12:25:06 +00:00
|
|
|
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-17 12:25:06 +00:00
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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:
|
2017-08-17 12:25:06 +00:00
|
|
|
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-17 12:25:06 +00:00
|
|
|
|
2017-08-18 20:16:50 +00:00
|
|
|
self.collection().decks.dconf[configId] = config
|
2017-08-17 12:25:06 +00:00
|
|
|
self.collection().decks.changed = True
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-18 20:09:28 +00:00
|
|
|
def setDeckConfigId(self, decks, configId):
|
2017-08-17 12:25:06 +00:00
|
|
|
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:
|
2017-08-17 12:25:06 +00:00
|
|
|
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
|
2017-08-17 12:25:06 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-18 19:54:32 +00:00
|
|
|
def cloneDeckConfigId(self, name, cloneFrom=1):
|
2017-08-17 12:25:06 +00:00
|
|
|
if not str(cloneFrom) in self.collection().decks.dconf:
|
|
|
|
return False
|
|
|
|
|
|
|
|
cloneFrom = self.collection().decks.getConf(cloneFrom)
|
|
|
|
return self.collection().decks.confId(name, cloneFrom)
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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:
|
2017-08-17 12:25:06 +00:00
|
|
|
return False
|
|
|
|
|
2017-08-18 19:37:46 +00:00
|
|
|
self.collection().decks.remConf(configId)
|
2017-08-17 12:25:06 +00:00
|
|
|
return True
|
2017-07-27 17:32:34 +00:00
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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']
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-03 21:31:47 +00:00
|
|
|
def findNotes(self, query=None):
|
|
|
|
if query is not None:
|
2017-08-06 01:29:59 +00:00
|
|
|
return self.collection().findNotes(query)
|
2017-08-03 21:31:47 +00:00
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-03 21:31:47 +00:00
|
|
|
def findCards(self, query=None):
|
|
|
|
if query is not None:
|
2017-08-06 01:29:59 +00:00
|
|
|
return self.collection().findCards(query)
|
2017-08-03 21:31:47 +00:00
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
|
|
|
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,
|
|
|
|
'question': card._getQA()['q'],
|
|
|
|
'answer': card._getQA()['a'],
|
|
|
|
'modelName': model['name'],
|
|
|
|
'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,
|
|
|
|
'note': card.nid
|
|
|
|
})
|
|
|
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
|
|
|
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-01-09 13:24:56 +00:00
|
|
|
'cards': self.collection().db.list(
|
2018-03-11 22:10:07 +00:00
|
|
|
'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({})
|
|
|
|
return result
|
|
|
|
|
2017-08-03 21:31:47 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2018-02-22 18:34:12 +00:00
|
|
|
def createDeck(self, deck):
|
|
|
|
self.startEditing()
|
|
|
|
deckId = self.collection().decks.id(deck)
|
|
|
|
self.stopEditing()
|
|
|
|
|
|
|
|
return deckId
|
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-10 17:34:43 +00:00
|
|
|
def changeDeck(self, cards, deck):
|
|
|
|
self.startEditing()
|
|
|
|
|
2017-08-12 14:57:28 +00:00
|
|
|
did = self.collection().decks.id(deck)
|
2017-08-10 17:34:43 +00:00
|
|
|
mod = anki.utils.intTime()
|
2017-08-12 14:57:28 +00:00
|
|
|
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
|
2017-08-12 14:57:28 +00:00
|
|
|
self.collection().sched.remFromDyn(cards)
|
2017-08-10 17:34:43 +00:00
|
|
|
|
|
|
|
# then move into new deck
|
2017-08-12 14:57:28 +00:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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()
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
2017-05-27 12:14:40 +00:00
|
|
|
return browser.model.cards
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-05-28 20:21:20 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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-02 12:19:33 +00:00
|
|
|
|
2017-06-29 04:17:11 +00:00
|
|
|
if card is not None:
|
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,
|
2017-06-26 22:57:41 +00:00
|
|
|
'question': card._getQA()['q'],
|
|
|
|
'answer': card._getQA()['a'],
|
2017-08-18 15:15:08 +00:00
|
|
|
'buttons': [b[0] for b in reviewer._answerButtonList()],
|
2017-07-05 20:01:13 +00:00
|
|
|
'modelName': model['name'],
|
2017-10-01 15:22:04 +00:00
|
|
|
'deckName': self.deckNameFromId(card.did),
|
|
|
|
'css': model['css']
|
2017-06-26 22:57:41 +00:00
|
|
|
}
|
2017-06-29 04:17:11 +00:00
|
|
|
|
2017-06-02 12:19:33 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-07-02 17:35:14 +00:00
|
|
|
def guiDeckBrowser(self):
|
2017-07-03 00:27:31 +00:00
|
|
|
self.window().moveToState('deckBrowser')
|
|
|
|
|
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2017-08-29 02:24:08 +00:00
|
|
|
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.
|
2017-06-02 12:19:33 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
2018-05-06 19:59:31 +00:00
|
|
|
@webApi()
|
2018-02-21 13:16:52 +00:00
|
|
|
def sync(self):
|
2018-02-21 13:28:28 +00:00
|
|
|
self.window().onSync()
|
2018-02-21 13:16:52 +00:00
|
|
|
|
2018-03-31 21:40:01 +00:00
|
|
|
|
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(
|
2018-05-06 19:59:31 +00:00
|
|
|
self.window(),
|
2017-02-19 20:57:55 +00:00
|
|
|
'AnkiConnect',
|
|
|
|
'Upgrade to the latest version?',
|
|
|
|
QMessageBox.Yes | QMessageBox.No
|
|
|
|
)
|
|
|
|
|
|
|
|
if response == QMessageBox.Yes:
|
|
|
|
data = download(URL_UPGRADE)
|
|
|
|
if data is None:
|
2018-05-06 19:59:31 +00:00
|
|
|
QMessageBox.critical(self.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))
|
2018-05-06 19:59:31 +00:00
|
|
|
QMessageBox.information(self.window(), 'AnkiConnect', 'Upgraded to the latest version, please restart Anki.')
|
2017-02-19 23:02:09 +00:00
|
|
|
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()
|
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
|
|
|
|
2018-01-06 19:48:25 +00:00
|
|
|
@webApi()
|
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
|
|
|
|
|
|
|
|
2016-05-21 22:10:12 +00:00
|
|
|
#
|
|
|
|
# Entry
|
|
|
|
#
|
|
|
|
|
|
|
|
ac = AnkiConnect()
|