diff --git a/.gitignore b/.gitignore index 5032f0d..35cc9c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.tmp \ No newline at end of file +*.tmp +YDdictBasic \ No newline at end of file diff --git a/AnkiConnect.py b/AnkiConnect.py new file mode 100644 index 0000000..ccf4a58 --- /dev/null +++ b/AnkiConnect.py @@ -0,0 +1,417 @@ +# 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 hashlib +import json +import select +import socket +import urllib2 + + +# +# Constants +# + +API_VERSION = 1 + + +# +# Audio helpers +# + +def audioBuildFilename(kana, kanji): + filename = u'yomichan_{}'.format(kana) + if kanji: + filename += u'_{}'.format(kanji) + filename += u'.mp3' + return filename + + +def audioDownload(kana, kanji): + url = 'http://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji={}'.format(urllib2.quote(kanji.encode('utf-8'))) + if kana: + url += '&kana={}'.format(urllib2.quote(kana.encode('utf-8'))) + + try: + resp = urllib2.urlopen(url) + except urllib2.URLError: + return None + + if resp.code != 200: + return None + + return resp.read() + + +def audioIsPlaceholder(data): + m = hashlib.md5() + m.update(data) + return m.hexdigest() == '7e2c2f954ef6051373ba916f000168dc' + + +def audioInject(note, fields, filename): + for field in fields: + if field in note: + note[field] += u'[sound:{}]'.format(filename) + + +# +# 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': 'text/json', + 'Content-Length': str(len(body)) + } + + 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, audio): + collection = self.collection() + if collection is None: + return + + note = self.createNote(deckName, modelName, fields, tags) + if note is None: + return + + if audio is not None and len(audio['fields']) > 0: + data = audioDownload(audio['kana'], audio['kanji']) + if data is not None and not audioIsPlaceholder(data): + filename = audioBuildFilename(audio['kana'], audio['kanji']) + audioInject(note, audio['fields'], filename) + self.media().writeData(filename, data) + + self.startEditing() + collection.addNote(note) + collection.autosave() + self.stopEditing() + + return note.id + + + def canAddNote(self, deckName, modelName, fields): + return bool(self.createNote(deckName, modelName, fields)) + + + def createNote(self, deckName, modelName, fields, tags=[]): + collection = self.collection() + if collection is None: + return + + model = collection.models.byName(modelName) + if model is None: + return + + deck = collection.decks.byName(deckName) + if deck is None: + return + + note = anki.notes.Note(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() is not None: + self.window().maybeReset() + + + def window(self): + return aqt.mw + + + def collection(self): + return self.window().col + + + def media(self): + collection = self.collection() + if collection is not None: + return collection.media + + + def modelNames(self): + collection = self.collection() + if collection is not None: + return collection.models.allNames() + + + def modelFieldNames(self, modelName): + collection = self.collection() + if collection is None: + return + + model = collection.models.byName(modelName) + if model is not None: + return [field['name'] for field in model['flds']] + + + def deckNames(self): + collection = self.collection() + if collection is not None: + return collection.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') or '') + if hasattr(self, action): + return getattr(self, action)(**(request.get('params') or {})) + + + def api_deckNames(self): + return self.anki.deckNames() + + + def api_modelNames(self): + return self.anki.modelNames() + + + def api_modelFieldNames(self, modelName): + return self.anki.modelFieldNames(modelName) + + + def api_addNote(self, note): + return self.anki.addNote( + note['deckName'], + note['modelName'], + note['fields'], + note['tags'], + note.get('audio') + ) + + + def api_canAddNotes(self, notes): + results = [] + for note in notes: + results.append(self.anki.canAddNote( + note['deckName'], + note['modelName'], + note['fields'] + )) + + return results + + + def api_features(self): + features = {} + for name in dir(self): + method = getattr(self, name) + if name.startswith('api_') and callable(method): + features[name[4:]] = list(method.func_code.co_varnames[1:]) + + return features + + + def api_version(self): + return API_VERSION + + +# +# Entry +# + +ac = AnkiConnect() diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..04dcab9 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +yd2anki.nocode.site \ No newline at end of file diff --git a/README.md b/README.md index ea11b7f..ea7c1e3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# anki-youdao2anki +# youdao2anki.py 此脚本可以批量转换有道词典导出的xml格式生词本到Anki格式的纯文本文件 -# 运行环境 +## 运行环境 Python环境,推荐linux系统 -# 使用方法 +## 使用方法 将有道词典导出的xml文件和本脚本放入同一目录,然后运行 @@ -18,7 +18,7 @@ python youdao2anki.py filename > 推荐最后保存为.txt文件 -# 提取字段 +## 提取字段 本脚本只提取三个字段,以制表符分隔每个字段 @@ -26,6 +26,11 @@ python youdao2anki.py filename - phonetic 音标 - trans 译文 -> 推荐使用本项目的SimpleEnglish主题配合导入使用效果更加哟:smile: +# index.html +此为youdao2anki的web版本全平台通用:smile: + +[YD2Anki传送门](http://yd2anki.nocode.site) + +> web版本只有一个index.html文件,只用一个静态服务器甚至本地就能运行:clap: diff --git a/anki-theme-SimpleEnglish.apkg b/anki-theme-SimpleEnglish.apkg deleted file mode 100644 index 61653ed..0000000 Binary files a/anki-theme-SimpleEnglish.apkg and /dev/null differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..a327b82 --- /dev/null +++ b/index.html @@ -0,0 +1,185 @@ + + + + + + + + + YDdict2Anki + + + + +
有道词典转Anki记忆库
+
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/origin.xml b/origin.xml index 7e26614..335427a 100644 --- a/origin.xml +++ b/origin.xml @@ -1,6 +1,40 @@ - incentive - - + lord + + + + -1 + expel + + + + -1 + mechanism + + + + -1 + symptom + + + + -1 + eliminate poverty + + + + -1 + elaborate + + + + -1 + warranty + + -1 diff --git a/out.txt b/out.txt index ffb160d..e7e8c67 100644 --- a/out.txt +++ b/out.txt @@ -1 +1,7 @@ -incentive [ɪn'sɛntɪv] n. 动机;刺激
adj. 激励的;刺激的 \ No newline at end of file +lord [lɔːd] n. 主;上帝
int. 主,天啊
vt. 使成贵族
vi. 作威作福,称王称霸
n. (Lord)人名;(瑞典)洛德;(法)洛尔 +expel [ɪk'spɛl] vt. 驱逐;开除 +mechanism ['mɛkənɪzəm] n. 机制;原理,途径;进程;机械装置;技巧 +symptom ['sɪmptəm] n. [临床] 症状;征兆 +eliminate poverty [eliminate+poverty] 消除贫困 +elaborate [ɪ'læbəret] adj. 精心制作的;详尽的;煞费苦心的
vt. 精心制作;详细阐述;从简单成分合成(复杂有机物)
vi. 详细描述;变复杂 +warranty ['wɔrənti] n. 保证;担保;授权;(正当)理由 \ No newline at end of file