├── .editorconfig ├── .gitignore ├── .gitlab-ci.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── requirements.txt ├── setup.py └── xiaoaitts ├── __init__.py ├── const.py ├── get_device.py ├── lib ├── __init__.py ├── error.py ├── invoke.py └── request.py ├── login.py ├── miot ├── __init__.py ├── get_device.py └── get_device_spec.py ├── nlp.py ├── player ├── __init__.py ├── get_context.py ├── get_playlist.py ├── get_playlist_songs.py ├── get_song_info.py ├── get_status.py ├── get_volume.py ├── next.py ├── pause.py ├── play.py ├── play_album_playlist.py ├── play_url.py ├── prev.py ├── set_play_loop.py ├── set_volume.py └── toggle_play_state.py └── tts.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | 15 | [*.yml] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | *.log 4 | *.pyc 5 | *.spec 6 | 7 | # Packages 8 | venv 9 | dist 10 | build 11 | tests/report/ 12 | *.egg-info/ 13 | 14 | .idea 15 | .DS_Store 16 | Thumbs.db 17 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: python:3.7-slim 2 | 3 | variables: 4 | PYPI_TOKEN: $PYPI_TOKEN 5 | PYPI_TEST_TOKEN: $PYPI_TEST_TOKEN 6 | 7 | cache: 8 | paths: 9 | - dist 10 | 11 | before_script: 12 | - python --version 13 | - python -m pip install --upgrade pip 14 | 15 | build: 16 | stage: build 17 | before_script: 18 | - pip install build 19 | script: 20 | - python -m build 21 | tags: 22 | - shared-ci 23 | only: 24 | - tags 25 | - master 26 | 27 | upload-pypi: 28 | stage: deploy 29 | before_script: 30 | - pip install twine 31 | script: 32 | - twine upload -u __token__ -p $PYPI_TOKEN --repository-url https://upload.pypi.org/legacy/ dist/* 33 | tags: 34 | - shared-ci 35 | only: 36 | - tags 37 | 38 | upload-pypi-test: 39 | stage: deploy 40 | before_script: 41 | - pip install twine 42 | script: 43 | - twine upload -u __token__ -p $PYPI_TEST_TOKEN --repository-url https://test.pypi.org/legacy/ dist/* 44 | tags: 45 | - shared-ci 46 | only: 47 | - master 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [QASP] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include MANIFEST.in 4 | 5 | include requirements.txt 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xiaoaitts 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/xiaoaitts.svg)](https://pypi.python.org/pypi/xiaoaitts) 4 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/xiaoaitts) 5 | ![PyPI - License](https://img.shields.io/pypi/l/xiaoaitts) 6 | ![GitHub last commit](https://img.shields.io/github/last-commit/cnmaxqi/xiaoaitts) 7 | 8 | 小爱音箱自定义文本朗读。 9 | 10 | > 不止是 TTS 11 | 12 | ## 安装 13 | 14 | ```bash 15 | pip install xiaoaitts 16 | ``` 17 | 18 | ## 使用 19 | 20 | ```python 21 | from xiaoaitts import XiaoAi 22 | 23 | # 输入小米账户名,密码 24 | client = XiaoAi('fish', '123456') 25 | # 朗读文本 26 | client.say('你好,我是小爱') 27 | ``` 28 | 29 | ## API 30 | 31 | ### Class: XiaoAi 32 | 33 | #### XiaoAi(user, password) 34 | 35 | - `username` 小米账户用户名 36 | - `password` 账户密码 37 | 38 | 使用小米账户登录小爱音箱 39 | 40 | ### instance 41 | 42 | XiaoAi 实例对象 43 | 44 | #### say(text) 45 | 46 | - `text` 文本信息 47 | 48 | 朗读指定文本,返回接口调用结果 49 | 50 | ```python 51 | client.say('你好,我是小爱') 52 | ``` 53 | 54 | #### get_device(name=None) 55 | 56 | - `name` 设备名称(别名) 57 | - Returns: 设备信息 58 | 59 | 获取**在线**设备列表 60 | 61 | ```python 62 | # 获取所有在线设备 63 | online_devices = client.get_device() 64 | # 获取单个设备,未找到时返回 null 65 | room_device = client.get_device('卧室小爱') 66 | ``` 67 | 68 | #### use_device(device_id) 69 | 70 | - `device_id` 设备id 71 | 72 | 切换指定设备。`xiaomitts` 实例化后默认使用 `get_device()` 方法返回的第一个设备,可使用此方法切换为指定设备。 73 | 74 | ```python 75 | room_device = client.get_device('卧室小爱') 76 | # 使用“卧室小爱” 77 | client.use_device(room_device.get('deviceID')) 78 | client.say('你好,我是卧室的小爱') 79 | ``` 80 | 81 | #### test() 82 | 83 | 测试连通性 84 | 85 | ```python 86 | client.test() 87 | ``` 88 | 89 | ### 媒体控制 90 | 91 | #### set_volume(volume) 92 | 93 | - `volume` 音量值 94 | 95 | 设置音量 96 | 97 | ```python 98 | client.set_volume(30) 99 | ``` 100 | 101 | #### get_volume() 102 | 103 | - Returns: 音量值 104 | 105 | 获取音量 106 | 107 | ```python 108 | volume = client.get_volume() 109 | ``` 110 | 111 | #### volume_up() 112 | 113 | 调高音量,幅度 5 114 | 115 | #### volume_down() 116 | 117 | 调低音量,幅度 5 118 | 119 | #### get_status() 120 | 121 | - Returns: 状态信息 122 | 123 | 获取设备运行状态 124 | 125 | #### play() 126 | 127 | 继续播放 128 | 129 | #### pause() 130 | 131 | 暂停播放 132 | 133 | #### prev() 134 | 135 | 播放上一曲 136 | 137 | #### next() 138 | 139 | 播放下一曲 140 | 141 | #### set_play_loop(type=1) 142 | 143 | - `type` 0-单曲循环 1-列表循环 3-列表随机 144 | 145 | 设置循环播放 146 | 147 | #### get_song_info(song_id) 148 | 149 | - `song_id` 歌曲id 150 | - Returns: 歌曲信息 151 | 152 | 查询歌曲信息 153 | 154 | ```python 155 | song_info = client.get_song_info('7519904358300484678') 156 | ``` 157 | 158 | #### get_my_playlist(list_id=None) 159 | 160 | - `list_id` 歌单id 161 | - Returns: 歌曲信息 162 | 163 | 获取用户自建歌单,当指定 `list_id` 时,将返回目标歌单内的歌曲列表 164 | 165 | ```python 166 | # 获取歌单列表 167 | my_playlist = client.get_my_playlist() 168 | # 获取歌单内的歌曲列表 169 | song_list = client.get_my_playlist('337361232731772372') 170 | ``` 171 | 172 | ## 参考链接 173 | - [https://github.com/Yonsm](https://github.com/Yonsm) 174 | - [https://github.com/vv314/xiaoai-tts](https://github.com/vv314/xiaoai-tts) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='xiaoaitts', 8 | version='0.1.1', 9 | author='Max', 10 | author_email='mr.qchao@gmail.com', 11 | description='小爱音箱自定义文本朗读', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/cnmax/xiaoaitts', 15 | project_urls={ 16 | 'Bug Tracker': 'https://github.com/cnmax/xiaoaitts/issues', 17 | }, 18 | classifiers=[ 19 | 'Programming Language :: Python :: 3', 20 | 'License :: OSI Approved :: MIT License', 21 | 'Operating System :: OS Independent', 22 | ], 23 | packages=find_packages(exclude=('tests', 'tests.*')), 24 | python_requires='>=3.6', 25 | install_requires=[ 26 | 'requests', 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /xiaoaitts/__init__.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts import player 2 | from xiaoaitts.get_device import get_device 3 | from xiaoaitts.lib.error import XiaoAiError, ErrorCode 4 | from xiaoaitts.lib.invoke import ubus 5 | from xiaoaitts.login import login, switch_session_device, Ticket 6 | from xiaoaitts.tts import tts 7 | 8 | 9 | class XiaoAi: 10 | 11 | def __init__(self, user, password): 12 | self.current_device = None 13 | self.mina_session = login('micoapi', user, password) 14 | devices = get_device(self.mina_session['cookies']) 15 | if len(devices) == 0: 16 | return 17 | self.current_device = devices[0] 18 | self.mina_session = switch_session_device(self.mina_session, self.current_device) 19 | 20 | self.miio_session = login('xiaomiio', user, password) 21 | self.miio_session = switch_session_device(self.miio_session, self.current_device) 22 | 23 | def test(self): 24 | return ubus(Ticket(**self.mina_session), 25 | message={'vendor_name': 'XiaoMi_M88'}, 26 | method='tts_vendor_switch', 27 | path='mibrain') 28 | 29 | def get_device(self, name=None): 30 | devices = get_device(self.mina_session['cookies']) 31 | if not name: 32 | return devices 33 | target = [] 34 | for device in devices: 35 | if name in device['name']: 36 | target.append(device) 37 | return target or None 38 | 39 | def use_device(self, device_id): 40 | session = self.mina_session 41 | devices = get_device(session['cookies']) 42 | device = None 43 | for _device in devices: 44 | if _device['deviceID'] == device_id: 45 | device = _device 46 | break 47 | if not device: 48 | raise XiaoAiError(ErrorCode.NO_DEVICE, device_id) 49 | self.current_device = device 50 | self.mina_session = switch_session_device(session, device) 51 | self.miio_session = switch_session_device(self.miio_session, device) 52 | 53 | def _call(self, method, **kwargs): 54 | if method.__name__ == 'tts': 55 | ticket = Ticket(**self.miio_session) 56 | else: 57 | ticket = Ticket(**self.mina_session) 58 | return method(ticket, **kwargs) 59 | 60 | def say(self, text): 61 | return self._call(tts, text=text) 62 | 63 | def set_volume(self, volume): 64 | if not isinstance(volume, int): 65 | raise XiaoAiError(ErrorCode.INVALID_INPUT) 66 | return self._call(player.set_volume, volume=volume) 67 | 68 | def get_volume(self): 69 | return self._call(player.get_volume) 70 | 71 | def volume_up(self): 72 | return self._call(player.volume_up) 73 | 74 | def volume_down(self): 75 | return self._call(player.volume_down) 76 | 77 | def play(self): 78 | return self._call(player.play) 79 | 80 | def pause(self): 81 | return self._call(player.pause) 82 | 83 | def prev(self): 84 | return self._call(player.prev) 85 | 86 | def next(self): 87 | return self._call(player.next) 88 | 89 | def set_play_loop(self, type=1): 90 | return self._call(player.set_play_loop, type=type) 91 | 92 | def get_status(self): 93 | return self._call(player.get_status) 94 | 95 | def get_song_info(self, song_id): 96 | return self._call(player.get_song_info, song_id=song_id) 97 | 98 | def get_my_playlist(self, list_id=None): 99 | return self._call(player.get_my_playlist, list_id=list_id) 100 | -------------------------------------------------------------------------------- /xiaoaitts/const.py: -------------------------------------------------------------------------------- 1 | APP_DEVICE_ID = '3C861A5820190429' 2 | SDK_VER = '3.9' 3 | APP_UA = 'APP/com.xiaomi.mihome APPV/6.0.103 iosPassportSDK/3.9.0 iOS/14.4 miHSTS' 4 | MINA_UA = 'MiHome/6.0.103 (com.xiaomi.mihome; build:6.0.103.1; iOS 14.4.0) Alamofire/6.0.103 MICO/iOSApp/appStore/6.0.103' 5 | MIIO_UA = 'iOS-14.4-6.0.103-iPhone12,3--D7744744F7AF32F0544445285880DD63E47D9BE9-8816080-84A3F44E137B71AE-iPhone' 6 | 7 | 8 | class API: 9 | MIIO = 'https://api.io.mi.com/app' 10 | USBS = 'https://api2.mina.mi.com/remote/ubus' 11 | SERVICE_AUTH = 'https://account.xiaomi.com/pass/serviceLoginAuth2' 12 | SERVICE_LOGIN = 'https://account.xiaomi.com/pass/serviceLogin' 13 | PLAYLIST = 'https://api2.mina.mi.com/music/playlist/v2/lists' 14 | PLAYLIST_SONGS = 'https://api2.mina.mi.com/music/playlist/v2/songs' 15 | SONG_INFO = 'https://api2.mina.mi.com/music/song_info' 16 | DEVICE_LIST = 'https://api2.mina.mi.com/admin/v2/device_list' 17 | -------------------------------------------------------------------------------- /xiaoaitts/get_device.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.const import API 2 | from xiaoaitts.lib.invoke import invoke 3 | 4 | 5 | def get_device(cookies): 6 | data = { 7 | 'master': 0 8 | } 9 | devices = invoke(url=API.DEVICE_LIST, data=data, cookies=cookies) 10 | live_devices = [] 11 | for device in devices: 12 | if device['presence'] == 'online': 13 | live_devices.append(device) 14 | return live_devices 15 | -------------------------------------------------------------------------------- /xiaoaitts/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnmax/xiaoaitts/2f3a02288f8184b099291716eb038ad02d5aeff2/xiaoaitts/lib/__init__.py -------------------------------------------------------------------------------- /xiaoaitts/lib/error.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from xiaoaitts.lib.request import HttpError 4 | 5 | 6 | class ErrorCode: 7 | AURH_ERR = 401, 8 | INVALID_INPUT = 1, 9 | NO_DEVICE = 2, 10 | UBUS_ERR = 3, 11 | INVALID_RESULT = 4 12 | 13 | 14 | ERROR_CODE_MAP = { 15 | ErrorCode.AURH_ERR: 'Session 校验失败,请重新登录', 16 | ErrorCode.NO_DEVICE: '未找到在线设备,请检查设备连接', 17 | ErrorCode.INVALID_INPUT: '参数不合法,请查阅文档', 18 | ErrorCode.INVALID_RESULT: '接口错误', 19 | ErrorCode.UBUS_ERR: '请检查设备连接' 20 | } 21 | 22 | 23 | class XiaoAiError(Exception): 24 | 25 | def __init__(self, code, error_message='') -> None: 26 | if isinstance(code, HttpError): 27 | if code.status in ERROR_CODE_MAP: 28 | message = ERROR_CODE_MAP[code.status] 29 | else: 30 | message = '网络请求错误' 31 | error_message = code.message 32 | elif code == ErrorCode.INVALID_RESULT: 33 | self.response = error_message 34 | message = ERROR_CODE_MAP[code] 35 | else: 36 | message = ERROR_CODE_MAP.get(code, '') 37 | 38 | if not isinstance(error_message, str): 39 | error_message = json.dumps(error_message) 40 | self.message = error_message + (' - ' + message if message else '') 41 | 42 | def __str__(self) -> str: 43 | return self.message 44 | -------------------------------------------------------------------------------- /xiaoaitts/lib/invoke.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import hashlib 3 | import hmac 4 | import json 5 | import os 6 | import random 7 | import string 8 | import time 9 | 10 | from xiaoaitts.const import API 11 | from xiaoaitts.lib.error import ErrorCode, XiaoAiError 12 | from xiaoaitts.lib.request import request 13 | 14 | 15 | def invoke(url, data=None, method='GET', **kwargs): 16 | resp = request(url, data=data, method=method, **kwargs) 17 | if resp['code'] == 0: 18 | return resp['result'] if 'io.mi.com' in url else resp['data'] 19 | raise XiaoAiError(ErrorCode.INVALID_RESULT, resp) 20 | 21 | 22 | def ubus(ticket, message, method, path): 23 | def gen_request_id(): 24 | return 'app_ios_' + ''.join(random.sample(string.digits + string.ascii_letters, 30)) 25 | 26 | data = { 27 | 'deviceId': ticket.device_id, 28 | 'message': json.dumps(message, ensure_ascii=False), 29 | 'method': method, 30 | 'path': path, 31 | 'requestId': gen_request_id() 32 | } 33 | try: 34 | return invoke(url=API.USBS, data=data, method='POST', cookies=ticket.cookies) 35 | except XiaoAiError as e: 36 | resp = e.response 37 | if resp['code'] == 101 and resp['data']: 38 | device_data = json.loads(resp['data']['device_data']) 39 | raise XiaoAiError(ErrorCode.UBUS_ERR, device_data['msg']) 40 | 41 | 42 | def miio(ticket, uri, data): 43 | def gen_nonce(): 44 | """Time based nonce.""" 45 | nonce = os.urandom(8) + int(time.time() / 60).to_bytes(4, 'big') 46 | return base64.b64encode(nonce).decode() 47 | 48 | def gen_signed_nonce(ssecret, nonce): 49 | """Nonce signed with ssecret.""" 50 | m = hashlib.sha256() 51 | m.update(base64.b64decode(ssecret)) 52 | m.update(base64.b64decode(nonce)) 53 | return base64.b64encode(m.digest()).decode() 54 | 55 | def gen_signature(url, signed_nonce, nonce, data): 56 | """Request signature based on url, signed_nonce, nonce and data.""" 57 | sign = '&'.join([url, signed_nonce, nonce, 'data=' + data]) 58 | signature = hmac.new(key=base64.b64decode(signed_nonce), 59 | msg=sign.encode(), 60 | digestmod=hashlib.sha256).digest() 61 | return base64.b64encode(signature).decode() 62 | 63 | def sign_data(uri, data, ssecurity): 64 | if not isinstance(data, str): 65 | data = json.dumps(data) 66 | nonce = gen_nonce() 67 | signed_nonce = gen_signed_nonce(ssecurity, nonce) 68 | signature = gen_signature(uri, signed_nonce, nonce, data) 69 | return {'_nonce': nonce, 'data': data, 'signature': signature} 70 | 71 | def prepare_data(): 72 | ticket.cookies['PassportDeviceId'] = ticket.device_id 73 | return sign_data(uri, data, ticket.ssecurity) 74 | 75 | headers = {'x-xiaomi-protocal-flag-cli': 'PROTOCAL-HTTP2'} 76 | return invoke(url=API.MIIO + uri, data=prepare_data(), method='POST', headers=headers, cookies=ticket.cookies) 77 | 78 | 79 | def miot(ticket, cmd, params): 80 | data = { 81 | 'params': params 82 | } 83 | return miio(ticket=ticket, uri='/miotspec/' + cmd, data=data) 84 | -------------------------------------------------------------------------------- /xiaoaitts/lib/request.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from xiaoaitts.const import MINA_UA, APP_UA, MIIO_UA 6 | 7 | 8 | class HttpError(Exception): 9 | 10 | def __init__(self, response) -> None: 11 | self.url = response.url 12 | self.status = response.status_code 13 | self.response = response.text 14 | self.message = '\n'.join([ 15 | 'Request Error', 16 | 'url: %s' % self.url, 17 | 'status: %d' % self.status, 18 | 'response: %s' % self.response 19 | ]) 20 | 21 | def __str__(self) -> str: 22 | return self.message 23 | 24 | 25 | def request(url, data, method='GET', response_type='json', headers=None, cookies=None, **kwargs): 26 | method = method.upper() 27 | content_type = 'application/x-www-form-urlencoded' if 'POST' == method else 'application/json' 28 | 29 | _headers = { 30 | 'Content-Type': content_type, 31 | 'Connection': 'keep-alive', 32 | 'User-Agent': MINA_UA if 'mina.mi.com' in url else MIIO_UA if 'io.mi.com' in url else APP_UA, 33 | 'Accept': '*/*', 34 | } 35 | _headers.update(headers or {}) 36 | 37 | if method == 'GET': 38 | resp = requests.get(url, params=data, headers=_headers, cookies=cookies, **kwargs) 39 | else: 40 | content_type = _headers['Content-Type'] 41 | if 'application/json' in content_type: 42 | resp = requests.post(url, json=data, headers=_headers, cookies=cookies, **kwargs) 43 | else: 44 | resp = requests.post(url, data=data, headers=_headers, cookies=cookies, **kwargs) 45 | 46 | if resp and resp.status_code == 200: 47 | if response_type == 'raw': 48 | return resp 49 | elif response_type == 'json': 50 | return json.loads(resp.text.replace('&&&START&&&', '')) 51 | else: 52 | return resp.text 53 | 54 | raise HttpError(resp) 55 | -------------------------------------------------------------------------------- /xiaoaitts/login.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from hashlib import sha1, md5 3 | 4 | from xiaoaitts import XiaoAiError, ErrorCode 5 | from xiaoaitts.const import APP_DEVICE_ID, SDK_VER, API 6 | from xiaoaitts.lib.request import request 7 | 8 | 9 | class Ticket: 10 | 11 | def __init__(self, **entries): 12 | self.__dict__.update(entries) 13 | 14 | def __repr__(self): 15 | return str(self.__dict__) 16 | 17 | 18 | def login(sid, user, password): 19 | if not user or not password: 20 | raise XiaoAiError(ErrorCode.INVALID_INPUT) 21 | sign = get_login_sign(sid) 22 | auth_info = service_auth(sid, sign, user, password) 23 | if auth_info['code'] != 0: 24 | raise XiaoAiError(auth_info['code'], auth_info['desc']) 25 | service_token = login_mi_ai(auth_info) 26 | session = { 27 | 'service_token': service_token, 28 | 'user_id': auth_info['userId'], 29 | 'device_id': APP_DEVICE_ID, 30 | 'ssecurity': auth_info['ssecurity'], 31 | } 32 | session['cookies'] = get_cookie(**session) 33 | return session 34 | 35 | 36 | def get_cookie(user_id=None, service_token=None, device_id=None, serial_number=None, **kwargs): 37 | cookies = { 38 | 'userId': str(user_id or ''), 39 | 'serviceToken': service_token or '', 40 | } 41 | if device_id and service_token: 42 | cookies.update({ 43 | 'deviceId': device_id, 44 | 'sn': serial_number, 45 | }) 46 | return cookies 47 | 48 | 49 | def get_login_sign(sid): 50 | data = { 51 | 'sid': sid, 52 | '_json': True 53 | } 54 | info = request(url=API.SERVICE_LOGIN, data=data) 55 | return {'_sign': info['_sign'], 'qs': info['qs'], 'callback': info['callback']} 56 | 57 | 58 | def service_auth(sid, sign, user, password): 59 | data = { 60 | 'user': user, 61 | 'hash': md5(password.encode()).hexdigest().upper(), 62 | **sign, 63 | 'sid': sid, 64 | '_json': True 65 | } 66 | cookies = { 67 | 'deviceId': APP_DEVICE_ID, 68 | 'sdkVersion': SDK_VER, 69 | } 70 | return request(url=API.SERVICE_AUTH, method='POST', data=data, cookies=cookies) 71 | 72 | 73 | def login_mi_ai(auth_info): 74 | def gen_client_sign(): 75 | s = 'nonce={nonce}&{ssecurity}'.format_map(auth_info) 76 | return base64.b64encode(sha1(s.encode()).digest()).decode() 77 | 78 | data = { 79 | 'clientSign': gen_client_sign() 80 | } 81 | try: 82 | resp = request(url=auth_info['location'], data=data, response_type='raw') 83 | for cookie in resp.cookies: 84 | if cookie.name == 'serviceToken': 85 | return cookie.value 86 | except Exception as e: 87 | raise XiaoAiError(e) 88 | return '' 89 | 90 | 91 | def switch_session_device(session, device): 92 | session.update({ 93 | 'device_id': device['deviceID'], 94 | 'serial_number': device['serialNumber'], 95 | 'miot_did': device['miotDID'], 96 | 'hardware': device['hardware'], 97 | }) 98 | session['cookies'] = get_cookie(**session) 99 | return session 100 | -------------------------------------------------------------------------------- /xiaoaitts/miot/__init__.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts import XiaoAiError, ErrorCode 2 | from xiaoaitts.miot.get_device import get_device 3 | from xiaoaitts.miot.get_device_spec import get_device_spec 4 | 5 | 6 | def get_my_device_spec(ticket): 7 | def get_current_device(): 8 | devices = get_device(ticket) 9 | for device in devices: 10 | if device['did'] == ticket.miot_did: 11 | return device 12 | raise XiaoAiError(ErrorCode.NO_DEVICE, ticket.miot_did) 13 | 14 | current_device = get_current_device() 15 | miot_specs = get_device_spec() 16 | for model, _type in miot_specs.items(): 17 | if model == current_device['model']: 18 | return get_device_spec(_type) 19 | raise XiaoAiError(ErrorCode.NO_DEVICE, ticket.miot_did) 20 | -------------------------------------------------------------------------------- /xiaoaitts/miot/get_device.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import miio 2 | 3 | 4 | def get_device(ticket, name=None, virtual_model=False, huami_devices=0): 5 | data = { 6 | 'getVirtualModel': bool(virtual_model), 7 | 'getHuamiDevices': int(huami_devices) 8 | } 9 | devices = miio(ticket, uri='/home/device_list', data=data)['list'] 10 | return [device for device in devices if not name or name in device['name']] 11 | -------------------------------------------------------------------------------- /xiaoaitts/miot/get_device_spec.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from functools import lru_cache 4 | 5 | from xiaoaitts.lib.request import request 6 | 7 | """ 8 | https://miot-spec.org/miot-spec-v2/instances?status=all 9 | https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:speaker:0000A015:xiaomi-x08c:1 10 | """ 11 | 12 | 13 | @lru_cache(maxsize=None) 14 | def get_device_spec(type=None): 15 | if not type or not type.startswith('urn'): 16 | def get_spec(all): 17 | if not type: 18 | return all 19 | ret = {} 20 | for m, t in all.items(): 21 | if type == m: 22 | return {m: t} 23 | elif type in m: 24 | ret[m] = t 25 | return ret 26 | 27 | import tempfile 28 | path = os.path.join(tempfile.gettempdir(), 'xiaoaitts_miot_specs.json') 29 | try: 30 | with open(path) as f: 31 | result = get_spec(json.load(f)) 32 | except: 33 | result = None 34 | if not result: 35 | resp = request('https://miot-spec.org/miot-spec-v2/instances?status=all', data={}) 36 | all = {i['model']: i['type'] for i in resp['instances']} 37 | with open(path, 'w') as f: 38 | json.dump(all, f) 39 | result = get_spec(all) 40 | if len(result) != 1: 41 | return result 42 | type = list(result.values())[0] 43 | 44 | return request('https://miot-spec.org/miot-spec-v2/instance?type=' + type, data={}) 45 | -------------------------------------------------------------------------------- /xiaoaitts/nlp.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from xiaoaitts.lib.invoke import ubus 4 | 5 | 6 | def nlp(ticket): 7 | data = ubus(ticket, message={}, method='nlp_result_get', path='mibrain') 8 | info = json.loads(data['info']) 9 | return json.loads(info['result'][0]['nlp']) 10 | -------------------------------------------------------------------------------- /xiaoaitts/player/__init__.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.player.get_context import get_context 2 | from xiaoaitts.player.get_playlist import get_playlist 3 | from xiaoaitts.player.get_playlist_songs import get_playlist_songs 4 | from xiaoaitts.player.get_song_info import get_song_info 5 | from xiaoaitts.player.get_status import get_status 6 | from xiaoaitts.player.get_volume import get_volume 7 | from xiaoaitts.player.next import next 8 | from xiaoaitts.player.pause import pause 9 | from xiaoaitts.player.play import play 10 | from xiaoaitts.player.play_album_playlist import play_album_playlist 11 | from xiaoaitts.player.play_url import play_url 12 | from xiaoaitts.player.prev import prev 13 | from xiaoaitts.player.set_play_loop import set_play_loop 14 | from xiaoaitts.player.set_volume import set_volume 15 | from xiaoaitts.player.toggle_play_state import toggle_play_state 16 | 17 | VOLUME_STEP = 5 18 | 19 | 20 | def volume_up(ticket): 21 | volume = get_volume(ticket) 22 | return set_volume(ticket, volume + VOLUME_STEP) 23 | 24 | 25 | def volume_down(ticket): 26 | volume = get_volume(ticket) 27 | return set_volume(ticket, volume - VOLUME_STEP) 28 | 29 | 30 | def get_my_playlist(ticket, list_id=None): 31 | playlist = get_playlist(ticket) 32 | if not list_id: 33 | return playlist 34 | song_list = None 35 | for item in playlist: 36 | if item['id'] == int(list_id): 37 | song_list = item 38 | break 39 | show_count = song_list['songCount'] if song_list else 0 40 | return get_playlist_songs(ticket, list_id=list_id, count=show_count) 41 | -------------------------------------------------------------------------------- /xiaoaitts/player/get_context.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def get_context(ticket): 5 | return ubus(ticket, message={}, method='player_get_context', path='mediaplayer') 6 | -------------------------------------------------------------------------------- /xiaoaitts/player/get_playlist.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.const import API 2 | from xiaoaitts.lib.invoke import invoke 3 | 4 | 5 | def get_playlist(ticket): 6 | return invoke(url=API.PLAYLIST, cookies=ticket.cookies) 7 | -------------------------------------------------------------------------------- /xiaoaitts/player/get_playlist_songs.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.const import API 2 | from xiaoaitts.lib.invoke import invoke 3 | 4 | 5 | def get_playlist_songs(ticket, list_id, count=20, offset=0): 6 | data = { 7 | 'listId': list_id, 8 | 'count': count, 9 | 'offset': offset, 10 | } 11 | return invoke(url=API.PLAYLIST_SONGS, data=data, cookies=ticket.cookies) 12 | -------------------------------------------------------------------------------- /xiaoaitts/player/get_song_info.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.const import API 2 | from xiaoaitts.lib.invoke import invoke 3 | 4 | 5 | def get_song_info(ticket, song_id): 6 | data = { 7 | 'songId': song_id 8 | } 9 | return invoke(url=API.SONG_INFO, data=data, cookies=ticket.cookies) 10 | -------------------------------------------------------------------------------- /xiaoaitts/player/get_status.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from xiaoaitts.lib.invoke import ubus 4 | 5 | 6 | def get_status(ticket): 7 | data = ubus(ticket, message={}, method='player_get_play_status', path='mediaplayer') 8 | return json.loads(data['info']) 9 | -------------------------------------------------------------------------------- /xiaoaitts/player/get_volume.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.player import get_status 2 | 3 | 4 | def get_volume(ticket): 5 | return get_status(ticket)['volume'] 6 | -------------------------------------------------------------------------------- /xiaoaitts/player/next.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def next(ticket): 5 | message = { 6 | 'action': 'next', 7 | 'media': 'app_ios' 8 | } 9 | return ubus(ticket, message=message, method='player_play_operation', path='mediaplayer') 10 | -------------------------------------------------------------------------------- /xiaoaitts/player/pause.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def pause(ticket): 5 | message = { 6 | 'action': 'pause', 7 | 'media': 'app_ios' 8 | } 9 | return ubus(ticket, message=message, method='player_play_operation', path='mediaplayer') 10 | -------------------------------------------------------------------------------- /xiaoaitts/player/play.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def play(ticket): 5 | message = { 6 | 'action': 'play', 7 | 'media': 'app_ios' 8 | } 9 | return ubus(ticket, message=message, method='player_play_operation', path='mediaplayer') 10 | -------------------------------------------------------------------------------- /xiaoaitts/player/play_album_playlist.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def play_album_playlist(ticket, album_playlist_id, type=1, start_offset=1): 5 | message = { 6 | 'type': type, 7 | 'id': album_playlist_id, 8 | 'startOffset': start_offset, 9 | 'media': 'common' 10 | } 11 | return ubus(ticket, message=message, method='player_play_album_playlist', path='mediaplayer') 12 | -------------------------------------------------------------------------------- /xiaoaitts/player/play_url.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def play_url(ticket, url, type=1): 5 | message = { 6 | 'type': type, 7 | 'url': url, 8 | 'media': 'app_ios' 9 | } 10 | return ubus(ticket, message=message, method='player_play_url', path='mediaplayer') 11 | -------------------------------------------------------------------------------- /xiaoaitts/player/prev.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def prev(ticket): 5 | message = { 6 | 'action': 'prev', 7 | 'media': 'app_ios' 8 | } 9 | return ubus(ticket, message=message, method='player_play_operation', path='mediaplayer') 10 | -------------------------------------------------------------------------------- /xiaoaitts/player/set_play_loop.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def set_play_loop(ticket, type=1): 5 | """ 6 | :param ticket: 7 | :param type: 0-单曲循环,1-列表循环,3-列表随机 8 | :return: 9 | """ 10 | message = { 11 | 'type': type, 12 | } 13 | return ubus(ticket, message=message, method='player_set_loop', path='mediaplayer') 14 | -------------------------------------------------------------------------------- /xiaoaitts/player/set_volume.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def set_volume(ticket, volume): 5 | volume = min(max(volume, 0), 100) 6 | message = { 7 | 'volume': volume, 8 | } 9 | return ubus(ticket, message=message, method='player_set_volume', path='mediaplayer') 10 | -------------------------------------------------------------------------------- /xiaoaitts/player/toggle_play_state.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import ubus 2 | 3 | 4 | def toggle_play_state(ticket): 5 | message = { 6 | 'action': 'toggle', 7 | } 8 | return ubus(ticket, message=message, method='player_play_operation', path='mediaplayer') 9 | -------------------------------------------------------------------------------- /xiaoaitts/tts.py: -------------------------------------------------------------------------------- 1 | from xiaoaitts.lib.invoke import miot 2 | from xiaoaitts.miot import get_my_device_spec 3 | 4 | 5 | def tts(ticket, text): 6 | def get_service_iid(): 7 | device_spec = get_my_device_spec(ticket) 8 | for service in device_spec['services']: 9 | if 'actions' not in service: 10 | continue 11 | for action in service['actions']: 12 | if 'action:play-text' in action['type']: 13 | return { 14 | 'siid': service['iid'], 15 | 'aiid': action['iid'] 16 | } 17 | return {'siid': 5, 'aiid': 1} 18 | 19 | params = { 20 | 'did': ticket.miot_did, 21 | 'in': [text], 22 | **get_service_iid() 23 | } 24 | return miot(ticket, cmd='action', params=params) 25 | --------------------------------------------------------------------------------