@@ 1,301 @@
+# 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 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()