feat: 支持多个设备同时播放 see #65
This commit is contained in:
parent
f2675e4340
commit
794e8dcd06
@ -61,6 +61,12 @@ def main():
|
||||
dest="ffmpeg_location",
|
||||
help="ffmpeg bin path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--enable_config_example",
|
||||
dest="enable_config_example",
|
||||
help="是否输出示例配置文件",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
print(LOGO.format(f"XiaoMusic v{__version__} by: github.com/hanxi"))
|
||||
|
||||
|
@ -59,10 +59,10 @@ def default_key_match_order():
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
hardware: str = os.getenv("MI_HARDWARE", "L07A")
|
||||
account: str = os.getenv("MI_USER", "")
|
||||
password: str = os.getenv("MI_PASS", "")
|
||||
mi_did: str = os.getenv("MI_DID", "")
|
||||
mi_did: str = os.getenv("MI_DID", "") # 逗号分割支持多设备
|
||||
hardware: str = os.getenv("MI_HARDWARE", "L07A") # 逗号分割支持多设备
|
||||
cookie: str = ""
|
||||
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
|
||||
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
|
||||
@ -107,6 +107,7 @@ class Config:
|
||||
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
|
||||
)
|
||||
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
|
||||
enable_config_example: bool = False
|
||||
|
||||
keywords_playlocal: str = os.getenv(
|
||||
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
|
||||
@ -139,10 +140,10 @@ class Config:
|
||||
self.append_user_keyword()
|
||||
|
||||
# 保存配置到 config-example.json 文件
|
||||
with open("config-example.json", "w") as f:
|
||||
data = asdict(self)
|
||||
print(data)
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
if self.enable_config_example:
|
||||
with open("config-example.json", "w") as f:
|
||||
data = asdict(self)
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
@classmethod
|
||||
def from_options(cls, options: argparse.Namespace) -> Config:
|
||||
|
@ -13,7 +13,7 @@ from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from miservice import MiAccount, MiIOService, MiNAService
|
||||
from miservice import MiAccount, MiNAService
|
||||
|
||||
from xiaomusic import (
|
||||
__version__,
|
||||
@ -55,12 +55,12 @@ class XiaoMusic:
|
||||
self.last_timestamp = int(time.time() * 1000) # timestamp last call mi speaker
|
||||
self.last_record = None
|
||||
self.cookie_jar = None
|
||||
self.device_id = ""
|
||||
self.mina_service = None
|
||||
self.miio_service = None
|
||||
self.polling_event = asyncio.Event()
|
||||
self.new_record_event = asyncio.Event()
|
||||
self.queue = queue.Queue()
|
||||
self.device2hardware = {}
|
||||
self.did2device = {}
|
||||
|
||||
# 下载对象
|
||||
self.download_proc = None
|
||||
@ -145,12 +145,14 @@ class XiaoMusic:
|
||||
|
||||
async def poll_latest_ask(self):
|
||||
async with ClientSession() as session:
|
||||
session._cookie_jar = self.cookie_jar
|
||||
while True:
|
||||
self.log.debug(
|
||||
"Listening new message, timestamp: %s", self.last_timestamp
|
||||
)
|
||||
await self.get_latest_ask_from_xiaoai(session)
|
||||
session._cookie_jar = self.cookie_jar
|
||||
# 拉取所有音箱的对话记录
|
||||
for device_id in self.device2hardware:
|
||||
await self.get_latest_ask_from_xiaoai(session, device_id)
|
||||
start = time.perf_counter()
|
||||
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
||||
await self.polling_event.wait()
|
||||
@ -161,7 +163,7 @@ class XiaoMusic:
|
||||
|
||||
async def init_all_data(self, session):
|
||||
await self.login_miboy(session)
|
||||
await self._init_data_hardware()
|
||||
await self.try_update_device_id()
|
||||
cookie_jar = self.get_cookie()
|
||||
if cookie_jar:
|
||||
session.cookie_jar.update_cookies(cookie_jar)
|
||||
@ -177,61 +179,26 @@ class XiaoMusic:
|
||||
# Forced login to refresh to refresh token
|
||||
await account.login("micoapi")
|
||||
self.mina_service = MiNAService(account)
|
||||
self.miio_service = MiIOService(account)
|
||||
|
||||
async def try_update_device_id(self):
|
||||
# fix multi xiaoai problems we check did first
|
||||
# why we use this way to fix?
|
||||
# some videos and articles already in the Internet
|
||||
# we do not want to change old way, so we check if miotDID in `env` first
|
||||
# to set device id
|
||||
|
||||
try:
|
||||
mi_dids = self.config.mi_did.split(",")
|
||||
hardware_data = await self.mina_service.device_list()
|
||||
self.device2hardware = {}
|
||||
self.did2device = {}
|
||||
for h in hardware_data:
|
||||
if did := self.config.mi_did:
|
||||
if h.get("miotDID", "") == str(did):
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
if h.get("hardware", "") == self.config.hardware:
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
self.log.error(
|
||||
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
||||
)
|
||||
device = h.get("deviceID", "")
|
||||
hardware = h.get("hardware", "")
|
||||
did = h.get("miotDID", "")
|
||||
if device and hardware and did and (did in mi_dids):
|
||||
self.device2hardware[device] = hardware
|
||||
self.did2device[did] = device
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
async def _init_data_hardware(self):
|
||||
if self.config.cookie:
|
||||
# if use cookie do not need init
|
||||
return
|
||||
await self.try_update_device_id()
|
||||
if not self.config.mi_did:
|
||||
devices = await self.miio_service.device_list()
|
||||
try:
|
||||
self.config.mi_did = next(
|
||||
d["did"]
|
||||
for d in devices
|
||||
if d["model"].endswith(self.config.hardware.lower())
|
||||
)
|
||||
except StopIteration:
|
||||
self.log.error(
|
||||
f"cannot find did for hardware: {self.config.hardware} "
|
||||
"please set it via MI_DID env"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption init hardware {e}")
|
||||
|
||||
def get_cookie(self):
|
||||
if self.config.cookie:
|
||||
cookie_jar = parse_cookie_string(self.config.cookie)
|
||||
# set attr from cookie fix #134
|
||||
cookie_dict = cookie_jar.get_dict()
|
||||
self.device_id = cookie_dict["deviceId"]
|
||||
return cookie_jar
|
||||
|
||||
if not os.path.exists(self.mi_token_home):
|
||||
@ -242,12 +209,18 @@ class XiaoMusic:
|
||||
user_data = json.loads(f.read())
|
||||
user_id = user_data.get("userId")
|
||||
service_token = user_data.get("micoapi")[1]
|
||||
device_id = self.get_one_device()
|
||||
cookie_string = COOKIE_TEMPLATE.format(
|
||||
device_id=self.device_id, service_token=service_token, user_id=user_id
|
||||
device_id=device_id, service_token=service_token, user_id=user_id
|
||||
)
|
||||
return parse_cookie_string(cookie_string)
|
||||
|
||||
async def get_latest_ask_from_xiaoai(self, session):
|
||||
def get_one_device(self):
|
||||
device_id = next(iter(self.device2hardware), "")
|
||||
return device_id
|
||||
|
||||
async def get_latest_ask_from_xiaoai(self, session, device_id):
|
||||
cookies = {"deviceId": device_id}
|
||||
retries = 3
|
||||
for i in range(retries):
|
||||
try:
|
||||
@ -257,7 +230,7 @@ class XiaoMusic:
|
||||
timestamp=str(int(time.time() * 1000)),
|
||||
)
|
||||
self.log.debug(f"url:{url}")
|
||||
r = await session.get(url, timeout=timeout)
|
||||
r = await session.get(url, timeout=timeout, cookies=cookies)
|
||||
except Exception as e:
|
||||
self.log.warning(
|
||||
"Execption when get latest ask from xiaoai: %s", str(e)
|
||||
@ -302,16 +275,21 @@ class XiaoMusic:
|
||||
return
|
||||
|
||||
await self.force_stop_xiaoai()
|
||||
try:
|
||||
await self.mina_service.text_to_speech(self.device_id, value)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
await self.text_to_speech(value)
|
||||
|
||||
# 最大等8秒
|
||||
sec = min(8, int(len(value) / 3))
|
||||
await asyncio.sleep(sec)
|
||||
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
|
||||
await self.check_replay()
|
||||
|
||||
async def text_to_speech(self, value):
|
||||
try:
|
||||
for device_id in self.device2hardware:
|
||||
await self.mina_service.text_to_speech(device_id, value)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
# 继续播放被打断的歌曲
|
||||
async def check_replay(self):
|
||||
if self.isplaying() and not self.isdownloading():
|
||||
@ -327,13 +305,17 @@ class XiaoMusic:
|
||||
value = int(value)
|
||||
self._volume = value
|
||||
self.log.info(f"声音设置为{value}")
|
||||
await self.player_set_volume(value)
|
||||
|
||||
async def player_set_volume(self, value):
|
||||
try:
|
||||
await self.mina_service.player_set_volume(self.device_id, value)
|
||||
for device_id in self.device2hardware:
|
||||
await self.mina_service.player_set_volume(device_id, value)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
async def get_if_xiaoai_is_playing(self):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
async def get_if_xiaoai_is_playing(self, device_id):
|
||||
playing_info = await self.mina_service.player_get_status(device_id)
|
||||
self.log.info(playing_info)
|
||||
# WTF xiaomi api
|
||||
is_playing = (
|
||||
@ -342,17 +324,25 @@ class XiaoMusic:
|
||||
)
|
||||
return is_playing
|
||||
|
||||
async def stop_if_xiaoai_is_playing(self):
|
||||
is_playing = await self.get_if_xiaoai_is_playing()
|
||||
async def stop_if_xiaoai_is_playing(self, device_id):
|
||||
is_playing = await self.get_if_xiaoai_is_playing(device_id)
|
||||
if is_playing:
|
||||
# stop it
|
||||
ret = await self.mina_service.player_stop(self.device_id)
|
||||
self.log.info(f"force_stop_xiaoai player_stop ret:{ret}")
|
||||
ret = await self.mina_service.player_stop(device_id)
|
||||
self.log.info(
|
||||
f"force_stop_xiaoai player_stop device_id:{device_id} ret:{ret}"
|
||||
)
|
||||
|
||||
async def force_stop_xiaoai(self):
|
||||
ret = await self.mina_service.player_pause(self.device_id)
|
||||
self.log.info(f"force_stop_xiaoai player_pause ret:{ret}")
|
||||
await self.stop_if_xiaoai_is_playing()
|
||||
try:
|
||||
for device_id in self.device2hardware:
|
||||
ret = await self.mina_service.player_pause(device_id)
|
||||
self.log.info(
|
||||
f"force_stop_xiaoai player_pause device_id:{device_id} ret:{ret}"
|
||||
)
|
||||
await self.stop_if_xiaoai_is_playing(device_id)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
# 是否在下载中
|
||||
def isdownloading(self):
|
||||
@ -722,17 +712,23 @@ class XiaoMusic:
|
||||
|
||||
async def play_url(self, **kwargs):
|
||||
url = kwargs.get("arg1", "")
|
||||
if self.config.use_music_api:
|
||||
ret = await self.play_by_music_url(self.device_id, url)
|
||||
self.log.info(
|
||||
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
|
||||
)
|
||||
else:
|
||||
ret = await self.mina_service.play_by_url(self.device_id, url)
|
||||
self.log.info(
|
||||
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
|
||||
)
|
||||
return ret
|
||||
await self.all_player_play(url)
|
||||
|
||||
async def all_player_play(self, url):
|
||||
try:
|
||||
for device_id in self.device2hardware:
|
||||
if self.config.use_music_api:
|
||||
ret = await self.play_by_music_url(device_id, url)
|
||||
self.log.info(
|
||||
f"player_play play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
|
||||
)
|
||||
else:
|
||||
ret = await self.mina_service.play_by_url(device_id, url)
|
||||
self.log.info(
|
||||
f"player_play play_by_url device_id:{device_id} ret:{ret} url:{url}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error(f"Execption {e}")
|
||||
|
||||
def find_real_music_name(self, name):
|
||||
if not self.config.enable_fuzzy_match:
|
||||
@ -927,7 +923,9 @@ class XiaoMusic:
|
||||
await self.do_set_volume(value)
|
||||
|
||||
async def get_volume(self, **kwargs):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
# 取一个音箱的声音
|
||||
device_id = self.get_one_device()
|
||||
playing_info = await self.mina_service.player_get_status(device_id)
|
||||
self.log.debug("get_volume. playing_info:%s", playing_info)
|
||||
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
||||
"volume", 0
|
||||
@ -1097,9 +1095,10 @@ class XiaoMusic:
|
||||
if arg1 is None:
|
||||
arg1 = {}
|
||||
data = arg1
|
||||
self.log.info(f"debug_play_by_music_url: {data}")
|
||||
device_id = self.get_one_device()
|
||||
self.log.info(f"debug_play_by_music_url: {data} {device_id}")
|
||||
return await self.mina_service.ubus_request(
|
||||
self.device_id,
|
||||
device_id,
|
||||
"player_play_music",
|
||||
"mediaplayer",
|
||||
data,
|
||||
|
Loading…
Reference in New Issue
Block a user