├── 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 |
183 |
184 |
185 |
Step1:
186 |
把有道词典导出的XX.xml选择用记事本打开后粘贴到右边的红色文本框中→
187 |
Step2:
188 |
点击右边的蓝色表格会出现Anki格式的文本
全选后另存为txt文件即可
(注意编码为utf-8)
189 |
190 |
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 |
--------------------------------------------------------------------------------