From cee629407c07bd851548c1aeda007d3ce2db2a90 Mon Sep 17 00:00:00 2001 From: Alex Yatskov Date: Sat, 21 May 2016 15:10:12 -0700 Subject: [PATCH] Initial commit --- anki_connect.py | 301 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 anki_connect.py diff --git a/anki_connect.py b/anki_connect.py new file mode 100644 index 0000000..f46510a --- /dev/null +++ b/anki_connect.py @@ -0,0 +1,301 @@ +# Copyright (C) 2016 Alex Yatskov +# Author: Alex Yatskov +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import PyQt4 +import anki +import aqt +import json +import select +import socket + + +# +# 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 + self.readBuff = '' + self.writeBuff = '' + + + 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 + + self.readBuff = '' + self.writeBuff = '' + + + def parseRequest(self, data): + parts = data.split('\r\n\r\n', 1) + if len(parts) == 1: + return None, 0 + + headers = {} + for line in parts[0].split('\r\n'): + pair = line.split(': ') + headers[pair[0]] = pair[1] if len(pair) > 1 else None + + headerLength = len(parts[0]) + 4 + bodyLength = int(headers['Content-Length']) + 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 + + + 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): + self.clients = filter(lambda c: c.advance(), self.clients) + + + def listen(self, address='127.0.0.1', port=8765, backlog=5): + 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) + self.sock.bind((address, port)) + self.sock.listen(backlog) + + + def handlerWrapper(self, req): + body = json.dumps(self.handler(json.loads(req.body))) + resp = '' + + headers = { + 'HTTP/1.1 200 OK': None, + 'Content-Type': 'application/json', + 'Content-Length': str(len(body)), + 'Access-Control-Allow-Origin': '*' + } + + for key, value in headers.items(): + if value is None: + resp += '{}\r\n'.format(key) + else: + resp += '{}: {}\r\n'.format(key, value) + + resp += '\r\n' + 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 = [] + + +# +# AnkiBridge +# + +class AnkiBridge: + def addNote(self, deckName, modelName, fields, tags=[]): + note = self.createNote(deckName, modelName, fields, tags) + if note is not None: + collection = self.collection() + collection.addNote(note) + collection.autosave() + self.startEditing() + return note.id + + + def canAddNote(self, deckName, modelName, fields): + return bool(self.createNote(deckName, modelName, fields)) + + + def createNote(self, deckName, modelName, fields, tags=[]): + model = self.models().byName(modelName) + if model is None: + return None + + deck = self.decks().byName(deckName) + if deck is None: + return None + + note = anki.notes.Note(self.collection(), model) + note.model()['did'] = deck['id'] + note.tags = tags + + for name, value in fields.items(): + if name in note: + note[name] = value + + if not note.dupeOrEmpty(): + return note + + + def browseNote(self, noteId): + browser = aqt.dialogs.open('Browser', self.window()) + browser.form.searchEdit.lineEdit().setText('nid:{0}'.format(noteId)) + browser.onSearch() + + + def startEditing(self): + self.window().requireReset() + + + def stopEditing(self): + if self.collection(): + self.window().maybeReset() + + + def window(self): + return aqt.mw + + + def addUiAction(self, action): + self.window().form.menuTools.addAction(action) + + + def collection(self): + return self.window().col + + + def models(self): + return self.collection().models + + + def modelNames(self): + return self.models().allNames() + + + def modelFieldNames(self, modelName): + model = self.models().byName(modelName) + if model is not None: + return [field['name'] for field in model['flds']] + + + def decks(self): + return self.collection().decks + + + def deckNames(self): + return self.decks().allNames() + + +# +# AnkiConnect +# + +class AnkiConnect: + def __init__(self, interval=25): + self.anki = AnkiBridge() + self.server = AjaxServer(self.handler) + self.server.listen() + + self.timer = PyQt4.QtCore.QTimer() + self.timer.timeout.connect(self.advance) + self.timer.start(interval) + + + def advance(self): + self.server.advance() + + + def handler(self, request): + action = 'api_' + request.get('action', '') + if hasattr(self, action): + return getattr(self, action)(**request.get('params', {})) + + +# +# Entry +# + +ac = AnkiConnect()