├── CNAME ├── favicon.ico ├── .gitignore ├── img ├── yd2anki.jpg ├── yd2anki.psd ├── 2015101495113.jpg ├── 90w58PICa9p_1024.png ├── 20140716060929343.jpg ├── 格式工厂2015101495113.jpg ├── 360截图20161210014216223.jpg ├── 360截图20161210015111812.jpg ├── 360截图20161210015139955.jpg ├── 360截图20161210015252819.jpg ├── 360截图20161210020140466.jpg ├── 360截图20161210020306865.jpg ├── 360截图20161210020335769.jpg ├── 360截图20161210020421554.jpg ├── 360截图20161210020504241.jpg ├── 360截图20161210020531344.jpg ├── 360截图20161210020610039.jpg ├── 360截图20161210022241479.jpg ├── 360截图20161210022337250.jpg ├── 360截图20161210022407297.jpg ├── 360截图20161210022627286.jpg └── 360截图20161210022657054.jpg ├── word-class.txt ├── README.md ├── LICENSE ├── youdao2anki.py ├── index.html ├── KindleImporter.py └── AnkiConnect.py /CNAME: -------------------------------------------------------------------------------- 1 | yd2anki.nocode.site -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tmp 2 | YDdictBasic/ 3 | test.py 4 | origin.xml 5 | out.txt 6 | .DS_Store -------------------------------------------------------------------------------- /img/yd2anki.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/yd2anki.jpg -------------------------------------------------------------------------------- /img/yd2anki.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/yd2anki.psd -------------------------------------------------------------------------------- /img/2015101495113.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/2015101495113.jpg -------------------------------------------------------------------------------- /img/90w58PICa9p_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/90w58PICa9p_1024.png -------------------------------------------------------------------------------- /img/20140716060929343.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/20140716060929343.jpg -------------------------------------------------------------------------------- /img/格式工厂2015101495113.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/格式工厂2015101495113.jpg -------------------------------------------------------------------------------- /img/360截图20161210014216223.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210014216223.jpg -------------------------------------------------------------------------------- /img/360截图20161210015111812.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210015111812.jpg -------------------------------------------------------------------------------- /img/360截图20161210015139955.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210015139955.jpg -------------------------------------------------------------------------------- /img/360截图20161210015252819.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210015252819.jpg -------------------------------------------------------------------------------- /img/360截图20161210020140466.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210020140466.jpg -------------------------------------------------------------------------------- /img/360截图20161210020306865.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210020306865.jpg -------------------------------------------------------------------------------- /img/360截图20161210020335769.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210020335769.jpg -------------------------------------------------------------------------------- /img/360截图20161210020421554.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210020421554.jpg -------------------------------------------------------------------------------- /img/360截图20161210020504241.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210020504241.jpg -------------------------------------------------------------------------------- /img/360截图20161210020531344.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210020531344.jpg -------------------------------------------------------------------------------- /img/360截图20161210020610039.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210020610039.jpg -------------------------------------------------------------------------------- /img/360截图20161210022241479.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210022241479.jpg -------------------------------------------------------------------------------- /img/360截图20161210022337250.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210022337250.jpg -------------------------------------------------------------------------------- /img/360截图20161210022407297.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210022407297.jpg -------------------------------------------------------------------------------- /img/360截图20161210022627286.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210022627286.jpg -------------------------------------------------------------------------------- /img/360截图20161210022657054.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecator/anki-youdao2anki/HEAD/img/360截图20161210022657054.jpg -------------------------------------------------------------------------------- /word-class.txt: -------------------------------------------------------------------------------- 1 | prep = 介系词;前置词,preposition的缩写 2 | pron = 代名词,pronoun的缩写 3 | n = 名词,noun的缩写 4 | v = 动词,兼指及物动词和不及物动词,verb的缩写 5 | conj = 连接词 ,conjunction的缩写 6 | s = 主词 7 | sc = 主词补语 8 | o = 受词 9 | oc = 受词补语 10 | vi = 不及物动词,intransitive verb的缩写 11 | vt = 及物动词,transitive verb的缩写 12 | aux.v = 助动词 ,auxiliary的缩写 13 | a = 形容词,adjective的缩写 14 | ad = 副词,adverb的缩写 15 | art = 冠词,article的缩写 16 | num = 数词,numeral的缩写 17 | int = 感叹词,interjection的缩写 18 | u = 不可数名词,uncountable noun的缩写 19 | c = 可数名词,countable noun的缩写 20 | pl = 复数,plural的缩写 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # youdao2anki.py 2 | 3 | 此脚本可以批量转换有道词典导出的xml格式生词本到Anki格式的纯文本文件 4 | 5 | ## 运行环境 6 | 7 | Python环境,推荐linux系统 8 | 9 | ## 使用方法 10 | 11 | 将有道词典导出的xml文件和本脚本放入同一目录,然后运行 12 | 13 | ``` 14 | python youdao2anki.py filename 15 | ``` 16 | 17 | 后面的filename是可选参数,为需要转换的xml文件名,默认为origin.xml,之后脚本会自动解析,最后提示输入保存的文件名后即可完成转换 18 | 19 | > 推荐最后保存为.txt文件 20 | 21 | ## 提取字段 22 | 23 | 本脚本只提取三个字段,以制表符分隔每个字段 24 | 25 | - word 单词 26 | - phonetic 音标 27 | - trans 译文 28 | 29 | # index.html 30 | 31 | 此为youdao2anki的web版本全平台通用:smile: 32 | 33 | [YD2Anki传送门](http://yd2anki.nocode.site) 34 | 35 | > web版本只有一个index.html文件,只用一个静态服务器甚至本地就能运行:clap: 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Martin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /youdao2anki.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import xml.sax 5 | import sys 6 | import re 7 | from xml.dom.minidom import parse 8 | import xml.dom.minidom 9 | reload(sys) 10 | sys.setdefaultencoding('utf8') 11 | 12 | origin='origin.xml' 13 | print sys.argv 14 | if len(sys.argv)>1 and sys.argv[1]!='': origin=sys.argv[1] 15 | # 使用minidom解析器打开 XML 文档 16 | DOMTree = xml.dom.minidom.parse(origin) 17 | collection = DOMTree.documentElement 18 | 19 | # 在集合中获取所有item 20 | items = collection.getElementsByTagName("item") 21 | 22 | # 遍历每个item的详细信息 23 | Items='' 24 | for item in items: 25 | Word=Phonetic=Trans='' 26 | word = item.getElementsByTagName('word')[0] 27 | if word.hasChildNodes(): Word = word.firstChild.data.replace('\n','
') 28 | phonetic = item.getElementsByTagName('phonetic')[0] 29 | if phonetic.hasChildNodes(): Phonetic = phonetic.firstChild.data.replace('\n','
') 30 | trans = item.getElementsByTagName('trans')[0] 31 | if trans.hasChildNodes(): Trans = trans.firstChild.data.replace('\n','
') 32 | Item=Word+'\t'+Phonetic+'\t'+Trans 33 | print u'解析 %s'%Item 34 | if Items=='': 35 | Items=Item 36 | else: 37 | Items+='\n'+Item 38 | 39 | #处理换行符,替换不同词性之间添加
标签 40 | def handleBrTag(matched): 41 | #print matched.group() 42 | return matched.group().replace(' ','
',1) 43 | 44 | patt=r'( (adj\.))|( (n\.))|( (adv\.))|( (prep\.))|( (pron\.))|( (v\.))|( (conj\.))|( (vi\.))|( (vt\.))|( (pl\.))|( (c\.))|( (num\.))' 45 | Items=re.sub(patt,handleBrTag,Items) 46 | 47 | #保存文件 48 | print u'解析完成,一共解析 %d 条数据'%len(items) 49 | print u'请输入保存文件名(不输入则保存为out.txt):' 50 | fileName=raw_input() 51 | if not fileName: 52 | fileName="out.txt" 53 | try: 54 | f=open(fileName,'w') 55 | f.write(Items) 56 | except Exception as e: 57 | print u'保存文件 %s 失败'%fileName 58 | else: 59 | print u'保存文件 %s 成功'%fileName 60 | finally: 61 | f.close() 62 | sys.exit(0) 63 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | YDdict2Anki 10 | 87 | 180 | 181 | 182 |
有道词典转Anki记忆库
183 |
184 | 190 |
191 | 192 |
193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /KindleImporter.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import sys 4 | reload(sys) 5 | sys.setdefaultencoding('utf-8') # helps with odd formatting 6 | import os 7 | import sqlite3 8 | import time 9 | import re 10 | import aqt 11 | from aqt import mw 12 | from aqt.qt import * 13 | import aqt.importing 14 | import anki 15 | from anki.db import DB 16 | from anki import importing 17 | from anki.importing.csvfile import TextImporter 18 | from anki.importing.apkg import AnkiPackageImporter 19 | from anki.importing.anki2 import Anki2Importer 20 | from anki.importing.supermemo_xml import SupermemoXmlImporter 21 | from anki.importing.mnemo import MnemosyneImporter 22 | from anki.importing.pauker import PaukerImporter 23 | from anki.importing.noteimp import NoteImporter, ForeignNote, ForeignCard 24 | from anki.stdmodels import addBasicModel, addClozeModel 25 | from anki.lang import ngettext, currentLang, _ 26 | from anki.hooks import addHook, runHook, wrap 27 | from aqt.utils import shortcut, showInfo, showText, tooltip 28 | 29 | 30 | if currentLang == 'zh_CN': 31 | tags = [u'单词', u'单词原型', u'例句', u'书籍', u'创建时间'] 32 | import_mode = u'导入单词字段相同但其他字段不同的生词' 33 | else: 34 | tags = [u'Word', u'Stem', u'Usage', u'Book', u'Create Time'] 35 | 36 | 37 | class KindleDbImporter(TextImporter): 38 | needDelimiter = False 39 | needMapper = True 40 | 41 | # def __init__ 42 | 43 | def openFile(self): 44 | db = DB(self.file) 45 | notes = {} 46 | note = None 47 | self.data = list() 48 | # showInfo('begin execute sql') 49 | results = db.execute( 50 | "select ws.id, ws.word, ws.stem, ws.lang, datetime(ws.timestamp*0.001, 'unixepoch', 'localtime'), lus.usage, bi.title, bi.authors from words as ws left join lookups as lus on ws.id=lus.word_key left join book_info as bi on lus.book_key=bi.id") 51 | self.fileobj = 1 # make sure file open ok 52 | # showInfo(str(len(results))) 53 | self.data = [] 54 | for item in results: 55 | item = list(item) 56 | for i, each in enumerate(item): 57 | if not each: 58 | item[i] = "" 59 | id, word, stem, create_time, useage, book_title, book_authors = item[ 60 | 0], item[1], item[2], item[4], item[5], item[6], item[7] 61 | self.data.append([word, stem, useage.replace( 62 | word, u'%s' % word), u'%s ---- %s' % (book_title, book_authors), create_time]) 63 | 64 | self.numFields = len(tags) 65 | self.initMapping() 66 | 67 | def foreignNotes(self): 68 | self.open() 69 | self.tagsToAdd = tags 70 | notes = [] 71 | for d in self.data: 72 | note = ForeignNote() 73 | note.fields.extend(d) 74 | notes.append(note) 75 | return notes 76 | 77 | 78 | def customImportMode(self): 79 | self.frm.importMode.insertItem( 80 | 4, u'Import when first field matches existing note but other fields does not match.') 81 | 82 | 83 | def showMapping(self, keepMapping=False, hook=None): 84 | if hook: 85 | hook() 86 | if not keepMapping: 87 | self.mapping = self.importer.mapping 88 | self.frm.mappingGroup.show() 89 | assert self.importer.fields() 90 | # set up the mapping grid 91 | if self.mapwidget: 92 | self.mapbox.removeWidget(self.mapwidget) 93 | self.mapwidget.deleteLater() 94 | self.mapwidget = QWidget() 95 | self.mapbox.addWidget(self.mapwidget) 96 | self.grid = QGridLayout(self.mapwidget) 97 | self.mapwidget.setLayout(self.grid) 98 | self.grid.setContentsMargins(3, 3, 3, 3) 99 | self.grid.setSpacing(6) 100 | fields = self.importer.fields() 101 | for num in range(len(self.mapping)): 102 | i = 0 103 | text = _("Field %d of file is:") % (num + 1) 104 | self.grid.addWidget(QLabel(text), num, i) 105 | i += 1 106 | # , tags[num] 107 | if isinstance(self.importer, KindleDbImporter): 108 | text = _(tags[num]) 109 | self.grid.addWidget(QLabel(text), num, i) 110 | i += 1 111 | if self.mapping[num] == "_tags": 112 | text = _("mapped to Tags") 113 | elif self.mapping[num]: 114 | text = _("mapped to %s") % self.mapping[num] 115 | else: 116 | text = _("") 117 | self.grid.addWidget(QLabel(text), num, i) 118 | i += 1 119 | button = QPushButton(_("Change")) 120 | self.grid.addWidget(button, num, i) 121 | button.clicked.connect(lambda _, s=self, n=num: s.changeMappingNum(n)) 122 | 123 | aqt.importing.ImportDialog.setupMappingFrame = wrap( 124 | aqt.importing.ImportDialog.setupMappingFrame, customImportMode) 125 | aqt.importing.ImportDialog.showMapping = showMapping 126 | 127 | 128 | def my_importFile(mw, file): 129 | if mw.custom == 'kindle db': 130 | file += '.kindle' 131 | 132 | 133 | def custom(self): 134 | self.import_src = 'kindle db' 135 | 136 | aqt.main.AnkiQt.custom = custom 137 | 138 | 139 | # def __my_init__(self, mw, importer): 140 | # ''' hide the autodetect button to avoid misleading ''' 141 | # QDialog.__init__(self, mw, Qt.Window) 142 | # self.mw = mw 143 | # self.importer = importer 144 | # self.frm = aqt.forms.importing.Ui_ImportDialog() 145 | # self.frm.setupUi(self) 146 | # self.frm.buttonBox.button(QDialogButtonBox.Help).clicked.connect( 147 | # self.helpRequested) 148 | # self.setupMappingFrame() 149 | # self.setupOptions() 150 | # self.modelChanged() 151 | # self.frm.autoDetect.setVisible(self.importer.needDelimiter) 152 | # addHook("currentModelChanged", self.modelChanged) 153 | # self.frm.autoDetect.clicked.connect(self.onDelimiter) 154 | # self.updateDelimiterButtonText() 155 | # self.frm.allowHTML.setChecked( 156 | # self.mw.pm.profile.get('allowHTML', True)) 157 | # self.frm.importMode.setCurrentIndex( 158 | # self.mw.pm.profile.get('importMode', 1)) 159 | # # import button 160 | # b = QPushButton(_("Import")) 161 | # self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole) 162 | # if importer is KindleDbImporter: 163 | # self.frm.autoDetect.setVisible(False) 164 | # self.exec_() 165 | 166 | # aqt.importing.ImportDialog.__init__ = __my_init__ 167 | 168 | # 原始importFile函数根据后缀名选择第一个符合条件的导入器,db后缀与原有mnemosyne导入器一致 169 | # 如果要修改原始的importFile函数,只能在函数中间代码中加,不能应用monkey patch 170 | # 所以这里或者全部重写importFile 171 | # 或者修改kindle db文件在选择列表中的位置,但这样的话无法导入mnemosyne的db文件 172 | # 或者导入前更改kindle db的后缀名 173 | importing.Importers = ( 174 | (_("Text separated by tabs or semicolons (*)"), TextImporter), 175 | (_("Packaged Anki Deck (*.apkg *.zip)"), AnkiPackageImporter), 176 | (_("Kindle vocab (*.db)"), KindleDbImporter), 177 | (_("Mnemosyne 2.0 Deck (*.db)"), MnemosyneImporter), 178 | (_("Supermemo XML export (*.xml)"), SupermemoXmlImporter), 179 | (_("Pauker 1.8 Lesson (*.pau.gz)"), PaukerImporter), 180 | ) 181 | -------------------------------------------------------------------------------- /AnkiConnect.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 Alex Yatskov 2 | # Author: Alex Yatskov 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | 18 | import PyQt4 19 | import anki 20 | import aqt 21 | import hashlib 22 | import json 23 | import select 24 | import socket 25 | import urllib2 26 | 27 | 28 | # 29 | # Constants 30 | # 31 | 32 | API_VERSION = 1 33 | 34 | 35 | # 36 | # Audio helpers 37 | # 38 | 39 | def audioBuildFilename(kana, kanji): 40 | filename = u'yomichan_{}'.format(kana) 41 | if kanji: 42 | filename += u'_{}'.format(kanji) 43 | filename += u'.mp3' 44 | return filename 45 | 46 | 47 | def audioDownload(kana, kanji): 48 | url = 'http://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji={}'.format(urllib2.quote(kanji.encode('utf-8'))) 49 | if kana: 50 | url += '&kana={}'.format(urllib2.quote(kana.encode('utf-8'))) 51 | 52 | try: 53 | resp = urllib2.urlopen(url) 54 | except urllib2.URLError: 55 | return None 56 | 57 | if resp.code != 200: 58 | return None 59 | 60 | return resp.read() 61 | 62 | 63 | def audioIsPlaceholder(data): 64 | m = hashlib.md5() 65 | m.update(data) 66 | return m.hexdigest() == '7e2c2f954ef6051373ba916f000168dc' 67 | 68 | 69 | def audioInject(note, fields, filename): 70 | for field in fields: 71 | if field in note: 72 | note[field] += u'[sound:{}]'.format(filename) 73 | 74 | 75 | # 76 | # AjaxRequest 77 | # 78 | 79 | class AjaxRequest: 80 | def __init__(self, headers, body): 81 | self.headers = headers 82 | self.body = body 83 | 84 | 85 | # 86 | # AjaxClient 87 | # 88 | 89 | class AjaxClient: 90 | def __init__(self, sock, handler): 91 | self.sock = sock 92 | self.handler = handler 93 | self.readBuff = '' 94 | self.writeBuff = '' 95 | 96 | 97 | def advance(self, recvSize=1024): 98 | if self.sock is None: 99 | return False 100 | 101 | rlist, wlist = select.select([self.sock], [self.sock], [], 0)[:2] 102 | 103 | if rlist: 104 | msg = self.sock.recv(recvSize) 105 | if not msg: 106 | self.close() 107 | return False 108 | 109 | self.readBuff += msg 110 | 111 | req, length = self.parseRequest(self.readBuff) 112 | if req is not None: 113 | self.readBuff = self.readBuff[length:] 114 | self.writeBuff += self.handler(req) 115 | 116 | if wlist and self.writeBuff: 117 | length = self.sock.send(self.writeBuff) 118 | self.writeBuff = self.writeBuff[length:] 119 | if not self.writeBuff: 120 | self.close() 121 | return False 122 | 123 | return True 124 | 125 | 126 | def close(self): 127 | if self.sock is not None: 128 | self.sock.close() 129 | self.sock = None 130 | 131 | self.readBuff = '' 132 | self.writeBuff = '' 133 | 134 | 135 | def parseRequest(self, data): 136 | parts = data.split('\r\n\r\n', 1) 137 | if len(parts) == 1: 138 | return None, 0 139 | 140 | headers = {} 141 | for line in parts[0].split('\r\n'): 142 | pair = line.split(': ') 143 | headers[pair[0]] = pair[1] if len(pair) > 1 else None 144 | 145 | headerLength = len(parts[0]) + 4 146 | bodyLength = int(headers['Content-Length']) 147 | totalLength = headerLength + bodyLength 148 | 149 | if totalLength > len(data): 150 | return None, 0 151 | 152 | body = data[headerLength : totalLength] 153 | return AjaxRequest(headers, body), totalLength 154 | 155 | 156 | # 157 | # AjaxServer 158 | # 159 | 160 | class AjaxServer: 161 | def __init__(self, handler): 162 | self.handler = handler 163 | self.clients = [] 164 | self.sock = None 165 | 166 | 167 | def advance(self): 168 | if self.sock is not None: 169 | self.acceptClients() 170 | self.advanceClients() 171 | 172 | 173 | def acceptClients(self): 174 | rlist = select.select([self.sock], [], [], 0)[0] 175 | if not rlist: 176 | return 177 | 178 | clientSock = self.sock.accept()[0] 179 | if clientSock is not None: 180 | clientSock.setblocking(False) 181 | self.clients.append(AjaxClient(clientSock, self.handlerWrapper)) 182 | 183 | 184 | def advanceClients(self): 185 | self.clients = filter(lambda c: c.advance(), self.clients) 186 | 187 | 188 | def listen(self, address='127.0.0.1', port=8765, backlog=5): 189 | self.close() 190 | 191 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 192 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 193 | self.sock.setblocking(False) 194 | self.sock.bind((address, port)) 195 | self.sock.listen(backlog) 196 | 197 | 198 | def handlerWrapper(self, req): 199 | body = json.dumps(self.handler(json.loads(req.body))) 200 | resp = '' 201 | 202 | headers = { 203 | 'HTTP/1.1 200 OK': None, 204 | 'Content-Type': 'text/json', 205 | 'Content-Length': str(len(body)) 206 | } 207 | 208 | for key, value in headers.items(): 209 | if value is None: 210 | resp += '{}\r\n'.format(key) 211 | else: 212 | resp += '{}: {}\r\n'.format(key, value) 213 | 214 | resp += '\r\n' 215 | resp += body 216 | 217 | return resp 218 | 219 | 220 | def close(self): 221 | if self.sock is not None: 222 | self.sock.close() 223 | self.sock = None 224 | 225 | for client in self.clients: 226 | client.close() 227 | 228 | self.clients = [] 229 | 230 | 231 | # 232 | # AnkiBridge 233 | # 234 | 235 | class AnkiBridge: 236 | def addNote(self, deckName, modelName, fields, tags, audio): 237 | collection = self.collection() 238 | if collection is None: 239 | return 240 | 241 | note = self.createNote(deckName, modelName, fields, tags) 242 | if note is None: 243 | return 244 | 245 | if audio is not None and len(audio['fields']) > 0: 246 | data = audioDownload(audio['kana'], audio['kanji']) 247 | if data is not None and not audioIsPlaceholder(data): 248 | filename = audioBuildFilename(audio['kana'], audio['kanji']) 249 | audioInject(note, audio['fields'], filename) 250 | self.media().writeData(filename, data) 251 | 252 | self.startEditing() 253 | collection.addNote(note) 254 | collection.autosave() 255 | self.stopEditing() 256 | 257 | return note.id 258 | 259 | 260 | def canAddNote(self, deckName, modelName, fields): 261 | return bool(self.createNote(deckName, modelName, fields)) 262 | 263 | 264 | def createNote(self, deckName, modelName, fields, tags=[]): 265 | collection = self.collection() 266 | if collection is None: 267 | return 268 | 269 | model = collection.models.byName(modelName) 270 | if model is None: 271 | return 272 | 273 | deck = collection.decks.byName(deckName) 274 | if deck is None: 275 | return 276 | 277 | note = anki.notes.Note(collection, model) 278 | note.model()['did'] = deck['id'] 279 | note.tags = tags 280 | 281 | for name, value in fields.items(): 282 | if name in note: 283 | note[name] = value 284 | 285 | if not note.dupeOrEmpty(): 286 | return note 287 | 288 | 289 | def browseNote(self, noteId): 290 | browser = aqt.dialogs.open('Browser', self.window()) 291 | browser.form.searchEdit.lineEdit().setText('nid:{0}'.format(noteId)) 292 | browser.onSearch() 293 | 294 | 295 | def startEditing(self): 296 | self.window().requireReset() 297 | 298 | 299 | def stopEditing(self): 300 | if self.collection() is not None: 301 | self.window().maybeReset() 302 | 303 | 304 | def window(self): 305 | return aqt.mw 306 | 307 | 308 | def collection(self): 309 | return self.window().col 310 | 311 | 312 | def media(self): 313 | collection = self.collection() 314 | if collection is not None: 315 | return collection.media 316 | 317 | 318 | def modelNames(self): 319 | collection = self.collection() 320 | if collection is not None: 321 | return collection.models.allNames() 322 | 323 | 324 | def modelFieldNames(self, modelName): 325 | collection = self.collection() 326 | if collection is None: 327 | return 328 | 329 | model = collection.models.byName(modelName) 330 | if model is not None: 331 | return [field['name'] for field in model['flds']] 332 | 333 | 334 | def deckNames(self): 335 | collection = self.collection() 336 | if collection is not None: 337 | return collection.decks.allNames() 338 | 339 | 340 | # 341 | # AnkiConnect 342 | # 343 | 344 | class AnkiConnect: 345 | def __init__(self, interval=25): 346 | self.anki = AnkiBridge() 347 | self.server = AjaxServer(self.handler) 348 | self.server.listen() 349 | 350 | self.timer = PyQt4.QtCore.QTimer() 351 | self.timer.timeout.connect(self.advance) 352 | self.timer.start(interval) 353 | 354 | 355 | def advance(self): 356 | self.server.advance() 357 | 358 | 359 | def handler(self, request): 360 | action = 'api_' + (request.get('action') or '') 361 | if hasattr(self, action): 362 | return getattr(self, action)(**(request.get('params') or {})) 363 | 364 | 365 | def api_deckNames(self): 366 | return self.anki.deckNames() 367 | 368 | 369 | def api_modelNames(self): 370 | return self.anki.modelNames() 371 | 372 | 373 | def api_modelFieldNames(self, modelName): 374 | return self.anki.modelFieldNames(modelName) 375 | 376 | 377 | def api_addNote(self, note): 378 | return self.anki.addNote( 379 | note['deckName'], 380 | note['modelName'], 381 | note['fields'], 382 | note['tags'], 383 | note.get('audio') 384 | ) 385 | 386 | 387 | def api_canAddNotes(self, notes): 388 | results = [] 389 | for note in notes: 390 | results.append(self.anki.canAddNote( 391 | note['deckName'], 392 | note['modelName'], 393 | note['fields'] 394 | )) 395 | 396 | return results 397 | 398 | 399 | def api_features(self): 400 | features = {} 401 | for name in dir(self): 402 | method = getattr(self, name) 403 | if name.startswith('api_') and callable(method): 404 | features[name[4:]] = list(method.func_code.co_varnames[1:]) 405 | 406 | return features 407 | 408 | 409 | def api_version(self): 410 | return API_VERSION 411 | 412 | 413 | # 414 | # Entry 415 | # 416 | 417 | ac = AnkiConnect() 418 | --------------------------------------------------------------------------------