feat: 支持多个设备同时播放 see #65
This commit is contained in:
parent
f2675e4340
commit
794e8dcd06
@ -61,6 +61,12 @@ def main():
|
|||||||
dest="ffmpeg_location",
|
dest="ffmpeg_location",
|
||||||
help="ffmpeg bin path",
|
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"))
|
print(LOGO.format(f"XiaoMusic v{__version__} by: github.com/hanxi"))
|
||||||
|
|
||||||
|
@ -59,10 +59,10 @@ def default_key_match_order():
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Config:
|
class Config:
|
||||||
hardware: str = os.getenv("MI_HARDWARE", "L07A")
|
|
||||||
account: str = os.getenv("MI_USER", "")
|
account: str = os.getenv("MI_USER", "")
|
||||||
password: str = os.getenv("MI_PASS", "")
|
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 = ""
|
cookie: str = ""
|
||||||
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
|
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
|
||||||
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
|
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
|
||||||
@ -107,6 +107,7 @@ class Config:
|
|||||||
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
|
os.getenv("XIAOMUSIC_ENABLE_FUZZY_MATCH", "true").lower() == "true"
|
||||||
)
|
)
|
||||||
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
|
stop_tts_msg: str = os.getenv("XIAOMUSIC_STOP_TTS_MSG", "收到,再见")
|
||||||
|
enable_config_example: bool = False
|
||||||
|
|
||||||
keywords_playlocal: str = os.getenv(
|
keywords_playlocal: str = os.getenv(
|
||||||
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
|
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
|
||||||
@ -139,9 +140,9 @@ class Config:
|
|||||||
self.append_user_keyword()
|
self.append_user_keyword()
|
||||||
|
|
||||||
# 保存配置到 config-example.json 文件
|
# 保存配置到 config-example.json 文件
|
||||||
|
if self.enable_config_example:
|
||||||
with open("config-example.json", "w") as f:
|
with open("config-example.json", "w") as f:
|
||||||
data = asdict(self)
|
data = asdict(self)
|
||||||
print(data)
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -13,7 +13,7 @@ from logging.handlers import RotatingFileHandler
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
from miservice import MiAccount, MiIOService, MiNAService
|
from miservice import MiAccount, MiNAService
|
||||||
|
|
||||||
from xiaomusic import (
|
from xiaomusic import (
|
||||||
__version__,
|
__version__,
|
||||||
@ -55,12 +55,12 @@ class XiaoMusic:
|
|||||||
self.last_timestamp = int(time.time() * 1000) # timestamp last call mi speaker
|
self.last_timestamp = int(time.time() * 1000) # timestamp last call mi speaker
|
||||||
self.last_record = None
|
self.last_record = None
|
||||||
self.cookie_jar = None
|
self.cookie_jar = None
|
||||||
self.device_id = ""
|
|
||||||
self.mina_service = None
|
self.mina_service = None
|
||||||
self.miio_service = None
|
|
||||||
self.polling_event = asyncio.Event()
|
self.polling_event = asyncio.Event()
|
||||||
self.new_record_event = asyncio.Event()
|
self.new_record_event = asyncio.Event()
|
||||||
self.queue = queue.Queue()
|
self.queue = queue.Queue()
|
||||||
|
self.device2hardware = {}
|
||||||
|
self.did2device = {}
|
||||||
|
|
||||||
# 下载对象
|
# 下载对象
|
||||||
self.download_proc = None
|
self.download_proc = None
|
||||||
@ -145,12 +145,14 @@ class XiaoMusic:
|
|||||||
|
|
||||||
async def poll_latest_ask(self):
|
async def poll_latest_ask(self):
|
||||||
async with ClientSession() as session:
|
async with ClientSession() as session:
|
||||||
session._cookie_jar = self.cookie_jar
|
|
||||||
while True:
|
while True:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Listening new message, timestamp: %s", self.last_timestamp
|
"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()
|
start = time.perf_counter()
|
||||||
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
|
||||||
await self.polling_event.wait()
|
await self.polling_event.wait()
|
||||||
@ -161,7 +163,7 @@ class XiaoMusic:
|
|||||||
|
|
||||||
async def init_all_data(self, session):
|
async def init_all_data(self, session):
|
||||||
await self.login_miboy(session)
|
await self.login_miboy(session)
|
||||||
await self._init_data_hardware()
|
await self.try_update_device_id()
|
||||||
cookie_jar = self.get_cookie()
|
cookie_jar = self.get_cookie()
|
||||||
if cookie_jar:
|
if cookie_jar:
|
||||||
session.cookie_jar.update_cookies(cookie_jar)
|
session.cookie_jar.update_cookies(cookie_jar)
|
||||||
@ -177,61 +179,26 @@ class XiaoMusic:
|
|||||||
# Forced login to refresh to refresh token
|
# Forced login to refresh to refresh token
|
||||||
await account.login("micoapi")
|
await account.login("micoapi")
|
||||||
self.mina_service = MiNAService(account)
|
self.mina_service = MiNAService(account)
|
||||||
self.miio_service = MiIOService(account)
|
|
||||||
|
|
||||||
async def try_update_device_id(self):
|
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:
|
try:
|
||||||
|
mi_dids = self.config.mi_did.split(",")
|
||||||
hardware_data = await self.mina_service.device_list()
|
hardware_data = await self.mina_service.device_list()
|
||||||
|
self.device2hardware = {}
|
||||||
|
self.did2device = {}
|
||||||
for h in hardware_data:
|
for h in hardware_data:
|
||||||
if did := self.config.mi_did:
|
device = h.get("deviceID", "")
|
||||||
if h.get("miotDID", "") == str(did):
|
hardware = h.get("hardware", "")
|
||||||
self.device_id = h.get("deviceID")
|
did = h.get("miotDID", "")
|
||||||
break
|
if device and hardware and did and (did in mi_dids):
|
||||||
else:
|
self.device2hardware[device] = hardware
|
||||||
continue
|
self.did2device[did] = device
|
||||||
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"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.error(f"Execption {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):
|
def get_cookie(self):
|
||||||
if self.config.cookie:
|
if self.config.cookie:
|
||||||
cookie_jar = parse_cookie_string(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
|
return cookie_jar
|
||||||
|
|
||||||
if not os.path.exists(self.mi_token_home):
|
if not os.path.exists(self.mi_token_home):
|
||||||
@ -242,12 +209,18 @@ class XiaoMusic:
|
|||||||
user_data = json.loads(f.read())
|
user_data = json.loads(f.read())
|
||||||
user_id = user_data.get("userId")
|
user_id = user_data.get("userId")
|
||||||
service_token = user_data.get("micoapi")[1]
|
service_token = user_data.get("micoapi")[1]
|
||||||
|
device_id = self.get_one_device()
|
||||||
cookie_string = COOKIE_TEMPLATE.format(
|
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)
|
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
|
retries = 3
|
||||||
for i in range(retries):
|
for i in range(retries):
|
||||||
try:
|
try:
|
||||||
@ -257,7 +230,7 @@ class XiaoMusic:
|
|||||||
timestamp=str(int(time.time() * 1000)),
|
timestamp=str(int(time.time() * 1000)),
|
||||||
)
|
)
|
||||||
self.log.debug(f"url:{url}")
|
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:
|
except Exception as e:
|
||||||
self.log.warning(
|
self.log.warning(
|
||||||
"Execption when get latest ask from xiaoai: %s", str(e)
|
"Execption when get latest ask from xiaoai: %s", str(e)
|
||||||
@ -302,16 +275,21 @@ class XiaoMusic:
|
|||||||
return
|
return
|
||||||
|
|
||||||
await self.force_stop_xiaoai()
|
await self.force_stop_xiaoai()
|
||||||
try:
|
await self.text_to_speech(value)
|
||||||
await self.mina_service.text_to_speech(self.device_id, value)
|
|
||||||
except Exception as e:
|
|
||||||
self.log.error(f"Execption {e}")
|
|
||||||
# 最大等8秒
|
# 最大等8秒
|
||||||
sec = min(8, int(len(value) / 3))
|
sec = min(8, int(len(value) / 3))
|
||||||
await asyncio.sleep(sec)
|
await asyncio.sleep(sec)
|
||||||
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
|
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
|
||||||
await self.check_replay()
|
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):
|
async def check_replay(self):
|
||||||
if self.isplaying() and not self.isdownloading():
|
if self.isplaying() and not self.isdownloading():
|
||||||
@ -327,13 +305,17 @@ class XiaoMusic:
|
|||||||
value = int(value)
|
value = int(value)
|
||||||
self._volume = value
|
self._volume = value
|
||||||
self.log.info(f"声音设置为{value}")
|
self.log.info(f"声音设置为{value}")
|
||||||
|
await self.player_set_volume(value)
|
||||||
|
|
||||||
|
async def player_set_volume(self, value):
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
self.log.error(f"Execption {e}")
|
self.log.error(f"Execption {e}")
|
||||||
|
|
||||||
async def get_if_xiaoai_is_playing(self):
|
async def get_if_xiaoai_is_playing(self, device_id):
|
||||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
playing_info = await self.mina_service.player_get_status(device_id)
|
||||||
self.log.info(playing_info)
|
self.log.info(playing_info)
|
||||||
# WTF xiaomi api
|
# WTF xiaomi api
|
||||||
is_playing = (
|
is_playing = (
|
||||||
@ -342,17 +324,25 @@ class XiaoMusic:
|
|||||||
)
|
)
|
||||||
return is_playing
|
return is_playing
|
||||||
|
|
||||||
async def stop_if_xiaoai_is_playing(self):
|
async def stop_if_xiaoai_is_playing(self, device_id):
|
||||||
is_playing = await self.get_if_xiaoai_is_playing()
|
is_playing = await self.get_if_xiaoai_is_playing(device_id)
|
||||||
if is_playing:
|
if is_playing:
|
||||||
# stop it
|
# stop it
|
||||||
ret = await self.mina_service.player_stop(self.device_id)
|
ret = await self.mina_service.player_stop(device_id)
|
||||||
self.log.info(f"force_stop_xiaoai player_stop ret:{ret}")
|
self.log.info(
|
||||||
|
f"force_stop_xiaoai player_stop device_id:{device_id} ret:{ret}"
|
||||||
|
)
|
||||||
|
|
||||||
async def force_stop_xiaoai(self):
|
async def force_stop_xiaoai(self):
|
||||||
ret = await self.mina_service.player_pause(self.device_id)
|
try:
|
||||||
self.log.info(f"force_stop_xiaoai player_pause ret:{ret}")
|
for device_id in self.device2hardware:
|
||||||
await self.stop_if_xiaoai_is_playing()
|
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):
|
def isdownloading(self):
|
||||||
@ -722,17 +712,23 @@ class XiaoMusic:
|
|||||||
|
|
||||||
async def play_url(self, **kwargs):
|
async def play_url(self, **kwargs):
|
||||||
url = kwargs.get("arg1", "")
|
url = kwargs.get("arg1", "")
|
||||||
|
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:
|
if self.config.use_music_api:
|
||||||
ret = await self.play_by_music_url(self.device_id, url)
|
ret = await self.play_by_music_url(device_id, url)
|
||||||
self.log.info(
|
self.log.info(
|
||||||
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
|
f"player_play play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ret = await self.mina_service.play_by_url(self.device_id, url)
|
ret = await self.mina_service.play_by_url(device_id, url)
|
||||||
self.log.info(
|
self.log.info(
|
||||||
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
|
f"player_play play_by_url device_id:{device_id} ret:{ret} url:{url}"
|
||||||
)
|
)
|
||||||
return ret
|
except Exception as e:
|
||||||
|
self.log.error(f"Execption {e}")
|
||||||
|
|
||||||
def find_real_music_name(self, name):
|
def find_real_music_name(self, name):
|
||||||
if not self.config.enable_fuzzy_match:
|
if not self.config.enable_fuzzy_match:
|
||||||
@ -927,7 +923,9 @@ class XiaoMusic:
|
|||||||
await self.do_set_volume(value)
|
await self.do_set_volume(value)
|
||||||
|
|
||||||
async def get_volume(self, **kwargs):
|
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.log.debug("get_volume. playing_info:%s", playing_info)
|
||||||
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
||||||
"volume", 0
|
"volume", 0
|
||||||
@ -1097,9 +1095,10 @@ class XiaoMusic:
|
|||||||
if arg1 is None:
|
if arg1 is None:
|
||||||
arg1 = {}
|
arg1 = {}
|
||||||
data = 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(
|
return await self.mina_service.ubus_request(
|
||||||
self.device_id,
|
device_id,
|
||||||
"player_play_music",
|
"player_play_music",
|
||||||
"mediaplayer",
|
"mediaplayer",
|
||||||
data,
|
data,
|
||||||
|
Loading…
Reference in New Issue
Block a user