xiaomusic/xiaomusic/xiaomusic.py

939 lines
32 KiB
Python
Raw Normal View History

2023-10-14 11:50:32 +00:00
#!/usr/bin/env python3
import asyncio
import json
import logging
import os
2024-06-14 01:58:10 +00:00
import queue
2023-10-14 11:50:32 +00:00
import random
import re
import time
import traceback
2024-06-14 01:58:10 +00:00
import urllib.parse
2023-10-14 11:50:32 +00:00
from pathlib import Path
2023-10-14 11:50:32 +00:00
from aiohttp import ClientSession, ClientTimeout
2024-06-24 00:45:41 +00:00
from miservice import MiAccount, MiIOService, MiNAService
2024-06-14 01:58:10 +00:00
from xiaomusic import (
__version__,
)
from xiaomusic.config import (
KEY_WORD_ARG_BEFORE_DICT,
Config,
)
2024-06-27 23:20:43 +00:00
from xiaomusic.const import (
2023-10-14 11:50:32 +00:00
COOKIE_TEMPLATE,
2024-06-14 01:58:10 +00:00
LATEST_ASK_API,
2024-01-27 12:56:07 +00:00
SUPPORT_MUSIC_TYPE,
2024-06-27 23:20:43 +00:00
)
2024-06-14 01:58:10 +00:00
from xiaomusic.httpserver import StartHTTPServer
2023-10-14 11:50:32 +00:00
from xiaomusic.utils import (
2024-06-20 15:45:23 +00:00
custom_sort_key,
2024-04-30 12:47:57 +00:00
fuzzyfinder,
2024-06-25 11:13:46 +00:00
get_local_music_duration,
get_random,
2024-06-25 11:13:46 +00:00
get_web_music_duration,
2024-06-14 01:58:10 +00:00
parse_cookie_string,
walk_to_depth,
2024-05-16 23:43:39 +00:00
)
2023-10-14 11:50:32 +00:00
EOF = object()
2024-01-27 12:56:07 +00:00
PLAY_TYPE_ONE = 0 # 单曲循环
PLAY_TYPE_ALL = 1 # 全部循环
PLAY_TYPE_RND = 2 # 随机播放
2024-01-27 12:56:07 +00:00
2024-06-14 01:58:10 +00:00
2023-10-14 11:50:32 +00:00
class XiaoMusic:
def __init__(self, config: Config):
self.config = config
self.mi_token_home = Path.home() / ".mi.token"
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()
2024-05-19 15:11:43 +00:00
self.queue = queue.Queue()
2023-10-14 11:50:32 +00:00
self.music_path = config.music_path
self.conf_path = config.conf_path
if not self.conf_path:
self.conf_path = config.music_path
2023-10-14 11:50:32 +00:00
self.hostname = config.hostname
self.port = config.port
2023-10-15 02:58:53 +00:00
self.proxy = config.proxy
2024-02-02 12:55:51 +00:00
self.search_prefix = config.search_prefix
2024-02-24 04:49:17 +00:00
self.ffmpeg_location = config.ffmpeg_location
self.active_cmd = config.active_cmd.split(",")
2024-06-16 05:40:30 +00:00
self.exclude_dirs = set(config.exclude_dirs.split(","))
self.music_path_depth = config.music_path_depth
2023-10-14 11:50:32 +00:00
# 下载对象
self.download_proc = None
# 单曲循环,全部循环
self.play_type = PLAY_TYPE_RND
2023-10-14 11:50:32 +00:00
self.cur_music = ""
self._next_timer = None
self._timeout = 0
2024-05-16 22:39:05 +00:00
self._volume = 0
self._all_music = {}
2024-06-25 11:13:46 +00:00
self._all_radio = {} # 电台列表
self._play_list = []
2024-06-12 17:12:07 +00:00
self._cur_play_list = ""
2024-06-14 01:58:10 +00:00
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
self._playing = False
2023-10-14 11:50:32 +00:00
# 关机定时器
self._stop_timer = None
2023-10-14 11:50:32 +00:00
# setup logger
2024-05-16 23:43:39 +00:00
logging.basicConfig(
2024-06-20 15:45:23 +00:00
format=f"%(asctime)s [{__version__}] [%(levelname)s] %(message)s",
2024-05-16 23:43:39 +00:00
datefmt="[%X]",
)
2023-10-14 11:50:32 +00:00
self.log = logging.getLogger("xiaomusic")
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
self.log.debug(config)
2024-05-19 15:11:43 +00:00
# 尝试从设置里加载配置
self.try_init_setting()
# 启动时重新生成一次播放列表
2024-06-12 17:12:07 +00:00
self._gen_all_music_list()
2024-05-16 22:39:05 +00:00
# 启动时初始化获取声音
self.set_last_record("get_volume#")
2024-06-26 00:22:23 +00:00
self.log.info("ffmpeg_location: %s", self.ffmpeg_location)
2023-10-14 11:50:32 +00:00
async def poll_latest_ask(self):
async with ClientSession() as session:
session._cookie_jar = self.cookie_jar
while True:
2024-02-04 06:04:18 +00:00
self.log.debug(
"Listening new message, timestamp: %s", self.last_timestamp
)
2023-10-14 11:50:32 +00:00
await self.get_latest_ask_from_xiaoai(session)
start = time.perf_counter()
2024-02-04 06:04:18 +00:00
self.log.debug("Polling_event, timestamp: %s", self.last_timestamp)
2023-10-14 11:50:32 +00:00
await self.polling_event.wait()
if (d := time.perf_counter() - start) < 1:
# sleep to avoid too many request
2024-02-04 06:04:18 +00:00
self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp)
2023-10-14 11:50:32 +00:00
await asyncio.sleep(1 - d)
async def init_all_data(self, session):
await self.login_miboy(session)
await self._init_data_hardware()
2024-06-24 16:03:10 +00:00
cookie_jar = self.get_cookie()
if cookie_jar:
session.cookie_jar.update_cookies(cookie_jar)
2023-10-14 11:50:32 +00:00
self.cookie_jar = session.cookie_jar
async def login_miboy(self, session):
account = MiAccount(
session,
self.config.account,
self.config.password,
str(self.mi_token_home),
)
# Forced login to refresh to refresh token
await account.login("micoapi")
self.mina_service = MiNAService(account)
self.miio_service = MiIOService(account)
2024-05-19 15:11:43 +00:00
async def try_update_device_id(self):
2023-10-14 11:50:32 +00:00
# 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
2024-06-24 15:38:41 +00:00
try:
hardware_data = await self.mina_service.device_list()
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:
2023-10-14 11:50:32 +00:00
self.device_id = h.get("deviceID")
break
2024-06-24 15:38:41 +00:00
else:
self.log.error(
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
)
2024-06-24 16:03:10 +00:00
except Exception as e:
self.log.error(f"Execption {e}")
2024-05-19 15:11:43 +00:00
async def _init_data_hardware(self):
if self.config.cookie:
# if use cookie do not need init
return
await self.try_update_device_id()
2023-10-14 11:50:32 +00:00
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(
2023-10-14 11:50:32 +00:00
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}")
2023-10-14 11:50:32 +00:00
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
2024-06-24 16:03:10 +00:00
if not os.path.exists(self.mi_token_home):
self.log.error(f"{self.mi_token_home} file not exist")
return None
with open(self.mi_token_home) as f:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
cookie_string = COOKIE_TEMPLATE.format(
device_id=self.device_id, service_token=service_token, user_id=user_id
)
return parse_cookie_string(cookie_string)
2023-10-14 11:50:32 +00:00
async def get_latest_ask_from_xiaoai(self, session):
retries = 3
for i in range(retries):
try:
timeout = ClientTimeout(total=15)
2024-06-20 15:45:23 +00:00
url = LATEST_ASK_API.format(
hardware=self.config.hardware,
timestamp=str(int(time.time() * 1000)),
2023-10-14 11:50:32 +00:00
)
2024-06-20 15:45:23 +00:00
self.log.debug(f"url:{url}")
r = await session.get(url, timeout=timeout)
2023-10-14 11:50:32 +00:00
except Exception as e:
self.log.warning(
"Execption when get latest ask from xiaoai: %s", str(e)
)
continue
try:
data = await r.json()
2024-06-24 16:03:10 +00:00
except Exception as e:
self.log.warning(f"get latest ask from xiaoai error {e}, retry")
2023-10-14 11:50:32 +00:00
if i == 2:
# tricky way to fix #282 #272 # if it is the third time we re init all data
self.log.info("Maybe outof date trying to re init it")
await self.init_all_data(self.session)
else:
return self._get_last_query(data)
def _get_last_query(self, data):
2024-06-21 14:16:28 +00:00
self.log.debug(f"_get_last_query:{data}")
2023-10-14 11:50:32 +00:00
if d := data.get("data"):
records = json.loads(d).get("records")
if not records:
return
last_record = records[0]
timestamp = last_record.get("time")
if timestamp > self.last_timestamp:
self.last_timestamp = timestamp
self.last_record = last_record
self.new_record_event.set()
# 手动发消息
def set_last_record(self, query):
self.last_record = {
"query": query,
"ctrl_panel": True,
}
self.new_record_event.set()
2024-05-16 22:39:05 +00:00
async def do_tts(self, value):
2023-10-14 11:50:32 +00:00
self.log.info("do_tts: %s", value)
2024-06-24 00:45:41 +00:00
await self.force_stop_xiaoai()
try:
await self.mina_service.text_to_speech(self.device_id, value)
2024-06-24 16:03:10 +00:00
except Exception as e:
self.log.error(f"Execption {e}")
2024-06-26 01:18:58 +00:00
self.log.debug(f"do_tts. cur_music:{self.cur_music}")
2024-06-26 01:10:26 +00:00
if self._playing and not self.is_downloading():
2024-06-25 01:03:01 +00:00
# 继续播放歌曲
await self.play()
2023-10-14 11:50:32 +00:00
2024-01-29 15:10:16 +00:00
async def do_set_volume(self, value):
2024-01-30 00:21:31 +00:00
value = int(value)
2024-05-16 23:06:30 +00:00
self._volume = value
2024-05-16 22:39:05 +00:00
self.log.info(f"声音设置为{value}")
2024-06-24 00:45:41 +00:00
try:
await self.mina_service.player_set_volume(self.device_id, value)
2024-06-24 16:03:10 +00:00
except Exception as e:
self.log.error(f"Execption {e}")
2024-01-29 15:10:16 +00:00
2024-05-16 22:39:05 +00:00
async def force_stop_xiaoai(self):
2024-05-17 12:27:47 +00:00
await self.mina_service.player_stop(self.device_id)
2023-10-14 11:50:32 +00:00
# 是否在下载中
def is_downloading(self):
if not self.download_proc:
return False
2024-06-14 01:58:10 +00:00
if (
self.download_proc.returncode is not None
and self.download_proc.returncode < 0
):
2023-10-14 11:50:32 +00:00
return False
return True
# 下载歌曲
2024-04-07 23:41:07 +00:00
async def download(self, search_key, name):
2023-10-14 11:50:32 +00:00
if self.download_proc:
try:
self.download_proc.kill()
except ProcessLookupError:
pass
2023-10-14 11:50:32 +00:00
2023-10-16 10:13:09 +00:00
sbp_args = (
2024-01-27 12:56:07 +00:00
"yt-dlp",
f"{self.search_prefix}{search_key}",
2024-01-27 12:56:07 +00:00
"-x",
"--audio-format",
"mp3",
"--paths",
self.music_path,
"-o",
f"{name}.mp3",
"--ffmpeg-location",
2024-02-24 04:49:17 +00:00
f"{self.ffmpeg_location}",
"--no-playlist",
2024-01-27 12:56:07 +00:00
)
2023-10-16 10:13:09 +00:00
if self.proxy:
sbp_args += ("--proxy", f"{self.proxy}")
2024-06-26 01:10:26 +00:00
self.log.info(f"download: {sbp_args}")
2023-10-16 10:13:09 +00:00
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
await self.do_tts(f"正在下载歌曲{search_key}")
2023-10-14 11:50:32 +00:00
def get_filename(self, name):
if name not in self._all_music:
2024-04-30 12:47:57 +00:00
self.log.debug("get_filename not in. name:%s", name)
return ""
filename = self._all_music[name]
self.log.debug("try get_filename. filename:%s", filename)
if os.path.exists(filename):
return filename
2024-01-27 12:56:07 +00:00
return ""
2023-10-14 11:50:32 +00:00
2024-06-25 11:13:46 +00:00
# 判断本地音乐是否存在,网络歌曲不判断
def is_music_exist(self, name):
if name not in self._all_music:
return False
if self.is_web_music(name):
return True
filename = self.get_filename(name)
if filename:
return True
return False
# 是否是网络电台
def is_web_radio_music(self, name):
return name in self._all_radio
# 是否是网络歌曲
def is_web_music(self, name):
if name not in self._all_music:
return False
url = self._all_music[name]
return url.startswith(("http://", "https://"))
2024-06-28 02:33:12 +00:00
# 获取歌曲播放时长,播放地址
async def get_music_sec_url(self, name):
sec = 0
2024-06-27 23:20:43 +00:00
url = self.get_music_url(name)
2024-06-28 02:33:12 +00:00
if self.is_web_radio_music(name):
self.log.info("电台不会有播放时长")
return 0, url
if self.is_web_music(name):
origin_url = url
duration, url = await get_web_music_duration(url)
sec = int(duration)
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
sec = int(get_local_music_duration(filename))
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec}")
if sec <= 0:
self.log.warning(f"获取歌曲时长失败 {name} {url}")
return sec, url
2024-06-27 23:20:43 +00:00
2024-06-25 11:13:46 +00:00
def get_music_url(self, name):
if self.is_web_music(name):
url = self._all_music[name]
self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
return url
filename = self.get_filename(name)
2024-06-25 11:13:46 +00:00
self.log.debug(
"get_music_url local music. name:%s, filename:%s", name, filename
)
encoded_name = urllib.parse.quote(filename)
2024-01-27 12:56:07 +00:00
return f"http://{self.hostname}:{self.port}/{encoded_name}"
2023-10-14 11:50:32 +00:00
# 递归获取目录下所有歌曲,生成随机播放列表
2024-06-12 17:12:07 +00:00
def _gen_all_music_list(self):
self._all_music = {}
2024-06-12 17:12:07 +00:00
all_music_by_dir = {}
for root, dirs, filenames in walk_to_depth(
self.music_path, depth=self.music_path_depth
):
2024-06-16 05:40:30 +00:00
dirs[:] = [d for d in dirs if d not in self.exclude_dirs]
2024-06-12 17:12:07 +00:00
self.log.debug("root:%s dirs:%s music_path:%s", root, dirs, self.music_path)
dir_name = os.path.basename(root)
if self.music_path == root:
dir_name = "其他"
if dir_name not in all_music_by_dir:
all_music_by_dir[dir_name] = {}
for filename in filenames:
self.log.debug("gen_all_music_list. filename:%s", filename)
# 过滤隐藏文件
if filename.startswith("."):
continue
# 过滤非音乐文件
(name, extension) = os.path.splitext(filename)
self.log.debug(
"gen_all_music_list. filename:%s, name:%s, extension:%s",
filename,
name,
extension,
)
2024-06-24 00:19:49 +00:00
if extension.lower() not in SUPPORT_MUSIC_TYPE:
continue
# 歌曲名字相同会覆盖
self._all_music[name] = os.path.join(root, filename)
2024-06-12 17:12:07 +00:00
all_music_by_dir[dir_name][name] = True
self._play_list = list(self._all_music.keys())
2024-06-12 17:12:07 +00:00
self._cur_play_list = "全部"
self._gen_play_list()
self.log.debug(self._all_music)
2024-06-12 17:12:07 +00:00
self._music_list = {}
self._music_list["全部"] = self._play_list
2024-06-14 01:58:10 +00:00
for dir_name, musics in all_music_by_dir.items():
2024-06-12 17:12:07 +00:00
self._music_list[dir_name] = list(musics.keys())
self.log.debug("dir_name:%s, list:%s", dir_name, self._music_list[dir_name])
2024-06-25 11:13:46 +00:00
try:
self._append_music_list()
except Exception as e:
self.log.error(f"Execption _append_music_list {e}")
# 给歌单里补充网络歌单
def _append_music_list(self):
if not self.config.music_list_json:
return
music_list = json.loads(self.config.music_list_json)
try:
for item in music_list:
list_name = item.get("name")
musics = item.get("musics")
if (not list_name) or (not musics):
continue
one_music_list = []
for music in musics:
name = music.get("name")
url = music.get("url")
music_type = music.get("type")
if (not name) or (not url):
continue
self._all_music[name] = url
one_music_list.append(name)
# 处理电台列表
if music_type == "radio":
self._all_radio[name] = url
self.log.debug(one_music_list)
2024-06-25 11:13:46 +00:00
# 歌曲名字相同会覆盖
self._music_list[list_name] = one_music_list
if self._all_radio:
self._music_list["所有电台"] = list(self._all_radio.keys())
self.log.debug(self._all_music)
self.log.debug(self._music_list)
except Exception as e:
self.log.error(f"Execption music_list:{music_list} {e}")
2024-06-12 17:12:07 +00:00
# 歌曲排序或者打乱顺序
def _gen_play_list(self):
if self.play_type == PLAY_TYPE_RND:
random.shuffle(self._play_list)
else:
self._play_list.sort(key=custom_sort_key)
self.log.debug("play_list:%s", self._play_list)
# 把下载的音乐加入播放列表
def add_download_music(self, name):
self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3")
if name not in self._play_list:
self._play_list.append(name)
self.log.debug("add_music %s", name)
self.log.debug(self._play_list)
# 获取下一首
def get_next_music(self):
play_list_len = len(self._play_list)
if play_list_len == 0:
2024-06-14 01:58:10 +00:00
self.log.warning("没有随机到歌曲")
2023-10-14 11:50:32 +00:00
return ""
# 随机选择一个文件
index = 0
try:
index = self._play_list.index(self.cur_music)
except ValueError:
pass
next_index = index + 1
if next_index >= play_list_len:
next_index = 0
name = self._play_list[next_index]
2024-06-25 11:13:46 +00:00
if not self.is_music_exist(name):
self._play_list.pop(next_index)
2024-06-14 01:58:10 +00:00
self.log.info(f"pop not exist music:{name}")
return self.get_next_music()
return name
2023-10-14 11:50:32 +00:00
# 设置下一首歌曲的播放定时器
2024-06-28 02:33:12 +00:00
async def set_next_music_timeout(self, sec):
if sec <= 0:
2024-06-25 11:13:46 +00:00
return
2023-10-14 11:50:32 +00:00
if self._next_timer:
self._next_timer.cancel()
2024-06-14 01:58:10 +00:00
self.log.info("定时器已取消")
2024-06-25 17:27:32 +00:00
2023-10-14 11:50:32 +00:00
self._timeout = sec
async def _do_next():
await asyncio.sleep(self._timeout)
2023-10-16 14:41:00 +00:00
try:
await self.play_next()
except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
2023-10-14 11:50:32 +00:00
self._next_timer = asyncio.ensure_future(_do_next())
2024-06-28 02:33:12 +00:00
self.log.info(f"{sec}秒后将会播放下一首歌曲")
2023-10-14 11:50:32 +00:00
async def run_forever(self):
2024-05-19 15:11:43 +00:00
StartHTTPServer(self.port, self.music_path, self)
2023-10-14 11:50:32 +00:00
async with ClientSession() as session:
self.session = session
await self.init_all_data(session)
task = asyncio.create_task(self.poll_latest_ask())
assert task is not None # to keep the reference to task, do not remove this
2024-06-14 01:58:10 +00:00
filtered_keywords = [
keyword for keyword in self.config.key_match_order if "#" not in keyword
2024-06-14 01:58:10 +00:00
]
2024-05-16 22:43:10 +00:00
joined_keywords = "/".join(filtered_keywords)
2024-06-14 01:58:10 +00:00
self.log.info(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制")
self.log.info(self.config.key_word_dict)
2023-10-14 11:50:32 +00:00
while True:
self.polling_event.set()
await self.new_record_event.wait()
self.new_record_event.clear()
new_record = self.last_record
2024-05-19 15:11:43 +00:00
if new_record is None:
# 其他线程的函数调用
try:
func, callback, arg1 = self.queue.get(False)
ret = await func(arg1=arg1)
callback(ret)
except queue.Empty:
pass
continue
2023-10-14 11:50:32 +00:00
self.polling_event.clear() # stop polling when processing the question
query = new_record.get("query", "").strip()
ctrl_panel = new_record.get("ctrl_panel", False)
self.log.debug("收到消息:%s 控制面板:%s", query, ctrl_panel)
2023-10-14 11:50:32 +00:00
# 匹配命令
opvalue, oparg = self.match_cmd(query, ctrl_panel)
if not opvalue:
2023-10-14 11:50:32 +00:00
await asyncio.sleep(1)
continue
try:
func = getattr(self, opvalue)
await func(arg1=oparg)
2023-10-14 11:50:32 +00:00
except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
# 匹配命令
def match_cmd(self, query, ctrl_panel):
for opkey in self.config.key_match_order:
patternarg = rf"(.*){opkey}(.*)"
# 匹配参数
matcharg = re.match(patternarg, query)
if not matcharg:
2024-01-29 15:10:16 +00:00
# self.log.debug(patternarg)
continue
argpre = matcharg.groups()[0]
argafter = matcharg.groups()[1]
self.log.debug(
"matcharg. opkey:%s, argpre:%s, argafter:%s",
opkey,
argpre,
argafter,
)
oparg = argafter
opvalue = self.config.key_word_dict.get(opkey)
if not ctrl_panel and not self._playing:
if self.active_cmd and opvalue not in self.active_cmd:
self.log.debug(f"不在激活命令中 {opvalue}")
continue
if opkey in KEY_WORD_ARG_BEFORE_DICT:
oparg = argpre
self.log.info(
"匹配到指令. opkey:%s opvalue:%s oparg:%s", opkey, opvalue, oparg
)
return (opvalue, oparg)
if self._playing:
self.log.info("未匹配到指令,自动停止")
return ("stop", {})
return (None, None)
2024-06-25 01:03:01 +00:00
# 判断是否播放下一首歌曲
def check_play_next(self):
# 当前没我在播放的歌曲
if self.cur_music == "":
return True
else:
# 当前播放的歌曲不存在了
2024-06-26 01:18:58 +00:00
if not self.is_music_exist(self.cur_music):
return True
return False
2024-06-27 16:36:53 +00:00
async def _play_by_music_url(self, device_id, url):
audio_id = get_random(30)
audio_type = ""
2024-06-28 02:33:12 +00:00
if self.config.hardware in ["LX04", "X10A", "X08A"]:
audio_type = "MUSIC"
2024-06-27 16:36:53 +00:00
music = {
"payload": {
"audio_items": [
{"item_id": {"audio_id": audio_id}, "stream": {"url": url}}
],
"audio_type": audio_type,
2024-06-27 16:36:53 +00:00
}
}
return await self.mina_service.ubus_request(
device_id,
"player_play_music",
"mediaplayer",
{"startaudioid": audio_id, "music": json.dumps(music)},
)
async def play_url(self, url):
if self.config.use_music_api:
2024-06-27 16:36:53 +00:00
ret = await self._play_by_music_url(self.device_id, url)
self.log.debug(
f"play_url play_by_music_url {self.config.hardware}. ret:{ret} url:{url}"
)
else:
2024-06-27 16:36:53 +00:00
ret = await self.mina_service.play_by_url(self.device_id, url)
self.log.debug(
f"play_url play_by_url {self.config.hardware}. ret:{ret} url:{url}"
)
# 播放本地歌曲
async def playlocal(self, **kwargs):
name = kwargs.get("arg1", "")
if name == "":
if self.check_play_next():
await self.play_next()
return
else:
name = self.cur_music
self.log.info(f"playlocal. name:{name}")
# 本地歌曲不存在时下载
if not self.is_music_exist(name):
await self.do_tts(f"本地不存在歌曲{name}")
return
await self._playmusic(name)
async def _playmusic(self, name):
self._playing = True
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
2024-06-28 02:33:12 +00:00
sec, url = await self.get_music_sec_url(name)
self.log.info(f"播放 {url}")
await self.force_stop_xiaoai()
await self.play_url(url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
2024-06-28 02:33:12 +00:00
await self.set_next_music_timeout(sec)
2023-10-14 11:50:32 +00:00
# 播放歌曲
async def play(self, **kwargs):
2024-06-26 00:17:29 +00:00
parts = kwargs.get("arg1", "").split("|")
2024-04-07 23:41:07 +00:00
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
2024-04-30 12:47:57 +00:00
if name == "":
name = search_key
if search_key == "" and name == "":
if self.check_play_next():
await self.play_next()
return
else:
name = self.cur_music
2024-06-26 01:10:26 +00:00
self.log.info("play. search_key:%s name:%s", search_key, name)
2024-06-25 11:13:46 +00:00
# 本地歌曲不存在时下载
if not self.is_music_exist(name):
if self.config.disable_download:
await self.do_tts(f"本地不存在歌曲{name}")
return
2024-04-07 23:41:07 +00:00
await self.download(search_key, name)
self.log.info("正在下载中 %s", search_key + ":" + name)
2023-10-14 11:50:32 +00:00
await self.download_proc.wait()
# 把文件插入到播放列表里
self.add_download_music(name)
await self._playmusic(name)
2023-10-14 11:50:32 +00:00
# 下一首
async def play_next(self, **kwargs):
self.log.info("下一首")
name = self.cur_music
2024-01-27 15:00:17 +00:00
self.log.debug("play_next. name:%s, cur_music:%s", name, self.cur_music)
if (
self.play_type == PLAY_TYPE_ALL
or self.play_type == PLAY_TYPE_RND
or name == ""
):
name = self.get_next_music()
2023-10-14 11:50:32 +00:00
if name == "":
2024-06-14 01:58:10 +00:00
await self.do_tts("本地没有歌曲")
2023-10-14 11:50:32 +00:00
return
2024-01-28 11:14:48 +00:00
await self.play(arg1=name)
2023-10-14 11:50:32 +00:00
# 单曲循环
async def set_play_type_one(self, **kwargs):
self.play_type = PLAY_TYPE_ONE
2024-06-14 01:58:10 +00:00
await self.do_tts("已经设置为单曲循环")
2023-10-14 11:50:32 +00:00
# 全部循环
async def set_play_type_all(self, **kwargs):
self.play_type = PLAY_TYPE_ALL
self._gen_play_list()
2024-06-14 01:58:10 +00:00
await self.do_tts("已经设置为全部循环")
2023-10-14 11:50:32 +00:00
2024-01-27 15:00:17 +00:00
# 随机播放
async def random_play(self, **kwargs):
self.play_type = PLAY_TYPE_RND
self._gen_play_list()
2024-06-14 01:58:10 +00:00
await self.do_tts("已经设置为随机播放")
2024-06-12 17:12:07 +00:00
2024-06-12 17:21:09 +00:00
# 刷新列表
2024-06-12 17:12:07 +00:00
async def gen_music_list(self, **kwargs):
self._gen_all_music_list()
2024-06-14 15:14:34 +00:00
# 删除歌曲
def del_music(self, name):
filename = self.get_filename(name)
if filename == "":
self.log.info(f"${name} not exist")
return
try:
os.remove(filename)
self.log.info(f"del ${filename} success")
except OSError:
self.log.error(f"del ${filename} failed")
self._gen_all_music_list()
2024-06-12 17:12:07 +00:00
# 播放一个播放列表
async def play_music_list(self, **kwargs):
2024-06-26 00:17:29 +00:00
parts = kwargs.get("arg1").split("|")
2024-06-12 17:12:07 +00:00
list_name = parts[0]
if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在")
return
self._play_list = self._music_list[list_name]
self._cur_play_list = list_name
self._gen_play_list()
2024-06-12 17:12:07 +00:00
self.log.info(f"开始播放列表{list_name}")
music_name = ""
if len(parts) > 1:
music_name = parts[1]
else:
music_name = self.get_next_music()
await self.play(arg1=music_name)
2024-01-27 15:00:17 +00:00
2023-10-14 11:50:32 +00:00
async def stop(self, **kwargs):
self._playing = False
2023-10-14 11:50:32 +00:00
if self._next_timer:
self._next_timer.cancel()
2024-06-14 01:58:10 +00:00
self.log.info("定时器已取消")
2024-05-16 22:39:05 +00:00
await self.force_stop_xiaoai()
async def stop_after_minute(self, **kwargs):
if self._stop_timer:
self._stop_timer.cancel()
2024-06-14 01:58:10 +00:00
self.log.info("关机定时器已取消")
2024-06-26 00:17:29 +00:00
minute = int(kwargs.get("arg1", 0))
async def _do_stop():
await asyncio.sleep(minute * 60)
try:
await self.stop()
except Exception as e:
self.log.warning(f"执行出错 {str(e)}\n{traceback.format_exc()}")
self._stop_timer = asyncio.ensure_future(_do_stop())
self.log.info(f"{minute}分钟后将关机")
2024-01-29 15:10:16 +00:00
async def set_volume(self, **kwargs):
2024-06-26 00:17:29 +00:00
value = kwargs.get("arg1", 0)
2024-01-29 15:10:16 +00:00
await self.do_set_volume(value)
2024-05-16 22:39:05 +00:00
async def get_volume(self, **kwargs):
playing_info = await self.mina_service.player_get_status(self.device_id)
self.log.debug("get_volume. playing_info:%s", playing_info)
2024-06-14 01:58:10 +00:00
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 5
)
2024-05-16 22:39:05 +00:00
self.log.info("get_volume. volume:%s", self._volume)
def get_volume_ret(self):
return self._volume
2024-04-30 12:47:57 +00:00
# 搜索音乐
def searchmusic(self, name):
2024-06-12 17:12:07 +00:00
all_music_list = list(self._all_music.keys())
search_list = fuzzyfinder(name, all_music_list)
2024-04-30 12:47:57 +00:00
self.log.debug("searchmusic. name:%s search_list:%s", name, search_list)
return search_list
2024-06-12 17:12:07 +00:00
# 获取播放列表
def get_music_list(self):
return self._music_list
# 获取当前的播放列表
def get_cur_play_list(self):
return self._cur_play_list
# 正在播放中的音乐
def playingmusic(self):
self.log.debug("playingmusic. cur_music:%s", self.cur_music)
return self.cur_music
2024-05-19 15:11:43 +00:00
2024-06-25 01:03:01 +00:00
# 当前是否正在播放歌曲
def isplaying(self):
return self._playing
2024-05-19 15:11:43 +00:00
# 获取当前配置
def getconfig(self):
return self.config
# 获取设置文件
def getsettingfile(self):
if not os.path.exists(self.conf_path):
os.makedirs(self.conf_path)
filename = os.path.join(self.conf_path, "setting.json")
return filename
2024-05-19 15:11:43 +00:00
def try_init_setting(self):
try:
filename = self.getsettingfile()
2024-05-19 15:11:43 +00:00
with open(filename) as f:
data = json.loads(f.read())
self.update_config_from_setting(data)
except FileNotFoundError:
self.log.info(f"The file {filename} does not exist.")
except json.JSONDecodeError:
self.log.warning(f"The file {filename} contains invalid JSON.")
except Exception as e:
self.log.error(f"Execption init setting {e}")
2024-05-19 15:11:43 +00:00
# 保存配置并重新启动
async def saveconfig(self, data):
# 默认暂时配置保存到 music 目录下
filename = self.getsettingfile()
2024-06-14 01:58:10 +00:00
with open(filename, "w", encoding="utf-8") as f:
2024-05-19 15:11:43 +00:00
json.dump(data, f, ensure_ascii=False, indent=4)
self.update_config_from_setting(data)
await self.call_main_thread_function(self.reinit)
def update_config_from_setting(self, data):
2024-06-25 11:13:46 +00:00
self.config.mi_did = data.get("mi_did")
self.config.hardware = data.get("mi_hardware")
self.config.search_prefix = data.get("xiaomusic_search")
self.config.proxy = data.get("xiaomusic_proxy")
self.config.music_list_url = data.get("xiaomusic_music_list_url")
self.config.music_list_json = data.get("xiaomusic_music_list_json")
2024-05-19 15:11:43 +00:00
self.search_prefix = self.config.search_prefix
self.proxy = self.config.proxy
self.log.debug("update_config_from_setting ok. data:%s", data)
2024-05-19 15:11:43 +00:00
# 重新初始化
async def reinit(self, **kwargs):
await self.try_update_device_id()
2024-06-25 11:13:46 +00:00
self._gen_all_music_list()
2024-05-19 15:11:43 +00:00
self.log.info("reinit success")
# 获取所有设备
async def getalldevices(self, **kwargs):
did_list = []
hardware_list = []
hardware_data = await self.mina_service.device_list()
for h in hardware_data:
did = h.get("miotDID", "")
if did != "":
did_list.append(did)
hardware = h.get("hardware", "")
if h.get("hardware", "") != "":
hardware_list.append(hardware)
alldevices = {
"did_list": did_list,
"hardware_list": hardware_list,
}
return alldevices
# 用于在web线程里调用
# 获取所有设备
async def call_main_thread_function(self, func, arg1=None):
loop = asyncio.get_event_loop()
future = loop.create_future()
2024-06-14 01:58:10 +00:00
2024-05-19 15:11:43 +00:00
def callback(ret):
nonlocal future
loop.call_soon_threadsafe(future.set_result, ret)
2024-06-14 01:58:10 +00:00
2024-05-19 15:11:43 +00:00
self.queue.put((func, callback, arg1))
self.last_record = None
self.new_record_event.set()
result = await future
return result