├── .gitignore ├── LICENSE ├── README-CN.md ├── README.md ├── _config.yml ├── docs ├── _config.yml ├── classes.md ├── custom-service.md ├── hooks.md ├── introduction.md └── issue_template.md ├── screenshots ├── add_dict_folders.png ├── adding.png ├── browser.gif ├── browser.png ├── card.png ├── dicts.png ├── editor.png ├── new.png ├── note_type.png ├── qa1.png └── setup.png └── src ├── __init__.py ├── constants.py ├── context.py ├── lang.py ├── libs ├── __init__.py ├── mdict │ ├── __init__.py │ ├── lzo.py │ ├── mdict_query.py │ ├── pureSalsa20.py │ ├── readmdict.py │ └── ripemd128.py └── pystardict.py ├── meta.json ├── prepare.py ├── progress.py ├── query.py ├── resources └── wqicon.png ├── service ├── LDOCE6.py ├── __init__.py ├── baicizhan.py ├── base.py ├── bing.py ├── bing3tp.py ├── esdict.py ├── frdic.py ├── iciba.py ├── longman.py ├── manager.py ├── oxford.py ├── remotemdx.py ├── static │ ├── _bing.css │ ├── _longman.css │ └── _youdao.css ├── txt.py ├── youdao.py └── youdaofr.py ├── ui.py └── utils ├── Queue.py ├── __init__.py ├── helper.py ├── importlib.py └── misc.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | 92 | *.html 93 | test.py 94 | dicts/ 95 | *wqcfg 96 | .vscode/ 97 | test/ 98 | anki.zip 99 | .wqcfg.json -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # WordQuery 插件(anki) 2 | 3 | ## 主要功能 4 | 5 | 1. 快速零散制卡 6 | 7 | 在添加卡片和编辑卡片界面,插件辅助完成单词释义的查询和自动填充。 8 | 9 | 2. 批量制卡 10 | 11 | 在浏览器界面选择多个单词,插件辅助完成选中单词释义的批量查询和自动填充。 12 | 13 | 3. 本地词典支持 14 | 15 | 支持**mdx格式**词典和**stardict格式**词典。 16 | 17 | 4. 网络词典支持 18 | 19 | 支持网络词典的查询,目前内置有道、百词斩等插件。 20 | 21 | 所有词典以插件形式实现,用户可自行定义、修改和删除。插件定义和实现方式可参考[该节](#词典服务插件定义)。 22 | 23 | 24 | 25 | ## 使用方法 26 | 27 | ### 安装 28 | 29 | 1. [https://github.com/finalion/WordQuery](https://github.com/finalion/WordQuery)下载并放到anki插件文件夹 30 | 31 | 2. 安装代码775418273 32 | 33 | ### 词典文件夹设置 34 | 35 | 1. “工具”菜单-->"WordQuery",弹出设置界面; 36 | 37 | 2. 点击“词典文件夹”按钮,在弹出的对话框中通过“+”或“-”增加或删除文件夹,支持递归查找。 38 | 39 | ![](screenshots/add_dict_folders.png) 40 | 41 | 3. 其他设置 42 | 43 | - 使用文件名作为词典名:不选中则使用词典中的特定标题字段作为词典名 44 | 45 | - 导出媒体文件:选中则导出词典解释中包含的**音频** 46 | 47 | 48 | ### 笔记类型选择 49 | 50 | 在设置界面中,点击“选择笔记类型”按钮,选择要设定的笔记类型; 51 | 52 | ![](screenshots/note_type.png) 53 | 54 | 55 | ### 查询单词字段设置 56 | 57 | 单选框选中要查询的单词字段. 58 | 59 | 60 | ### 待填充词典字段与笔记区域的映射 61 | 62 | 为每个笔记区域映射待查询的词典以及词典字段: 63 | 64 | ![](screenshots/dicts.png) 65 | 66 | 词典下拉框选项中包括三部分,各部分之间有分割线: 67 | 68 | - 第一部分:“不是词典字段” 69 | 70 | - 第二部分:设定文件夹中包含的可支持的本地词典 71 | 72 | - 第三部分:网络词典 73 | 74 | 75 | ### 查询并填充释义 76 | 77 | 插件可在多种编辑模式下快速查询并添加单词释义。 78 | 79 | 1. “添加笔记”界面和“编辑笔记”界面 80 | 81 | - 点击“Query”按钮查询并填充全部字段的释义; 82 | 83 | - 右键菜单“Query All Fields”查询并填充全部字段的释义; 84 | 85 | - 右键菜单“Query Current Field”查询并填充当前字段的释义; 86 | 87 | - 右键菜单“Options”查看修改笔记区域和词典字段的映射; 88 | 89 | 2. 浏览器 90 | 91 | - 选择多个卡片,工具栏菜单“WoryQuery”选择“查询选中单词”,查询并填充所有选中单词全部字段的释义; 92 | 93 | ![](screenshots/editor.png) 94 | 95 | ![](screenshots/browser.png) 96 | 97 | 所有操作均支持快捷键,默认为"Ctrl+Q",可[修改](#快捷键自定义)。 98 | 99 | 100 | ## 其他Tips 101 | 102 | ### 快捷键自定义 103 | 104 | “工具”菜单-->“插件”-->"wordquery"-->编辑,找到并修改快捷键设置: 105 | 106 | ```python 107 | # shortcut 108 | shortcut = 'Ctrl+Q' 109 | ``` 110 | 111 | 112 | ## 词典服务插件定义 113 | 114 | ### 实现类 115 | 116 | 继承WebService,使用```@register(label)``` 装饰。参数```label```作为词典标签,出现在词典下拉列表中。例如 117 | ```python 118 | @register(u'有道词典') 119 | class Youdao(WebService): 120 | """service implementation""" 121 | ``` 122 | 如果不注册```label```,则默认使用**类名称**作为标签。 123 | 124 | ### 词典字段导出函数定义 125 | 126 | 词典字段导出函数返回查询词典相应字段的释义,使用```@export(fld_name, order)``` 装饰。 127 | 128 | - 参数```fld_name```为词典字段名称,出现在词典字段下拉列表中 129 | 130 | - 参数```order```为词典字段在下拉列表中的顺序,小号在上,大号在下,但号码无需连续。 131 | 132 | 例如, 133 | ```python 134 | @export(u'美式音标', 1) 135 | def fld_phonetic_us(self): 136 | return self._get_field('phonitic_us') 137 | 138 | @export(u'英式音标', 2) 139 | def fld_phonetic_uk(self): 140 | return self._get_field('phonitic_uk') 141 | ``` 142 | 143 | ### 字段修饰(可选) 144 | 145 | 使用```@with_style(**kwargs)```修饰导出词典字段函数,支持参数包括, 146 | 147 | - ```cssfile``` 148 | 149 | 词典(字段)使用的css文件,需放置在```service```模块的```static```文件夹下。 150 | 151 | - ```css``` 152 | 153 | 词典(字段)使用的css字符串。 154 | 155 | - ```jsfile``` 156 | 157 | 词典(字段)使用的js文件,需放置在```service```模块的```static```文件夹下。 158 | 159 | - ```js``` 160 | 161 | 词典(字段)使用的js字符串。 162 | 163 | - ```need_wrap_css``` 164 | 165 | 为了避免不同字典css样式命名重复可能带来的样式混乱,设置该参数为```True```,插件可通过添加全局```div```对样式表和词典释义结果进行包装。需要定义添加的全局```div```的类名```wrap_class```。 166 | 167 | 包装之后的css文件为```*orig_name*_wrap.css```。 168 | 169 | *目前包装方法比较粗糙,待持续验证和改进。* 170 | 171 | - ```wrap_class``` 172 | 173 | 全局```div```类名,```need_wrap_css```为```True```时有效。 174 | 175 | 例如, 176 | ```python 177 | @with_styles(cssfile='_youdao.css', need_wrap_css=True, wrap_class='youdao') 178 | def _get_singledict(self, single_dict, lang='eng'): 179 | url = "http://m.youdao.com/singledict?q=%s&dict=%s&le=%s&more=false" % ( 180 | self.word, single_dict, lang) 181 | try: 182 | return urllib2.urlopen(url, timeout=5).read() 183 | except: 184 | return '' 185 | ``` 186 | 187 | ### Cache使用 188 | 189 | 为了避免对网络词典服务的重复查询,可在必要时对中间结果进行缓存。方法包括, 190 | 191 | - ```cache_this(result)``` 192 | 193 | 缓存当前结果。 194 | 195 | - ```cached(key)``` 196 | 197 | 检查```key```是否被缓存。 198 | 199 | - ```cache_result(key)``` 200 | 201 | 返回缓存结果。 202 | 203 | 具体可参考[有道词典 youdao.py](wquery/service/youdao.py)实现方式。 204 | 205 | 206 | ## 插件所使用的外部库 207 | 208 | - [mdict-query](https://github.com/mmjang/mdict-query) 209 | - [pystardict](https://github.com/lig/pystardict) 210 | 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordQuery Addon for Anki 2 | 3 | [中文说明](README-CN.md) 4 | 5 | ## Main Features 6 | 7 | This addon is developed to emancipate you from the tedious work of looking up words in dictionary and pasting the explanations to anki. 8 | 9 | **Querying Words and Making Cards, IMMEDIATELY!** 10 | 11 | **Support querying in mdx and stardict dictionaries** 12 | 13 | **Support querying in web dictionaries (having provided many ones, and more others need to be [customized](#customize))** 14 | 15 | ## Installation 16 | 17 | 1. Place "wordquery.py" and "wquery" folder in this repository in the anki add folder. 18 | **OR** 19 | 2. Use the installation code: 775418273 20 | 21 | ## How to Set 22 | 23 | ### Set Local Dictionaries 24 | 25 | *If you do not use local dictionaries, you can skip this step.* 26 | 27 | 1. Click menu "Tool"->"WordQuery", popup the "Options" dialog 28 | 29 | 2. Click "Dict folders" button, add or remove the dictionary folders (support recursive searching) 30 | 31 | ![](screenshots/add_dict_folders.png) 32 | 33 | - "Use filename as dict label" 34 | - "Export media files" indicates if the audios will be exported. 35 | 36 | 37 | ### Set Note Type 38 | 39 | In the "Options" dialog, click "Choose note type" and set the note type you want to use. 40 | 41 | ![](screenshots/note_type.png) 42 | 43 | 44 | ### Set the word field 45 | 46 | Click the radio button to set the word field you want to query. 47 | 48 | 49 | ### Set the mappings from note fields to dictionary explanations 50 | 51 | ![](screenshots/dicts.png) 52 | 53 | The "Dict" comoboxes are used to specify the dictionaries. 54 | 55 | The "Dict fields" comoboxes are used to specify the available dictionary fields. 56 | 57 | ## How to Use 58 | 59 | ### "Add" dialog 60 | 61 | Once the word to query is ready, click "Query" button or popup the context menu and use relevant commands. 62 | 63 | * "Query" button 64 | Query the explanations for all the fields. 65 | * “Query All Fields” menu 66 | Query the explanations for all the fields. 67 | * "Query Current Field" menu 68 | Query the explanation for current focused field. 69 | 70 | ![](screenshots/editor.png) 71 | 72 | ### "Browse" window 73 | 74 | Select single word or multiple words, click menu "WordQuery"->"Query selected". 75 | 76 | ![](screenshots/browser.png) 77 | 78 | All above query actions can be trigged also by the shortcut (default "Ctrl+Q"), but you could change it through the addon's "Edit" menu. 79 | ```python 80 | # shortcut 81 | shortcut = 'Ctrl+Q' 82 | ``` 83 | 84 | 85 | ## Service Customization 86 | 87 | The advanced users can implement new web dictionary services. See [a typical reference](wquery/service/youdao.py) for the details. 88 | 89 | ### Inherit `WebService` class 90 | 91 | ```@register(label)``` is used to register the service, and parameter ```label``` as the dictionary name will be shown in the dictioary list. 92 | 93 | ```python 94 | @register(u'有道词典') 95 | class Youdao(WebService): 96 | """service implementation""" 97 | ``` 98 | 99 | ### Define Dictionary Field 100 | 101 | The field export function has to be decorated with ```@export(fld_name, order)```. 102 | 103 | - para ```fld_name```: name of the dictionary field 104 | 105 | - para ```order```: order of the field, the smaller number will be shown on the upper of the field list. 106 | 107 | ```python 108 | @export(u'美式音标', 1) 109 | def fld_phonetic_us(self): 110 | return self._get_field('phonitic_us') 111 | 112 | @export(u'英式音标', 2) 113 | def fld_phonetic_uk(self): 114 | return self._get_field('phonitic_uk') 115 | ``` 116 | 117 | ### Decorating the Field (optional) 118 | 119 | Using ```@with_style(**kwargs)``` to specify the css style strings or files, javascript strings or files, whether wrapping the css to avoid latent style interference. 120 | 121 | ```python 122 | @with_styles(cssfile='_youdao.css', need_wrap_css=True, wrap_class='youdao') 123 | def _get_singledict(self, single_dict, lang='eng'): 124 | url = "http://m.youdao.com/singledict?q=%s&dict=%s&le=%s&more=false" % ( 125 | self.word, single_dict, lang) 126 | try: 127 | return urllib2.urlopen(url, timeout=5).read() 128 | except: 129 | return '' 130 | ``` 131 | 132 | 133 | 134 | ## Other Projects Used 135 | 136 | - [mdict-query](https://github.com/mmjang/mdict-query) 137 | - [pystardict](https://github.com/lig/pystardict) 138 | 139 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/classes.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | props: 4 | 5 | ```guid, mid, mod, usn, tags, fields, flags, data``` 6 | 7 | methods: 8 | ```python 9 | cards() 10 | model() 11 | keys() 12 | values() # fields 13 | items() # [(f['name'],field_value)] 14 | dupeOrEmpty() # 1: first is empty, 2: first is duplicate, False: otherwise. 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/custom-service.md: -------------------------------------------------------------------------------- 1 | 2 | ## 实现Service类 3 | 4 | 1. 词典标签定义 ```__register_label__``` 5 | 词典标签将出现在词典下拉列表中。 6 | 7 | 2. ```@export(fld_name, order)``` 修饰导出字典字段函数 8 | - 参数```fld_name```为字典字段名称,出现在字段字段下拉列表中; 9 | - 参数```order```为字典字段在下拉列表中的顺序,小号在上,大号在下。 10 | 11 | 具体可参考有道词典和海词实现方式。 -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | - hooks: take some arguments and return no value 4 | - filters: take a value and return it 5 | 6 | ## Available Hooks 7 | 8 | #### anki.cards 9 | - odueInvalid 10 | 11 | #### anki.collection 12 | - remNotes 13 | 14 | #### anki.decks 15 | - newDeck 16 | 17 | #### anki.exporting 18 | - exportedMediaFiles 19 | - exportersList 20 | 21 | #### anki.find 22 | - search 23 | 24 | #### anki.models 25 | - newModel 26 | 27 | #### anki.sched 28 | - leech 29 | 30 | #### anki.sync 31 | - sync 32 | - syncMsg 33 | 34 | #### anki.tags 35 | - newTag 36 | 37 | #### aqt.addcards 38 | - AddCards.onHistory 39 | 40 | #### aqt.browser 41 | - browser.setupMenus 42 | 43 | #### aqt.deckbrowser 44 | - showDeckOptions 45 | 46 | #### aqt.editor 47 | - editTimer 48 | - editFocusGained 49 | - tagsUpdated 50 | - EditorWebView.contextMenuEvent 51 | 52 | #### aqt.main 53 | - profileLoaded 54 | - uploadProfile 55 | - beforeStateChange 56 | - afterStateChange 57 | - colLoading 58 | - noteChanged 59 | - reset 60 | - undoState 61 | 62 | #### aqt.modelchoooser 63 | - currentModelChanged 64 | 65 | #### reviewer 66 | - reviewCleanup 67 | - showQuestion 68 | - showAnswer 69 | - Reviewer.contextMenuEvent 70 | 71 | #### aqt.sync 72 | - httpSend 73 | - httpRecv 74 | 75 | #### aqt.webview 76 | - AnkiWebView.contextMenuEvent 77 | 78 | ## Available Filters 79 | 80 | #### anki.collection 81 | - modSchema 82 | - mungeFields 83 | - mungeQA 84 | 85 | #### anki.template 86 | - fmod_* 87 | 88 | #### aqt.editor 89 | - setupEditorShortcuts 90 | - editorFocusLost 91 | 92 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Query Introduction 2 | 3 | Query’s ultimate goal is to free Anki users from tedious works of manually copy/pasting dictionary definitions whenever new language-learning cards are to be made.It supports most mdict dictionary files, from which word definitions can be added to designated card fields. 4 | 5 | New features are spot-on useful and now it can run multiple .mdx dictionaries simultaneously, providing more definition reviewing options.Query also supports updating your old cards of any given deck, complementing the add-on eco-system. 6 | Last not but least, with the templates provided with toggle functions, cards display can be concise and elegant on the eye, yet sacrificing not the richness of its content. 7 | 8 | New Features (Nov. 26th. 2016) 9 | 10 | 1. Supports loading multiple .mdx dictionaries simultaneously. 11 | 2. Supports “Query” to old cards of any given deck, with “word” field provided. 12 | 13 | User How-to 14 | 15 | 1. Install the add-on 16 | 2. Mdx dictionaries are available online and this add-on can’t provide any due to potential copyright infringement. 17 | 3. On the main Tools menu, choose Query and set paths for designated .mdx dictionaries to template fields accordingly. 18 | 4. Upon adding a new card, type the word and press “Query” button, and voila it’s done. 19 | 20 | ![](screenshots/setup.png) 21 | 22 | ![](screenshots/adding.png) 23 | 24 | ![](screenshots/card.png) 25 | 26 | 27 | User Wiki 28 | 29 | 1. What is a .mdx dictionary? 30 | 31 | See [https://en.wikipedia.org/wiki/GoldenDict](https://en.wikipedia.org/wiki/GoldenDict) 32 | 33 | 2. Where can I find such .mdx dictionaries? 34 | 35 | Go google it. For Chinese users, can go to dictionary forums like pdawiki and you may find what you want. 36 | 37 | Thanks 38 | 39 | 1. [https://ninja33.github.io](https://ninja33.github.io) 40 | 2. [https://github.com/mmjang/mdict-query](https://github.com/mmjang/mdict-query) 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Wrong behavior 2 | 3 | ### When it happened 4 | 5 | ### Whether it can be reproduced 6 | -------------------------------------------------------------------------------- /screenshots/add_dict_folders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/add_dict_folders.png -------------------------------------------------------------------------------- /screenshots/adding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/adding.png -------------------------------------------------------------------------------- /screenshots/browser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/browser.gif -------------------------------------------------------------------------------- /screenshots/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/browser.png -------------------------------------------------------------------------------- /screenshots/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/card.png -------------------------------------------------------------------------------- /screenshots/dicts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/dicts.png -------------------------------------------------------------------------------- /screenshots/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/editor.png -------------------------------------------------------------------------------- /screenshots/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/new.png -------------------------------------------------------------------------------- /screenshots/note_type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/note_type.png -------------------------------------------------------------------------------- /screenshots/qa1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/qa1.png -------------------------------------------------------------------------------- /screenshots/setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/screenshots/setup.png -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | from anki.hooks import addHook 21 | 22 | ############## other config here ################## 23 | # update all fields ignoring the original field content 24 | update_all = False 25 | # shortcut 26 | shortcut = 'Ctrl+Q' 27 | ################################################### 28 | 29 | 30 | def start_here(): 31 | from . import prepare 32 | # wquery.config.read() 33 | if not prepare.have_setup: 34 | prepare.setup_options_menu() 35 | prepare.customize_addcards() 36 | prepare.setup_browser_menu() 37 | prepare.setup_context_menu() 38 | # wquery.start_services() 39 | # prepare.set_shortcut(shortcut) 40 | 41 | addHook("profileLoaded", start_here) 42 | -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | from .lang import _ 3 | 4 | 5 | VERSION = '4.2.20180101' 6 | 7 | 8 | class Endpoint: 9 | repository = u'https://github.com/finalion/WordQuery' 10 | feedback_issue = u'https://github.com/finalion/WordQuery/issues' 11 | feedback_mail = u'finalion@gmail.com' 12 | check_version = u'https://raw.githubusercontent.com/finalion/WordQuery/gh-pages/version' 13 | new_version = u'https://github.com/finalion/WordQuery' 14 | service_shop = u'https://finalion.github.io/WordQuery/shop.html' 15 | user_guide = u'https://finalion.github.io/WordQuery/' 16 | 17 | 18 | class Template: 19 | tmpl_about = u'{t0}
{version}
{t1}
{url}
{t2}
{feedback0}
{feedback1}'.format( 20 | t0=_('VERSION'), version=VERSION, t1=_('REPOSITORY'), url=Endpoint.repository, 21 | t2=_('FEEDBACK'), feedback0=Endpoint.feedback_issue, feedback1=Endpoint.feedback_mail) 22 | new_version = u'{info} V{version}'.format( 23 | info=_('NEW_VERSION'), url=Endpoint.new_version, version='{version}') 24 | latest_version = _('LATEST_VERSION') 25 | abnormal_version = _('ABNORMAL_VERSION') 26 | check_failure = u'{msg}' 27 | miss_css = u'MDX dictonary {dict} misses css file {css}.
Please choose the file.' 28 | -------------------------------------------------------------------------------- /src/context.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import json 21 | import os 22 | 23 | from aqt import mw 24 | from aqt.utils import shortcut, showInfo, showText 25 | from .constants import VERSION 26 | from .lang import _ 27 | from .utils import get_icon 28 | 29 | CONFIG_FILENAME = '_wqcfg.json' 30 | ICON_FILE = 'wqicon.png' 31 | 32 | app_icon = get_icon(ICON_FILE) 33 | 34 | 35 | class Config(object): 36 | 37 | def __init__(self, window): 38 | self.path = CONFIG_FILENAME 39 | self.window = window 40 | self.version = '0' 41 | self.read() 42 | 43 | @property 44 | def pmname(self): 45 | return self.window.pm.name 46 | 47 | def update(self, data): 48 | data['version'] = VERSION 49 | data['%s_last' % self.pmname] = data.get('last_model', self.last_model_id) 50 | self.data.update(data) 51 | with open(self.path, 'w', encoding='utf-8') as f: 52 | json.dump(self.data, f, ensure_ascii=False) 53 | 54 | def read(self): 55 | try: 56 | f = open(self.path, 'r',encoding="utf-8") 57 | self.data = json.load(f) 58 | # self.version = self.data.get('version', '0') 59 | # if VERSION != self.version: 60 | # # showInfo(VERSION + self.version) 61 | # self.last_model_id, self.dirs = 0, list() 62 | except: 63 | self.data = dict() 64 | 65 | def get_maps(self, model_id): 66 | return self.data.get(str(model_id), list()) 67 | 68 | @property 69 | def last_model_id(self): 70 | return self.data.get('%s_last' % self.pmname, 0) 71 | 72 | @property 73 | def dirs(self): 74 | return self.data.get('dirs', list()) 75 | 76 | @property 77 | def use_filename(self): 78 | return self.data.get('use_filename', True) 79 | 80 | @property 81 | def export_media(self): 82 | return self.data.get('export_media', False) 83 | 84 | @property 85 | def force_update(self): 86 | return self.data.get('force_update', False) 87 | 88 | 89 | config = Config(mw) 90 | -------------------------------------------------------------------------------- /src/lang.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | from anki.lang import currentLang 21 | trans = { 22 | 'CHECK_FILENAME_LABEL': {'zh_CN': u'使用文件名作为标签', 'en': u'Use filename as dict label', 'fr': r"Utiliser le nom de fichier en tant qu'étiquette de dico"}, 23 | 'EXPORT_MEDIA': {'zh_CN': u'导出媒体文件', 'en': u'Export media files', 'fr': u'Exporter les fichiers multimédias'}, 24 | 'DICTS_FOLDERS': {'zh_CN': u'字典文件夹', 'en': u'Dict folders', 'fr': u'Dossiers dico'}, 25 | 'CHOOSE_NOTE_TYPES': {'zh_CN': u'选择笔记类型', 'en': u'Choose note types', 'fr': u'Choisir le type de note '}, 26 | 'CURRENT_NOTE_TYPE': {'zh_CN': u'当前类型', 'en': u'Current type', 'fr': u'Type utilisé en cours'}, 27 | 'MDX_SERVER': {'zh_CN': u'MDX服务器', 'en': u'MDX server', 'fr': u'serveur MDX'}, 28 | 'USE_DICTIONARY': {'zh_CN': u'使用字典', 'en': u'Use dict', 'fr': u'Utilisé un dico'}, 29 | 'UPDATED': {'zh_CN': u'更新', 'en': u'Updated', 'fr': u'Mettre à jour'}, 30 | 'CARDS': {'zh_CN': u'卡片', 'en': u'Cards', 'fr': u'Cartes'}, 31 | 'QUERIED': {'zh_CN': u'查询', 'en': u'Queried', 'fr': u'Quêté'}, 32 | 'FIELDS': {'zh_CN': u'字段', 'en': u'Fields', 'fr': u'Champs'}, 33 | 'WORDS': {'zh_CN': u'单词', 'en': u'Words', 'fr': u'Mots'}, 34 | 'NOT_DICT_FIELD': {'zh_CN': u'不是字典字段', 'en': u'Not dict field', 'fr': u'Pas un champ de dico'}, 35 | 'NOTE_TYPE_FIELDS': {'zh_CN': u'笔记字段', 'en': u'Note fields', 'fr': u'Champ de note'}, 36 | 'DICTS': {'zh_CN': u'字典', 'en': u'Dict', 'fr': u'Dico'}, 37 | 'DICT_FIELDS': {'zh_CN': u'字典字段', 'en': u'Dict fields', 'fr': u'Champ de dico'}, 38 | 'RADIOS_DESC': {'zh_CN': u'单选框选中为待查询单词字段', 'en': u'Word field needs to be selected.', 'fr': u'Champ de mot doit d\'être sélectionné. '}, 39 | 'NO_QUERY_WORD': {'zh_CN': u'查询字段无单词', 'en': u'No word is found in the query field', 'fr': u'Aucun est trouvé dans le champ'}, 40 | 'CSS_NOT_FOUND': {'zh_CN': u'没有找到CSS文件,请手动选择', 'en': u'No valid css stylesheets found, please choose the file', 'fr': u'Aucun fichier de style CSS est valide, veuillez choisir le fichier'}, 41 | 'ABOUT': {'zh_CN': u'关于', 'en': u'About', 'fr': u'À propos'}, 42 | 'REPOSITORY': {'zh_CN': u'项目地址', 'en': u'Project homepage', 'fr': u'Accueil du projet'}, 43 | 'FEEDBACK': {'zh_CN': u'反馈', 'en': u'Feedback', 'fr': u'Retourner de l\'information'}, 44 | 'VERSION': {'zh_CN': u'版本', 'en': u'Version', 'fr': u'Version'}, 45 | 'LATEST_VERSION': {'zh_CN': u'无更新版本.', 'en': u'No update version.', 'fr': u'Pas de mise à jour.'}, 46 | 'ABNORMAL_VERSION': {'zh_CN': u'当前版本异常.', 'en': u'The current version is abnormal.', 'fr': u'La version actuelle est anormale.'}, 47 | 'CHECK_FAILURE': {'zh_CN': u'版本检查失败.', 'en': u'Version check failure.', 'fr': u'Erreur de vérifier la version.'}, 48 | 'NEW_VERSION': {'zh_CN': u'检查到新版本:', 'en': u'New version:', 'fr': u'Nouvelle version:'}, 49 | 'UPDATE': {'zh_CN': u'更新', 'en': u'Update', 'fr': u'Mise à jour'}, 50 | 'FORCE_UPDATE': {'zh_CN': u'强制更新字段', 'en': u'Force update', 'fr': u'Mise à jour forcée'}, 51 | 'SETTINGS': {'zh_CN': u'参数', 'en': u'Settings', 'fr': u'Paramètres'}, 52 | } 53 | 54 | 55 | def _(key, lang=currentLang): 56 | if lang != 'zh_CN' and lang != 'en' and lang != 'fr': 57 | lang = 'en' # fallback 58 | 59 | def disp(s): 60 | return s.lower().capitalize() 61 | 62 | if key not in trans or lang not in trans[key]: 63 | return disp(key) 64 | return trans[key][lang] 65 | 66 | 67 | def _sl(key): 68 | return trans[key].values() 69 | -------------------------------------------------------------------------------- /src/libs/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | from .mdict import IndexBuilder as MdxBuilder 21 | from .pystardict import Dictionary as StardictBuilder 22 | -------------------------------------------------------------------------------- /src/libs/mdict/__init__.py: -------------------------------------------------------------------------------- 1 | from .mdict_query import IndexBuilder 2 | -------------------------------------------------------------------------------- /src/libs/mdict/lzo.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | class FlexBuffer(): 5 | 6 | def __init__(self): 7 | 8 | self.blockSize = None 9 | self.c = None 10 | self.l = None 11 | self.buf = None 12 | 13 | def require(self, n): 14 | 15 | r = self.c - self.l + n 16 | if r > 0: 17 | self.l = self.l + self.blockSize * math.ceil(r / self.blockSize) 18 | #tmp = bytearray(self.l) 19 | #for i in len(self.buf): 20 | # tmp[i] = self.buf[i] 21 | #self.buf = tmp 22 | self.buf = self.buf + bytearray(self.l - len(self.buf)) 23 | self.c = self.c + n 24 | return self.buf 25 | 26 | def alloc(self, initSize, blockSize): 27 | 28 | if blockSize: 29 | sz = blockSize 30 | else: 31 | sz = 4096 32 | self.blockSize = self.roundUp(sz) 33 | self.c = 0 34 | self.l = self.roundUp(initSize) | 0 35 | self.l += self.blockSize - (self.l % self.blockSize) 36 | self.buf = bytearray(self.l) 37 | return self.buf 38 | 39 | def roundUp(self, n): 40 | 41 | r = n % 4 42 | if r == 0: 43 | return n 44 | else: 45 | return n + 4 - r 46 | 47 | def reset(self): 48 | 49 | self.c = 0 50 | self.l = len(self.buf) 51 | 52 | def pack(self, size): 53 | 54 | return self.buf[0:size] 55 | 56 | def _decompress(inBuf, outBuf): 57 | 58 | c_top_loop = 1 59 | c_first_literal_run = 2 60 | c_match = 3 61 | c_copy_match = 4 62 | c_match_done = 5 63 | c_match_next = 6 64 | 65 | out = outBuf.buf 66 | op = 0 67 | ip = 0 68 | t = inBuf[ip] 69 | state = c_top_loop 70 | m_pos = 0 71 | ip_end = len(inBuf) 72 | 73 | if t > 17: 74 | ip = ip + 1 75 | t = t - 17 76 | if t < 4: 77 | state = c_match_next 78 | else: 79 | out = outBuf.require(t) 80 | while True: 81 | out[op] = inBuf[ip] 82 | op = op + 1 83 | ip = ip + 1 84 | t = t - 1 85 | if not t > 0: break 86 | state = c_first_literal_run 87 | 88 | while True: 89 | if_block = False 90 | 91 | ## 92 | if state == c_top_loop: 93 | t = inBuf[ip] 94 | ip = ip + 1 95 | if t >= 16: 96 | state = c_match 97 | continue 98 | if t == 0: 99 | while inBuf[ip] == 0: 100 | t = t + 255 101 | ip = ip + 1 102 | t = t + 15 + inBuf[ip] 103 | ip = ip + 1 104 | 105 | t = t + 3 106 | out = outBuf.require(t) 107 | while True: 108 | out[op] = inBuf[ip] 109 | op = op + 1 110 | ip = ip + 1 111 | t = t - 1 112 | if not t > 0: break 113 | # emulate c switch 114 | state = c_first_literal_run 115 | 116 | ## 117 | if state == c_first_literal_run: 118 | t = inBuf[ip] 119 | ip = ip + 1 120 | if t >= 16: 121 | state = c_match 122 | continue 123 | m_pos = op - 0x801 - (t >> 2) - (inBuf[ip] << 2) 124 | ip = ip + 1 125 | out = outBuf.require(3) 126 | out[op] = out[m_pos] 127 | op = op + 1 128 | m_pos = m_pos + 1 129 | out[op] = out[m_pos] 130 | op = op + 1 131 | m_pos = m_pos + 1 132 | out[op] = out[m_pos] 133 | op = op + 1 134 | 135 | state = c_match_done 136 | continue 137 | 138 | ## 139 | if state == c_match: 140 | if t >= 64: 141 | m_pos = op - 1 - ((t >> 2) & 7) - (inBuf[ip] << 3) 142 | ip = ip + 1 143 | t = (t >> 5) - 1 144 | state = c_copy_match 145 | continue 146 | elif t >= 32: 147 | t = t & 31 148 | if t == 0: 149 | while inBuf[ip] == 0: 150 | t = t + 255 151 | ip = ip + 1 152 | t = t + 31 + inBuf[ip] 153 | ip = ip + 1 154 | m_pos = op - 1 - ((inBuf[ip] + (inBuf[ip + 1] << 8)) >> 2) 155 | ip = ip + 2 156 | elif t >= 16: 157 | m_pos = op - ((t & 8) << 11) 158 | t = t & 7 159 | if t == 0: 160 | while inBuf[ip] == 0: 161 | t = t + 255 162 | ip = ip + 1 163 | t = t + 7 + inBuf[ip] 164 | ip = ip + 1 165 | m_pos = m_pos - ((inBuf[ip] + (inBuf[ip + 1] << 8)) >> 2) 166 | ip = ip + 2 167 | if m_pos == op: 168 | break 169 | m_pos = m_pos - 0x4000 170 | else: 171 | m_pos = op - 1 - (t >> 2) - (inBuf[ip] << 2); 172 | ip = ip + 1 173 | out = outBuf.require(2) 174 | out[op] = out[m_pos] 175 | op = op + 1 176 | m_pos = m_pos + 1 177 | out[op] = out[m_pos] 178 | op = op + 1 179 | state = c_match_done 180 | continue 181 | 182 | if t >= 6 and (op - m_pos) >= 4: 183 | if_block = True 184 | t += 2 185 | out = outBuf.require(t) 186 | while True: 187 | out[op] = out[m_pos] 188 | op += 1 189 | m_pos += 1 190 | t -= 1 191 | if not t > 0: break 192 | #emulate c switch 193 | state = c_copy_match 194 | 195 | ## 196 | if state == c_copy_match: 197 | if not if_block: 198 | t += 2 199 | out = outBuf.require(t) 200 | while True: 201 | out[op] = out[m_pos] 202 | op += 1 203 | m_pos += 1 204 | t -= 1 205 | if not t > 0: break 206 | #emulating c switch 207 | state = c_match_done 208 | 209 | ## 210 | if state == c_match_done: 211 | t = inBuf[ip - 2] & 3 212 | if t == 0: 213 | state = c_top_loop 214 | continue 215 | #emulate c switch 216 | state = c_match_next 217 | 218 | ## 219 | if state == c_match_next: 220 | out = outBuf.require(1) 221 | out[op] = inBuf[ip] 222 | op += 1 223 | ip += 1 224 | if t > 1: 225 | out = outBuf.require(1) 226 | out[op] = inBuf[ip] 227 | op += 1 228 | ip += 1 229 | if t > 2: 230 | out = outBuf.require(1) 231 | out[op] = inBuf[ip] 232 | op += 1 233 | ip += 1 234 | t = inBuf[ip] 235 | ip += 1 236 | state = c_match 237 | continue 238 | 239 | return bytes(outBuf.pack(op)) 240 | 241 | def decompress(input, initSize = 16000, blockSize = 8192): 242 | output = FlexBuffer() 243 | output.alloc(initSize, blockSize) 244 | return _decompress(bytearray(input), output) 245 | 246 | 247 | -------------------------------------------------------------------------------- /src/libs/mdict/mdict_query.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from struct import pack, unpack 3 | from io import BytesIO 4 | import re 5 | import sys 6 | import os 7 | import sqlite3 8 | import json 9 | from aqt.utils import showInfo, showText, tooltip 10 | from .readmdict import MDX, MDD 11 | 12 | # zlib compression is used for engine version >=2.0 13 | import zlib 14 | # LZO compression is used for engine version < 2.0 15 | try: 16 | import lzo 17 | except ImportError: 18 | lzo = None 19 | #print("LZO compression support is not available") 20 | 21 | # 2x3 compatible 22 | if sys.hexversion >= 0x03000000: 23 | unicode = str 24 | 25 | version = '1.1' 26 | 27 | 28 | class IndexBuilder(object): 29 | # todo: enable history 30 | 31 | def __init__(self, fname, encoding="", passcode=None, force_rebuild=False, 32 | enable_history=False, sql_index=True, check=False): 33 | self._mdx_file = fname 34 | self._encoding = '' 35 | self._stylesheet = {} 36 | self._title = '' 37 | self._version = '' 38 | self._description = '' 39 | self._sql_index = sql_index 40 | self._check = check 41 | self._force_rebuild = force_rebuild 42 | _filename, _file_extension = os.path.splitext(fname) 43 | # assert(_file_extension == '.mdx') 44 | # assert(os.path.isfile(fname)) 45 | self._mdx_db = _filename + ".mdx.db" 46 | self._mdd_db = _filename + ".mdd.db" 47 | self._mdd_file = _filename + ".mdd" 48 | self.header_build_flag = False 49 | 50 | def get_header(self): 51 | 52 | def _(): 53 | self.header_build_flag = True 54 | mdx = MDX(self._mdx_file, only_header=True) 55 | self._encoding = mdx.meta['encoding'] 56 | self._stylesheet = json.loads(mdx.meta['stylesheet']) 57 | self._title = mdx.meta['title'] 58 | self._description = mdx.meta['description'] 59 | 60 | if os.path.isfile(self._mdx_db): 61 | # read from META table 62 | try: 63 | conn = sqlite3.connect(self._mdx_db) 64 | #cursor = conn.execute("SELECT * FROM META") 65 | cursor = conn.execute( 66 | 'SELECT value FROM META WHERE key IN ("encoding","stylesheet","title","description","version")') 67 | self._encoding, stylesheet,\ 68 | self._title, self._description, self._version = ( 69 | each[0] for each in cursor) 70 | self._stylesheet = json.loads(stylesheet) 71 | conn.close() 72 | if not self._version: 73 | _() 74 | except: 75 | _() 76 | else: 77 | _() 78 | 79 | def rebuild(self): 80 | self._make_mdx_index() 81 | if os.path.isfile(self._mdd_file): 82 | self._make_mdd_index() 83 | 84 | def check_build(self): 85 | # check if the mdx.db and mdd.db file is available 86 | if self.header_build_flag or not os.path.isfile(self._mdx_db): 87 | self._make_mdx_index() 88 | if os.path.isfile(self._mdd_file) and not os.path.isfile(self._mdd_db): 89 | self._make_mdd_index() 90 | self.header_build_flag = False 91 | 92 | @property 93 | def meta(self): 94 | return {'title': self._title, 'description': self._description, 95 | 'encoding': self._encoding, 'version': self._version, 96 | 'stylesheet': self._stylesheet} 97 | 98 | def _replace_stylesheet(self, txt): 99 | # substitute stylesheet definition 100 | txt_list = re.split('`\d+`', txt) 101 | txt_tag = re.findall('`\d+`', txt) 102 | txt_styled = txt_list[0] 103 | for j, p in enumerate(txt_list[1:]): 104 | style = self._stylesheet[txt_tag[j][1:-1]] 105 | if p and p[-1] == '\n': 106 | txt_styled = txt_styled + style[0] + p.rstrip() + \ 107 | style[1] + '\r\n' 108 | else: 109 | txt_styled = txt_styled + style[0]+ p + style[1] 110 | return txt_styled 111 | 112 | def _make_mdx_index(self): 113 | if os.path.exists(self._mdx_db): 114 | os.remove(self._mdx_db) 115 | mdx = MDX(self._mdx_file, only_header=False) 116 | index_list = mdx.get_index(check_block=self._check) 117 | conn = sqlite3.connect(self._mdx_db) 118 | c = conn.cursor() 119 | c.execute( 120 | ''' CREATE TABLE MDX_INDEX 121 | (key_text text not null, 122 | file_pos integer, 123 | compressed_size integer, 124 | decompressed_size integer, 125 | record_block_type integer, 126 | record_start integer, 127 | record_end integer, 128 | offset integer 129 | )''' 130 | ) 131 | 132 | tuple_list = [ 133 | (item['key_text'], 134 | item['file_pos'], 135 | item['compressed_size'], 136 | item['decompressed_size'], 137 | item['record_block_type'], 138 | item['record_start'], 139 | item['record_end'], 140 | item['offset'] 141 | ) 142 | for item in index_list 143 | ] 144 | c.executemany('INSERT INTO MDX_INDEX VALUES (?,?,?,?,?,?,?,?)', 145 | tuple_list) 146 | # build the metadata table 147 | c.execute( 148 | '''CREATE TABLE META 149 | (key text, 150 | value text 151 | )''') 152 | c.executemany( 153 | 'INSERT INTO META VALUES (?,?)', 154 | [('encoding', self.meta['encoding']), 155 | ('stylesheet', json.dumps(self.meta['stylesheet'])), 156 | ('title', self.meta['title']), 157 | ('description', self.meta['description']), 158 | ('version', version) 159 | ] 160 | ) 161 | 162 | if self._sql_index: 163 | c.execute( 164 | ''' 165 | CREATE INDEX key_index ON MDX_INDEX (key_text) 166 | ''' 167 | ) 168 | 169 | conn.commit() 170 | conn.close() 171 | 172 | def _make_mdd_index(self): 173 | if os.path.exists(self._mdd_db): 174 | os.remove(self._mdd_db) 175 | mdd = MDD(self._mdd_file) 176 | index_list = mdd.get_index(check_block=self._check) 177 | conn = sqlite3.connect(self._mdd_db) 178 | c = conn.cursor() 179 | c.execute( 180 | ''' CREATE TABLE MDX_INDEX 181 | (key_text text not null unique, 182 | file_pos integer, 183 | compressed_size integer, 184 | decompressed_size integer, 185 | record_block_type integer, 186 | record_start integer, 187 | record_end integer, 188 | offset integer 189 | )''' 190 | ) 191 | 192 | tuple_list = [ 193 | (item['key_text'], 194 | item['file_pos'], 195 | item['compressed_size'], 196 | item['decompressed_size'], 197 | item['record_block_type'], 198 | item['record_start'], 199 | item['record_end'], 200 | item['offset'] 201 | ) 202 | for item in index_list 203 | ] 204 | c.executemany('INSERT INTO MDX_INDEX VALUES (?,?,?,?,?,?,?,?)', 205 | tuple_list) 206 | if self._sql_index: 207 | c.execute( 208 | ''' 209 | CREATE UNIQUE INDEX key_index ON MDX_INDEX (key_text) 210 | ''' 211 | ) 212 | 213 | conn.commit() 214 | conn.close() 215 | 216 | @staticmethod 217 | def get_data_by_index(fmdx, index): 218 | fmdx.seek(index['file_pos']) 219 | record_block_compressed = fmdx.read(index['compressed_size']) 220 | record_block_type = record_block_compressed[:4] 221 | record_block_type = index['record_block_type'] 222 | decompressed_size = index['decompressed_size'] 223 | #adler32 = unpack('>I', record_block_compressed[4:8])[0] 224 | if record_block_type == 0: 225 | _record_block = record_block_compressed[8:] 226 | # lzo compression 227 | elif record_block_type == 1: 228 | if lzo is None: 229 | print("LZO compression is not supported") 230 | # decompress 231 | header = b'\xf0' + pack('>I', index['decompressed_size']) 232 | _record_block = lzo.decompress(record_block_compressed[ 233 | 8:], initSize=decompressed_size, blockSize=1308672) 234 | # zlib compression 235 | elif record_block_type == 2: 236 | # decompress 237 | _record_block = zlib.decompress(record_block_compressed[8:]) 238 | data = _record_block[index['record_start'] - 239 | index['offset']:index['record_end'] - index['offset']] 240 | return data 241 | 242 | def get_mdx_by_index(self, fmdx, index): 243 | data = self.get_data_by_index(fmdx, index) 244 | record = data.decode(self._encoding, errors='ignore').strip( 245 | u'\x00') # .encode('utf-8') #20180914 246 | if self._stylesheet: 247 | record = self._replace_stylesheet(record) 248 | return record 249 | 250 | def get_mdd_by_index(self, fmdx, index): 251 | return self.get_data_by_index(fmdx, index) 252 | 253 | @staticmethod 254 | def lookup_indexes(db, keyword, ignorecase=None): 255 | indexes = [] 256 | if ignorecase: 257 | sql = u'SELECT * FROM MDX_INDEX WHERE lower(key_text) = lower("{}")'.format( 258 | keyword) 259 | else: 260 | sql = u'SELECT * FROM MDX_INDEX WHERE key_text = "{}"'.format( 261 | keyword) 262 | with sqlite3.connect(db) as conn: 263 | cursor = conn.execute(sql) 264 | for result in cursor: 265 | index = {} 266 | index['file_pos'] = result[1] 267 | index['compressed_size'] = result[2] 268 | index['decompressed_size'] = result[3] 269 | index['record_block_type'] = result[4] 270 | index['record_start'] = result[5] 271 | index['record_end'] = result[6] 272 | index['offset'] = result[7] 273 | indexes.append(index) 274 | return indexes 275 | 276 | def mdx_lookup(self, keyword, ignorecase=None): 277 | lookup_result_list = [] 278 | indexes = self.lookup_indexes(self._mdx_db, keyword, ignorecase) 279 | with open(self._mdx_file, 'rb') as mdx_file: 280 | for index in indexes: 281 | lookup_result_list.append( 282 | self.get_mdx_by_index(mdx_file, index)) 283 | return lookup_result_list 284 | 285 | def mdd_lookup(self, keyword, ignorecase=None): 286 | lookup_result_list = [] 287 | indexes = self.lookup_indexes(self._mdd_db, keyword, ignorecase) 288 | with open(self._mdd_file, 'rb') as mdd_file: 289 | for index in indexes: 290 | lookup_result_list.append( 291 | self.get_mdd_by_index(mdd_file, index)) 292 | return lookup_result_list 293 | 294 | @staticmethod 295 | def get_keys(db, query=''): 296 | if not db: 297 | return [] 298 | if query: 299 | if '*' in query: 300 | query = query.replace('*', '%') 301 | else: 302 | query = query + '%' 303 | sql = 'SELECT key_text FROM MDX_INDEX WHERE key_text LIKE \"' + query + '\"' 304 | else: 305 | sql = 'SELECT key_text FROM MDX_INDEX' 306 | with sqlite3.connect(db) as conn: 307 | cursor = conn.execute(sql) 308 | keys = [item[0] for item in cursor] 309 | return keys 310 | 311 | def get_mdd_keys(self, query=''): 312 | try: 313 | return self.get_keys(self._mdd_db, query) 314 | except: 315 | return [] 316 | 317 | def get_mdx_keys(self, query=''): 318 | try: 319 | return self.get_keys(self._mdx_db, query) 320 | except: 321 | return [] 322 | 323 | 324 | # mdx_builder = IndexBuilder("oald.mdx") 325 | # text = mdx_builder.mdx_lookup('dedication') 326 | # keys = mdx_builder.get_mdx_keys() 327 | # keys1 = mdx_builder.get_mdx_keys('abstrac') 328 | # keys2 = mdx_builder.get_mdx_keys('*tion') 329 | # for key in keys2: 330 | # text = mdx_builder.mdx_lookup(key)[0] 331 | # pass 332 | -------------------------------------------------------------------------------- /src/libs/mdict/pureSalsa20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | Copyright by https://github.com/zhansliu/writemdict 6 | 7 | pureSalsa20.py -- a pure Python implementation of the Salsa20 cipher, ported to Python 3 8 | 9 | v4.0: Added Python 3 support, dropped support for Python <= 2.5. 10 | 11 | // zhansliu 12 | 13 | Original comments below. 14 | 15 | ==================================================================== 16 | There are comments here by two authors about three pieces of software: 17 | comments by Larry Bugbee about 18 | Salsa20, the stream cipher by Daniel J. Bernstein 19 | (including comments about the speed of the C version) and 20 | pySalsa20, Bugbee's own Python wrapper for salsa20.c 21 | (including some references), and 22 | comments by Steve Witham about 23 | pureSalsa20, Witham's pure Python 2.5 implementation of Salsa20, 24 | which follows pySalsa20's API, and is in this file. 25 | 26 | Salsa20: a Fast Streaming Cipher (comments by Larry Bugbee) 27 | ----------------------------------------------------------- 28 | 29 | Salsa20 is a fast stream cipher written by Daniel Bernstein 30 | that basically uses a hash function and XOR making for fast 31 | encryption. (Decryption uses the same function.) Salsa20 32 | is simple and quick. 33 | 34 | Some Salsa20 parameter values... 35 | design strength 128 bits 36 | key length 128 or 256 bits, exactly 37 | IV, aka nonce 64 bits, always 38 | chunk size must be in multiples of 64 bytes 39 | 40 | Salsa20 has two reduced versions, 8 and 12 rounds each. 41 | 42 | One benchmark (10 MB): 43 | 1.5GHz PPC G4 102/97/89 MB/sec for 8/12/20 rounds 44 | AMD Athlon 2500+ 77/67/53 MB/sec for 8/12/20 rounds 45 | (no I/O and before Python GC kicks in) 46 | 47 | Salsa20 is a Phase 3 finalist in the EU eSTREAM competition 48 | and appears to be one of the fastest ciphers. It is well 49 | documented so I will not attempt any injustice here. Please 50 | see "References" below. 51 | 52 | ...and Salsa20 is "free for any use". 53 | 54 | 55 | pySalsa20: a Python wrapper for Salsa20 (Comments by Larry Bugbee) 56 | ------------------------------------------------------------------ 57 | 58 | pySalsa20.py is a simple ctypes Python wrapper. Salsa20 is 59 | as it's name implies, 20 rounds, but there are two reduced 60 | versions, 8 and 12 rounds each. Because the APIs are 61 | identical, pySalsa20 is capable of wrapping all three 62 | versions (number of rounds hardcoded), including a special 63 | version that allows you to set the number of rounds with a 64 | set_rounds() function. Compile the version of your choice 65 | as a shared library (not as a Python extension), name and 66 | install it as libsalsa20.so. 67 | 68 | Sample usage: 69 | from pySalsa20 import Salsa20 70 | s20 = Salsa20(key, IV) 71 | dataout = s20.encryptBytes(datain) # same for decrypt 72 | 73 | This is EXPERIMENTAL software and intended for educational 74 | purposes only. To make experimentation less cumbersome, 75 | pySalsa20 is also free for any use. 76 | 77 | THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF 78 | ANY KIND. USE AT YOUR OWN RISK. 79 | 80 | Enjoy, 81 | 82 | Larry Bugbee 83 | bugbee@seanet.com 84 | April 2007 85 | 86 | 87 | References: 88 | ----------- 89 | http://en.wikipedia.org/wiki/Salsa20 90 | http://en.wikipedia.org/wiki/Daniel_Bernstein 91 | http://cr.yp.to/djb.html 92 | http://www.ecrypt.eu.org/stream/salsa20p3.html 93 | http://www.ecrypt.eu.org/stream/p3ciphers/salsa20/salsa20_p3source.zip 94 | 95 | 96 | Prerequisites for pySalsa20: 97 | ---------------------------- 98 | - Python 2.5 (haven't tested in 2.4) 99 | 100 | 101 | pureSalsa20: Salsa20 in pure Python 2.5 (comments by Steve Witham) 102 | ------------------------------------------------------------------ 103 | 104 | pureSalsa20 is the stand-alone Python code in this file. 105 | It implements the underlying Salsa20 core algorithm 106 | and emulates pySalsa20's Salsa20 class API (minus a bug(*)). 107 | 108 | pureSalsa20 is MUCH slower than libsalsa20.so wrapped with pySalsa20-- 109 | about 1/1000 the speed for Salsa20/20 and 1/500 the speed for Salsa20/8, 110 | when encrypting 64k-byte blocks on my computer. 111 | 112 | pureSalsa20 is for cases where portability is much more important than 113 | speed. I wrote it for use in a "structured" random number generator. 114 | 115 | There are comments about the reasons for this slowness in 116 | http://www.tiac.net/~sw/2010/02/PureSalsa20 117 | 118 | Sample usage: 119 | from pureSalsa20 import Salsa20 120 | s20 = Salsa20(key, IV) 121 | dataout = s20.encryptBytes(datain) # same for decrypt 122 | 123 | I took the test code from pySalsa20, added a bunch of tests including 124 | rough speed tests, and moved them into the file testSalsa20.py. 125 | To test both pySalsa20 and pureSalsa20, type 126 | python testSalsa20.py 127 | 128 | (*)The bug (?) in pySalsa20 is this. The rounds variable is global to the 129 | libsalsa20.so library and not switched when switching between instances 130 | of the Salsa20 class. 131 | s1 = Salsa20( key, IV, 20 ) 132 | s2 = Salsa20( key, IV, 8 ) 133 | In this example, 134 | with pySalsa20, both s1 and s2 will do 8 rounds of encryption. 135 | with pureSalsa20, s1 will do 20 rounds and s2 will do 8 rounds. 136 | Perhaps giving each instance its own nRounds variable, which 137 | is passed to the salsa20wordtobyte() function, is insecure. I'm not a 138 | cryptographer. 139 | 140 | pureSalsa20.py and testSalsa20.py are EXPERIMENTAL software and 141 | intended for educational purposes only. To make experimentation less 142 | cumbersome, pureSalsa20.py and testSalsa20.py are free for any use. 143 | 144 | Revisions: 145 | ---------- 146 | p3.2 Fixed bug that initialized the output buffer with plaintext! 147 | Saner ramping of nreps in speed test. 148 | Minor changes and print statements. 149 | p3.1 Took timing variability out of add32() and rot32(). 150 | Made the internals more like pySalsa20/libsalsa . 151 | Put the semicolons back in the main loop! 152 | In encryptBytes(), modify a byte array instead of appending. 153 | Fixed speed calculation bug. 154 | Used subclasses instead of patches in testSalsa20.py . 155 | Added 64k-byte messages to speed test to be fair to pySalsa20. 156 | p3 First version, intended to parallel pySalsa20 version 3. 157 | 158 | More references: 159 | ---------------- 160 | http://www.seanet.com/~bugbee/crypto/salsa20/ [pySalsa20] 161 | http://cr.yp.to/snuffle.html [The original name of Salsa20] 162 | http://cr.yp.to/snuffle/salsafamily-20071225.pdf [ Salsa20 design] 163 | http://www.tiac.net/~sw/2010/02/PureSalsa20 164 | 165 | THIS PROGRAM IS PROVIDED WITHOUT WARRANTY OR GUARANTEE OF 166 | ANY KIND. USE AT YOUR OWN RISK. 167 | 168 | Cheers, 169 | 170 | Steve Witham sw at remove-this tiac dot net 171 | February, 2010 172 | """ 173 | import sys 174 | assert(sys.version_info >= (2, 6)) 175 | 176 | if sys.version_info >= (3,): 177 | integer_types = (int,) 178 | python3 = True 179 | else: 180 | integer_types = (int, long) 181 | python3 = False 182 | 183 | from struct import Struct 184 | little_u64 = Struct( "= 2**64" 238 | ctx = self.ctx 239 | ctx[ 8],ctx[ 9] = little2_i32.unpack( little_u64.pack( counter ) ) 240 | 241 | def getCounter( self ): 242 | return little_u64.unpack( little2_i32.pack( *self.ctx[ 8:10 ] ) ) [0] 243 | 244 | 245 | def setRounds(self, rounds, testing=False ): 246 | assert testing or rounds in [8, 12, 20], 'rounds must be 8, 12, 20' 247 | self.rounds = rounds 248 | 249 | 250 | def encryptBytes(self, data): 251 | assert type(data) == bytes, 'data must be byte string' 252 | assert self._lastChunk64, 'previous chunk not multiple of 64 bytes' 253 | lendata = len(data) 254 | munged = bytearray(lendata) 255 | for i in range( 0, lendata, 64 ): 256 | h = salsa20_wordtobyte( self.ctx, self.rounds, checkRounds=False ) 257 | self.setCounter( ( self.getCounter() + 1 ) % 2**64 ) 258 | # Stopping at 2^70 bytes per nonce is user's responsibility. 259 | for j in range( min( 64, lendata - i ) ): 260 | if python3: 261 | munged[ i+j ] = data[ i+j ] ^ h[j] 262 | else: 263 | munged[ i+j ] = ord(data[ i+j ]) ^ ord(h[j]) 264 | 265 | self._lastChunk64 = not lendata % 64 266 | return bytes(munged) 267 | 268 | decryptBytes = encryptBytes # encrypt and decrypt use same function 269 | 270 | #-------------------------------------------------------------------------- 271 | 272 | def salsa20_wordtobyte( input, nRounds=20, checkRounds=True ): 273 | """ Do nRounds Salsa20 rounds on a copy of 274 | input: list or tuple of 16 ints treated as little-endian unsigneds. 275 | Returns a 64-byte string. 276 | """ 277 | 278 | assert( type(input) in ( list, tuple ) and len(input) == 16 ) 279 | assert( not(checkRounds) or ( nRounds in [ 8, 12, 20 ] ) ) 280 | 281 | x = list( input ) 282 | 283 | def XOR( a, b ): return a ^ b 284 | ROTATE = rot32 285 | PLUS = add32 286 | 287 | for i in range( nRounds // 2 ): 288 | # These ...XOR...ROTATE...PLUS... lines are from ecrypt-linux.c 289 | # unchanged except for indents and the blank line between rounds: 290 | x[ 4] = XOR(x[ 4],ROTATE(PLUS(x[ 0],x[12]), 7)); 291 | x[ 8] = XOR(x[ 8],ROTATE(PLUS(x[ 4],x[ 0]), 9)); 292 | x[12] = XOR(x[12],ROTATE(PLUS(x[ 8],x[ 4]),13)); 293 | x[ 0] = XOR(x[ 0],ROTATE(PLUS(x[12],x[ 8]),18)); 294 | x[ 9] = XOR(x[ 9],ROTATE(PLUS(x[ 5],x[ 1]), 7)); 295 | x[13] = XOR(x[13],ROTATE(PLUS(x[ 9],x[ 5]), 9)); 296 | x[ 1] = XOR(x[ 1],ROTATE(PLUS(x[13],x[ 9]),13)); 297 | x[ 5] = XOR(x[ 5],ROTATE(PLUS(x[ 1],x[13]),18)); 298 | x[14] = XOR(x[14],ROTATE(PLUS(x[10],x[ 6]), 7)); 299 | x[ 2] = XOR(x[ 2],ROTATE(PLUS(x[14],x[10]), 9)); 300 | x[ 6] = XOR(x[ 6],ROTATE(PLUS(x[ 2],x[14]),13)); 301 | x[10] = XOR(x[10],ROTATE(PLUS(x[ 6],x[ 2]),18)); 302 | x[ 3] = XOR(x[ 3],ROTATE(PLUS(x[15],x[11]), 7)); 303 | x[ 7] = XOR(x[ 7],ROTATE(PLUS(x[ 3],x[15]), 9)); 304 | x[11] = XOR(x[11],ROTATE(PLUS(x[ 7],x[ 3]),13)); 305 | x[15] = XOR(x[15],ROTATE(PLUS(x[11],x[ 7]),18)); 306 | 307 | x[ 1] = XOR(x[ 1],ROTATE(PLUS(x[ 0],x[ 3]), 7)); 308 | x[ 2] = XOR(x[ 2],ROTATE(PLUS(x[ 1],x[ 0]), 9)); 309 | x[ 3] = XOR(x[ 3],ROTATE(PLUS(x[ 2],x[ 1]),13)); 310 | x[ 0] = XOR(x[ 0],ROTATE(PLUS(x[ 3],x[ 2]),18)); 311 | x[ 6] = XOR(x[ 6],ROTATE(PLUS(x[ 5],x[ 4]), 7)); 312 | x[ 7] = XOR(x[ 7],ROTATE(PLUS(x[ 6],x[ 5]), 9)); 313 | x[ 4] = XOR(x[ 4],ROTATE(PLUS(x[ 7],x[ 6]),13)); 314 | x[ 5] = XOR(x[ 5],ROTATE(PLUS(x[ 4],x[ 7]),18)); 315 | x[11] = XOR(x[11],ROTATE(PLUS(x[10],x[ 9]), 7)); 316 | x[ 8] = XOR(x[ 8],ROTATE(PLUS(x[11],x[10]), 9)); 317 | x[ 9] = XOR(x[ 9],ROTATE(PLUS(x[ 8],x[11]),13)); 318 | x[10] = XOR(x[10],ROTATE(PLUS(x[ 9],x[ 8]),18)); 319 | x[12] = XOR(x[12],ROTATE(PLUS(x[15],x[14]), 7)); 320 | x[13] = XOR(x[13],ROTATE(PLUS(x[12],x[15]), 9)); 321 | x[14] = XOR(x[14],ROTATE(PLUS(x[13],x[12]),13)); 322 | x[15] = XOR(x[15],ROTATE(PLUS(x[14],x[13]),18)); 323 | 324 | for i in range( len( input ) ): 325 | x[i] = PLUS( x[i], input[i] ) 326 | return little16_i32.pack( *x ) 327 | 328 | #--------------------------- 32-bit ops ------------------------------- 329 | 330 | def trunc32( w ): 331 | """ Return the bottom 32 bits of w as a Python int. 332 | This creates longs temporarily, but returns an int. """ 333 | w = int( ( w & 0x7fffFFFF ) | -( w & 0x80000000 ) ) 334 | assert type(w) == int 335 | return w 336 | 337 | 338 | def add32( a, b ): 339 | """ Add two 32-bit words discarding carry above 32nd bit, 340 | and without creating a Python long. 341 | Timing shouldn't vary. 342 | """ 343 | lo = ( a & 0xFFFF ) + ( b & 0xFFFF ) 344 | hi = ( a >> 16 ) + ( b >> 16 ) + ( lo >> 16 ) 345 | return ( -(hi & 0x8000) | ( hi & 0x7FFF ) ) << 16 | ( lo & 0xFFFF ) 346 | 347 | 348 | def rot32( w, nLeft ): 349 | """ Rotate 32-bit word left by nLeft or right by -nLeft 350 | without creating a Python long. 351 | Timing depends on nLeft but not on w. 352 | """ 353 | nLeft &= 31 # which makes nLeft >= 0 354 | if nLeft == 0: 355 | return w 356 | 357 | # Note: now 1 <= nLeft <= 31. 358 | # RRRsLLLLLL There are nLeft RRR's, (31-nLeft) LLLLLL's, 359 | # => sLLLLLLRRR and one s which becomes the sign bit. 360 | RRR = ( ( ( w >> 1 ) & 0x7fffFFFF ) >> ( 31 - nLeft ) ) 361 | sLLLLLL = -( (1<<(31-nLeft)) & w ) | (0x7fffFFFF>>nLeft) & w 362 | return RRR | ( sLLLLLL << nLeft ) 363 | 364 | 365 | # --------------------------------- end ----------------------------------- 366 | -------------------------------------------------------------------------------- /src/libs/mdict/ripemd128.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright by https://github.com/zhansliu/writemdict 3 | 4 | ripemd128.py - A simple ripemd128 library in pure Python. 5 | 6 | Supports both Python 2 (versions >= 2.6) and Python 3. 7 | 8 | Usage: 9 | from ripemd128 import ripemd128 10 | digest = ripemd128(b"The quick brown fox jumps over the lazy dog") 11 | assert(digest == b"\x3f\xa9\xb5\x7f\x05\x3c\x05\x3f\xbe\x27\x35\xb2\x38\x0d\xb5\x96") 12 | 13 | """ 14 | 15 | 16 | 17 | import struct 18 | 19 | 20 | # follows this description: http://homes.esat.kuleuven.be/~bosselae/ripemd/rmd128.txt 21 | 22 | def f(j, x, y, z): 23 | assert(0 <= j and j < 64) 24 | if j < 16: 25 | return x ^ y ^ z 26 | elif j < 32: 27 | return (x & y) | (z & ~x) 28 | elif j < 48: 29 | return (x | (0xffffffff & ~y)) ^ z 30 | else: 31 | return (x & z) | (y & ~z) 32 | 33 | def K(j): 34 | assert(0 <= j and j < 64) 35 | if j < 16: 36 | return 0x00000000 37 | elif j < 32: 38 | return 0x5a827999 39 | elif j < 48: 40 | return 0x6ed9eba1 41 | else: 42 | return 0x8f1bbcdc 43 | 44 | def Kp(j): 45 | assert(0 <= j and j < 64) 46 | if j < 16: 47 | return 0x50a28be6 48 | elif j < 32: 49 | return 0x5c4dd124 50 | elif j < 48: 51 | return 0x6d703ef3 52 | else: 53 | return 0x00000000 54 | 55 | def padandsplit(message): 56 | """ 57 | returns a two-dimensional array X[i][j] of 32-bit integers, where j ranges 58 | from 0 to 16. 59 | First pads the message to length in bytes is congruent to 56 (mod 64), 60 | by first adding a byte 0x80, and then padding with 0x00 bytes until the 61 | message length is congruent to 56 (mod 64). Then adds the little-endian 62 | 64-bit representation of the original length. Finally, splits the result 63 | up into 64-byte blocks, which are further parsed as 32-bit integers. 64 | """ 65 | origlen = len(message) 66 | padlength = 64 - ((origlen - 56) % 64) #minimum padding is 1! 67 | message += b"\x80" 68 | message += b"\x00" * (padlength - 1) 69 | message += struct.pack("> (32-s)) & 0xffffffff 86 | 87 | r = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15, 88 | 7, 4,13, 1,10, 6,15, 3,12, 0, 9, 5, 2,14,11, 8, 89 | 3,10,14, 4, 9,15, 8, 1, 2, 7, 0, 6,13,11, 5,12, 90 | 1, 9,11,10, 0, 8,12, 4,13, 3, 7,15,14, 5, 6, 2] 91 | rp = [ 5,14, 7, 0, 9, 2,11, 4,13, 6,15, 8, 1,10, 3,12, 92 | 6,11, 3, 7, 0,13, 5,10,14,15, 8,12, 4, 9, 1, 2, 93 | 15, 5, 1, 3, 7,14, 6, 9,11, 8,12, 2,10, 0, 4,13, 94 | 8, 6, 4, 1, 3,11,15, 0, 5,12, 2,13, 9, 7,10,14] 95 | s = [11,14,15,12, 5, 8, 7, 9,11,13,14,15, 6, 7, 9, 8, 96 | 7, 6, 8,13,11, 9, 7,15, 7,12,15, 9,11, 7,13,12, 97 | 11,13, 6, 7,14, 9,13,15,14, 8,13, 6, 5,12, 7, 5, 98 | 11,12,14,15,14,15, 9, 8, 9,14, 5, 6, 8, 6, 5,12] 99 | sp = [ 8, 9, 9,11,13,15,15, 5, 7, 7, 8,11,14,14,12, 6, 100 | 9,13,15, 7,12, 8, 9,11, 7, 7,12, 7, 6,15,13,11, 101 | 9, 7,15,11, 8, 6, 6,14,12,13, 5,14,13,13, 7, 5, 102 | 15, 5, 8,11,14,14, 6,14, 6, 9,12, 9,12, 5,15, 8] 103 | 104 | 105 | def ripemd128(message): 106 | h0 = 0x67452301 107 | h1 = 0xefcdab89 108 | h2 = 0x98badcfe 109 | h3 = 0x10325476 110 | X = padandsplit(message) 111 | for i in range(len(X)): 112 | (A,B,C,D) = (h0,h1,h2,h3) 113 | (Ap,Bp,Cp,Dp) = (h0,h1,h2,h3) 114 | for j in range(64): 115 | T = rol(s[j], add(A, f(j,B,C,D), X[i][r[j]], K(j))) 116 | (A,D,C,B) = (D,C,B,T) 117 | T = rol(sp[j], add(Ap, f(63-j,Bp,Cp,Dp), X[i][rp[j]], Kp(j))) 118 | (Ap,Dp,Cp,Bp)=(Dp,Cp,Bp,T) 119 | T = add(h1,C,Dp) 120 | h1 = add(h2,D,Ap) 121 | h2 = add(h3,A,Bp) 122 | h3 = add(h0,B,Cp) 123 | h0 = T 124 | 125 | 126 | return struct.pack(" 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | 21 | import anki 22 | import aqt 23 | from aqt import mw 24 | from aqt.qt import * 25 | from anki.hooks import addHook, wrap 26 | from aqt.addcards import AddCards 27 | from aqt.utils import showInfo, shortcut 28 | from .ui import show_options 29 | from .query import query_from_browser, query_from_editor_all_fields, query_from_editor_current_field 30 | from .context import config, app_icon 31 | 32 | 33 | ############## other config here ################## 34 | # update all fields ignoring the original field content 35 | update_all = False 36 | # shortcut 37 | my_shortcut = 'Ctrl+Q' 38 | ################################################### 39 | 40 | have_setup = False 41 | 42 | 43 | def query_decor(func, obj): 44 | def callback(): 45 | return func(obj) 46 | return callback 47 | 48 | 49 | def add_query_button(self): 50 | bb = self.form.buttonBox 51 | ar = QDialogButtonBox.ActionRole 52 | self.queryButton = bb.addButton(_(u"Query"), ar) 53 | self.queryButton.clicked.connect(query_decor( 54 | query_from_editor_all_fields, self.editor)) 55 | self.queryButton.setShortcut(QKeySequence(my_shortcut)) 56 | self.queryButton.setToolTip( 57 | shortcut(_(u"Query (shortcut: %s)" % my_shortcut))) 58 | 59 | 60 | def setup_browser_menu(): 61 | 62 | def on_setup_menus(browser): 63 | menu = QMenu("WordQuery", browser.form.menubar) 64 | browser.form.menubar.addMenu(menu) 65 | action_queryselected = QAction("Query Selected", browser) 66 | action_queryselected.triggered.connect(query_decor( 67 | query_from_browser, browser)) 68 | action_queryselected.setShortcut(QKeySequence(my_shortcut)) 69 | action_options = QAction("Options", browser) 70 | action_options.triggered.connect(show_options) 71 | menu.addAction(action_queryselected) 72 | menu.addAction(action_options) 73 | 74 | anki.hooks.addHook('browser.setupMenus', on_setup_menus) 75 | 76 | 77 | def setup_context_menu(): 78 | 79 | def on_setup_menus(web_view, menu): 80 | """ 81 | add context menu to webview 82 | """ 83 | wqmenu = menu.addMenu('Word Query') 84 | action1 = wqmenu.addAction('Query All Fields') 85 | action2 = wqmenu.addAction('Query Current Field') 86 | action3 = wqmenu.addAction('Options') 87 | action1.triggered.connect(query_decor( 88 | query_from_editor_all_fields, web_view.editor)) 89 | action2.triggered.connect(query_decor( 90 | query_from_editor_current_field, web_view.editor)) 91 | action3.triggered.connect(show_options) 92 | needs_separator = True 93 | # menu.addMenu(submenu) 94 | anki.hooks.addHook('EditorWebView.contextMenuEvent', on_setup_menus) 95 | # shortcuts = [(my_shortcut, query), ] 96 | # anki.hooks.addHook('setupEditorShortcuts', shortcuts) 97 | 98 | 99 | def customize_addcards(): 100 | AddCards.setupButtons = wrap( 101 | AddCards.setupButtons, add_query_button, "before") 102 | 103 | 104 | def setup_options_menu(): 105 | # add options submenu to Tools menu 106 | action = QAction(app_icon, "WordQuery...", mw) 107 | action.triggered.connect(show_options) 108 | mw.form.menuTools.addAction(action) 109 | global have_setup 110 | have_setup = True 111 | 112 | 113 | # def start_here(): 114 | # # config.read() 115 | # if not have_setup: 116 | # setup_options_menu() 117 | # customize_addcards() 118 | # setup_browser_menu() 119 | # setup_context_menu() 120 | # # wquery.start_services() 121 | 122 | 123 | # addHook("profileLoaded", start_here) 124 | -------------------------------------------------------------------------------- /src/progress.py: -------------------------------------------------------------------------------- 1 | # Copyright: Damien Elmes 2 | # -*- coding: utf-8 -*- 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | import time 6 | from collections import defaultdict 7 | 8 | from aqt.qt import * 9 | from .lang import _ 10 | 11 | 12 | # fixme: if mw->subwindow opens a progress dialog with mw as the parent, mw 13 | # gets raised on finish on compiz. perhaps we should be using the progress 14 | # dialog as the parent? 15 | 16 | # Progress info 17 | ########################################################################## 18 | 19 | 20 | class ProgressManager(QObject): 21 | 22 | def __init__(self, mw): 23 | self.mw = mw 24 | self.app = QApplication.instance() 25 | self.blockUpdates = False 26 | self._win = None 27 | self._levels = 0 28 | self.aborted = False 29 | self.rows_number = 0 30 | self._msg_info = defaultdict(dict) 31 | self._msg_count = defaultdict(int) 32 | # Creating progress dialogs 33 | ########################################################################## 34 | 35 | # @pyqtSlot(dict) 36 | def update_labels(self, data): 37 | if data.type == 'count': 38 | self._msg_count.update(data) 39 | else: 40 | self._msg_info[data.index] = data 41 | 42 | lst = [] 43 | for index in range(self.rows_number): 44 | info = self._msg_info.get(index, None) 45 | if not info: 46 | continue 47 | if info.type == 'text': 48 | lst.append(info.text) 49 | else: 50 | lst.append(u"{2} [{0}] {1}".format( 51 | info.service_name, info.field_name, info.flag)) 52 | 53 | number_info = '' 54 | words_number, fields_number = ( 55 | self._msg_count['words_number'], self._msg_count['fields_number']) 56 | if words_number or fields_number: 57 | number_info += '
' + 45 * '-' + '
' 58 | number_info += u'{0} {1} {2}, {3} {4}'.format( 59 | _('QUERIED'), words_number, _( 60 | 'WORDS'), fields_number, _('FIELDS') 61 | ) 62 | 63 | self.update('
'.join(lst) + number_info) 64 | self._win.adjustSize() 65 | 66 | def update_title(self, title): 67 | self._win.setWindowTitle(title) 68 | 69 | def update_rows(self, number): 70 | self.rows_number = number 71 | self._msg_info.clear() 72 | 73 | def reset_count(self): 74 | self._msg_count.clear() 75 | 76 | def start(self, max=0, min=0, label=None, parent=None, immediate=False, rows=0): 77 | self._msg_info.clear() 78 | self._msg_count.clear() 79 | self.rows_number = rows 80 | self.aborted = False 81 | self._levels += 1 82 | if self._levels > 1: 83 | return 84 | # setup window 85 | parent = parent or self.app.activeWindow() or self.mw 86 | label = label or _("Processing...") 87 | cancel_btn = QPushButton("Cancel") 88 | self._win = QProgressDialog(label, "", min, max, parent) 89 | self._win.setWindowTitle("Querying...") 90 | self._win.setCancelButton(None) 91 | # cancel_btn.clicked.connect(self.abort) 92 | # self._win.setAutoClose(False) 93 | # self._win.setAutoReset(False) 94 | self._win.setWindowModality(Qt.ApplicationModal) 95 | # we need to manually manage minimum time to show, as qt gets confused 96 | # by the db handler 97 | self._win.setMinimumDuration(100000) 98 | if immediate: 99 | self._shown = True 100 | self._win.show() 101 | self.app.processEvents() 102 | else: 103 | self._shown = False 104 | self._counter = min 105 | self._min = min 106 | self._max = max 107 | self._firstTime = time.time() 108 | self._lastUpdate = time.time() 109 | self._disabled = False 110 | 111 | def update(self, label=None, value=None, process=True, maybeShow=True): 112 | # print self._min, self._counter, self._max, label, time.time() - 113 | # self._lastTime 114 | if maybeShow: 115 | self._maybeShow() 116 | elapsed = time.time() - self._lastUpdate 117 | if label: 118 | self._win.setLabelText(label) 119 | if self._max and self._shown: 120 | self._counter = value or (self._counter + 1) 121 | self._win.setValue(self._counter) 122 | if process and elapsed >= 0.2: 123 | self.app.processEvents(QEventLoop.ExcludeUserInputEvents) 124 | self._lastUpdate = time.time() 125 | 126 | def abort(self): 127 | # self.aborted = True 128 | return self._win.wasCanceled() 129 | 130 | def finish(self): 131 | self._levels -= 1 132 | self._levels = max(0, self._levels) 133 | if self._levels == 0 and self._win: 134 | self._win.cancel() 135 | self._unsetBusy() 136 | 137 | def clear(self): 138 | "Restore the interface after an error." 139 | if self._levels: 140 | self._levels = 1 141 | self.finish() 142 | 143 | def _maybeShow(self): 144 | if not self._levels: 145 | return 146 | if self._shown: 147 | self.update(maybeShow=False) 148 | return 149 | delta = time.time() - self._firstTime 150 | if delta > 0.5: 151 | self._shown = True 152 | self._win.show() 153 | self._setBusy() 154 | 155 | def _setBusy(self): 156 | self._disabled = True 157 | self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor)) 158 | 159 | def _unsetBusy(self): 160 | self._disabled = False 161 | self.app.restoreOverrideCursor() 162 | 163 | def busy(self): 164 | "True if processing." 165 | return self._levels 166 | -------------------------------------------------------------------------------- /src/query.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import os 21 | import re 22 | import shutil 23 | import sys 24 | import time 25 | from collections import defaultdict 26 | 27 | from aqt import mw 28 | from aqt.qt import QFileDialog, QObject, QThread, pyqtSignal, pyqtSlot 29 | from aqt.utils import showInfo, showText, tooltip 30 | from .constants import Endpoint, Template 31 | from .context import config 32 | from .lang import _, _sl 33 | from .progress import ProgressManager 34 | from .service import service_manager, QueryResult, copy_static_file 35 | from .utils import Empty, MapDict, Queue, wrap_css 36 | 37 | 38 | def inspect_note(note): 39 | ''' 40 | inspect the note, and get necessary input parameters 41 | return word_ord: field index of the word in current note 42 | return word: the word 43 | return maps: dicts map of current note 44 | ''' 45 | maps = config.get_maps(note.model()['id']) 46 | for i, m in enumerate(maps): 47 | if m.get('word_checked', False): 48 | word_ord = i 49 | break 50 | else: 51 | # if no field is checked to be the word field, default the 52 | # first one. 53 | word_ord = 0 54 | 55 | def purify_word(word): 56 | return word.strip() if word else '' 57 | 58 | word = purify_word(note.fields[word_ord]) 59 | return word_ord, word, maps 60 | 61 | 62 | def query_from_browser(browser): 63 | if not browser: 64 | return 65 | work_manager.reset_query_counts() 66 | notes = [browser.mw.col.getNote(note_id) 67 | for note_id in browser.selectedNotes()] 68 | if len(notes) == 0: 69 | return 70 | if len(notes) == 1: 71 | query_from_editor_all_fields(browser.editor) 72 | if len(notes) > 1: 73 | fields_number = 0 74 | progress.start(immediate=True) 75 | for i, note in enumerate(notes): 76 | # user cancels the progress 77 | if progress.abort(): 78 | break 79 | try: 80 | results = query_all_flds(note) 81 | update_note_fields(note, results) 82 | fields_number += len(results) 83 | progress.update_labels( 84 | MapDict(type='count', words_number=i + 1, fields_number=fields_number)) 85 | except InvalidWordException: 86 | showInfo(_("NO_QUERY_WORD")) 87 | promot_choose_css() 88 | browser.model.reset() 89 | progress.finish() 90 | # browser.model.reset() 91 | # browser.endReset() 92 | tooltip(u'{0} {1} {2}, {3} {4}'.format( 93 | _('UPDATED'), i + 1, _('CARDS'), work_manager.completed_query_counts(), _('FIELDS'))) 94 | 95 | 96 | def query_from_editor_all_fields(editor): 97 | if not editor or not editor.note: 98 | return 99 | work_manager.reset_query_counts() 100 | time.sleep(0.1) 101 | progress.start(immediate=True) 102 | try: 103 | results = query_all_flds(editor.note) 104 | update_note_fields(editor.note, results) 105 | except InvalidWordException: 106 | showInfo(_("NO_QUERY_WORD")) 107 | progress.finish() 108 | promot_choose_css() 109 | editor.setNote(editor.note, focusTo=0) 110 | editor.saveNow(lambda:None) 111 | 112 | 113 | def query_from_editor_current_field(editor): 114 | if not editor or not editor.note: 115 | return 116 | work_manager.reset_query_counts() 117 | progress.start(immediate=True) 118 | # if the focus falls into the word field, then query all note fields, 119 | # else only query the current focused field. 120 | fld_index = editor.currentField 121 | word_ord = inspect_note(editor.note)[0] 122 | try: 123 | if fld_index == word_ord: 124 | results = query_all_flds(editor.note) 125 | else: 126 | results = query_single_fld(editor.note, fld_index) 127 | update_note_fields(editor.note, results) 128 | except InvalidWordException: 129 | showInfo(_("NO_QUERY_WORD")) 130 | # editor.note.flush() 131 | # showText(str(editor.note.model()['tmpls'])) 132 | progress.finish() 133 | promot_choose_css() 134 | editor.setNote(editor.note, focusTo=0) 135 | editor.saveNow() 136 | 137 | 138 | def update_note_fields(note, results): 139 | for i, q in results.items(): 140 | if isinstance(q, QueryResult) and i < len(note.fields): 141 | update_note_field(note, i, q) 142 | 143 | 144 | def update_note_field(note, fld_index, fld_result): 145 | result, js, jsfile = fld_result.result, fld_result.js, fld_result.jsfile 146 | # js process: add to template of the note model 147 | add_to_tmpl(note, js=js, jsfile=jsfile) 148 | # if not result: 149 | # return 150 | if not config.force_update and not result: 151 | return 152 | note.fields[fld_index] = result if result else '' 153 | note.flush() 154 | 155 | 156 | def promot_choose_css(): 157 | for local_service in service_manager.local_services: 158 | try: 159 | missed_css = local_service.missed_css.pop() 160 | showInfo(Template.miss_css.format( 161 | dict=local_service.title, css=missed_css)) 162 | filepath = QFileDialog.getOpenFileName( 163 | caption=u'Choose css file', filter=u'CSS (*.css)')[0] 164 | if filepath: 165 | shutil.copy(filepath, u'_' + missed_css) 166 | wrap_css(u'_' + missed_css) 167 | local_service.missed_css.clear() 168 | 169 | except KeyError as e: 170 | pass 171 | 172 | 173 | def add_to_tmpl(note, **kwargs): 174 | # templates 175 | ''' 176 | [{u'name': u'Card 1', u'qfmt': u'{{Front}}\n\n', u'did': None, u'bafmt': u'', 177 | u'afmt': u'{{FrontSide}}\n\n
\n\n{{Back}}\n\n{{12}}\n\n{{44}}\n\n', u'ord': 0, u'bqfmt': u''}] 178 | ''' 179 | # showInfo(str(kwargs)) 180 | afmt = note.model()['tmpls'][0]['afmt'] 181 | if kwargs: 182 | jsfile, js = kwargs.get('jsfile', None), kwargs.get('js', None) 183 | if js and js.strip(): 184 | addings = js.strip() 185 | if addings not in afmt: 186 | if not addings.startswith(u''): 187 | addings = u'\r\n'.format(addings) 188 | afmt += addings 189 | if jsfile: 190 | new_jsfile = u'_' + \ 191 | jsfile if not jsfile.startswith(u'_') else jsfile 192 | copy_static_file(jsfile, new_jsfile) 193 | addings = u'\r\n'.format(new_jsfile) 194 | afmt += addings 195 | note.model()['tmpls'][0]['afmt'] = afmt 196 | 197 | 198 | class InvalidWordException(Exception): 199 | """Invalid word exception""" 200 | 201 | 202 | def join_result(query_func): 203 | def wrap(*args, **kwargs): 204 | query_func(*args, **kwargs) 205 | for name, worker in work_manager.workers.items(): 206 | while not worker.isFinished(): 207 | mw.app.processEvents() 208 | worker.wait(100) 209 | return handle_results('__query_over__') 210 | return wrap 211 | 212 | 213 | @join_result 214 | def query_all_flds(note): 215 | handle_results.total = defaultdict(QueryResult) 216 | word_ord, word, maps = inspect_note(note) 217 | if not word: 218 | raise InvalidWordException 219 | progress.update_title(u'Querying [[ %s ]]' % word) 220 | for i, each in enumerate(maps): 221 | if i == word_ord: 222 | continue 223 | if i == len(note.fields): 224 | break 225 | dict_name = each.get('dict', '').strip() 226 | dict_field = each.get('dict_field', '').strip() 227 | dict_unique = each.get('dict_unique', '').strip() 228 | if dict_name and dict_name not in _sl('NOT_DICT_FIELD') and dict_field: 229 | worker = work_manager.get_worker(dict_unique) 230 | worker.target(i, dict_field, word) 231 | work_manager.start_all_workers() 232 | 233 | 234 | @join_result 235 | def query_single_fld(note, fld_index): 236 | handle_results.total = defaultdict(QueryResult) 237 | word_ord, word, maps = inspect_note(note) 238 | if not word: 239 | raise InvalidWordException 240 | progress.update_title(u'Querying [[ %s ]]' % word) 241 | # assert fld_index > 0 242 | if fld_index >= len(maps): 243 | return QueryResult() 244 | dict_name = maps[fld_index].get('dict', '').strip() 245 | dict_field = maps[fld_index].get('dict_field', '').strip() 246 | dict_unique = maps[fld_index].get('dict_unique', '').strip() 247 | if dict_name and dict_name not in _sl('NOT_DICT_FIELD') and dict_field: 248 | worker = work_manager.get_worker(dict_unique) 249 | worker.target(fld_index, dict_field, word) 250 | work_manager.start_all_workers() 251 | 252 | 253 | @pyqtSlot(dict) 254 | def handle_results(result): 255 | # showInfo('slot: ' + str(result)) 256 | if result != '__query_over__': 257 | # progress. 258 | handle_results.total.update(result) 259 | return handle_results.total 260 | 261 | 262 | class QueryWorkerManager(object): 263 | 264 | def __init__(self): 265 | self.workers = defaultdict(QueryWorker) 266 | 267 | def get_worker(self, service_unique): 268 | if service_unique not in self.workers: 269 | worker = QueryWorker(service_unique) 270 | # check whether the service is available 271 | if worker.service: 272 | self.workers[service_unique] = worker 273 | else: 274 | worker = self.workers[service_unique] 275 | return worker 276 | 277 | def start_worker(self, worker): 278 | worker.start() 279 | 280 | def start_all_workers(self): 281 | progress.update_rows(len(self.workers)) 282 | for i, worker in enumerate(self.workers.values()): 283 | worker.index = i 284 | worker.start() 285 | 286 | def reset_query_counts(self): 287 | for worker in self.workers.values(): 288 | worker.completed_counts = 0 289 | 290 | def completed_query_counts(self): 291 | return sum([worker.completed_counts for worker in self.workers.values()]) 292 | 293 | 294 | class QueryWorker(QThread): 295 | 296 | result_ready = pyqtSignal(dict) 297 | progress_update = pyqtSignal(dict) 298 | 299 | def __init__(self, service_unique): 300 | super(QueryWorker, self).__init__() 301 | self.service_unique = service_unique 302 | self.index = 0 303 | self.service = service_manager.get_service(service_unique) 304 | self.completed_counts = 0 305 | self.queue = Queue() 306 | self.result_ready.connect(handle_results) 307 | self.progress_update.connect(progress.update_labels) 308 | 309 | def target(self, index, service_field, word): 310 | self.queue.put((index, service_field, word)) 311 | 312 | def run(self): 313 | # self.completed_counts = 0 314 | while True: 315 | if progress.abort(): 316 | break 317 | try: 318 | index, service_field, word = self.queue.get(timeout=0.1) 319 | # self.progress_update.emit({ 320 | # 'service_name': self.service.title, 321 | # 'word': word, 322 | # 'field_name': service_field 323 | # }) 324 | result = self.query(service_field, word) 325 | self.result_ready.emit({index: result}) 326 | self.completed_counts += 1 327 | # rest a moment 328 | self.rest() 329 | except Empty: 330 | break 331 | 332 | def rest(self): 333 | time.sleep(self.service.query_interval) 334 | 335 | def query(self, service_field, word): 336 | self.service.set_notifier(self.progress_update, self.index) 337 | return self.service.active(service_field, word) 338 | 339 | 340 | progress = ProgressManager(mw) 341 | 342 | work_manager = QueryWorkerManager() 343 | -------------------------------------------------------------------------------- /src/resources/wqicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/finalion/WordQuery/cd986d3ba795d1a2d2e17609f68b518c48922617/src/resources/wqicon.png -------------------------------------------------------------------------------- /src/service/LDOCE6.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import re 3 | 4 | from aqt.utils import showInfo, showText 5 | from .base import MdxService, export, register, with_styles 6 | 7 | path = u'/Users/yu/Documents/english study/mdx/LDOCE6双解/L6mp3.mdx' 8 | 9 | 10 | @register(u'本地词典-LDOCE6') 11 | class Ldoce6(MdxService): 12 | 13 | def __init__(self, dict_path): 14 | super(Ldoce6, self).__init__(path) 15 | 16 | @property 17 | def unique(self): 18 | return self.__class__.__name__ 19 | 20 | @property 21 | def title(self): 22 | return self.__register_label__ 23 | 24 | @export(u'音标', 1) 25 | def fld_phonetic(self): 26 | html = self.get_html() 27 | m = re.search(r'(.*?)', html) 28 | if m: 29 | return m.groups()[0] 30 | 31 | @export(u'Bre单词发音', 2) 32 | def fld_voicebre(self): 33 | html = self.get_html() 34 | m = re.search(r'(.*?)', html) 35 | if m: 36 | return m.groups()[0] 37 | return '' 38 | 39 | @export(u'Ame单词发音', 3) 40 | def fld_voiceame(self): 41 | html = self.get_html() 42 | m = re.search(r'(.*?)', html) 43 | if m: 44 | return m.groups()[0] 45 | return '' 46 | 47 | @export(u'sentence', 4) 48 | def fld_sentence(self): 49 | html = self.get_html() 50 | m = re.search(r'(.*?)', html) 51 | if m: 52 | return re.sub('', '', m.groups()[0]) 53 | return '' 54 | 55 | @export(u'def', 5) 56 | def fld_definate(self): 57 | html = self.get_html() 58 | m = re.search(r'(.*?)', html) 59 | if m: 60 | return m.groups()[0] 61 | return '' 62 | 63 | @export(u'random_sentence', 6) 64 | def fld_random_sentence(self): 65 | html = self.get_html() 66 | m = re.findall(r'(.*?)', html) 67 | if m: 68 | number = len(m) 69 | index = random.randrange(0, number - 1, 1) 70 | return re.sub('', '', m[index]) 71 | return '' 72 | 73 | @export(u'all sentence', 7) 74 | def fld_allsentence(self): 75 | html = self.get_html() 76 | m = re.findall( 77 | r'(.+?.+?)', html) 78 | if m: 79 | items = 0 80 | my_str = '' 81 | for items in range(len(m)): 82 | my_str = my_str + m[items] 83 | return my_str 84 | return '' 85 | -------------------------------------------------------------------------------- /src/service/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | from .manager import service_manager 21 | from .base import QueryResult, copy_static_file 22 | 23 | 24 | 25 | 26 | 27 | # def start_services(): 28 | # service_manager.fetch_headers() 29 | -------------------------------------------------------------------------------- /src/service/baicizhan.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import json 3 | import os 4 | import re 5 | import urllib 6 | try: 7 | import urllib2 8 | except: 9 | import urllib.request as urllib2 10 | import xml.etree.ElementTree 11 | from collections import defaultdict 12 | 13 | from aqt.utils import showInfo 14 | 15 | from .base import WebService, export, register 16 | from ..utils import ignore_exception 17 | 18 | bcz_download_mp3 = True 19 | bcz_download_img = True 20 | 21 | 22 | @register(u'百词斩') 23 | class Baicizhan(WebService): 24 | 25 | def __init__(self): 26 | super(Baicizhan, self).__init__() 27 | 28 | def _get_from_api(self): 29 | url = u"http://mall.baicizhan.com/ws/search?w={word}".format( 30 | word=self.word) 31 | try: 32 | html = urllib2.urlopen(url, timeout=5).read() 33 | return self.cache_this(json.loads(html)) 34 | except: 35 | return defaultdict(str) 36 | 37 | # @ignore_exception 38 | @export(u'发音', 0) 39 | def fld_phonetic(self): 40 | url = u'http://baicizhan.qiniucdn.com/word_audios/{word}.mp3'.format( 41 | word=self.word) 42 | audio_name = 'bcz_%s.mp3' % self.word 43 | if bcz_download_mp3: 44 | if self.download(url, audio_name): 45 | # urllib.urlretrieve(url, audio_name) 46 | with open(audio_name, 'rb') as f: 47 | if f.read().strip() == '{"error":"Document not found"}': 48 | res = '' 49 | else: 50 | res = self.get_anki_label(audio_name, 'audio') 51 | if not res: 52 | os.remove(audio_name) 53 | else: 54 | res = '' 55 | return res 56 | else: 57 | return url 58 | 59 | def _get_field(self, key, default=u''): 60 | return self.cache_result(key) if self.cached(key) else self._get_from_api().get(key, default) 61 | 62 | @export(u'音标', 1) 63 | def fld_explains(self): 64 | return self._get_field('accent') 65 | 66 | @export(u'图片', 2) 67 | def fld_img(self): 68 | url = self._get_field('img') 69 | if url and bcz_download_img: 70 | filename = url[url.rindex('/') + 1:] 71 | if self.download(url, filename): 72 | return self.get_anki_label(filename, 'img') 73 | return self.get_anki_label(url, 'img') 74 | 75 | @export(u'象形', 3) 76 | def fld_df(self): 77 | url = self._get_field('df') 78 | if url and bcz_download_img: 79 | filename = url[url.rindex('/') + 1:] 80 | if self.download(url, filename): 81 | return self.get_anki_label(filename, 'img') 82 | return self.get_anki_label(url, 'img') 83 | 84 | @export(u'中文释义', 6) 85 | def fld_mean(self): 86 | return self._get_field('mean_cn') 87 | 88 | @export(u'英文例句', 4) 89 | def fld_st(self): 90 | return self._get_field('st') 91 | 92 | @export(u'例句翻译', 5) 93 | def fld_sttr(self): 94 | return self._get_field('sttr') 95 | 96 | @export(u'单词tv', 7) 97 | def fld_tv_url(self): 98 | return self.get_anki_label(self._get_field('tv'), 'video') 99 | -------------------------------------------------------------------------------- /src/service/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import inspect 21 | import os 22 | # use ntpath module to ensure the windows-style (e.g. '\\LDOCE.css') 23 | # path can be processed on Unix platform. 24 | # However, anki version on mac platforms doesn't including this package? 25 | # import ntpath 26 | import re 27 | import shutil 28 | import sqlite3 29 | import urllib 30 | 31 | try: 32 | import urllib2 33 | except: 34 | import urllib.request as urllib2 35 | import zlib 36 | from collections import defaultdict 37 | from functools import wraps 38 | 39 | try: 40 | from cookielib import CookieJar 41 | except: 42 | from http.cookiejar import CookieJar 43 | from aqt import mw 44 | from aqt.qt import QFileDialog 45 | from aqt.utils import showInfo, showText 46 | from ..context import config 47 | from ..lang import _ 48 | from ..libs import MdxBuilder, StardictBuilder 49 | from ..utils import MapDict, wrap_css 50 | import requests 51 | 52 | 53 | def register(label): 54 | """register the dict service with a label, which will be shown in the dicts list.""" 55 | 56 | def _deco(cls): 57 | cls.__register_label__ = label 58 | return cls 59 | 60 | return _deco 61 | 62 | 63 | def export(label, index): 64 | """export dict field function with a label, which will be shown in the fields list.""" 65 | 66 | def _with(fld_func): 67 | @wraps(fld_func) 68 | def _deco(cls, *args, **kwargs): 69 | res = fld_func(cls, *args, **kwargs) 70 | return QueryResult(result=res) if not isinstance(res, QueryResult) else res 71 | 72 | _deco.__export_attrs__ = (label, index) 73 | return _deco 74 | 75 | return _with 76 | 77 | 78 | def copy_static_file(filename, new_filename=None, static_dir='static'): 79 | """ 80 | copy file in static directory to media folder 81 | """ 82 | abspath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 83 | static_dir, 84 | filename) 85 | shutil.copy(abspath, new_filename if new_filename else filename) 86 | 87 | 88 | def with_styles(**styles): 89 | """ 90 | cssfile: specify the css file in static folder 91 | css: css strings 92 | js: js strings 93 | jsfile: specify the js file in static folder 94 | """ 95 | 96 | def _with(fld_func): 97 | @wraps(fld_func) 98 | def _deco(cls, *args, **kwargs): 99 | res = fld_func(cls, *args, **kwargs) 100 | cssfile, css, jsfile, js, need_wrap_css, class_wrapper = \ 101 | styles.get('cssfile', None), \ 102 | styles.get('css', None), \ 103 | styles.get('jsfile', None), \ 104 | styles.get('js', None), \ 105 | styles.get('need_wrap_css', False), \ 106 | styles.get('wrap_class', '') 107 | 108 | def wrap(html, css_obj, is_file=True): 109 | # wrap css and html 110 | if need_wrap_css and class_wrapper: 111 | html = u'
{}
'.format( 112 | class_wrapper, html) 113 | return html, wrap_css(css_obj, is_file=is_file, class_wrapper=class_wrapper)[0] 114 | return html, css_obj 115 | 116 | if cssfile: 117 | new_cssfile = cssfile if cssfile.startswith('_') \ 118 | else u'_' + cssfile 119 | # copy the css file to media folder 120 | copy_static_file(cssfile, new_cssfile) 121 | # wrap the css file 122 | res, new_cssfile = wrap(res, new_cssfile) 123 | res = u'{1}'.format( 124 | new_cssfile, res) 125 | if css: 126 | res, css = wrap(res, css, is_file=False) 127 | res = u'{1}'.format(css, res) 128 | 129 | if not isinstance(res, QueryResult): 130 | return QueryResult(result=res, jsfile=jsfile, js=js) 131 | else: 132 | res.set_styles(jsfile=jsfile, js=js) 133 | return res 134 | 135 | return _deco 136 | 137 | return _with 138 | 139 | 140 | class Service(object): 141 | '''service base class''' 142 | 143 | def __init__(self): 144 | self._exporters = self.get_exporters() 145 | self._fields, self._actions = zip(*self._exporters) \ 146 | if self._exporters else (None, None) 147 | # query interval: default 500ms 148 | self.query_interval = 0.5 149 | 150 | @property 151 | def fields(self): 152 | return self._fields 153 | 154 | @property 155 | def actions(self): 156 | return self._actions 157 | 158 | @property 159 | def exporters(self): 160 | return self._exporters 161 | 162 | def get_exporters(self): 163 | flds = dict() 164 | methods = inspect.getmembers(self, predicate=inspect.ismethod) 165 | for method in methods: 166 | export_attrs = getattr(method[1], '__export_attrs__', None) 167 | if export_attrs: 168 | label, index = export_attrs 169 | flds.update({int(index): (label, method[1])}) 170 | sorted_flds = sorted(flds) 171 | return [flds[key] for key in sorted_flds] 172 | 173 | def active(self, action_label, word): 174 | self.word = word 175 | # if the service instance is LocalService, 176 | # then have to build then index. 177 | if isinstance(self, LocalService): 178 | self.notify(MapDict(type='text', index=self.work_id, 179 | text=u'Building %s...' % self._filename)) 180 | if isinstance(self, MdxService) or isinstance(self, StardictService): 181 | self.builder.check_build() 182 | 183 | for each in self.exporters: 184 | if action_label == each[0]: 185 | self.notify(MapDict(type='info', index=self.work_id, 186 | service_name=self.title, 187 | field_name=action_label, 188 | flag=u'->')) 189 | result = each[1]() 190 | self.notify(MapDict(type='info', index=self.work_id, 191 | service_name=self.title, 192 | field_name=action_label, 193 | flag=u'√')) 194 | return result 195 | return QueryResult.default() 196 | 197 | def set_notifier(self, progress_update, index): 198 | self.notify_signal = progress_update 199 | self.work_id = index 200 | 201 | def notify(self, data): 202 | self.notify_signal.emit(data) 203 | 204 | @staticmethod 205 | def get_anki_label(filename, type_): 206 | formats = {'audio': u'[sound:{0}]', 207 | 'img': u'', 208 | 'video': u''} 209 | return formats[type_].format(filename) 210 | 211 | 212 | class WebService(Service): 213 | '''web service class''' 214 | 215 | def __init__(self): 216 | super(WebService, self).__init__() 217 | self.cache = defaultdict(defaultdict) 218 | self._cookie = CookieJar() 219 | self._opener = urllib2.build_opener( 220 | urllib2.HTTPCookieProcessor(self._cookie)) 221 | self.query_interval = 1 222 | 223 | def cache_this(self, result): 224 | self.cache[self.word].update(result) 225 | return result 226 | 227 | def cached(self, key): 228 | return (self.word in self.cache) and (key in self.cache[self.word]) 229 | 230 | def cache_result(self, key): 231 | return self.cache[self.word].get(key, u'') 232 | 233 | @property 234 | def title(self): 235 | return self.__register_label__ 236 | 237 | @property 238 | def unique(self): 239 | return self.__class__.__name__ 240 | 241 | def get_response(self, url, data=None, headers=None, timeout=10): 242 | default_headers = {'User-Agent': 'Anki WordQuery', 243 | 'Accept-Encoding': 'gzip'} 244 | if headers: 245 | default_headers.update(headers) 246 | 247 | request = urllib2.Request(url, headers=default_headers) 248 | try: 249 | response = self._opener.open(request, data=data, timeout=timeout) 250 | data = response.read() # return bytes 251 | if response.info().get('Content-Encoding') == 'gzip': 252 | data = zlib.decompress(data, 16 + zlib.MAX_WBITS) # return bytes 253 | return data.decode('utf-8') # TODO: all data can be decoded by utf-8?? 254 | except: 255 | return '' 256 | 257 | @classmethod 258 | def download(cls, url, filename): 259 | try: 260 | return urllib.urlretrieve(url, filename) 261 | except AttributeError: 262 | try: 263 | with open(filename, "wb") as f: 264 | f.write(requests.get(url).content) 265 | return True 266 | except Exception as e: 267 | pass 268 | except Exception as e: 269 | pass 270 | 271 | 272 | class LocalService(Service): 273 | 274 | def __init__(self, dict_path): 275 | super(LocalService, self).__init__() 276 | self.dict_path = dict_path 277 | self.builder = None 278 | self.missed_css = set() 279 | 280 | @property 281 | def unique(self): 282 | return self.dict_path 283 | 284 | @property 285 | def title(self): 286 | return self.__register_label__ 287 | 288 | @property 289 | def _filename(self): 290 | return os.path.splitext(os.path.basename(self.dict_path))[0] 291 | 292 | 293 | class MdxService(LocalService): 294 | 295 | def __init__(self, dict_path): 296 | super(MdxService, self).__init__(dict_path) 297 | self.media_cache = defaultdict(set) 298 | self.cache = defaultdict(str) 299 | self.query_interval = 0.01 300 | self.styles = [] 301 | self.builder = MdxBuilder(dict_path) 302 | self.builder.get_header() 303 | 304 | @staticmethod 305 | def support(dict_path): 306 | return os.path.isfile(dict_path) and dict_path.lower().endswith('.mdx') 307 | 308 | @property 309 | def title(self): 310 | if config.use_filename or not self.builder._title or self.builder._title.startswith('Title'): 311 | return self._filename 312 | else: 313 | return self.builder.meta['title'] 314 | 315 | @export(u"default", 0) 316 | def fld_whole(self): 317 | html = self.get_html() 318 | js = re.findall(r'.*?', html, re.DOTALL) 319 | return QueryResult(result=html, js=u'\n'.join(js)) 320 | 321 | def get_html(self): 322 | if not self.cache[self.word]: 323 | html = '' 324 | result = self.builder.mdx_lookup(self.word) # self.word: unicode 325 | if result: 326 | if result[0].upper().find(u"@@@LINK=") > -1: 327 | # redirect to a new word behind the equal symol. 328 | self.word = result[0][len(u"@@@LINK="):].strip() 329 | return self.get_html() 330 | else: 331 | html = self.adapt_to_anki(result[0]) 332 | self.cache[self.word] = html 333 | return self.cache[self.word] 334 | 335 | def adapt_to_anki(self, html): 336 | """ 337 | 1. convert the media path to actual path in anki's collection media folder. 338 | 2. remove the js codes (js inside will expires.) 339 | """ 340 | # convert media path, save media files 341 | media_files_set = set() 342 | mcss = re.findall(r'href="(\S+?\.css)"', html) 343 | media_files_set.update(set(mcss)) 344 | mjs = re.findall(r'src="([\w\./]\S+?\.js)"', html) 345 | media_files_set.update(set(mjs)) 346 | msrc = re.findall(r'', html) 347 | media_files_set.update(set(msrc)) 348 | msound = re.findall(r'href="sound:(.*?\.(?:mp3|wav))"', html) 349 | if config.export_media: 350 | media_files_set.update(set(msound)) 351 | for each in media_files_set: 352 | html = html.replace(each, u'_' + each.split('/')[-1]) 353 | # find sounds 354 | p = re.compile( 355 | r']+?href=\"(sound:_.*?\.(?:mp3|wav))\"[^>]*?>(.*?)') 356 | html = p.sub(u"[\\1]\\2", html) 357 | self.save_media_files(media_files_set) 358 | for cssfile in mcss: 359 | cssfile = '_' + \ 360 | os.path.basename(cssfile.replace('\\', os.path.sep)) 361 | # if not exists the css file, the user can place the file to media 362 | # folder first, and it will also execute the wrap process to generate 363 | # the desired file. 364 | if not os.path.exists(cssfile): 365 | self.missed_css.add(cssfile[1:]) 366 | new_css_file, wrap_class_name = wrap_css(cssfile) 367 | html = html.replace(cssfile, new_css_file) 368 | # add global div to the result html 369 | html = u'
{1}
'.format( 370 | wrap_class_name, html) 371 | 372 | return html 373 | 374 | def save_file(self, filepath_in_mdx, savepath=None): 375 | basename = os.path.basename(filepath_in_mdx.replace('\\', os.path.sep)) 376 | if savepath is None: 377 | savepath = '_' + basename 378 | try: 379 | bytes_list = self.builder.mdd_lookup(filepath_in_mdx) 380 | if bytes_list and not os.path.exists(savepath): 381 | with open(savepath, 'wb') as f: 382 | f.write(bytes_list[0]) 383 | return savepath 384 | except sqlite3.OperationalError as e: 385 | showInfo(str(e)) 386 | 387 | def save_media_files(self, data): 388 | """ 389 | get the necessary static files from local mdx dictionary 390 | ** kwargs: data = list 391 | """ 392 | diff = data.difference(self.media_cache['files']) 393 | self.media_cache['files'].update(diff) 394 | lst, errors = list(), list() 395 | wild = [ 396 | '*' + os.path.basename(each.replace('\\', os.path.sep)) for each in diff] 397 | try: 398 | for each in wild: 399 | # TODO : refract get_mdd_keys method 400 | keys = self.builder.get_mdd_keys(each) 401 | if not keys: 402 | errors.append(each) 403 | lst.extend(keys) 404 | for each in lst: 405 | self.save_file(each) 406 | except AttributeError: 407 | pass 408 | 409 | return errors 410 | 411 | 412 | class StardictService(LocalService): 413 | 414 | def __init__(self, dict_path): 415 | super(StardictService, self).__init__(dict_path) 416 | self.query_interval = 0.05 417 | self.builder = StardictBuilder(self.dict_path, in_memory=False) 418 | self.builder.get_header() 419 | 420 | @staticmethod 421 | def support(dict_path): 422 | return os.path.isfile(dict_path) and dict_path.lower().endswith('.ifo') 423 | 424 | @property 425 | def title(self): 426 | if config.use_filename or not self.builder.ifo.bookname: 427 | return self._filename 428 | else: 429 | return self.builder.ifo.bookname.decode('utf-8') 430 | 431 | @export(u"default", 0) 432 | def fld_whole(self): 433 | self.builder.check_build() 434 | try: 435 | result = self.builder[self.word] 436 | result = result.strip().replace('\r\n', '
') \ 437 | .replace('\r', '
').replace('\n', '
') 438 | return QueryResult(result=result) 439 | except KeyError: 440 | return QueryResult.default() 441 | 442 | 443 | class QueryResult(MapDict): 444 | """Query Result structure""" 445 | 446 | def __init__(self, *args, **kwargs): 447 | super(QueryResult, self).__init__(*args, **kwargs) 448 | # avoid return None 449 | if self['result'] == None: 450 | self['result'] = "" 451 | 452 | def set_styles(self, **kwargs): 453 | for key, value in kwargs.items(): 454 | self[key] = value 455 | 456 | @classmethod 457 | def default(cls): 458 | return QueryResult(result="") 459 | -------------------------------------------------------------------------------- /src/service/bing.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import re 3 | 4 | from aqt.utils import showInfo, showText 5 | from BeautifulSoup import BeautifulSoup 6 | from .base import WebService, export, register, with_styles 7 | 8 | 9 | @register(u'Bing') 10 | class Bing(WebService): 11 | 12 | def __init__(self): 13 | super(Bing, self).__init__() 14 | 15 | def _get_content(self): 16 | data = self.get_response( 17 | "http://cn.bing.com/dict/search?q={}&mkt=zh-cn".format(self.word)) 18 | 19 | soup = BeautifulSoup(data) 20 | 21 | def _get_element(soup, tag, id=None, class_=None, subtag=None): 22 | # element = soup.find(tag, id=id, class_=class_) # bs4 23 | element = None 24 | if id: 25 | element = soup.find(tag, {"id": id}) 26 | if class_: 27 | element = soup.find(tag, {"class": class_}) 28 | if subtag and element: 29 | element = getattr(element, subtag, '') 30 | return element 31 | 32 | result = {} 33 | element = _get_element(soup, 'div', class_='hd_prUS') 34 | if element: 35 | result['phonitic_us'] = str(element).decode('utf-8') 36 | element = _get_element(soup, 'div', class_='hd_pr') 37 | if element: 38 | result['phonitic_uk'] = str(element).decode('utf-8') 39 | element = _get_element(soup, 'div', class_='hd_if') 40 | if element: 41 | result['participle'] = str(element).decode('utf-8') 42 | element = _get_element(soup, 'div', class_='qdef', subtag='ul') 43 | if element: 44 | result['def'] = u''.join([str(content).decode('utf-8') 45 | for content in element.contents]) 46 | # for pair in pairs]) 47 | # result = _get_from_element( 48 | # result, 'advanced_ec', soup, 'div', id='authid') 49 | # result = _get_from_element( 50 | # result, 'ec', soup, 'div', id='crossid') 51 | # result = _get_from_element( 52 | # result, 'ee', soup, 'div', id='homoid') 53 | # result = _get_from_element( 54 | # result, 'web_definition', soup, 'div', id='webid') 55 | # result = _get_from_element( 56 | # result, 'collocation', soup, 'div', id='colid') 57 | # result = _get_from_element( 58 | # result, 'synonym', soup, 'div', id='synoid') 59 | # result = _get_from_element( 60 | # result, 'antonym', soup, 'div', id='antoid') 61 | # result = _get_from_element( 62 | # result, 'samples', soup, 'div', id='sentenceCon') 63 | return self.cache_this(result) 64 | # except Exception as e: 65 | # showInfo(str(e)) 66 | # return {} 67 | 68 | def _get_field(self, key, default=u''): 69 | return self.cache_result(key) if self.cached(key) else self._get_content().get(key, default) 70 | 71 | @export(u'美式音标', 1) 72 | def fld_phonetic_us(self): 73 | return self._get_field('phonitic_us') 74 | 75 | @export(u'英式音标', 2) 76 | def fld_phonetic_uk(self): 77 | return self._get_field('phonitic_uk') 78 | 79 | @export(u'词语时态', 3) 80 | def fld_participle(self): 81 | return self._get_field('participle') 82 | 83 | @export(u'释义', 4) 84 | @with_styles(css='.pos{font-weight:bold;margin-right:4px;}', need_wrap_css=True, wrap_class='bing') 85 | def fld_definition(self): 86 | return self._get_field('def') 87 | 88 | # @export(u'权威英汉双解', 5) 89 | # def fld_advanced_ec(self): 90 | # return self._get_field('advanced_ec') 91 | 92 | # @export(u'英汉', 6) 93 | # def fld_ec(self): 94 | # return self._get_field('ec') 95 | 96 | # @export(u'英英', 7) 97 | # def fld_ee(self): 98 | # return self._get_field('ee') 99 | 100 | # @export(u'网络释义', 8) 101 | # def fld_web_definition(self): 102 | # return self._get_field('web_definition') 103 | 104 | # @export(u'搭配', 9) 105 | # def fld_collocation(self): 106 | # return self._get_field('collocation') 107 | 108 | # @export(u'同义词', 10) 109 | # def fld_synonym(self): 110 | # return self._get_field('synonym') 111 | 112 | # @export(u'反义词', 11) 113 | # def fld_antonym(self): 114 | # return self._get_field('antonym') 115 | 116 | # @export(u'例句', 12) 117 | # def fld_samples(self): 118 | # return self._get_field('samples') 119 | -------------------------------------------------------------------------------- /src/service/bing3tp.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import json 3 | import re 4 | try: 5 | import urllib2 6 | except: 7 | import urllib.request as urllib2 8 | 9 | from aqt.utils import showInfo, showText 10 | from .base import WebService, export, register, with_styles 11 | 12 | bing_download_mp3 = True 13 | 14 | 15 | @register(u'Bing xtk') 16 | class BingXtk(WebService): 17 | 18 | def __init__(self): 19 | super(BingXtk, self).__init__() 20 | 21 | def _get_content(self): 22 | resp = {'pronunciation': '', 'defs': '', 'sams': ''} 23 | headers = { 24 | 'Accept-Language': 'en-US,zh-CN;q=0.8,zh;q=0.6,en;q=0.4', 25 | 'User-Agent': 'WordQuery Addon (Anki)', 26 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'} 27 | try: 28 | request = urllib2.Request( 29 | 'http://xtk.azurewebsites.net/BingDictService.aspx?Word=' + self.word.encode('utf-8'), headers=headers) 30 | resp = json.loads(urllib2.urlopen(request).read()) 31 | return self.cache_this(resp) 32 | except Exception as e: 33 | return resp 34 | 35 | def _get_field(self, key, default=u''): 36 | return self.cache_result(key) if self.cached(key) else self._get_content().get(key, default) 37 | 38 | def _get_subfield(self, field, key, default=u''): 39 | subfield = default 40 | if field: 41 | subfield = field.get(key, default) 42 | if subfield is None: 43 | subfield = default 44 | return subfield 45 | 46 | @export(u'美式音标', 1) 47 | def fld_phonetic_us(self): 48 | seg = self._get_field('pronunciation') 49 | return self._get_subfield(seg, 'AmE') 50 | 51 | @export(u'英式音标', 2) 52 | def fld_phonetic_uk(self): 53 | seg = self._get_field('pronunciation') 54 | return self._get_subfield(seg, 'BrE') 55 | 56 | @export(u'美式发音', 3) 57 | def fld_mp3_us(self): 58 | seg = audio_url = self._get_field('pronunciation') 59 | audio_url = self._get_subfield(seg, 'AmEmp3') 60 | if bing_download_mp3 and audio_url: 61 | filename = u'bing_{}_us.mp3'.format(self.word) 62 | if self.download(audio_url, filename): 63 | return self.get_anki_label(filename, 'audio') 64 | return audio_url 65 | 66 | @export(u'英式发音', 4) 67 | def fld_mp3_uk(self): 68 | seg = self._get_field('pronunciation') 69 | audio_url = self._get_subfield(seg, 'BrEmp3') 70 | if bing_download_mp3 and audio_url: 71 | filename = u'bing_{}_br.mp3'.format(self.word) 72 | if self.download(audio_url, filename): 73 | return self.get_anki_label(filename, 'audio') 74 | return audio_url 75 | 76 | @export(u'释义', 5) 77 | def fld_definition(self): 78 | segs = self._get_field('defs') 79 | return u'
'.join([u'{0} {1}'.format(seg['pos'], seg['def']) for seg in segs]) 80 | 81 | @export(u'例句', 6) 82 | # @with_styles(cssfile='_bing2.css', need_wrap_css=True, wrap_class=u'bing') 83 | def fld_samples(self): 84 | max_numbers = 10 85 | segs = self._get_field('sams') 86 | sentences = '' 87 | for i, seg in enumerate(segs): 88 | sentences = sentences +\ 89 | u"""
  • 90 |
    91 |
    {0}
    92 |
    {1}
    93 |
    94 |
  • """.format(seg['eng'], seg['chn'], i + 1) 95 | if i == 9: 96 | break 97 | return u"""
    98 |
    99 |
      {}
    100 |
    101 |
    """.format(sentences) 102 | -------------------------------------------------------------------------------- /src/service/esdict.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import base64 3 | import re 4 | import urllib 5 | try: 6 | import urllib2 7 | except: 8 | import urllib.request as urllib2 9 | 10 | from aqt.utils import showInfo 11 | from BeautifulSoup import BeautifulSoup 12 | from .base import WebService, export, register, with_styles 13 | 14 | # Anki buit-in BeautifulSoup is bs3 not bs4 15 | 16 | 17 | css = '' 18 | 19 | 20 | @register(u'西语助手') 21 | class Esdict(WebService): 22 | 23 | def __init__(self): 24 | super(Esdict, self).__init__() 25 | 26 | def _get_content(self): 27 | url = 'https://www.esdict.cn/mdicts/es/{word}'.format( 28 | word=urllib.quote(self.word.encode('utf-8'))) 29 | try: 30 | result = {} 31 | html = urllib2.urlopen(url, timeout=5).read() 32 | soup = BeautifulSoup(html) 33 | 34 | def _get_from_element(dict, key, soup, tag, id=None, class_=None): 35 | baseURL = 'https://www.esdict.cn/' 36 | # element = soup.find(tag, id=id, class_=class_) # bs4 37 | if id: 38 | element = soup.find(tag, {"id": id}) 39 | if class_: 40 | element = soup.find(tag, {"class": class_}) 41 | if element: 42 | dict[key] = str(element) 43 | dict[key] = re.sub( 44 | r'href="/', 'href="' + baseURL, dict[key]) 45 | dict[key] = re.sub(r'声明:.*。', '', dict[key]) 46 | dict[key] = dict[key].decode('utf-8') 47 | return dict 48 | 49 | # '[bɔ̃ʒur]' 50 | result = _get_from_element( 51 | result, 'phonitic', soup, 'span', class_='Phonitic') 52 | # '
    ' 53 | result = _get_from_element( 54 | result, 'fccf', soup, 'div', id='FCChild') # 西汉-汉西词典 55 | result = _get_from_element( 56 | result, 'example', soup, 'div', id='LJChild') # 西语例句库 57 | result = _get_from_element( 58 | result, 'syn', soup, 'div', id='SYNChild') # 近义、反义、派生词典 59 | result = _get_from_element( 60 | result, 'ff', soup, 'div', id='FFChild') # 西西词典 61 | result = _get_from_element( 62 | result, 'fe', soup, 'div', id='FEChild') # 西英词典 63 | 64 | return self.cache_this(result) 65 | except Exception as e: 66 | return {} 67 | 68 | def _get_field(self, key, default=u''): 69 | return self.cache_result(key) if self.cached(key) else self._get_content().get(key, default) 70 | 71 | @export(u'真人发音', 0) 72 | def fld_sound(self): 73 | # base64.b64encode('bonjour') == 'Ym9uam91cg==' 74 | # https://api.frdic.com/api/v2/speech/speakweb?langid=fr&txt=QYNYm9uam91cg%3d%3d 75 | url = 'https://api.frdic.com/api/v2/speech/speakweb?langid=es&txt=QYN{word}'.format( 76 | word=urllib.quote(base64.b64encode(self.word.encode('utf-8')))) 77 | audio_name = u'esdict_{word}.mp3'.format(word=self.word) 78 | try: 79 | urllib.urlretrieve(url, audio_name) 80 | return self.get_anki_label(audio_name, 'audio') 81 | except Exception as e: 82 | return '' 83 | 84 | @export(u'音标', 1) 85 | def fld_phonetic(self): 86 | return self._get_field('phonitic') 87 | 88 | @export(u'西汉-汉西词典', 2) 89 | @with_styles(css=css) 90 | def fld_fccf(self): 91 | return self._get_field('fccf') 92 | 93 | @export(u'西语例句库', 3) 94 | @with_styles(css=css) 95 | def fld_example(self): 96 | return self._get_field('example') 97 | 98 | @export(u'近义、反义、派生词典', 4) 99 | def fld_syn(self): 100 | return self._get_field('syn') 101 | 102 | @export(u'西西词典', 5) 103 | @with_styles(css=css) 104 | def fld_ff(self): 105 | return self._get_field('ff') 106 | 107 | @export(u'西英词典', 6) 108 | @with_styles(css=css) 109 | def fld_fe(self): 110 | return self._get_field('fe') 111 | -------------------------------------------------------------------------------- /src/service/frdic.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import base64 3 | import re 4 | import urllib 5 | import urllib2 6 | 7 | from aqt.utils import showInfo 8 | from BeautifulSoup import BeautifulSoup 9 | 10 | from .base import WebService, export, with_styles, register 11 | 12 | # Anki buit-in BeautifulSoup is bs3 not bs4 13 | 14 | 15 | css = '' 16 | 17 | 18 | @register(u'法语助手') 19 | class Frdic(WebService): 20 | 21 | def __init__(self): 22 | super(Frdic, self).__init__() 23 | 24 | def _get_content(self): 25 | url = 'https://frdic.com/mdicts/fr/{word}'.format( 26 | word=urllib.quote(self.word.encode('utf-8'))) 27 | try: 28 | result = {} 29 | html = urllib2.urlopen(url, timeout=5).read() 30 | soup = BeautifulSoup(html) 31 | 32 | def _get_from_element(dict, key, soup, tag, id=None, class_=None): 33 | baseURL = 'https://frdic.com/' 34 | # element = soup.find(tag, id=id, class_=class_) # bs4 35 | if id: 36 | element = soup.find(tag, {"id": id}) 37 | if class_: 38 | element = soup.find(tag, {"class": class_}) 39 | if element: 40 | dict[key] = str(element) 41 | dict[key] = re.sub( 42 | r'href="/', 'href="' + baseURL, dict[key]) 43 | dict[key] = re.sub(r'声明:.*。', '', dict[key]) 44 | dict[key] = dict[key].decode('utf-8') 45 | return dict 46 | 47 | # '[bɔ̃ʒur]' 48 | result = _get_from_element( 49 | result, 'phonitic', soup, 'span', class_='Phonitic') 50 | # '
    ' 51 | result = _get_from_element( 52 | result, 'fccf', soup, 'div', id='FCChild') # 法汉-汉法词典 53 | result = _get_from_element( 54 | result, 'example', soup, 'div', id='LJChild') # 法语例句库 55 | result = _get_from_element( 56 | result, 'syn', soup, 'div', id='SYNChild') # 近义、反义、派生词典 57 | result = _get_from_element( 58 | result, 'ff', soup, 'div', id='FFChild') # 法法词典 59 | result = _get_from_element( 60 | result, 'fe', soup, 'div', id='FEChild') # 法英词典 61 | 62 | return self.cache_this(result) 63 | except Exception as e: 64 | return {} 65 | 66 | @export(u'真人发音', 0) 67 | def fld_sound(self): 68 | # base64.b64encode('bonjour') == 'Ym9uam91cg==' 69 | # https://api.frdic.com/api/v2/speech/speakweb?langid=fr&txt=QYNYm9uam91cg%3d%3d 70 | url = 'https://api.frdic.com/api/v2/speech/speakweb?langid=fr&txt=QYN{word}'.format( 71 | word=urllib.quote(base64.b64encode(self.word.encode('utf-8')))) 72 | audio_name = u'frdic_{word}.mp3'.format(word=self.word) 73 | try: 74 | urllib.urlretrieve(url, audio_name) 75 | return self.get_anki_label(audio_name, 'audio') 76 | except Exception as e: 77 | return '' 78 | 79 | def _get_field(self, key, default=u''): 80 | return self.cache_result(key) if self.cached(key) else self._get_content().get(key, default) 81 | 82 | @export(u'音标', 1) 83 | def fld_phonetic(self): 84 | return self._get_field('phonitic') 85 | 86 | @export(u'法汉-汉法词典', 2) 87 | def fld_fccf(self): 88 | return self._get_field('fccf') 89 | 90 | @export(u'法语例句库', 3) 91 | @with_styles(css=css) 92 | def fld_example(self): 93 | return self._get_field('example') 94 | 95 | @export(u'近义、反义、派生词典', 4) 96 | @with_styles(css=css) 97 | def fld_syn(self): 98 | return self._get_field('syn') 99 | 100 | @export(u'法法词典', 5) 101 | @with_styles(css=css) 102 | def fld_ff(self): 103 | return self._get_field('ff') 104 | 105 | @export(u'法英词典', 6) 106 | @with_styles(css=css) 107 | def fld_fe(self): 108 | return self._get_field('fe') 109 | -------------------------------------------------------------------------------- /src/service/iciba.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import os 3 | import re 4 | try: 5 | import urllib2 6 | except: 7 | import urllib.request as urllib2 8 | import json 9 | from collections import defaultdict 10 | from aqt.utils import showInfo, showText 11 | 12 | from .base import WebService, export, with_styles, register 13 | from ..utils import ignore_exception 14 | 15 | iciba_download_mp3 = True 16 | 17 | 18 | @register(u'爱词霸') 19 | class ICIBA(WebService): 20 | 21 | def __init__(self): 22 | super(ICIBA, self).__init__() 23 | 24 | def _get_content(self): 25 | resp = defaultdict(str) 26 | headers = { 27 | 'Accept-Language': 'en-US,zh-CN;q=0.8,zh;q=0.6,en;q=0.4', 28 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', 29 | 'Accept': 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01'} 30 | # try: 31 | request = urllib2.Request( 32 | 'http://www.iciba.com/index.php?a=getWordMean&c=search&word=' + self.word.encode('utf-8'), headers=headers) 33 | resp = json.loads(urllib2.urlopen(request).read()) 34 | # self.cache_this(resp['baesInfo']['symbols'][0]) 35 | # self.cache_this(resp['sentence']) 36 | # showText(str(self.cache[self.word])) 37 | # return self.cache[self.word] 38 | return self.cache_this(resp) 39 | # except Exception as e: 40 | # return resp 41 | 42 | def _get_field(self, key, default=u''): 43 | return self.cache_result(key) if self.cached(key) else self._get_content().get(key, default) 44 | 45 | @ignore_exception 46 | @export(u'美式音标', 1) 47 | def fld_phonetic_us(self): 48 | seg = self._get_field('baesInfo') 49 | return seg['symbols'][0]['ph_am'] 50 | 51 | @ignore_exception 52 | @export(u'英式音标', 2) 53 | def fld_phonetic_uk(self): 54 | seg = self._get_field('baesInfo') 55 | return seg['symbols'][0]['ph_en'] 56 | 57 | @ignore_exception 58 | @export(u'美式发音', 3) 59 | def fld_mp3_us(self): 60 | seg = self._get_field('baesInfo') 61 | audio_url, t = seg['symbols'][0]['ph_am_mp3'], 'am' 62 | if not audio_url: 63 | audio_url, t = seg['symbols'][0]['ph_tts_mp3'], 'tts' 64 | if iciba_download_mp3 and audio_url: 65 | filename = u'iciba_{0}_{1}.mp3'.format(self.word, t) 66 | if os.path.exists(filename) or self.download(audio_url, filename): 67 | return self.get_anki_label(filename, 'audio') 68 | return audio_url 69 | 70 | @ignore_exception 71 | @export(u'英式发音', 4) 72 | def fld_mp3_uk(self): 73 | seg = self._get_field('baesInfo') 74 | audio_url, t = seg['symbols'][0]['ph_en_mp3'], 'en' 75 | if not audio_url: 76 | audio_url, t = seg['symbols'][0]['ph_tts_mp3'], 'tts' 77 | if iciba_download_mp3 and audio_url: 78 | filename = u'iciba_{0}_{1}.mp3'.format(self.word, t) 79 | if os.path.exists(filename) or self.download(audio_url, filename): 80 | return self.get_anki_label(filename, 'audio') 81 | return audio_url 82 | 83 | @ignore_exception 84 | @export(u'释义', 5) 85 | def fld_definition(self): 86 | seg = self._get_field('baesInfo') 87 | parts = seg['symbols'][0]['parts'] 88 | return u'
    '.join([part['part'] + ' ' + '; '.join(part['means']) for part in parts]) 89 | 90 | @ignore_exception 91 | @export(u'双语例句', 6) 92 | def fld_samples(self): 93 | sentences = '' 94 | segs = self._get_field('sentence') 95 | for i, seg in enumerate(segs): 96 | sentences = sentences +\ 97 | u"""
  • 98 |
    {0}
    99 |
    {1}
    100 |
  • """.format(seg['Network_en'], seg['Network_cn']) 101 | return u"""
      {}
    """.format(sentences) 102 | 103 | @ignore_exception 104 | @export(u'权威例句', 7) 105 | def fld_auth_sentence(self): 106 | sentences = '' 107 | segs = self._get_field('auth_sentence') 108 | for i, seg in enumerate(segs): 109 | sentences = sentences +\ 110 | u"""
  • {0} [{1}]
  • """.format( 111 | seg['res_content'], seg['source']) 112 | return u"""
      {}
    """.format(sentences) 113 | 114 | @ignore_exception 115 | @export(u'句式用法', 8) 116 | def fld_usage(self): 117 | sentences = '' 118 | segs = self._get_field('jushi') 119 | for i, seg in enumerate(segs): 120 | sentences = sentences +\ 121 | u"""
  • 122 |
    {0}
    123 |
    {1}
    124 |
  • """.format(seg['english'], seg['chinese']) 125 | return u"""
      {}
    """.format(sentences) 126 | 127 | @ignore_exception 128 | @export(u'使用频率', 9) 129 | def fld_frequence(self): 130 | seg = self._get_field('baesInfo') 131 | return str(seg['frequence']) 132 | -------------------------------------------------------------------------------- /src/service/longman.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright: khuang6 3 | # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html 4 | 5 | """ 6 | Project : wq 7 | Created: 12/20/2017 8 | """ 9 | import os 10 | from warnings import filterwarnings 11 | 12 | import requests as rq 13 | from bs4 import BeautifulSoup, Tag 14 | 15 | from .base import WebService, export, register, with_styles 16 | 17 | filterwarnings('ignore') 18 | 19 | 20 | @register(u'朗文') 21 | class Longman(WebService): 22 | 23 | def __init__(self): 24 | super(Longman, self).__init__() 25 | 26 | def _get_singledict(self, single_dict): 27 | """ 28 | 29 | :type word: str 30 | :return: 31 | """ 32 | 33 | if not (self.cached(single_dict) and self.cache_result(single_dict)): 34 | rsp = rq.get("https://www.ldoceonline.com/dictionary/{}".format(self.word), headers={ 35 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36' 36 | }) 37 | 38 | if rsp.status_code == 200: 39 | bs = BeautifulSoup(rsp.content.decode('utf-8'), 'html.parser') 40 | # Top Container 41 | dictlinks = bs.find_all('span', {'class': 'dictlink'}) 42 | body_html = "" 43 | 44 | word_info = { 45 | } 46 | ee_ = '' 47 | for dic_link in dictlinks: 48 | assert isinstance(dic_link, Tag) 49 | 50 | # Remove related Topics Container 51 | related_topic_tag = dic_link.find('div', {'class': "topics_container"}) 52 | if related_topic_tag: 53 | related_topic_tag.decompose() 54 | 55 | # Remove Tail 56 | tail_tag = dic_link.find("span", {'class': 'Tail'}) 57 | if tail_tag: 58 | tail_tag.decompose() 59 | 60 | # Remove SubEntry 61 | sub_entries = dic_link.find_all('span', {'class': 'SubEntry'}) 62 | for sub_entry in sub_entries: 63 | sub_entry.decompose() 64 | 65 | # word elements 66 | head_tag = dic_link.find('span', {'class': "Head"}) 67 | if head_tag and not word_info: 68 | try: 69 | hyphenation = head_tag.find("span", {'class': 'HYPHENATION'}).string # Hyphenation 70 | except: 71 | hyphenation = '' 72 | try: 73 | pron_codes = "".join( 74 | list(head_tag.find("span", {'class': 'PronCodes'}).strings)) # Hyphenation 75 | except: 76 | pron_codes = '' 77 | try: 78 | POS = head_tag.find("span", {'class': 'POS'}).string # Hyphenation 79 | except: 80 | POS = '' 81 | 82 | try: 83 | Inflections = head_tag.find('span', {'class': 'Inflections'}) 84 | if Inflections: 85 | Inflections = str(Inflections) 86 | else: 87 | Inflections = '' 88 | except: 89 | Inflections = '' 90 | 91 | word_info = { 92 | 'phonetic': pron_codes, 93 | 'hyphenation': hyphenation, 94 | 'pos': POS, 95 | 'inflections': Inflections, 96 | } 97 | self.cache_this(word_info) 98 | if head_tag: 99 | head_tag.decompose() 100 | 101 | # remove script tag 102 | script_tags = dic_link.find_all('script') 103 | for t in script_tags: 104 | t.decompose() 105 | 106 | # remove img tag 107 | img_tags = dic_link.find_all('img') 108 | for t in img_tags: 109 | self.cache_this({'img': 'https://www.ldoceonline.com' + t['src']}) 110 | t.decompose() 111 | 112 | # remove sound tag 113 | am_s_tag = dic_link.find("span", title='Play American pronunciation of {}'.format(self.word)) 114 | br_s_tag = dic_link.find("span", title='Play British pronunciation of {}'.format(self.word)) 115 | if am_s_tag: 116 | am_s_tag.decompose() 117 | if br_s_tag: 118 | br_s_tag.decompose() 119 | 120 | # remove example sound tag 121 | emp_s_tags = dic_link.find_all('span', {'class': 'speaker exafile fa fa-volume-up'}) 122 | for t in emp_s_tags: 123 | t.decompose() 124 | 125 | body_html += str(dic_link) 126 | ee_ = body_html 127 | self.cache_this({ 128 | 'ee': ee_ 129 | }) 130 | 131 | else: 132 | return '' 133 | return self.cache_result(single_dict) 134 | 135 | @export(u'音标', 2) 136 | def phonetic(self): 137 | return self._get_singledict('phonetic') 138 | 139 | @export(u'断字单词', 3) 140 | def hyphenation(self): 141 | return self._get_singledict('hyphenation') 142 | 143 | @export(u'词性', 1) 144 | def pos(self): 145 | return self._get_singledict('pos') 146 | 147 | @export(u'英英解释', 0) 148 | @with_styles(cssfile='_longman.css') 149 | def ee(self): 150 | return self._get_singledict('ee') 151 | 152 | @export('图片', 4) 153 | def pic(self): 154 | url = self._get_singledict('img') 155 | filename = u'longman_img_{}'.format(os.path.basename(url)) 156 | if url and self.download(url, filename): 157 | return self.get_anki_label(filename, 'img') 158 | return '' 159 | 160 | @export(u'变形', 5) 161 | @with_styles(cssfile='_longman.css') 162 | def inflections(self): 163 | return self._get_singledict('inflections') 164 | -------------------------------------------------------------------------------- /src/service/manager.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import inspect 21 | import os 22 | from functools import wraps 23 | 24 | from aqt import mw 25 | from aqt.qt import QThread 26 | from aqt.utils import showInfo 27 | from .base import LocalService, MdxService, StardictService, WebService 28 | from ..context import config 29 | from ..utils import MapDict, importlib 30 | 31 | 32 | class ServiceManager(object): 33 | 34 | def __init__(self): 35 | self.update_services() 36 | 37 | @property 38 | def services(self): 39 | return self.web_services | self.local_services 40 | 41 | # def start_all(self): 42 | # self.fetch_headers() 43 | # make all local services available 44 | # for service in self.local_services: 45 | # if not service.index(only_header=True): 46 | # self.local_services.remove(service) 47 | 48 | def update_services(self): 49 | self.web_services, self.local_custom_services = self._get_services_from_files() 50 | self.local_services = self._get_available_local_services() 51 | # self.fetch_headers() 52 | # combine the customized local services into local services 53 | self.local_services.update(self.local_custom_services) 54 | 55 | def get_service(self, unique): 56 | # webservice unique: class name 57 | # mdxservice unique: dict filepath 58 | for each in self.services: 59 | if each.unique == unique: 60 | return each 61 | 62 | def get_service_action(self, service, label): 63 | for each in service.fields: 64 | if each.label == label: 65 | return each 66 | 67 | def _get_services_from_files(self, *args): 68 | """ 69 | get service from service packages, available type is 70 | WebService, LocalService 71 | """ 72 | web_services, local_custom_services = set(), set() 73 | mypath = os.path.dirname(os.path.realpath(__file__)) 74 | files = [f for f in os.listdir(mypath) 75 | if f not in ('__init__.py', 'base.py', 'manager.py') and not f.endswith('.pyc')] 76 | base_class = (WebService, LocalService, 77 | MdxService, StardictService) 78 | 79 | for f in files: 80 | try: 81 | module = importlib.import_module( 82 | '.%s' % os.path.splitext(f)[0], __package__) 83 | for name, cls in inspect.getmembers(module, predicate=inspect.isclass): 84 | if cls in base_class: 85 | continue 86 | try: 87 | service = cls(*args) 88 | if issubclass(cls, WebService): 89 | web_services.add(service) 90 | # get the customized local services 91 | if issubclass(cls, LocalService): 92 | local_custom_services.add(service) 93 | except Exception: 94 | # exclude the local service whose path has error. 95 | pass 96 | except ImportError: 97 | continue 98 | return web_services, local_custom_services 99 | 100 | def _get_available_local_services(self): 101 | services = set() 102 | for each in config.dirs: 103 | for dirpath, dirnames, filenames in os.walk(each): 104 | for filename in filenames: 105 | dict_path = os.path.join(dirpath, filename) 106 | if MdxService.support(dict_path): 107 | services.add(MdxService(dict_path)) 108 | if StardictService.support(dict_path): 109 | services.add(StardictService(dict_path)) 110 | # support mdx dictionary and stardict format dictionary 111 | return services 112 | 113 | service_manager = ServiceManager() -------------------------------------------------------------------------------- /src/service/oxford.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | try: 3 | import urllib2 4 | except: 5 | import urllib.request as urllib2 6 | import json 7 | from aqt.utils import showInfo 8 | from .base import WebService, export, register, with_styles 9 | 10 | 11 | @register("Oxford") 12 | class Oxford(WebService): 13 | 14 | def __init__(self): 15 | super(Oxford, self).__init__() 16 | 17 | def _get_from_api(self, lang="en"): 18 | word = self.word 19 | baseurl = "https://od-api.oxforddictionaries.com/api/v1" 20 | app_id = "45aecf84" 21 | app_key = "bb36fd6a1259e5baf8df6110a2f7fc8f" 22 | headers = {"app_id": app_id, "app_key": app_key} 23 | 24 | word_id = urllib2.quote(word.lower().replace(" ", "_")) 25 | url = baseurl + "/entries/" + lang + "/" + word_id 26 | url = urllib2.Request(url, headers=headers) 27 | try: 28 | return json.loads(urllib2.urlopen(url).read()) 29 | except: 30 | pass 31 | 32 | @export("Lexical Category", 1) 33 | def _fld_category(self): 34 | result = self._get_from_api() 35 | if result: 36 | return self._get_from_api()[0]["lexicalEntries"][0]["lexicalCategory"] 37 | else: 38 | return str() 39 | -------------------------------------------------------------------------------- /src/service/remotemdx.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import os 21 | # import ntpath 22 | import re 23 | import urllib 24 | try: 25 | import urllib2 26 | except: 27 | import urllib.request as urllib2 28 | import urlparse 29 | from collections import defaultdict 30 | 31 | from aqt.utils import showInfo, showText 32 | from .base import QueryResult, WebService, export, register, with_styles 33 | 34 | 35 | @register(u'MDX server') 36 | class RemoteMdx(WebService): 37 | 38 | def __init__(self): 39 | super(RemoteMdx, self).__init__() 40 | self.cache = defaultdict(set) 41 | 42 | def active(self, dict_path, word): 43 | self.word = word 44 | self.url = dict_path + \ 45 | '/' if not dict_path.endswith('/') else dict_path 46 | try: 47 | req = urllib2.urlopen(self.url + word) 48 | result, js = self.adapt_to_anki(req.read()) 49 | return QueryResult(result=result, js=js) 50 | except: 51 | return QueryResult.default() 52 | 53 | def download_media_files(self, data): 54 | diff = data.difference(self.cache[self.url]) 55 | self.cache[self.url].update(diff) 56 | errors, styles = list(), list() 57 | for each in diff: 58 | basename = os.path.basename(each.replace('\\', os.path.sep)) 59 | saved_basename = '_' + basename 60 | abs_url = urlparse.urljoin(self.url, each) 61 | if basename.endswith('.css') or basename.endswith('.js'): 62 | styles.append(saved_basename) 63 | if not os.path.exists(saved_basename): 64 | try: 65 | urllib.urlretrieve(abs_url, saved_basename) 66 | except: 67 | errors.append(each) 68 | return errors, styles 69 | 70 | def adapt_to_anki(self, html): 71 | """ 72 | 1. convert the media path to actual path in anki's collection media folder. 73 | 2. remove the js codes 74 | 3. import css, to make sure the css file can be synced. TO VALIDATE! 75 | """ 76 | media_files_set = set() 77 | mcss = re.findall(r'href="(\S+?\.css)"', html) 78 | media_files_set.update(set(mcss)) 79 | mjs = re.findall(r'src="([\w\./]\S+?\.js)"', html) 80 | media_files_set.update(set(mjs)) 81 | msrc = re.findall(r'', html) 82 | media_files_set.update(set(msrc)) 83 | for each in media_files_set: 84 | html = html.replace(each, '_' + each.split('/')[-1]) 85 | errors, styles = self.download_media_files(media_files_set) 86 | html = u'
    '.join([u"".format(style) 87 | for style in styles if style.endswith('.css')]) + html 88 | js = re.findall(r'.*?', html, re.DOTALL) 89 | # for each in js: 90 | # html = html.replace(each, '') 91 | # showText(html) 92 | return unicode(html), u'\n'.join(js) 93 | -------------------------------------------------------------------------------- /src/service/static/_bing.css: -------------------------------------------------------------------------------- 1 | ul, 2 | li, 3 | form, 4 | table, 5 | tr, 6 | th, 7 | td, 8 | blockquote { 9 | border: 0; 10 | border-collapse: collapse; 11 | border-spacing: 0; 12 | list-style: none; 13 | margin: 0; 14 | padding: 0 15 | } 16 | .qdef ul { 17 | padding-top: 10px 18 | } 19 | 20 | #sentenceSeg .sen_li { 21 | display: inline-block; 22 | padding-right: 10px; 23 | line-height: 19px; 24 | vertical-align: middle; 25 | margin: 0 0 10px 0; 26 | width: auto 27 | } 28 | 29 | #sentenceSeg .mm_div { 30 | margin-bottom: 10px; 31 | line-height: 19px; 32 | vertical-align: middle; 33 | padding: 0; 34 | display: inline-block 35 | } 36 | .sen_li { 37 | margin: 5px 0 2px 0; 38 | width: 100% 39 | } 40 | 41 | .sen_con { 42 | color: #333; 43 | padding: 2px 0; 44 | font-size: 14px; 45 | line-height: 22px 46 | } 47 | 48 | .sen_con strong { 49 | color: #c00 50 | } 51 | 52 | .sen_count { 53 | font-size: 14px 54 | } 55 | 56 | .sen_count a { 57 | color: #a1a1a1 58 | } 59 | 60 | .sen_count a:hover { 61 | color: #04c 62 | } 63 | 64 | .sen_count a:visited { 65 | color: #639 66 | } 67 | .sen_en>span { 68 | font-size: 14px; 69 | text-decoration: none; 70 | outline: medium none 71 | } 72 | 73 | .sen_en, 74 | .sen_cn, 75 | .sen_ime { 76 | width: 100%; 77 | font-size: 14px; 78 | padding-left: 0; 79 | margin-left: 0 80 | } 81 | 82 | .sen_en { 83 | line-height: 14px; 84 | margin-bottom: 2px; 85 | color: #000 86 | } 87 | 88 | .sen_cn { 89 | line-height: 22px; 90 | margin-bottom: 2px; 91 | color: #777 92 | } 93 | 94 | .sen_ime { 95 | line-height: 17.5px; 96 | color: #777; 97 | margin-bottom: 2px 98 | } 99 | 100 | 101 | .se_d, 102 | .se_def_nu { 103 | line-height: 22px 104 | } 105 | 106 | 107 | .se_d { 108 | width: 20px; 109 | float: left 110 | } 111 | 112 | .se_lis, 113 | .idmdef_li { 114 | margin-left: 20px 115 | } 116 | 117 | 118 | .se_buf { 119 | margin-left: 24px; 120 | float: left; 121 | margin-top: 4px 122 | } 123 | 124 | .se_d, 125 | .def_pa, 126 | .idmdef_li { 127 | color: #000; 128 | font-size: 14px 129 | } 130 | .se_div { 131 | margin-top: 10px; 132 | padding-bottom: 0; 133 | overflow: hidden 134 | } 135 | 136 | .se_li { 137 | list-style-type: none; 138 | padding-top: 0; 139 | padding-left: 0; 140 | clear: both 141 | } 142 | 143 | .se_li1 { 144 | margin-bottom: 5px; 145 | margin-left: 0; 146 | padding-left: 0; 147 | overflow: hidden; 148 | float: left; 149 | max-width: 508px 150 | } 151 | 152 | .se_n_d { 153 | float: left; 154 | width: 24px; 155 | padding-top: 3px 156 | } 157 | 158 | .qdef .hd_prUS, 159 | .qdef .hd_pr { 160 | color: #777; 161 | font-size: 14px 162 | } 163 | 164 | .qdef .pos { 165 | width: 35px; 166 | font-size: 93%; 167 | background-color: #aaa; 168 | color: #fff; 169 | line-height: 18px; 170 | vertical-align: middle; 171 | text-align: center 172 | } 173 | 174 | .qdef .pos1 { 175 | margin-top: 2px 176 | } 177 | 178 | .qdef .web { 179 | background-color: #333 180 | } 181 | 182 | .qdef ul { 183 | padding-top: 10px 184 | } 185 | 186 | .qdef li { 187 | padding-top: 4px; 188 | font-weight: bold 189 | } 190 | 191 | .qdef .def { 192 | padding-left: 15px; 193 | line-height: 20px; 194 | vertical-align: top; 195 | font-size: 14px; 196 | width: 90% 197 | } 198 | 199 | .qdef .def a { 200 | color: #000 201 | } 202 | 203 | .qdef .def a:hover { 204 | color: #04c 205 | } 206 | 207 | .qdef .hd_div1 .p2-1 { 208 | font-weight: bold; 209 | color: #777 210 | } 211 | 212 | .qdef .hd_div1 { 213 | color: #777; 214 | padding-top: 9px 215 | } 216 | 217 | .qdef:after { 218 | clear: both 219 | } 220 | 221 | .qdef .hd_if a { 222 | margin: 0 6px 0 0; 223 | font-size: 14px 224 | } 225 | 226 | .qdef div.simg, 227 | .qdef div.simgmore { 228 | margin-top: 10px 229 | } 230 | 231 | .qdef .simg { 232 | left: 1px 233 | } 234 | 235 | .qdef df_div { 236 | margin-top: 60px 237 | } 238 | 239 | .qdef .hd_tf_lh { 240 | line-height: 19px; 241 | padding-top: 3px 242 | } 243 | 244 | .qdef .wd_div { 245 | margin-top: 10px 246 | } 247 | 248 | .qdef .df_div { 249 | padding-top: 10px 250 | } 251 | 252 | .qdef .hd_area { 253 | float: none; 254 | overflow: hidden; 255 | margin-bottom: 0 256 | }.qdef h1 { 257 | font-size: 100% 258 | } -------------------------------------------------------------------------------- /src/service/static/_youdao.css: -------------------------------------------------------------------------------- 1 | a,a:active,a:hover{color:#138bff}a{cursor:pointer}body{font-size:14px}.nav-label .nav,.page,.page a{text-align:center}.emphasis,b,h4,h5{font-weight:400}.baike_detail .content,.collins h4 .title,.ec h2 span,.fanyi,strong{word-wrap:break-word}html{-webkit-text-size-adjust:none}.collins h4 .phonetic,.ec .phonetic{font-family:lucida sans unicode,arial,sans-serif}h1,h2,h3,h4,h5,input,li,ol,p,textarea,ul{margin:0;padding:0;outline:0}li,ol,ul{list-style:none}a{text-decoration:none}strong{color:#c50000}#bd{padding:7px 0;background:#f4f4f4}.p-index_entry #bd{padding:0;background:#fff}.p-index #hd{border-bottom:1px solid #e8e8e8}.content-wrp{margin:7px}#ft{padding:7px 0;background:#fff}.empty-content{padding:35px 7px;background:#fff;vertical-align:middle}#dictNav,#dictNavList{position:absolute;right:7px}.dict-dl{font-size:9pt}.page{display:block;border:1px solid #e1e1e1;border-radius:2px;background:#fff;color:#313131;-o-border-radius:2px}.page a{display:inline-block;padding:11px 0;width:49%}.dict_sents_result .col3,.source{text-align:right}.dict-container{background:#fff}.dict-container h4{padding:0 7px;border-bottom:1px solid #e1e1e1}.dict-container .trans-container{padding:7px}.dict-container .trans-title{display:block;padding:7px 0;color:#313131}.dict-dl{display:block;padding:7px;border:1px solid #e1e1e1;background-color:#fafafa}.core,.secondary,.secondaryFont{font-size:15px}#dictNav{top:350px;-webkit-transition:top .3s;transition:top .3s}#dictNavBtn{display:inline-block;overflow:hidden;padding-top:3pc;width:3pc;height:0;background:url(http://shared.ydstatic.com/dict/youdaowap/changeImg/dict_unfold_icon_normal.png) no-repeat;background-size:100%}#dictNavBtn.selected{background:url(http://shared.ydstatic.com/dict/youdaowap/changeImg/dict_unfold_icon_pressed.png) no-repeat;background-size:100%}#dictNavList{bottom:49px;display:none;overflow-y:scroll;width:200px;height:0;background:rgba(0,0,0,.7);-webkit-transition:height .5s}#dictNavList li{padding:7px 0;border-bottom:1px solid hsla(0,0%,100%,.3)}#dictNavList li a{display:inline-block;box-sizing:border-box;padding:0 7px;width:100%;color:#fff}.core,body{color:#4e4c4a}h2{margin:0;padding:0}.secondary{color:#847965}.emphasis,b{color:#9c0;font-style:italic}.clickable{color:#279aec}.grey{color:#847965}.serial{margin-right:.7em}.source{padding-top:.7em}.mcols-layout{overflow:hidden}.mcols-layout .col1{float:left;width:1.33em}.mcols-layout .col2{overflow:auto}.mcols-layout .col3{float:right;vertical-align:top}a.pronounce{display:inline-block;width:24px;height:24px;background-image:url(http://shared.ydstatic.com/dict/youdaowap/icon/dict_pronounce-_icon_normal.png);background-size:24px 24px;text-indent:-9999px;font-size:0}a.pronounce:active{background-image:url(http://shared.ydstatic.com/dict/youdaowap/icon/dict_pronounce-_icon_pressed.png)}a.pronounce.disbled{background-image:url(http://shared.ydstatic.com/dict/youdaowap/icon/dict_pronounce-_icon_disable.png)}.ec a.pronounce{vertical-align:middle}.dict-container .trans-container h4{padding:0;border:none;-webkit-tap-highlight-color:rgb(255,255,0)}.dict-container .dict_sents_result{padding:0}.dict-container .dict_sents_result .content{padding:7px}.dict_sents_result .clickable{font-size:13px}.dict_sents_result li{padding-top:.6em}.dict_sents_result li:first-child{padding-top:0}.dict_sents_result .speech-size{margin-top:-10px}.dict_sents_result .more-sents{display:block;padding:7px}.collins .star{display:inline-block;overflow:hidden;margin:0 7px;width:77px;height:13px;background:url(http://shared.ydstatic.com/dict/youdaowap/icon/dict_star_live.png);background-size:77px 13px}.collins .star1{width:14px}.collins .star2{width:28px}.collins .star3{width:45px}.collins .star4{width:61px}.collins .star5{width:77px}.cj .colExam,.ck .colExam,.jc .colExam,.kc .colExam{width:3em}.collins .per-collins-entry li{padding:.6em 0 0}.collins li .mcols-layout{padding:.4em 0 0}.collins h4 .title{overflow:hidden}.ec h2{margin-bottom:.7em;font-size:1pc;line-height:1.5em}.ec h2 .amend{float:right;text-decoration:underline}.ec h2 span{display:inline-block}.ec .sub{margin-top:.7em;padding-top:.7em}.ec .core{padding-left:.33em}.web_trans .webPhrase{margin-top:.6em}.web_trans .trans-list>li{padding:.6em 0 0}.web_trans .trans-list>li:first-child{padding-top:0}ce_new .trans-list li{padding:.7em 0 0}.ce_new .source,.ce_new>ul .per-tran{padding:.7em 0}.ce_new>ul .per-tran:first-child{border:none}.cj>ul,.ck>ul{padding-bottom:.7em}.cj h4{font-weight:700}.cj .grey{font-weight:400}.cj>h4{padding-top:.7em}.cj>h4:first-child{padding-top:0}.ck>h4{padding-top:.7em}.ck>h4:first-child{padding-top:0}.ec21 .phrs,.ec21 .posType>li,.ec21 .source,.ec21 .wfs,.ee>ul>li{padding:.7em 0}.ec21 ins{margin:0 .5em;color:#b6afa2;text-decoration:none}.ec21 .serial{margin-right:.7em}.ee .per-tran,.hh>ul li{padding:.3em 0}.ee>ul{margin-top:.7em}.fanyi{overflow:hidden}.hh>h4{padding-top:.7em}.hh>h4:first-child{padding-top:0}.hh .source{padding:.7em 0}.jc h4{font-weight:700}.jc .origin,.jc sup{font-weight:400}.jc .origin{font-size:.86em}.kc h4{font-weight:700}.loading{padding:2em;color:#4e4c4a}.rel_word>ul li{padding:.3em 0}.rel_word>ul li:first-child{padding-top:0}.special>ul>li{padding:.7em 0}.special>ul>li:first-child{padding-top:0}.special .source{padding:.7em 0}.syno>ul li{padding:.3em 0}.syno>ul li:first-child{padding-top:0}.baike .target{position:relative;display:block;padding-right:40px}.baike .source,.baike>ul li{padding:.7em 0}.baike>ul li:first-child{padding-top:0}.typo .clickable{margin-right:7px}.typo li{padding:7px 0 0}.baike_detail>h4{padding:0 0 .5em;font-size:24px}.baike_detail li{padding:.5em 0}.baike_detail p{margin-bottom:.5em}.baike_detail p:first-child{padding-bottom:.5em}.baike_detail .grey{display:block;padding:.8em 0}.baike_detail .img{clear:both;display:block;overflow:hidden;width:auto!important;font-weight:400;font-size:9pt;line-height:18px}.baike_detail .img_r{float:right;margin-left:1em}.baike_detail img{width:88px;height:auto;-webkit-border-radius:3px}.baike_detail .img strong{display:none}.baike_detail strong{display:block;padding:.5em 0}.baike_detail a,.baike_detail a b{color:#279aec} -------------------------------------------------------------------------------- /src/service/txt.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import re 3 | 4 | from aqt.utils import showInfo, showText 5 | from .base import LocalService, export, register, with_styles 6 | 7 | path = u'D:\\dicts\\LDOCE\\d.txt' 8 | 9 | 10 | @register(u'txt测试') 11 | class TxtTest(LocalService): 12 | 13 | def __init__(self): 14 | super(TxtTest, self).__init__(path) 15 | try: 16 | self.handle = open(path, 'rb') 17 | except: 18 | self.handle = None 19 | 20 | @export(u'all', 1) 21 | def fld_phonetic(self): 22 | if not self.handle: 23 | return 24 | for line in self.handle: 25 | line = line.decode("UTF-8") 26 | m = re.search(self.word, line) 27 | if m: 28 | return line 29 | -------------------------------------------------------------------------------- /src/service/youdao.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import re 3 | 4 | try: 5 | import urllib2 6 | except: 7 | import urllib.request as urllib2 8 | import xml.etree.ElementTree 9 | 10 | from .base import WebService, export, register, with_styles 11 | 12 | from bs4 import BeautifulSoup, Tag, NavigableString 13 | 14 | from warnings import filterwarnings 15 | 16 | filterwarnings("ignore") 17 | 18 | js = ''' 19 | var initVoice = function () { 20 | var player = document.getElementById('dictVoice'); 21 | document.addEventListener('click', function (e) { 22 | var target = e.target; 23 | if (target.hasAttribute('role') && target.getAttribute('role').indexOf('dict_audio_js') >= 0) { 24 | var url = target.getAttribute('data-rel'); 25 | player.setAttribute('src', url); 26 | player.volume = 1; 27 | player.play(); 28 | e.preventDefault(); 29 | } 30 | }, false); 31 | }; 32 | initVoice(); 33 | ''' 34 | 35 | youdao_download_mp3 = True 36 | 37 | 38 | @register(u'有道词典') 39 | class Youdao(WebService): 40 | 41 | def __init__(self): 42 | super(Youdao, self).__init__() 43 | 44 | def _get_from_api(self, lang='eng'): 45 | url = "http://dict.youdao.com/fsearch?client=deskdict&keyfrom=chrome.extension&pos=-1&doctype=xml&xmlVersion=3.2&dogVersion=1.0&vendor=unknown&appVer=3.1.17.4208&le=%s&q=%s" % ( 46 | lang, self.word) 47 | phonetics, explains = '', '' 48 | try: 49 | result = urllib2.urlopen(url, timeout=5).read() 50 | # showInfo(str(result)) 51 | doc = xml.etree.ElementTree.fromstring(result) 52 | # fetch symbols 53 | symbol, uk_symbol, us_symbol = doc.findtext(".//phonetic-symbol"), doc.findtext( 54 | ".//uk-phonetic-symbol"), doc.findtext(".//us-phonetic-symbol") 55 | if uk_symbol and us_symbol: 56 | phonetics = 'UK [%s] US [%s]' % (uk_symbol, us_symbol) 57 | elif symbol: 58 | phonetics = '[%s]' % symbol 59 | else: 60 | phonetics = '' 61 | # fetch explanations 62 | explains = '
    '.join([node.text for node in doc.findall( 63 | ".//custom-translation/translation/content")]) 64 | return self.cache_this({'phonetic': phonetics, 'explains': explains}) 65 | except: 66 | return {'phonetic': phonetics, 'explains': explains} 67 | 68 | @export(u'音标', 0) 69 | def fld_phonetic(self): 70 | return self.cache_result('phonetic') if self.cached('phonetic') else self._get_from_api()['phonetic'] 71 | 72 | @export(u'基本释义', 1) 73 | def fld_explains(self): 74 | return self.cache_result('explains') if self.cached('explains') else self._get_from_api()['explains'] 75 | 76 | @with_styles(cssfile='_youdao.css', js=js, need_wrap_css=True, wrap_class='youdao') 77 | def _get_singledict(self, single_dict, lang='eng'): 78 | url = u"http://m.youdao.com/singledict?q={0}&dict={1}&le={2}&more=false".format( 79 | self.word, 'collins' if single_dict == 'collins_eng' else single_dict, lang) 80 | try: 81 | result = urllib2.urlopen(url, timeout=5).read() 82 | html = """ 83 |
    84 |
    {1}
    85 |
    86 |
    87 | 88 |
    89 | """.format('collins' if single_dict == 'collins_eng' else single_dict, result.decode('utf-8')) 90 | 91 | if single_dict != "collins_eng": 92 | return html 93 | 94 | # For collins_eng 95 | def replace_chinese_tag(soup): 96 | tags = [] 97 | assert isinstance(soup, (Tag, NavigableString)) 98 | try: 99 | children = list(soup.children) 100 | except AttributeError: 101 | children = [] 102 | if children.__len__() > 1: 103 | for tag in children: 104 | if not isinstance(tag, (Tag, NavigableString)): 105 | continue 106 | tags.extend(replace_chinese_tag(tag)) 107 | else: 108 | match = re.search("[\u4e00-\u9fa5]", soup.text if isinstance(soup, Tag) else str(soup)) 109 | try: 110 | has_title_attr = 'title' in soup.attrs 111 | except AttributeError: 112 | has_title_attr = False 113 | if not match or has_title_attr: 114 | if has_title_attr: 115 | soup.string = soup['title'] 116 | if re.match("(\s+)?\d{1,2}\.(\s+)?", soup.string if soup.string else ""): 117 | p_tag = Tag(name="p") 118 | p_tag.insert(0, Tag(name="br")) 119 | tags.append(p_tag) 120 | tags.append(soup) 121 | else: 122 | if match: 123 | hanzi_pos = soup.string.find(match.group(0)) 124 | if hanzi_pos >= 5: 125 | soup = soup.string[:hanzi_pos] 126 | tags.append(soup) 127 | return tags 128 | 129 | if len(result.decode('utf-8')) <= 40: # 32 130 | return self._get_singledict('ee')['result'] 131 | bs = BeautifulSoup(html) 132 | ul_tag = bs.find("ul") 133 | ul_html = BeautifulSoup("".join([str(tag) for tag in replace_chinese_tag(ul_tag)])) 134 | bs.ul.replace_with(ul_html) 135 | return bs.prettify() 136 | 137 | except: 138 | return '' 139 | 140 | @export(u'柯林斯英英', 17) 141 | def fld_collins_eng(self): 142 | return self._get_singledict('collins_eng') 143 | 144 | @export(u'英式发音', 2) 145 | def fld_british_audio(self): 146 | audio_url = u'https://dict.youdao.com/dictvoice?audio={}&type=1'.format( 147 | self.word) 148 | if youdao_download_mp3: 149 | filename = u'_youdao_{}_uk.mp3'.format(self.word) 150 | if self.download(audio_url, filename): 151 | return self.get_anki_label(filename, 'audio') 152 | return audio_url 153 | 154 | @export(u'美式发音', 3) 155 | def fld_american_audio(self): 156 | audio_url = u'https://dict.youdao.com/dictvoice?audio={}&type=2'.format( 157 | self.word) 158 | if youdao_download_mp3: 159 | filename = u'_youdao_{}_us.mp3'.format(self.word) 160 | if self.download(audio_url, filename): 161 | return self.get_anki_label(filename, 'audio') 162 | return audio_url 163 | 164 | @export(u'柯林斯英汉', 4) 165 | def fld_collins(self): 166 | return self._get_singledict('collins') 167 | 168 | @export(u'21世纪', 5) 169 | def fld_ec21(self): 170 | return self._get_singledict('ec21') 171 | 172 | @export(u'英英释义', 6) 173 | def fld_ee(self): 174 | return self._get_singledict('ee') 175 | 176 | @export(u'网络释义', 7) 177 | def fld_web_trans(self): 178 | return self._get_singledict('web_trans') 179 | 180 | @export(u'同根词', 8) 181 | def fld_rel_word(self): 182 | return self._get_singledict('rel_word') 183 | 184 | @export(u'同近义词', 9) 185 | def fld_syno(self): 186 | return self._get_singledict('syno') 187 | 188 | @export(u'双语例句', 10) 189 | def fld_blng_sents_part(self): 190 | return self._get_singledict('blng_sents_part') 191 | 192 | @export(u'原生例句', 11) 193 | def fld_media_sents_part(self): 194 | return self._get_singledict('media_sents_part') 195 | 196 | @export(u'权威例句', 12) 197 | def fld_auth_sents_part(self): 198 | return self._get_singledict('auth_sents_part') 199 | 200 | @export(u'新英汉大辞典(中)', 13) 201 | def fld_ce_new(self): 202 | return self._get_singledict('ce_new') 203 | 204 | @export(u'百科', 14) 205 | def fld_baike(self): 206 | return self._get_singledict('baike') 207 | 208 | @export(u'汉语词典(中)', 15) 209 | def fld_hh(self): 210 | return self._get_singledict('hh') 211 | 212 | @export(u'专业释义(中)', 16) 213 | def fld_special(self): 214 | return self._get_singledict('special') 215 | 216 | @export(u'美式发音', 3) 217 | def fld_american_audio(self): 218 | audio_url = u'https://dict.youdao.com/dictvoice?audio={}&type=2'.format( 219 | self.word) 220 | if youdao_download_mp3: 221 | filename = u'_youdao_{}_us.mp3'.format(self.word) 222 | if self.download(audio_url, filename): 223 | return self.get_anki_label(filename, 'audio') 224 | return audio_url 225 | -------------------------------------------------------------------------------- /src/service/youdaofr.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import re 3 | try: 4 | import urllib2 5 | except: 6 | import urllib.request as urllib2 7 | import xml.etree.ElementTree 8 | 9 | from aqt.utils import showInfo 10 | from .base import WebService, export, register, with_styles 11 | 12 | 13 | @register(u'有道词典-法语') 14 | class Youdaofr(WebService): 15 | 16 | def __init__(self): 17 | super(Youdaofr, self).__init__() 18 | 19 | def _get_from_api(self, lang='fr'): 20 | url = "http://dict.youdao.com/fsearch?client=deskdict&keyfrom=chrome.extension&pos=-1&doctype=xml&xmlVersion=3.2&dogVersion=1.0&vendor=unknown&appVer=3.1.17.4208&le=%s&q=%s" % ( 21 | lang, self.word) 22 | explains = '' 23 | try: 24 | result = urllib2.urlopen(url, timeout=5).read() 25 | # showInfo(str(result)) 26 | doc = xml.etree.ElementTree.fromstring(result) 27 | # fetch explanations 28 | explains = '
    '.join([node.text for node in doc.findall( 29 | ".//custom-translation/translation/content")]) 30 | return self.cache_this({'explains': explains}) 31 | except: 32 | return {'explains': explains} 33 | 34 | @export(u'基本释义', 1) 35 | def fld_explains(self): 36 | return self.cache_result('explains') if self.cached('explains') else \ 37 | self._get_from_api().get('explains', '') 38 | 39 | @with_styles(cssfile='_youdao.css', need_wrap_css=True, wrap_class='youdao') 40 | def _get_singledict(self, single_dict, lang='fr'): 41 | url = "http://m.youdao.com/singledict?q=%s&dict=%s&le=%s&more=false" % ( 42 | self.word, single_dict, lang) 43 | try: 44 | result = urllib2.urlopen(url, timeout=5).read() 45 | return '
    %s
    ' % (single_dict, single_dict, single_dict, result) 46 | except: 47 | return '' 48 | 49 | # @export(u'英英释义', 4) 50 | # def fld_ee(self): 51 | # return self._get_singledict('ee') 52 | 53 | @export(u'网络释义', 5) 54 | def fld_web_trans(self): 55 | return self._get_singledict('web_trans') 56 | 57 | @export(u'双语例句', 8) 58 | def fld_blng_sents_part(self): 59 | return self._get_singledict('blng_sents_part') 60 | 61 | @export(u'百科', 11) 62 | def fld_baike(self): 63 | return self._get_singledict('baike') 64 | 65 | @export(u'汉语词典(中)', 13) 66 | def fld_hh(self): 67 | return self._get_singledict('hh') 68 | -------------------------------------------------------------------------------- /src/ui.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import os 21 | import sys 22 | from collections import namedtuple 23 | 24 | import anki 25 | import aqt 26 | import aqt.models 27 | from aqt import mw 28 | from aqt.qt import * 29 | from aqt.studydeck import StudyDeck 30 | from aqt.utils import shortcut, showInfo 31 | from .constants import VERSION, Endpoint, Template 32 | from .context import app_icon, config 33 | from .lang import _, _sl 34 | from .service import service_manager 35 | from .utils import MapDict, get_icon, get_model_byId, get_ord_from_fldname 36 | 37 | DICT_COMBOS, DICT_FILED_COMBOS, ALL_COMBOS = [0, 1, 2] 38 | 39 | widget_size = namedtuple('WidgetSize', ['dialog_width', 'dialog_height_margin', 'map_min_height', 40 | 'map_max_height', 'map_fld_width', 'map_dictname_width', 41 | 'map_dictfield_width'])(450, 120, 0, 30, 100, 130, 130) 42 | 43 | class ParasDialog(QDialog): 44 | 45 | def __init__(self, parent=0): 46 | QDialog.__init__(self, parent) 47 | self.parent = parent 48 | self.setWindowTitle(u"Settings") 49 | self.setFixedWidth(400) 50 | # self.setFixedHeight(300) 51 | 52 | self.build() 53 | 54 | def build(self): 55 | layout = QVBoxLayout() 56 | check_force_update = QCheckBox(_("FORCE_UPDATE")) 57 | layout.addWidget(check_force_update) 58 | check_force_update.setChecked(config.force_update) 59 | check_force_update.clicked.connect(lambda checked: config.update({'force_update':checked})) 60 | layout.setAlignment(Qt.AlignTop|Qt.AlignLeft) 61 | self.setLayout(layout) 62 | 63 | 64 | class FoldersManageDialog(QDialog): 65 | 66 | def __init__(self, parent=0): 67 | QDialog.__init__(self, parent) 68 | self.parent = parent 69 | self.setWindowTitle(u"Set Dicts") 70 | self._dict_paths = [] 71 | self.build() 72 | 73 | def build(self): 74 | layout = QVBoxLayout() 75 | btn_layout = QHBoxLayout() 76 | add_btn = QPushButton("+") 77 | remove_btn = QPushButton("-") 78 | btn_layout.addWidget(add_btn) 79 | btn_layout.addWidget(remove_btn) 80 | add_btn.clicked.connect(self.add_folder) 81 | remove_btn.clicked.connect(self.remove_folder) 82 | self.folders_lst = QListWidget() 83 | self.folders_lst.addItems(config.dirs) 84 | self.chk_use_filename = QCheckBox(_('CHECK_FILENAME_LABEL')) 85 | self.chk_export_media = QCheckBox(_('EXPORT_MEDIA')) 86 | self.chk_use_filename.setChecked(config.use_filename) 87 | self.chk_export_media.setChecked(config.export_media) 88 | chk_layout = QHBoxLayout() 89 | chk_layout.addWidget(self.chk_use_filename) 90 | chk_layout.addWidget(self.chk_export_media) 91 | btnbox = QDialogButtonBox(QDialogButtonBox.Ok, Qt.Horizontal, self) 92 | btnbox.accepted.connect(self.accept) 93 | layout.addLayout(btn_layout) 94 | layout.addWidget(self.folders_lst) 95 | layout.addLayout(chk_layout) 96 | layout.addWidget(btnbox) 97 | self.setLayout(layout) 98 | 99 | def add_folder(self): 100 | dir_ = QFileDialog.getExistingDirectory(self, 101 | caption=u"Select Folder", directory="", options=QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks) 102 | if dir_: 103 | self.folders_lst.addItem(dir_) 104 | 105 | def remove_folder(self): 106 | item = self.folders_lst.takeItem(self.folders_lst.currentRow()) 107 | del item 108 | 109 | def find_mdxes(self): 110 | for each in self.dirs: 111 | for dirpath, dirnames, filenames in os.walk(each): 112 | self._dict_paths.extend([os.path.join(dirpath, filename) 113 | for filename in filenames if filename.lower().endswith(u'.mdx')]) 114 | return list(set(self._dict_paths)) 115 | 116 | @property 117 | def dict_paths(self): 118 | return self.find_mdxes() 119 | 120 | @property 121 | def dirs(self): 122 | return [self.folders_lst.item(i).text() 123 | for i in range(self.folders_lst.count())] 124 | 125 | def save(self): 126 | data = {'dirs': self.dirs, 127 | 'use_filename': self.chk_use_filename.isChecked(), 128 | 'export_media': self.chk_export_media.isChecked()} 129 | config.update(data) 130 | 131 | 132 | class OptionsDialog(QDialog): 133 | 134 | def __init__(self, parent=0): 135 | super(OptionsDialog, self).__init__() 136 | self.setWindowFlags(Qt.CustomizeWindowHint | 137 | Qt.WindowTitleHint | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint) 138 | self.parent = parent 139 | # from PyQt4 import QtCore, QtGui 140 | self.setWindowIcon(app_icon) 141 | self.setWindowTitle(u"Options") 142 | self.build() 143 | 144 | def build(self): 145 | self.main_layout = QVBoxLayout() 146 | models_layout = QHBoxLayout() 147 | # add buttons 148 | mdx_button = QPushButton(_('DICTS_FOLDERS')) 149 | mdx_button.clicked.connect(self.show_fm_dialog) 150 | self.models_button = QPushButton(_('CHOOSE_NOTE_TYPES')) 151 | self.models_button.clicked.connect(self.btn_models_pressed) 152 | models_layout.addWidget(mdx_button) 153 | models_layout.addWidget(self.models_button) 154 | self.main_layout.addLayout(models_layout) 155 | # add dicts mapping 156 | dicts_widget = QWidget() 157 | self.dicts_layout = QGridLayout() 158 | self.dicts_layout.setSizeConstraint(QLayout.SetMinAndMaxSize) 159 | dicts_widget.setLayout(self.dicts_layout) 160 | 161 | scroll_area = QScrollArea() 162 | scroll_area.setWidgetResizable(True) 163 | scroll_area.setWidget(dicts_widget) 164 | 165 | self.main_layout.addWidget(scroll_area) 166 | # add description of radio buttons AND ok button 167 | bottom_layout = QHBoxLayout() 168 | paras_btn = QPushButton(_('SETTINGS')) 169 | paras_btn.clicked.connect(self.show_paras) 170 | about_btn = QPushButton(_('ABOUT')) 171 | about_btn.clicked.connect(self.show_about) 172 | # about_btn.clicked.connect(self.show_paras) 173 | chk_update_btn = QPushButton(_('UPDATE')) 174 | chk_update_btn.clicked.connect(self.check_updates) 175 | home_label = QLabel( 176 | 'User Guide'.format(url=Endpoint.user_guide)) 177 | home_label.setOpenExternalLinks(True) 178 | # shop_label = QLabel( 179 | # 'Service Shop'.format(url=Endpoint.service_shop)) 180 | # shop_label.setOpenExternalLinks(True) 181 | btnbox = QDialogButtonBox(QDialogButtonBox.Ok, Qt.Horizontal, self) 182 | btnbox.accepted.connect(self.accept) 183 | bottom_layout.addWidget(paras_btn) 184 | bottom_layout.addWidget(chk_update_btn) 185 | bottom_layout.addWidget(about_btn) 186 | bottom_layout.addWidget(home_label) 187 | # bottom_layout.addWidget(shop_label) 188 | bottom_layout.addWidget(btnbox) 189 | self.main_layout.addLayout(bottom_layout) 190 | self.setLayout(self.main_layout) 191 | # init from saved data 192 | self.current_model = None 193 | if config.last_model_id: 194 | self.current_model = get_model_byId( 195 | mw.col.models, config.last_model_id) 196 | if self.current_model: 197 | self.models_button.setText( 198 | u'%s [%s]' % (_('CHOOSE_NOTE_TYPES'), self.current_model['name'])) 199 | # build fields -- dicts layout 200 | self.build_mappings_layout(self.current_model) 201 | 202 | def show_paras(self): 203 | dialog = ParasDialog(self) 204 | dialog.exec_() 205 | 206 | def show_about(self): 207 | QMessageBox.about(self, _('ABOUT'), Template.tmpl_about) 208 | 209 | def show_fm_dialog(self): 210 | fm_dialog = FoldersManageDialog(self) 211 | fm_dialog.activateWindow() 212 | fm_dialog.raise_() 213 | if fm_dialog.exec_() == QDialog.Accepted: 214 | dict_paths = fm_dialog.dict_paths 215 | fm_dialog.save() 216 | # update local services 217 | service_manager.update_services() 218 | # update_dicts_combo 219 | dict_cbs = self._get_combos(DICT_COMBOS) 220 | for i, cb in enumerate(dict_cbs): 221 | current_text = cb.currentText() 222 | self.fill_dict_combo_options(cb, current_text) 223 | 224 | def accept(self): 225 | self.save() 226 | self.close() 227 | 228 | def btn_models_pressed(self): 229 | self.save() 230 | self.current_model = self.show_models() 231 | if self.current_model: 232 | self.build_mappings_layout(self.current_model) 233 | 234 | def build_mappings_layout(self, model): 235 | 236 | def clear_layout(layout): 237 | if layout is not None: 238 | while layout.count(): 239 | item = layout.takeAt(0) 240 | widget = item.widget() 241 | if widget is not None: 242 | widget.deleteLater() 243 | else: 244 | clear_layout(item.layout()) 245 | 246 | clear_layout(self.dicts_layout) 247 | 248 | label1 = QLabel("") 249 | label1.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 250 | label2 = QLabel(_("DICTS")) 251 | label2.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 252 | label3 = QLabel(_("DICT_FIELDS")) 253 | label3.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) 254 | self.dicts_layout.addWidget(label1, 0, 0) 255 | self.dicts_layout.addWidget(label2, 0, 1) 256 | self.dicts_layout.addWidget(label3, 0, 2) 257 | 258 | maps = config.get_maps(model['id']) 259 | self.radio_group = QButtonGroup() 260 | for i, fld in enumerate(model['flds']): 261 | ord = fld['ord'] 262 | name = fld['name'] 263 | if maps: 264 | for j, each in enumerate(maps): 265 | if each.get('fld_ord', -1) == ord: 266 | self.add_dict_layout(j, fld_name=name, **each) 267 | break 268 | else: 269 | self.add_dict_layout(i, fld_name=name) 270 | else: 271 | self.add_dict_layout(i, fld_name=name) 272 | 273 | self.setLayout(self.main_layout) 274 | self.resize(widget_size.dialog_width, 275 | (i + 1) * widget_size.map_max_height + widget_size.dialog_height_margin) 276 | 277 | def show_models(self): 278 | edit = QPushButton(anki.lang._("Manage"), 279 | clicked=lambda: aqt.models.Models(mw, self)) 280 | ret = StudyDeck(mw, names=lambda: sorted(mw.col.models.allNames()), 281 | accept=anki.lang._("Choose"), title=anki.lang._("Choose Note Type"), 282 | help="_notes", parent=self, buttons=[edit], 283 | cancel=True, geomKey="selectModel") 284 | if ret.name: 285 | model = mw.col.models.byName(ret.name) 286 | self.models_button.setText( 287 | u'%s [%s]' % (_('CHOOSE_NOTE_TYPES'), ret.name)) 288 | return model 289 | 290 | def dict_combobox_index_changed(self, index): 291 | # showInfo("combo index changed") 292 | dict_combos, field_combos = self._get_combos(ALL_COMBOS) 293 | assert len(dict_combos) == len(field_combos) 294 | for i, dict_combo in enumerate(dict_combos): 295 | # in windows and linux: the combo has current focus, 296 | # in mac: the combo's listview has current focus, and the listview can 297 | # be got by view() 298 | # showInfo('to check focus') 299 | if dict_combo.hasFocus() or dict_combo.view().hasFocus(): 300 | self.fill_field_combo_options( 301 | field_combos[i], dict_combo.currentText(), dict_combo.itemData(index)) 302 | break 303 | 304 | def fill_dict_combo_options(self, dict_combo, current_text): 305 | dict_combo.clear() 306 | dict_combo.addItem(_('NOT_DICT_FIELD')) 307 | dict_combo.insertSeparator(dict_combo.count()) 308 | for service in service_manager.local_services: 309 | # combo_data.insert("data", each.label) 310 | dict_combo.addItem( 311 | service.title, userData=service.unique) 312 | dict_combo.insertSeparator(dict_combo.count()) 313 | for service in service_manager.web_services: 314 | dict_combo.addItem( 315 | service.title, userData=service.unique) 316 | 317 | def set_dict_combo_index(): 318 | dict_combo.setCurrentIndex(-1) 319 | for i in range(dict_combo.count()): 320 | if current_text in _sl('NOT_DICT_FIELD'): 321 | dict_combo.setCurrentIndex(0) 322 | if dict_combo.itemText(i) == current_text: 323 | dict_combo.setCurrentIndex(i) 324 | 325 | set_dict_combo_index() 326 | 327 | def fill_field_combo_options(self, field_combo, dict_combo_text, dict_combo_itemdata): 328 | field_combo.clear() 329 | field_combo.setEnabled(True) 330 | if dict_combo_text in _sl('NOT_DICT_FIELD'): 331 | field_combo.setEnabled(False) 332 | elif dict_combo_text in _sl('MDX_SERVER'): 333 | field_combo.setEditText('http://') 334 | field_combo.setFocus(Qt.MouseFocusReason) # MouseFocusReason 335 | else: 336 | field_text = field_combo.currentText() 337 | service_unique = dict_combo_itemdata 338 | current_service = service_manager.get_service(service_unique) 339 | # problem 340 | if current_service and current_service.fields: 341 | for each in current_service.fields: 342 | field_combo.addItem(each) 343 | if each == field_text: 344 | field_combo.setEditText(field_text) 345 | 346 | def radio_btn_checked(self): 347 | rbs = self.findChildren(QRadioButton) 348 | dict_cbs, fld_cbs = self._get_combos(ALL_COMBOS) 349 | for i, rb in enumerate(rbs): 350 | dict_cbs[i].setEnabled(not rb.isChecked()) 351 | fld_cbs[i].setEnabled( 352 | (dict_cbs[i].currentText() != _('NOT_DICT_FIELD')) and (not rb.isChecked())) 353 | 354 | def add_dict_layout(self, i, **kwargs): 355 | """ 356 | kwargs: 357 | word_checked dict fld_name dict_field 358 | """ 359 | word_checked, dict_name, dict_unique, fld_name, dict_field = ( 360 | kwargs.get('word_checked', False), 361 | kwargs.get('dict', _('NOT_DICT_FIELD')), 362 | kwargs.get('dict_unique', ''), 363 | kwargs.get('fld_name', ''), 364 | kwargs.get('dict_field', ''),) 365 | 366 | word_check_btn = QRadioButton(fld_name) 367 | word_check_btn.setMinimumSize(widget_size.map_fld_width, 0) 368 | word_check_btn.setMaximumSize(widget_size.map_fld_width, 369 | widget_size.map_max_height) 370 | word_check_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 371 | word_check_btn.setCheckable(True) 372 | word_check_btn.clicked.connect(self.radio_btn_checked) 373 | if i == 0: 374 | word_checked = True 375 | word_check_btn.setChecked(word_checked) 376 | self.radio_group.addButton(word_check_btn) 377 | 378 | dict_combo = QComboBox() 379 | dict_combo.setMinimumSize(widget_size.map_dictname_width, 0) 380 | dict_combo.setFocusPolicy( 381 | Qt.TabFocus | Qt.ClickFocus | Qt.StrongFocus | Qt.WheelFocus) 382 | dict_combo.setEnabled(not word_checked) 383 | dict_combo.currentIndexChanged.connect( 384 | self.dict_combobox_index_changed) 385 | self.fill_dict_combo_options(dict_combo, dict_name) 386 | 387 | field_combo = QComboBox() 388 | field_combo.setMinimumSize(widget_size.map_dictfield_width, 0) 389 | # field_combo.setMaximumSize(130, 30) 390 | field_combo.setEnabled((not word_checked) and ( 391 | dict_name != _('NOT_DICT_FIELD'))) 392 | field_combo.setEditable(True) 393 | field_combo.setEditText(dict_field) 394 | self.fill_field_combo_options(field_combo, dict_name, dict_unique) 395 | 396 | self.dicts_layout.addWidget(word_check_btn, i + 1, 0) 397 | self.dicts_layout.addWidget(dict_combo, i + 1, 1) 398 | self.dicts_layout.addWidget(field_combo, i + 1, 2) 399 | 400 | def _get_combos(self, flag): 401 | # 0 : dict_combox, 1:field_combox 402 | dict_combos = self.findChildren(QComboBox) 403 | if flag in [DICT_COMBOS, DICT_FILED_COMBOS]: 404 | return dict_combos[flag::2] 405 | if flag == ALL_COMBOS: 406 | return dict_combos[::2], dict_combos[1::2] 407 | 408 | def save(self): 409 | if not self.current_model: 410 | return 411 | data = dict() 412 | labels = self.findChildren(QRadioButton) 413 | dict_cbs, field_cbs = self._get_combos(ALL_COMBOS) 414 | maps = [{"word_checked": label.isChecked(), 415 | "dict": dict_cb.currentText().strip(), 416 | "dict_unique": dict_cb.itemData(dict_cb.currentIndex()) if dict_cb.itemData(dict_cb.currentIndex()) else "", 417 | "dict_field": field_cb.currentText().strip(), 418 | "fld_ord": get_ord_from_fldname(self.current_model, label.text() 419 | )} 420 | for (dict_cb, field_cb, label) in zip(dict_cbs, field_cbs, labels)] 421 | current_model_id = str(self.current_model['id']) 422 | data[current_model_id] = maps 423 | data['last_model'] = self.current_model['id'] 424 | config.update(data) 425 | 426 | def check_updates(self): 427 | 428 | self.updater = Updater() 429 | self.updater.chk_finish_signal.connect(self._show_update_result) 430 | self.updater.start() 431 | 432 | @pyqtSlot(dict) 433 | def _show_update_result(self, data): 434 | if data['result'] == 'ok': 435 | version = data['version'] 436 | if version.decode() > VERSION: 437 | showInfo(Template.new_version.format(version=version)) 438 | elif version.decode() == VERSION: 439 | showInfo(Template.latest_version) 440 | else: 441 | showInfo(Template.abnormal_version) 442 | else: 443 | showInfo(Template.check_failure.format(msg=data['msg'])) 444 | 445 | 446 | class Updater(QThread): 447 | chk_finish_signal = pyqtSignal(dict) 448 | 449 | def __init__(self): 450 | super(QThread, self).__init__() 451 | 452 | def run(self): 453 | try: 454 | import urllib2 455 | except: 456 | import urllib.request as urllib2 457 | try: 458 | req = urllib2.Request(Endpoint.check_version) 459 | req.add_header('Pragma', 'no-cache') 460 | resp = urllib2.urlopen(req, timeout=10) 461 | version = resp.read().strip() 462 | data = {'result': 'ok', 'version': version} 463 | except: 464 | info = _('CHECK_FAILURE') 465 | data = {'result': 'error', 'msg': info} 466 | 467 | self.chk_finish_signal.emit(data) 468 | 469 | 470 | def show_options(): 471 | config.read() 472 | opt_dialog = OptionsDialog(mw) 473 | opt_dialog.exec_() 474 | opt_dialog.activateWindow() 475 | opt_dialog.raise_() 476 | # service_manager.fetch_headers() 477 | -------------------------------------------------------------------------------- /src/utils/Queue.py: -------------------------------------------------------------------------------- 1 | """A multi-producer, multi-consumer queue.""" 2 | 3 | from time import time as _time 4 | try: 5 | import threading as _threading 6 | except ImportError: 7 | import dummy_threading as _threading 8 | from collections import deque 9 | import heapq 10 | 11 | __all__ = ['Empty', 'Full', 'Queue', 'PriorityQueue', 'LifoQueue'] 12 | 13 | 14 | class Empty(Exception): 15 | "Exception raised by Queue.get(block=0)/get_nowait()." 16 | pass 17 | 18 | 19 | class Full(Exception): 20 | "Exception raised by Queue.put(block=0)/put_nowait()." 21 | pass 22 | 23 | 24 | class Queue: 25 | """Create a queue object with a given maximum size. 26 | 27 | If maxsize is <= 0, the queue size is infinite. 28 | """ 29 | 30 | def __init__(self, maxsize=0): 31 | self.maxsize = maxsize 32 | self._init(maxsize) 33 | # mutex must be held whenever the queue is mutating. All methods 34 | # that acquire mutex must release it before returning. mutex 35 | # is shared between the three conditions, so acquiring and 36 | # releasing the conditions also acquires and releases mutex. 37 | self.mutex = _threading.Lock() 38 | # Notify not_empty whenever an item is added to the queue; a 39 | # thread waiting to get is notified then. 40 | self.not_empty = _threading.Condition(self.mutex) 41 | # Notify not_full whenever an item is removed from the queue; 42 | # a thread waiting to put is notified then. 43 | self.not_full = _threading.Condition(self.mutex) 44 | # Notify all_tasks_done whenever the number of unfinished tasks 45 | # drops to zero; thread waiting to join() is notified to resume 46 | self.all_tasks_done = _threading.Condition(self.mutex) 47 | self.unfinished_tasks = 0 48 | 49 | def task_done(self): 50 | """Indicate that a formerly enqueued task is complete. 51 | 52 | Used by Queue consumer threads. For each get() used to fetch a task, 53 | a subsequent call to task_done() tells the queue that the processing 54 | on the task is complete. 55 | 56 | If a join() is currently blocking, it will resume when all items 57 | have been processed (meaning that a task_done() call was received 58 | for every item that had been put() into the queue). 59 | 60 | Raises a ValueError if called more times than there were items 61 | placed in the queue. 62 | """ 63 | self.all_tasks_done.acquire() 64 | try: 65 | unfinished = self.unfinished_tasks - 1 66 | if unfinished <= 0: 67 | if unfinished < 0: 68 | raise ValueError('task_done() called too many times') 69 | self.all_tasks_done.notify_all() 70 | self.unfinished_tasks = unfinished 71 | finally: 72 | self.all_tasks_done.release() 73 | 74 | def join(self): 75 | """Blocks until all items in the Queue have been gotten and processed. 76 | 77 | The count of unfinished tasks goes up whenever an item is added to the 78 | queue. The count goes down whenever a consumer thread calls task_done() 79 | to indicate the item was retrieved and all work on it is complete. 80 | 81 | When the count of unfinished tasks drops to zero, join() unblocks. 82 | """ 83 | self.all_tasks_done.acquire() 84 | try: 85 | while self.unfinished_tasks: 86 | self.all_tasks_done.wait() 87 | finally: 88 | self.all_tasks_done.release() 89 | 90 | def qsize(self): 91 | """Return the approximate size of the queue (not reliable!).""" 92 | self.mutex.acquire() 93 | n = self._qsize() 94 | self.mutex.release() 95 | return n 96 | 97 | def empty(self): 98 | """Return True if the queue is empty, False otherwise (not reliable!).""" 99 | self.mutex.acquire() 100 | n = not self._qsize() 101 | self.mutex.release() 102 | return n 103 | 104 | def full(self): 105 | """Return True if the queue is full, False otherwise (not reliable!).""" 106 | self.mutex.acquire() 107 | n = 0 < self.maxsize == self._qsize() 108 | self.mutex.release() 109 | return n 110 | 111 | def put(self, item, block=True, timeout=None): 112 | """Put an item into the queue. 113 | 114 | If optional args 'block' is true and 'timeout' is None (the default), 115 | block if necessary until a free slot is available. If 'timeout' is 116 | a non-negative number, it blocks at most 'timeout' seconds and raises 117 | the Full exception if no free slot was available within that time. 118 | Otherwise ('block' is false), put an item on the queue if a free slot 119 | is immediately available, else raise the Full exception ('timeout' 120 | is ignored in that case). 121 | """ 122 | self.not_full.acquire() 123 | try: 124 | if self.maxsize > 0: 125 | if not block: 126 | if self._qsize() == self.maxsize: 127 | raise Full 128 | elif timeout is None: 129 | while self._qsize() == self.maxsize: 130 | self.not_full.wait() 131 | elif timeout < 0: 132 | raise ValueError("'timeout' must be a non-negative number") 133 | else: 134 | endtime = _time() + timeout 135 | while self._qsize() == self.maxsize: 136 | remaining = endtime - _time() 137 | if remaining <= 0.0: 138 | raise Full 139 | self.not_full.wait(remaining) 140 | self._put(item) 141 | self.unfinished_tasks += 1 142 | self.not_empty.notify() 143 | finally: 144 | self.not_full.release() 145 | 146 | def put_nowait(self, item): 147 | """Put an item into the queue without blocking. 148 | 149 | Only enqueue the item if a free slot is immediately available. 150 | Otherwise raise the Full exception. 151 | """ 152 | return self.put(item, False) 153 | 154 | def get(self, block=True, timeout=None): 155 | """Remove and return an item from the queue. 156 | 157 | If optional args 'block' is true and 'timeout' is None (the default), 158 | block if necessary until an item is available. If 'timeout' is 159 | a non-negative number, it blocks at most 'timeout' seconds and raises 160 | the Empty exception if no item was available within that time. 161 | Otherwise ('block' is false), return an item if one is immediately 162 | available, else raise the Empty exception ('timeout' is ignored 163 | in that case). 164 | """ 165 | self.not_empty.acquire() 166 | try: 167 | if not block: 168 | if not self._qsize(): 169 | raise Empty 170 | elif timeout is None: 171 | while not self._qsize(): 172 | self.not_empty.wait() 173 | elif timeout < 0: 174 | raise ValueError("'timeout' must be a non-negative number") 175 | else: 176 | endtime = _time() + timeout 177 | while not self._qsize(): 178 | remaining = endtime - _time() 179 | if remaining <= 0.0: 180 | raise Empty 181 | self.not_empty.wait(remaining) 182 | item = self._get() 183 | self.not_full.notify() 184 | return item 185 | finally: 186 | self.not_empty.release() 187 | 188 | def get_nowait(self): 189 | """Remove and return an item from the queue without blocking. 190 | 191 | Only get an item if one is immediately available. Otherwise 192 | raise the Empty exception. 193 | """ 194 | return self.get(False) 195 | 196 | # Override these methods to implement other queue organizations 197 | # (e.g. stack or priority queue). 198 | # These will only be called with appropriate locks held 199 | 200 | # Initialize the queue representation 201 | def _init(self, maxsize): 202 | self.queue = deque() 203 | 204 | def _qsize(self, len=len): 205 | return len(self.queue) 206 | 207 | # Put a new item in the queue 208 | def _put(self, item): 209 | self.queue.append(item) 210 | 211 | # Get an item from the queue 212 | def _get(self): 213 | return self.queue.popleft() 214 | 215 | 216 | class PriorityQueue(Queue): 217 | '''Variant of Queue that retrieves open entries in priority order (lowest first). 218 | 219 | Entries are typically tuples of the form: (priority number, data). 220 | ''' 221 | 222 | def _init(self, maxsize): 223 | self.queue = [] 224 | 225 | def _qsize(self, len=len): 226 | return len(self.queue) 227 | 228 | def _put(self, item, heappush=heapq.heappush): 229 | heappush(self.queue, item) 230 | 231 | def _get(self, heappop=heapq.heappop): 232 | return heappop(self.queue) 233 | 234 | 235 | class LifoQueue(Queue): 236 | '''Variant of Queue that retrieves most recently added entries first.''' 237 | 238 | def _init(self, maxsize): 239 | self.queue = [] 240 | 241 | def _qsize(self, len=len): 242 | return len(self.queue) 243 | 244 | def _put(self, item): 245 | self.queue.append(item) 246 | 247 | def _get(self): 248 | return self.queue.pop() 249 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .Queue import Queue, Empty, Full 2 | from . import importlib 3 | from .misc import * 4 | from .helper import * 5 | -------------------------------------------------------------------------------- /src/utils/helper.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import re 3 | import os 4 | 5 | __all__ = ['add_metaclass', 'wrap_css'] 6 | 7 | 8 | def add_metaclass(metaclass): 9 | """Class decorator for creating a class with a metaclass.""" 10 | def wrapper(cls): 11 | orig_vars = cls.__dict__.copy() 12 | slots = orig_vars.get('__slots__') 13 | if slots is not None: 14 | if isinstance(slots, str): 15 | slots = [slots] 16 | for slots_var in slots: 17 | orig_vars.pop(slots_var) 18 | orig_vars.pop('__dict__', None) 19 | orig_vars.pop('__weakref__', None) 20 | return metaclass(cls.__name__, cls.__bases__, orig_vars) 21 | return wrapper 22 | 23 | 24 | def wrap_css(orig_css, is_file=True, class_wrapper=None, new_cssfile_suffix=u'wrap'): 25 | 26 | def process(content): 27 | # clean the comments 28 | regx = re.compile(r'/\*.*?\*/', re.DOTALL) 29 | content = regx.sub(r'', content).strip() 30 | # add wrappers to all the selectors except the first one 31 | regx = re.compile(r'([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)', re.DOTALL) 32 | new_css = regx.sub(u'.{} \\1\\2'.format(class_wrapper), content) 33 | return new_css 34 | 35 | if is_file: 36 | if not class_wrapper: 37 | class_wrapper = os.path.splitext(os.path.basename(orig_css))[0] 38 | new_cssfile = u'{css_name}_{suffix}.css'.format( 39 | css_name=orig_css[:orig_css.rindex('.css')], 40 | suffix=new_cssfile_suffix) 41 | # if new css file exists, not process 42 | # if input original css file doesn't exist, return the new css filename and class wrapper 43 | # to make the subsequent process easy. 44 | if os.path.exists(new_cssfile) or not os.path.exists(orig_css): 45 | return new_cssfile, class_wrapper 46 | result = '' 47 | with open(orig_css, 'rb') as f: 48 | try: 49 | result = process(f.read().strip().decode('utf-8', 'ignore')) 50 | except: 51 | showInfo('error: ' + orig_css) 52 | 53 | if result: 54 | with open(new_cssfile, 'wb') as f: 55 | f.write(result.encode('utf-8')) 56 | return new_cssfile, class_wrapper 57 | else: 58 | # class_wrapper must be valid. 59 | assert class_wrapper 60 | return process(orig_css), class_wrapper 61 | -------------------------------------------------------------------------------- /src/utils/importlib.py: -------------------------------------------------------------------------------- 1 | """Backport of importlib.import_module from 3.x.""" 2 | # While not critical (and in no way guaranteed!), it would be nice to keep this 3 | # code compatible with Python 2.3. 4 | import sys 5 | 6 | def _resolve_name(name, package, level): 7 | """Return the absolute name of the module to be imported.""" 8 | if not hasattr(package, 'rindex'): 9 | raise ValueError("'package' not set to a string") 10 | dot = len(package) 11 | for x in range(level, 1, -1): 12 | try: 13 | dot = package.rindex('.', 0, dot) 14 | except ValueError: 15 | raise ValueError("attempted relative import beyond top-level " 16 | "package") 17 | return "%s.%s" % (package[:dot], name) 18 | 19 | 20 | def import_module(name, package=None): 21 | """Import a module. 22 | 23 | The 'package' argument is required when performing a relative import. It 24 | specifies the package to use as the anchor point from which to resolve the 25 | relative import to an absolute import. 26 | 27 | """ 28 | if name.startswith('.'): 29 | if not package: 30 | raise TypeError("relative imports require the 'package' argument") 31 | level = 0 32 | for character in name: 33 | if character != '.': 34 | break 35 | level += 1 36 | name = _resolve_name(name[level:], package, level) 37 | 38 | __import__(name) 39 | return sys.modules[name] -------------------------------------------------------------------------------- /src/utils/misc.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | # 3 | # Copyright © 2016–2017 Liang Feng 4 | # 5 | # Support: Report an issue at https://github.com/finalion/WordQuery/issues 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # any later version; http://www.gnu.org/copyleft/gpl.html. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | import os 21 | from functools import wraps 22 | from aqt.utils import showInfo 23 | from aqt.qt import QIcon 24 | 25 | __all__ = ['ignore_exception', 26 | 'get_model_byId', 27 | 'get_icon', 28 | 'get_ord_from_fldname', 29 | 'MapDict'] 30 | 31 | 32 | def ignore_exception(func): 33 | @wraps(func) 34 | def wrap(*args, **kwargs): 35 | try: 36 | return func(*args, **kwargs) 37 | except: 38 | return '' 39 | return wrap 40 | 41 | 42 | def get_model_byId(models, id): 43 | for m in list(models.all()): 44 | # showInfo(str(m['id']) + ', ' + m['name']) 45 | if m['id'] == id: 46 | return m 47 | 48 | 49 | def get_ord_from_fldname(model, name): 50 | flds = model['flds'] 51 | for fld in flds: 52 | if fld['name'] == name: 53 | return fld['ord'] 54 | 55 | 56 | def get_icon(filename): 57 | curdir = os.path.dirname(os.path.abspath(__file__)) 58 | pardir = os.path.join(curdir, os.pardir) 59 | path = os.path.join(pardir, 'resources', filename) 60 | return QIcon(path) 61 | 62 | 63 | class MapDict(dict): 64 | """ 65 | Example: 66 | m = Map({'first_name': 'Eduardo'}, 67 | last_name='Pool', age=24, sports=['Soccer']) 68 | """ 69 | 70 | def __init__(self, *args, **kwargs): 71 | super(MapDict, self).__init__(*args, **kwargs) 72 | for arg in args: 73 | if isinstance(arg, dict): 74 | for k, v in arg.items(): 75 | self[k] = v 76 | 77 | if kwargs: 78 | for k, v in kwargs.items(): 79 | self[k] = v 80 | 81 | def __getattr__(self, attr): 82 | return self.get(attr) 83 | 84 | def __setattr__(self, key, value): 85 | self.__setitem__(key, value) 86 | 87 | def __setitem__(self, key, value): 88 | super(MapDict, self).__setitem__(key, value) 89 | self.__dict__.update({key: value}) 90 | 91 | def __delattr__(self, item): 92 | self.__delitem__(item) 93 | 94 | def __delitem__(self, key): 95 | super(MapDict, self).__delitem__(key) 96 | del self.__dict__[key] 97 | --------------------------------------------------------------------------------