mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-11 15:38:14 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a178278576 | ||
|
|
474fea8434 | ||
|
|
d271f7b0f7 | ||
|
|
7e2af515ed | ||
|
|
2d403ff18c | ||
|
|
f08244a990 | ||
|
|
4869e5cf80 | ||
|
|
b887504f9f | ||
|
|
ad43a4f732 | ||
|
|
8699938b61 | ||
|
|
40bd099153 | ||
|
|
750923d5ca | ||
|
|
6d99b30e2d | ||
|
|
a155f16560 | ||
|
|
d4aa045487 | ||
|
|
8080dd9822 | ||
|
|
2eab8d8113 | ||
|
|
b51e56718a | ||
|
|
088d448e10 | ||
|
|
4a89b4bce5 |
@@ -6,7 +6,6 @@ COPY install_dependencies.sh .
|
|||||||
RUN bash install_dependencies.sh
|
RUN bash install_dependencies.sh
|
||||||
|
|
||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/.venv /app/.venv
|
COPY --from=builder /app/.venv /app/.venv
|
||||||
COPY --from=builder /app/ffmpeg /app/ffmpeg
|
COPY --from=builder /app/ffmpeg /app/ffmpeg
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -76,7 +76,7 @@ pdm run xiaomusic.py
|
|||||||
## 在 Docker 里使用
|
## 在 Docker 里使用
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run -e MI_USER=<your-xiaomi-account> \
|
docker run -e MI_USER='your-xiaomi-account' \
|
||||||
-e MI_PASS='your-xiaomi-password' \
|
-e MI_PASS='your-xiaomi-password' \
|
||||||
-e MI_DID='your-xiaomi-speaker-mid' \
|
-e MI_DID='your-xiaomi-speaker-mid' \
|
||||||
-e MI_HARDWARE='L07A' \
|
-e MI_HARDWARE='L07A' \
|
||||||
@@ -186,6 +186,17 @@ services:
|
|||||||
- XIAOMUSIC_SEARCH
|
- XIAOMUSIC_SEARCH
|
||||||
- XIAOMUSIC_PROXY
|
- XIAOMUSIC_PROXY
|
||||||
|
|
||||||
|
## 更多其他可选配置
|
||||||
|
|
||||||
|
- XIAOMUSIC_ACTIVE_CMD 配置唤醒命令,具体见 <https://github.com/hanxi/xiaomusic/pull/43>
|
||||||
|
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录
|
||||||
|
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
|
||||||
|
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台,具体见 <https://github.com/hanxi/xiaomusic/issues/47>
|
||||||
|
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户
|
||||||
|
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码
|
||||||
|
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,记得把目录映射到主机,默认情况会把配置存放在music目录,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
|
||||||
|
- XIAOMUSIC_VERBOSE 设置为 true 时开启 debug 日志,用于排查问题
|
||||||
|
|
||||||
## 讨论区
|
## 讨论区
|
||||||
|
|
||||||
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
|
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
|
||||||
@@ -206,3 +217,6 @@ services:
|
|||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#hanxi/xiaomusic&Date)
|
[](https://star-history.com/#hanxi/xiaomusic&Date)
|
||||||
|
|
||||||
|
## 赞赏
|
||||||
|
谢谢就够了
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "xiaomusic"
|
name = "xiaomusic"
|
||||||
version = "0.1.55"
|
version = "0.1.58"
|
||||||
description = "Play Music with xiaomi AI speaker"
|
description = "Play Music with xiaomi AI speaker"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.1.55"
|
__version__ = "0.1.58"
|
||||||
|
|||||||
@@ -27,20 +27,6 @@ def main():
|
|||||||
dest="cookie",
|
dest="cookie",
|
||||||
help="xiaomi cookie",
|
help="xiaomi cookie",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--use_command",
|
|
||||||
dest="use_command",
|
|
||||||
action="store_true",
|
|
||||||
default=None,
|
|
||||||
help="use command to tts",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--mute_xiaoai",
|
|
||||||
dest="mute_xiaoai",
|
|
||||||
action="store_true",
|
|
||||||
default=None,
|
|
||||||
help="try to mute xiaoai answer",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--verbose",
|
"--verbose",
|
||||||
dest="verbose",
|
dest="verbose",
|
||||||
|
|||||||
@@ -9,28 +9,6 @@ from xiaomusic.utils import validate_proxy
|
|||||||
|
|
||||||
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2"
|
LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2"
|
||||||
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"
|
COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}"
|
||||||
HARDWARE_COMMAND_DICT = {
|
|
||||||
# hardware: (tts_command, wakeup_command, volume_command)
|
|
||||||
"LX06": ("5-1", "5-5", "2-1"),
|
|
||||||
"L05B": ("5-3", "5-4", "2-1"),
|
|
||||||
"S12": ("5-1", "5-5", "2-1"), # 第一代小爱,型号MDZ-25-DA
|
|
||||||
"S12A": ("5-1", "5-5", "2-1"),
|
|
||||||
"LX01": ("5-1", "5-5", "2-1"),
|
|
||||||
"L06A": ("5-1", "5-5", "2-1"),
|
|
||||||
"LX04": ("5-1", "5-4", "2-1"),
|
|
||||||
"L05C": ("5-3", "5-4", "2-1"),
|
|
||||||
"L17A": ("7-3", "7-4", "2-1"),
|
|
||||||
"X08E": ("7-3", "7-4", "2-1"),
|
|
||||||
"LX05A": ("5-1", "5-5", "2-1"), # 小爱红外版
|
|
||||||
"LX5A": ("5-1", "5-5", "2-1"), # 小爱红外版
|
|
||||||
"L07A": ("5-1", "5-5", "2-1"), # Redmi小爱音箱Play(l7a)
|
|
||||||
"L15A": ("7-3", "7-4", "2-1"),
|
|
||||||
"X6A": ("7-3", "7-4", "2-1"), # 小米智能家庭屏6
|
|
||||||
"X10A": ("7-3", "7-4", "2-1"), # 小米智能家庭屏10
|
|
||||||
# add more here
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_COMMAND = ("5-1", "5-5", "2-1")
|
|
||||||
|
|
||||||
KEY_WORD_DICT = {
|
KEY_WORD_DICT = {
|
||||||
"播放歌曲": "play",
|
"播放歌曲": "play",
|
||||||
@@ -73,6 +51,9 @@ KEY_MATCH_ORDER = [
|
|||||||
SUPPORT_MUSIC_TYPE = [
|
SUPPORT_MUSIC_TYPE = [
|
||||||
".mp3",
|
".mp3",
|
||||||
".flac",
|
".flac",
|
||||||
|
".wav",
|
||||||
|
".ape",
|
||||||
|
".cue",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -82,9 +63,7 @@ class Config:
|
|||||||
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", "")
|
||||||
mute_xiaoai: bool = True
|
|
||||||
cookie: str = ""
|
cookie: str = ""
|
||||||
use_command: bool = False
|
|
||||||
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")
|
||||||
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", None)
|
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", None)
|
||||||
@@ -108,18 +87,6 @@ class Config:
|
|||||||
if self.proxy:
|
if self.proxy:
|
||||||
validate_proxy(self.proxy)
|
validate_proxy(self.proxy)
|
||||||
|
|
||||||
@property
|
|
||||||
def tts_command(self) -> str:
|
|
||||||
return HARDWARE_COMMAND_DICT.get(self.hardware, DEFAULT_COMMAND)[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def wakeup_command(self) -> str:
|
|
||||||
return HARDWARE_COMMAND_DICT.get(self.hardware, DEFAULT_COMMAND)[1]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def volume_command(self) -> str:
|
|
||||||
return HARDWARE_COMMAND_DICT.get(self.hardware, DEFAULT_COMMAND)[2]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_options(cls, options: argparse.Namespace) -> Config:
|
def from_options(cls, options: argparse.Namespace) -> Config:
|
||||||
config = {}
|
config = {}
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ def playingmusic():
|
|||||||
return xiaomusic.playingmusic()
|
return xiaomusic.playingmusic()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/isplaying", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def isplaying():
|
||||||
|
return xiaomusic.isplaying()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", methods=["GET"])
|
@app.route("/", methods=["GET"])
|
||||||
def index():
|
def index():
|
||||||
return send_from_directory("static", "index.html")
|
return send_from_directory("static", "index.html")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import mutagen
|
import mutagen
|
||||||
from aiohttp import ClientSession, ClientTimeout
|
from aiohttp import ClientSession, ClientTimeout
|
||||||
from miservice import MiAccount, MiIOService, MiNAService, miio_command
|
from miservice import MiAccount, MiIOService, MiNAService
|
||||||
|
|
||||||
from xiaomusic import (
|
from xiaomusic import (
|
||||||
__version__,
|
__version__,
|
||||||
@@ -127,7 +127,9 @@ 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._init_data_hardware()
|
||||||
session.cookie_jar.update_cookies(self.get_cookie())
|
cookie_jar = self.get_cookie()
|
||||||
|
if cookie_jar:
|
||||||
|
session.cookie_jar.update_cookies(cookie_jar)
|
||||||
self.cookie_jar = session.cookie_jar
|
self.cookie_jar = session.cookie_jar
|
||||||
|
|
||||||
async def login_miboy(self, session):
|
async def login_miboy(self, session):
|
||||||
@@ -143,13 +145,14 @@ class XiaoMusic:
|
|||||||
self.miio_service = MiIOService(account)
|
self.miio_service = MiIOService(account)
|
||||||
|
|
||||||
async def try_update_device_id(self):
|
async def try_update_device_id(self):
|
||||||
hardware_data = await self.mina_service.device_list()
|
|
||||||
# fix multi xiaoai problems we check did first
|
# fix multi xiaoai problems we check did first
|
||||||
# why we use this way to fix?
|
# why we use this way to fix?
|
||||||
# some videos and articles already in the Internet
|
# some videos and articles already in the Internet
|
||||||
# we do not want to change old way, so we check if miotDID in `env` first
|
# we do not want to change old way, so we check if miotDID in `env` first
|
||||||
# to set device id
|
# to set device id
|
||||||
|
|
||||||
|
try:
|
||||||
|
hardware_data = await self.mina_service.device_list()
|
||||||
for h in hardware_data:
|
for h in hardware_data:
|
||||||
if did := self.config.mi_did:
|
if did := self.config.mi_did:
|
||||||
if h.get("miotDID", "") == str(did):
|
if h.get("miotDID", "") == str(did):
|
||||||
@@ -164,6 +167,9 @@ class XiaoMusic:
|
|||||||
self.log.error(
|
self.log.error(
|
||||||
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(f"Execption {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
async def _init_data_hardware(self):
|
async def _init_data_hardware(self):
|
||||||
if self.config.cookie:
|
if self.config.cookie:
|
||||||
@@ -193,7 +199,11 @@ class XiaoMusic:
|
|||||||
cookie_dict = cookie_jar.get_dict()
|
cookie_dict = cookie_jar.get_dict()
|
||||||
self.device_id = cookie_dict["deviceId"]
|
self.device_id = cookie_dict["deviceId"]
|
||||||
return cookie_jar
|
return cookie_jar
|
||||||
else:
|
|
||||||
|
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:
|
with open(self.mi_token_home) as f:
|
||||||
user_data = json.loads(f.read())
|
user_data = json.loads(f.read())
|
||||||
user_id = user_data.get("userId")
|
user_id = user_data.get("userId")
|
||||||
@@ -221,8 +231,8 @@ class XiaoMusic:
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
data = await r.json()
|
data = await r.json()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.log.warning("get latest ask from xiaoai error, retry")
|
self.log.warning(f"get latest ask from xiaoai error {e}, retry")
|
||||||
if i == 2:
|
if i == 2:
|
||||||
# tricky way to fix #282 #272 # if it is the third time we re init all data
|
# 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")
|
self.log.info("Maybe outof date trying to re init it")
|
||||||
@@ -253,42 +263,25 @@ class XiaoMusic:
|
|||||||
|
|
||||||
async def do_tts(self, value):
|
async def do_tts(self, value):
|
||||||
self.log.info("do_tts: %s", value)
|
self.log.info("do_tts: %s", value)
|
||||||
|
|
||||||
if self.config.mute_xiaoai:
|
|
||||||
await self.force_stop_xiaoai()
|
await self.force_stop_xiaoai()
|
||||||
else:
|
|
||||||
# waiting for xiaoai speaker done
|
|
||||||
await asyncio.sleep(8)
|
|
||||||
|
|
||||||
if not self.config.use_command:
|
|
||||||
try:
|
try:
|
||||||
await self.mina_service.text_to_speech(self.device_id, value)
|
await self.mina_service.text_to_speech(self.device_id, value)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
self.log.error(f"Execption {e}")
|
||||||
pass
|
pass
|
||||||
else:
|
if self._playing:
|
||||||
await miio_command(
|
# 继续播放歌曲
|
||||||
self.miio_service,
|
await self.play()
|
||||||
self.config.mi_did,
|
|
||||||
f"{self.config.tts_command} {value}",
|
|
||||||
)
|
|
||||||
|
|
||||||
async def do_set_volume(self, value):
|
async def do_set_volume(self, value):
|
||||||
value = int(value)
|
value = int(value)
|
||||||
self._volume = value
|
self._volume = value
|
||||||
self.log.info(f"声音设置为{value}")
|
self.log.info(f"声音设置为{value}")
|
||||||
if not self.config.use_command:
|
|
||||||
try:
|
try:
|
||||||
self.log.debug("do_set_volume not use_command value:%d", value)
|
|
||||||
await self.mina_service.player_set_volume(self.device_id, value)
|
await self.mina_service.player_set_volume(self.device_id, value)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
self.log.error(f"Execption {e}")
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
self.log.debug("do_set_volume use_command value:%d", value)
|
|
||||||
await miio_command(
|
|
||||||
self.miio_service,
|
|
||||||
self.config.mi_did,
|
|
||||||
f"{self.config.volume_command}=#{value}",
|
|
||||||
)
|
|
||||||
|
|
||||||
async def force_stop_xiaoai(self):
|
async def force_stop_xiaoai(self):
|
||||||
await self.mina_service.player_stop(self.device_id)
|
await self.mina_service.player_stop(self.device_id)
|
||||||
@@ -379,7 +372,7 @@ class XiaoMusic:
|
|||||||
name,
|
name,
|
||||||
extension,
|
extension,
|
||||||
)
|
)
|
||||||
if extension not in SUPPORT_MUSIC_TYPE:
|
if extension.lower() not in SUPPORT_MUSIC_TYPE:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 歌曲名字相同会覆盖
|
# 歌曲名字相同会覆盖
|
||||||
@@ -544,7 +537,7 @@ class XiaoMusic:
|
|||||||
return ("stop", {})
|
return ("stop", {})
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
# 判断是否播放一下私募歌曲
|
# 判断是否播放下一首歌曲
|
||||||
def check_play_next(self):
|
def check_play_next(self):
|
||||||
# 当前没我在播放的歌曲
|
# 当前没我在播放的歌曲
|
||||||
if self.cur_music == "":
|
if self.cur_music == "":
|
||||||
@@ -629,7 +622,6 @@ class XiaoMusic:
|
|||||||
# 刷新列表
|
# 刷新列表
|
||||||
async def gen_music_list(self, **kwargs):
|
async def gen_music_list(self, **kwargs):
|
||||||
self._gen_all_music_list()
|
self._gen_all_music_list()
|
||||||
await self.do_tts("生成播放列表完毕")
|
|
||||||
|
|
||||||
# 删除歌曲
|
# 删除歌曲
|
||||||
def del_music(self, name):
|
def del_music(self, name):
|
||||||
@@ -722,6 +714,10 @@ class XiaoMusic:
|
|||||||
self.log.debug("playingmusic. cur_music:%s", self.cur_music)
|
self.log.debug("playingmusic. cur_music:%s", self.cur_music)
|
||||||
return self.cur_music
|
return self.cur_music
|
||||||
|
|
||||||
|
# 当前是否正在播放歌曲
|
||||||
|
def isplaying(self):
|
||||||
|
return self._playing
|
||||||
|
|
||||||
# 获取当前配置
|
# 获取当前配置
|
||||||
def getconfig(self):
|
def getconfig(self):
|
||||||
return self.config
|
return self.config
|
||||||
|
|||||||
Reference in New Issue
Block a user