├── .circleci └── config.yml ├── .gitignore ├── FAAFF273-243F-47BD-9C42-D2C2DFEF010A.png ├── Makefile ├── README.md ├── VERSION ├── alfred ├── __init__.py ├── cache.py ├── config.py ├── core.py └── feedback.py ├── icon.png ├── info.plist.template ├── it_shanbay.py ├── requirements-dev.txt ├── shanbay.py ├── snapshot ├── sb_auth.png ├── sb_love.png └── sb_sound.png └── test_shanbay.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | python: circleci/python@0.2.1 5 | 6 | jobs: 7 | build-and-test: 8 | docker: 9 | - image: circleci/python:2.7.17 10 | steps: 11 | - checkout 12 | - run: 13 | command: python -m unittest it_shanbay 14 | name: Test 15 | 16 | workflows: 17 | main: 18 | jobs: 19 | - build-and-test 20 | 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.pyc 3 | *.log 4 | *~ 5 | .DS_Store 6 | .idea/ 7 | ._.DS_Store 8 | token 9 | tags 10 | dist 11 | info.plist 12 | -------------------------------------------------------------------------------- /FAAFF273-243F-47BD-9C42-D2C2DFEF010A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alswl/shanbay-alfred2/a2bc3e5abea0b0062dbdbce0f118e1bb3752b6e6/FAAFF273-243F-47BD-9C42-D2C2DFEF010A.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version = $(shell cat VERSION) 2 | git_hash = $(shell git rev-parse --short HEAD) 3 | 4 | .PHONY: build package clean 5 | 6 | all: clean build package 7 | 8 | build: 9 | cp info.plist.template info.plist 10 | gsed -i -e "s/\$${VERSION}/${version}/" info.plist 11 | 12 | 13 | package: 14 | mkdir dist 15 | zip -r dist/shanbay-alfred2-${version}-${git_hash}.alfredworkflow . -x \*.git\* -x .idea\* -x token -x tags -x dist\* -x \*.swp -x info.plist.template -x \*.DS_Store\* -x \*.pyc\* -x \*snapshot\* 16 | 17 | clean: 18 | rm -rf dist info.plist 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shanbay-Alfred2-Workflow 2 | 3 | [![CircleCI](https://circleci.com/gh/alswl/shanbay-alfred2.svg?style=svg)](https://circleci.com/gh/alswl/shanbay-alfred2) 4 | 5 | 扇贝网 Alfred2 workflow,主要用于单词查询、添加单词到词库、发音。 6 | 7 | 8 | ## 安装 9 | 10 | 下载 [`Shanbay.alfredworkflow`](https://github.com/alswl/shanbay-alfred2/releases) 里面的最新版本。 11 | 12 | 双击文件导入即可。 13 | 14 | 15 | ## 使用 16 | 17 | - 授权 18 | - 任意使用查询功能,如 `sb love` 会检查权限情况,如果权限不存在或过期,会自动进入授权页面 19 | - 在扇贝官网的授权界面输入帐号密码,然后授权 20 | - 出现 `Shanbay OAuth2` 之后,将完整的 URL 复制下来,并在 Alfred 里面输入 21 | `sbauth https://www.shanbay.com/oauth2/auth/success/#access_token=SAMPLE&token_type=Bearer&state=&expires_in=2592000&scope=read+write` 22 | (后面的 URL 请替换为你自己复制的 URL) 23 | - 注意:在 2019-10 月之后,这个页面打开会是 404 页面,但是不影响使用 24 | - 授权完成,使用 `sb love` 测试,按回车即可以添加到单词库 25 | - 查单词 26 | - 使用 `sb love` 查询单词 27 | - 添加到单词库 28 | - 使用 `sb love` 查询单词,然后使用回车即添加到单词库 29 | - 打开单词 30 | - 使用 `sb love` 查询单词,按住 `Command` + 回车,打开扇贝官网对应的单词页面 31 | - 听发音 32 | - 使用 `sb love` 查询单词,按住 `Ctrl` + 回车,即可播放语音 33 | 34 | 35 | 注:授权有效期为一个月,过期需重新授权。 36 | 37 | 38 | ## 截图 39 | 40 | - ![love](https://github.com/alswl/shanbay-alfred2/raw/master/snapshot/sb_love.png) 41 | - ![auth](https://github.com/alswl/shanbay-alfred2/raw/master/snapshot/sb_auth.png) 42 | - ![sound](https://github.com/alswl/shanbay-alfred2/raw/master/snapshot/sb_sound.png) 43 | 44 | 45 | ## 其它 46 | 47 | 感谢原作者 [henter/Shanbay-Alfred2](https://github.com/henter/Shanbay-Alfred2) 开发初始版本。 48 | 49 | 我的改进: 50 | 51 | - 不使用 code 授权模式,改为 token 直接授权,不用打开一个第三方网站(也是因为原来那个授权网站挂掉,我才改造的) 52 | - 支持发音 53 | - 移除例句查询功能 54 | - 移除 `requests` 依赖,即装即用 55 | - 支持新版本检查 56 | 57 | 58 | ## 打包 59 | 60 | ``` 61 | make 62 | ``` 63 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.6.1 2 | -------------------------------------------------------------------------------- /alfred/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ## 4 | # Alfred 2 Workflow python module 5 | # by JinnLynn 2013.04.02 http://jeeker.net 6 | # License under the MIT license 7 | ## 8 | 9 | from .core import * 10 | 11 | from .cache import Cache 12 | 13 | from .config import Config 14 | 15 | from .feedback import Feedback, Item -------------------------------------------------------------------------------- /alfred/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, json, time, shutil 3 | 4 | import core 5 | ## 6 | # { 7 | # 'expire_time' : 0, 8 | # 'data' : {} 9 | # } 10 | 11 | CACHE_FOLDER = os.path.expanduser('~/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/') 12 | 13 | CACHE_DEFAULT_EXPIRE = 60 * 60 * 24 14 | 15 | class Cache(object): 16 | def __init__(self): 17 | self.cache_dir = os.path.join(CACHE_FOLDER, core.bundleID()) 18 | if not os.path.exists(self.cache_dir): 19 | os.makedirs(self.cache_dir) 20 | 21 | def getCacheFile(self, name): 22 | return os.path.join(self.cache_dir, '{}.json'.format(name)) 23 | 24 | def get(self, name): 25 | path = self.getCacheFile(name) 26 | if not os.path.exists(path): 27 | return 28 | try: 29 | with open(path, 'r') as f: 30 | cache = json.load(f) 31 | except Exception, e: 32 | os.remove(path) 33 | return 34 | # 过期 35 | if time.time() > cache['expire_time']: 36 | os.remove(path) 37 | return 38 | return cache['data'] 39 | 40 | def set(self, name, data, expire = CACHE_DEFAULT_EXPIRE): 41 | path = self.getCacheFile(name) 42 | try: 43 | with open(path, 'w') as f: 44 | cache = { 45 | 'expire_time' : time.time() + expire, 46 | 'data' : data 47 | } 48 | json.dump(cache, f) 49 | except Exception, e: 50 | pass 51 | 52 | def delete(self, name): 53 | path = self.getCacheFile(name) 54 | if os.path.exists(path): 55 | os.remove(path) 56 | 57 | def clean(self): 58 | shutil.rmtree(self.cache_dir) -------------------------------------------------------------------------------- /alfred/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, json 3 | 4 | import core 5 | 6 | CONFIG_FOLDER = os.path.expanduser('~/Library/Application Support/Alfred 2/Workflow Data/') 7 | 8 | class Config(object): 9 | def __init__(self, config_file = 'config.json'): 10 | self.configs = {} 11 | self.configFile = '' 12 | path = os.path.join(CONFIG_FOLDER, core.bundleID()) 13 | if not os.path.exists(path): 14 | os.makedirs(path) 15 | self.configFile = os.path.join(path, config_file) 16 | if os.path.exists(path): 17 | try: 18 | with open(self.configFile, 'r') as f: 19 | self.configs = json.load(f) 20 | except Exception, e: 21 | pass 22 | if not isinstance(self.configs, dict): 23 | self.configs = {} 24 | 25 | def save(self): 26 | with open(self.configFile, 'w') as f: 27 | json.dump(self.configs, f) 28 | 29 | def get(self, key, default = None): 30 | return self.configs.get(key, default) 31 | 32 | def set(self, **kwargs): 33 | for (k, v) in kwargs.iteritems(): 34 | self.configs[k] = v 35 | self.save() 36 | 37 | def delete(self, key): 38 | if not self.configs.has_key(key): 39 | return 40 | self.configs.pop(key) 41 | self.save() 42 | 43 | def clean(self): 44 | self.configs = {} 45 | self.save() -------------------------------------------------------------------------------- /alfred/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, plistlib, time 3 | 4 | BundleID = None 5 | 6 | def bundleID(): 7 | global BundleID 8 | if BundleID: 9 | return BundleID 10 | path = os.path.abspath('./info.plist') 11 | try: 12 | info = plistlib.readPlist(path) 13 | BundleID = info['bundleid'] 14 | except Exception, e: 15 | raise Exception('get Bundle ID fail. {}'.format(e)) 16 | return BundleID 17 | 18 | def log(s): 19 | log_text = '[{} {}]: {}\n'.format(bundleID(), time.strftime('%Y-%m-%d %H:%M:%S'), s) 20 | log_file = os.path.abspath('./log.txt') 21 | if not os.path.exists(log_file): 22 | with open(log_file, 'w') as f: 23 | f.write(log_text) 24 | else: 25 | with open(log_file, 'a') as f: 26 | f.write(log_text) -------------------------------------------------------------------------------- /alfred/feedback.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from xml.etree import ElementTree 3 | import xml.sax.saxutils as saxutils 4 | import os, copy, random 5 | 6 | import core 7 | 8 | class Item(object): 9 | def __init__(self, **kwargs): 10 | self.content = { 11 | 'title' : kwargs.get('title', ''), 12 | 'subtitle' : kwargs.get('subtitle', ''), 13 | 'icon' : kwargs.get('icon', 'icon.png') 14 | } 15 | 16 | it = kwargs.get('icontype', '').lower() 17 | self.icon_type = it if it in ['fileicon', 'filetype'] else None 18 | 19 | valid = kwargs.get('valid', None) 20 | if isinstance(valid, (str, unicode)) and valid.lower() == 'no': 21 | valid = 'no' 22 | elif isinstance(valid, bool) and not valid: 23 | valid = 'no' 24 | else: 25 | valid = None 26 | 27 | self.attrb = { 28 | 'uid' : kwargs.get('uid', '{0}.{1}'.format(core.bundleID(), random.getrandbits(40))), 29 | 'arg' : kwargs.get('arg', None), 30 | 'valid' : valid, 31 | 'autocomplete' : kwargs.get('autocomplete', None), 32 | 'type' : kwargs.get('type', None) 33 | } 34 | 35 | for key in self.content.keys(): 36 | if self.content[key] is None: 37 | del self.content[key] 38 | 39 | for key in self.attrb.keys(): 40 | if self.attrb[key] is None: 41 | del self.attrb[key] 42 | 43 | def copy(self): 44 | return copy.copy(self) 45 | 46 | def getXMLElement(self): 47 | item = ElementTree.Element('item', self.attrb) 48 | for (k, v) in self.content.iteritems(): 49 | attrb = {} 50 | if k == 'icon' and self.icon_type: 51 | attrb['type'] = self.icon_type 52 | sub = ElementTree.SubElement(item, k, attrb) 53 | sub.text = v 54 | return item 55 | 56 | class Feedback(object): 57 | def __init__(self): 58 | self.items = [] 59 | 60 | def addItem(self, **kwargs): 61 | item = kwargs.pop('item', None) 62 | if not isinstance(item, Item): 63 | item = Item(**kwargs) 64 | self.items.append(item) 65 | 66 | def clean(self): 67 | self.items = [] 68 | 69 | def isEmpty(self): 70 | return not bool(self.items) 71 | 72 | def get(self, unescape = False): 73 | ele_tree = ElementTree.Element('items') 74 | for item in self.items: 75 | ele_tree.append(item.getXMLElement()) 76 | res = ElementTree.tostring(ele_tree, encoding='utf-8') 77 | if unescape: 78 | return saxutils.unescape(res) 79 | return res 80 | 81 | def output(self): 82 | print(self.get()) -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alswl/shanbay-alfred2/a2bc3e5abea0b0062dbdbce0f118e1bb3752b6e6/icon.png -------------------------------------------------------------------------------- /info.plist.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.alswl.alfred.shanbay 7 | category 8 | Productivity 9 | connections 10 | 11 | 33FF9C52-75C4-440D-8857-D827047E0215 12 | 13 | 14 | destinationuid 15 | 3C37AB5F-DB48-4DAF-A017-36A25ABAA4E8 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | 21 | 22 | 3493BFDD-DA65-43A5-94EF-C595F87298F5 23 | 24 | 25 | destinationuid 26 | DCA04B6B-99A7-48E8-BE9A-3C5728F99001 27 | modifiers 28 | 0 29 | modifiersubtext 30 | 31 | 32 | 33 | 3C37AB5F-DB48-4DAF-A017-36A25ABAA4E8 34 | 35 | 36 | destinationuid 37 | 81B77168-E9EE-4DFD-B01A-8C5FFC8F74E2 38 | modifiers 39 | 0 40 | modifiersubtext 41 | 42 | 43 | 44 | F8F1B29B-584E-47DC-9905-A71467B0B0F1 45 | 46 | 47 | destinationuid 48 | BFBCC2BE-EB84-4FEA-8C37-8C8FF2310B49 49 | modifiers 50 | 0 51 | modifiersubtext 52 | 53 | 54 | 55 | FAAFF273-243F-47BD-9C42-D2C2DFEF010A 56 | 57 | 58 | destinationuid 59 | F8F1B29B-584E-47DC-9905-A71467B0B0F1 60 | modifiers 61 | 0 62 | modifiersubtext 63 | 64 | 65 | 66 | destinationuid 67 | 3493BFDD-DA65-43A5-94EF-C595F87298F5 68 | modifiers 69 | 1048576 70 | modifiersubtext 71 | 打开单词网页 72 | 73 | 74 | destinationuid 75 | BC572844-02F4-41E6-B880-4EBC3BAE5D4A 76 | modifiers 77 | 262144 78 | modifiersubtext 79 | 发音 80 | 81 | 82 | 83 | createdby 84 | alswl 85 | description 86 | 支持单词查询、加入单词库 87 | disabled 88 | 89 | name 90 | Shanbay v${VERSION} 91 | objects 92 | 93 | 94 | config 95 | 96 | lastpathcomponent 97 | 98 | onlyshowifquerypopulated 99 | 100 | output 101 | 0 102 | removeextension 103 | 104 | sticky 105 | 106 | text 107 | {query} 108 | title 109 | 扇贝单词 110 | 111 | type 112 | alfred.workflow.output.notification 113 | uid 114 | BFBCC2BE-EB84-4FEA-8C37-8C8FF2310B49 115 | version 116 | 0 117 | 118 | 119 | config 120 | 121 | argumenttype 122 | 0 123 | escaping 124 | 62 125 | keyword 126 | sb 127 | queuedelaycustom 128 | 3 129 | queuedelayimmediatelyinitially 130 | 131 | queuedelaymode 132 | 1 133 | queuemode 134 | 2 135 | runningsubtext 136 | 查询中... 137 | script 138 | /usr/bin/python shanbay.py --search "{query}" 139 | subtext 140 | 查询扇贝网,并添加到词库。 141 | title 142 | 扇贝词典 143 | type 144 | 0 145 | withspace 146 | 147 | 148 | type 149 | alfred.workflow.input.scriptfilter 150 | uid 151 | FAAFF273-243F-47BD-9C42-D2C2DFEF010A 152 | version 153 | 0 154 | 155 | 156 | config 157 | 158 | concurrently 159 | 160 | escaping 161 | 62 162 | script 163 | /usr/bin/python shanbay.py --learning "{query}" 164 | type 165 | 0 166 | 167 | type 168 | alfred.workflow.action.script 169 | uid 170 | F8F1B29B-584E-47DC-9905-A71467B0B0F1 171 | version 172 | 0 173 | 174 | 175 | config 176 | 177 | concurrently 178 | 179 | escaping 180 | 62 181 | script 182 | /usr/bin/python shanbay.py --open "{query}" 183 | type 184 | 0 185 | 186 | type 187 | alfred.workflow.action.script 188 | uid 189 | 3493BFDD-DA65-43A5-94EF-C595F87298F5 190 | version 191 | 0 192 | 193 | 194 | config 195 | 196 | lastpathcomponent 197 | 198 | onlyshowifquerypopulated 199 | 200 | output 201 | 0 202 | removeextension 203 | 204 | sticky 205 | 206 | text 207 | {query} 208 | title 209 | 扇贝单词 210 | 211 | type 212 | alfred.workflow.output.notification 213 | uid 214 | DCA04B6B-99A7-48E8-BE9A-3C5728F99001 215 | version 216 | 0 217 | 218 | 219 | config 220 | 221 | concurrently 222 | 223 | escaping 224 | 62 225 | script 226 | /usr/bin/python shanbay.py --sound "{query}" 227 | type 228 | 0 229 | 230 | type 231 | alfred.workflow.action.script 232 | uid 233 | BC572844-02F4-41E6-B880-4EBC3BAE5D4A 234 | version 235 | 0 236 | 237 | 238 | config 239 | 240 | argumenttype 241 | 0 242 | keyword 243 | sbauth 244 | subtext 245 | 粘贴授权码,授权后可以添加单词、查询例句、收藏例句等。 246 | text 247 | 扇贝网授权 248 | withspace 249 | 250 | 251 | type 252 | alfred.workflow.input.keyword 253 | uid 254 | 33FF9C52-75C4-440D-8857-D827047E0215 255 | version 256 | 0 257 | 258 | 259 | config 260 | 261 | lastpathcomponent 262 | 263 | onlyshowifquerypopulated 264 | 265 | output 266 | 0 267 | removeextension 268 | 269 | sticky 270 | 271 | text 272 | {query} 273 | title 274 | 扇贝网授权 275 | 276 | type 277 | alfred.workflow.output.notification 278 | uid 279 | 81B77168-E9EE-4DFD-B01A-8C5FFC8F74E2 280 | version 281 | 0 282 | 283 | 284 | config 285 | 286 | concurrently 287 | 288 | escaping 289 | 62 290 | script 291 | /usr/bin/python shanbay.py --token "{query}" 292 | type 293 | 0 294 | 295 | type 296 | alfred.workflow.action.script 297 | uid 298 | 3C37AB5F-DB48-4DAF-A017-36A25ABAA4E8 299 | version 300 | 0 301 | 302 | 303 | readme 304 | Pelease visit https://github.com/alswl/shanbay-alfred2 . 305 | uidata 306 | 307 | 33FF9C52-75C4-440D-8857-D827047E0215 308 | 309 | ypos 310 | 420 311 | 312 | 3493BFDD-DA65-43A5-94EF-C595F87298F5 313 | 314 | ypos 315 | 130 316 | 317 | 3C37AB5F-DB48-4DAF-A017-36A25ABAA4E8 318 | 319 | ypos 320 | 490 321 | 322 | 81B77168-E9EE-4DFD-B01A-8C5FFC8F74E2 323 | 324 | ypos 325 | 420 326 | 327 | BC572844-02F4-41E6-B880-4EBC3BAE5D4A 328 | 329 | ypos 330 | 260 331 | 332 | BFBCC2BE-EB84-4FEA-8C37-8C8FF2310B49 333 | 334 | ypos 335 | 10 336 | 337 | DCA04B6B-99A7-48E8-BE9A-3C5728F99001 338 | 339 | ypos 340 | 130 341 | 342 | F8F1B29B-584E-47DC-9905-A71467B0B0F1 343 | 344 | ypos 345 | 10 346 | 347 | FAAFF273-243F-47BD-9C42-D2C2DFEF010A 348 | 349 | ypos 350 | 10 351 | 352 | 353 | webaddress 354 | https://github.com/alswl/shanbay-alfred2 355 | 356 | 357 | -------------------------------------------------------------------------------- /it_shanbay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | 5 | import unittest 6 | import shanbay 7 | 8 | 9 | class ITShanbay(unittest.TestCase): 10 | 11 | def test_is_upgrade_availabe(self): 12 | self.assertFalse(shanbay.is_upgrade_available()) 13 | 14 | def test_resolve_dns(self): 15 | self.assertEqual('1.6.1.0', shanbay._resolve_dns(shanbay.VERSION_DOMAIN)) 16 | 17 | def test_fetch_version_by_domain(self): 18 | self.assertEqual('1.6.1', shanbay._fetch_version_by_domain(shanbay.VERSION_DOMAIN)) 19 | 20 | 21 | if __name__ == '__main__': 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | -------------------------------------------------------------------------------- /shanbay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import sys 5 | 6 | reload(sys) 7 | sys.setdefaultencoding('utf8') 8 | 9 | import os 10 | import urllib 11 | import urllib2 12 | import json 13 | import time 14 | import argparse 15 | from urlparse import urlparse 16 | import subprocess 17 | import re 18 | import socket 19 | from distutils.version import LooseVersion 20 | 21 | from alfred.feedback import Feedback 22 | 23 | 24 | TOKEN_FILE = os.path.abspath('token') 25 | ALFRED_WORD_AUDIO_MP3_FILE = '/tmp/alfred_word_audio.mp3' 26 | 27 | CLIENT_ID = '7f652079ade6d4fa4eec' 28 | 29 | SEARCH_API = 'https://api.shanbay.com/bdc/search/' 30 | LEARNING_API = 'https://api.shanbay.com/bdc/learning/' 31 | AUTHORIZE_API = 'https://api.shanbay.com/oauth2/authorize/' 32 | REDIRECT_URL = 'https://www.shanbay.com/oauth2/auth/success/' 33 | VOCABULARY_URL = 'https://www.shanbay.com/bdc/vocabulary/%d/' 34 | VERSION_DOMAIN = 'shanbay-alfred2-version.alswl.com' 35 | 36 | 37 | def _get_current_version(): 38 | with open('./VERSION', 'r') as version_file: 39 | return version_file.read().strip() 40 | 41 | 42 | CURRENT_VERSION = _get_current_version() 43 | 44 | 45 | def _request(path, params=None, method='GET', data=None, headers=None): 46 | params = params or {} 47 | headers = headers or {} 48 | if params: 49 | url = path + '?' + urllib.urlencode(params) 50 | else: 51 | url = path 52 | 53 | request = urllib2.Request(url, data, headers) 54 | request.get_method = lambda: method 55 | response = urllib2.urlopen(request) 56 | return response.read() 57 | 58 | 59 | def _api(path, params=None, method='GET', data=None, headers=None): 60 | response = _request(path=path, params=params, method=method, data=data, 61 | headers=headers) 62 | result = json.loads(response) 63 | if result['status_code'] != 0: 64 | return None 65 | return result['data'] 66 | 67 | 68 | def _fetch_version_by_domain(domain): 69 | """ 70 | eg. request ip is 1.5.0.0, but replace `.0$` to ``, then return 1.5.0 71 | :param domain: 72 | :return: version 73 | """ 74 | ip = _resolve_dns(domain) 75 | if ip is None: 76 | return ip 77 | return ip[:-2] 78 | 79 | 80 | def _resolve_dns(domain): 81 | try: 82 | return socket.gethostbyname(domain) 83 | except socket.gaierror: 84 | return None 85 | 86 | 87 | def is_upgrade_available(): 88 | available_version = _fetch_version_by_domain(VERSION_DOMAIN) 89 | if available_version is None: 90 | return False 91 | 92 | current_version = CURRENT_VERSION 93 | return LooseVersion(available_version) > LooseVersion(current_version) 94 | 95 | 96 | def save_token(url): 97 | parse_result = urlparse(url) 98 | data = dict(map(lambda x: x.split('='), parse_result.fragment.split('&'))) 99 | data['expires_in'] = int(data['expires_in']) 100 | data['timestamp'] = time.time() 101 | with(open(TOKEN_FILE, 'w')) as token_file: 102 | token_file.write(json.dumps(data)) 103 | 104 | 105 | def read_token(): 106 | if not os.path.isfile(TOKEN_FILE): 107 | return False 108 | token_json = json.loads(open(TOKEN_FILE).read()) 109 | if token_json['timestamp'] + token_json['expires_in'] < int(time.time()): 110 | return False 111 | return token_json['access_token'] 112 | 113 | 114 | def search(word): 115 | feedback = Feedback() 116 | data = _api(SEARCH_API, params={'word': word}) 117 | if data is None: 118 | return 119 | 120 | word = data['content'] 121 | pron = data['pron'] 122 | title = "%s [%s]" % (word, pron) 123 | feedback.addItem(title=title, arg=word) 124 | for chinese in data['definition'].decode("utf-8").split('\n'): 125 | feedback.addItem(title=chinese, arg=word) 126 | 127 | if 'en_definitions' in data and data['en_definitions']: 128 | for type_ in data['en_definitions']: 129 | for line in data['en_definitions'][type_]: 130 | title = type_ + ', ' + line 131 | if not title: 132 | continue 133 | feedback.addItem(title=title, arg=word) 134 | feedback.output() 135 | 136 | 137 | def token_url(url): 138 | try: 139 | if is_upgrade_available(): 140 | print('New version is available.') 141 | except: 142 | pass 143 | if not url.startswith('%s#' % REDIRECT_URL): 144 | return 145 | save_token(url) 146 | print('Authorize successful') 147 | 148 | 149 | def learning(word): 150 | access_token = read_token() 151 | if not access_token: 152 | return authorize() 153 | search_data = _api(SEARCH_API, params={'word': word}) 154 | if search_data is None: 155 | return 156 | try: 157 | data = _api(LEARNING_API, 158 | data=urllib.urlencode({'id': search_data['id']}), 159 | headers={'Authorization': 'Bearer %s' % access_token}, 160 | method='POST') 161 | except urllib2.HTTPError, e: 162 | if e.code == 401: 163 | return authorize() 164 | else: 165 | data = None 166 | print('"%s" Add Fail, e: %s' % (word, e)) 167 | if data is None: 168 | print('"%s" Add Fail' % word) 169 | return 170 | print('"%s" Add Successful' % word) 171 | 172 | 173 | def authorize(): 174 | url = '%s?client_id=%s&response_type=token' % (AUTHORIZE_API, CLIENT_ID) 175 | os.system('open "%s"' % url) 176 | 177 | 178 | def sound(word): 179 | data = _api(SEARCH_API, params={'word': word}) 180 | if data is None: 181 | return 182 | audio_address = data['audio_addresses']['us'][0] 183 | with open(ALFRED_WORD_AUDIO_MP3_FILE, 'w') as f: 184 | f.write(_request(audio_address)) 185 | subprocess.call(['/usr/bin/afplay', ALFRED_WORD_AUDIO_MP3_FILE]) 186 | 187 | 188 | def open_word(word): 189 | data = _api(SEARCH_API, params={'word': word}) 190 | if data is None: 191 | return 192 | url = VOCABULARY_URL % data['id'] 193 | os.system('open "%s"' % url) 194 | 195 | 196 | def main(): 197 | parser = argparse.ArgumentParser() 198 | 199 | parser.add_argument('--search', nargs='?', type=str) 200 | parser.add_argument('--learning', nargs='?') 201 | parser.add_argument('--tokenurl', nargs='?') 202 | parser.add_argument('--sound', nargs='?') 203 | parser.add_argument('--open', nargs='?') 204 | args = parser.parse_args() 205 | 206 | if args.search: 207 | search(args.search) 208 | elif args.learning: 209 | learning(args.learning) 210 | elif args.tokenurl: 211 | token_url(args.tokenurl) 212 | elif args.sound: 213 | sound(args.sound) 214 | elif args.open: 215 | open_word(args.open) 216 | else: 217 | raise ValueError() 218 | 219 | 220 | if __name__ == '__main__': 221 | main() 222 | -------------------------------------------------------------------------------- /snapshot/sb_auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alswl/shanbay-alfred2/a2bc3e5abea0b0062dbdbce0f118e1bb3752b6e6/snapshot/sb_auth.png -------------------------------------------------------------------------------- /snapshot/sb_love.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alswl/shanbay-alfred2/a2bc3e5abea0b0062dbdbce0f118e1bb3752b6e6/snapshot/sb_love.png -------------------------------------------------------------------------------- /snapshot/sb_sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alswl/shanbay-alfred2/a2bc3e5abea0b0062dbdbce0f118e1bb3752b6e6/snapshot/sb_sound.png -------------------------------------------------------------------------------- /test_shanbay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | 5 | import unittest 6 | from distutils.version import LooseVersion, StrictVersion 7 | 8 | import shanbay 9 | 10 | class TestShanbay(unittest.TestCase): 11 | 12 | def test__parse_version(self): 13 | version = LooseVersion('1.5') 14 | self.assertEqual(1, version.version[0]) 15 | self.assertEqual(5, version.version[1]) 16 | 17 | def test__version_compare(self): 18 | self.assertTrue(LooseVersion('1.4.0') < LooseVersion('1.5.0')) 19 | self.assertTrue(LooseVersion('1.5.0') == LooseVersion('1.5.0')) 20 | self.assertTrue(LooseVersion('1.5') < LooseVersion('1.5.0')) 21 | self.assertTrue(LooseVersion('2.5') > LooseVersion('1.7.0')) 22 | self.assertTrue(LooseVersion('0.1') < LooseVersion('1.0')) 23 | 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | 28 | --------------------------------------------------------------------------------