mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
006ea2d283 | ||
|
|
08a22ca03f | ||
|
|
7a32917b63 | ||
|
|
54b4417069 | ||
|
|
17d7f54c20 | ||
|
|
2261b5ba53 | ||
|
|
833cb1a24a | ||
|
|
4494b54c15 | ||
|
|
594db5aa0c |
30
.github/workflows/ci.yml
vendored
Normal file
30
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
# run unless event type is pull_request
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
|
||||
26
README.md
26
README.md
@@ -91,12 +91,12 @@ services:
|
||||
volumes:
|
||||
- ./music:/app/music
|
||||
environment:
|
||||
- MI_USER: '小米账号'
|
||||
- MI_PASS: '小米密码'
|
||||
- MI_DID: 00000
|
||||
- MI_HARDWARE: 'L07A'
|
||||
- XIAOMUSIC_SEARCH: 'bilisearch:'
|
||||
- XIAOMUSIC_HOSTNAME: '192.168.2.5'
|
||||
MI_USER: '小米账号'
|
||||
MI_PASS: '小米密码'
|
||||
MI_DID: 00000
|
||||
MI_HARDWARE: 'L07A'
|
||||
XIAOMUSIC_SEARCH: 'bilisearch:'
|
||||
XIAOMUSIC_HOSTNAME: '192.168.2.5'
|
||||
```
|
||||
|
||||
|
||||
@@ -115,13 +115,13 @@ services:
|
||||
volumes:
|
||||
- ./music:/app/music
|
||||
environment:
|
||||
- MI_USER: '小米账号'
|
||||
- MI_PASS: '小米密码'
|
||||
- MI_DID: 00000
|
||||
- MI_HARDWARE: 'L07A'
|
||||
- XIAOMUSIC_SEARCH: 'ytsearch:'
|
||||
- XIAOMUSIC_PROXY: 'http://192.168.2.5:8080'
|
||||
- XIAOMUSIC_HOSTNAME: '192.168.2.5'
|
||||
MI_USER: '小米账号'
|
||||
MI_PASS: '小米密码'
|
||||
MI_DID: 00000
|
||||
MI_HARDWARE: 'L07A'
|
||||
XIAOMUSIC_SEARCH: 'ytsearch:'
|
||||
XIAOMUSIC_PROXY: 'http://192.168.2.5:8080'
|
||||
XIAOMUSIC_HOSTNAME: '192.168.2.5'
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.1.14"
|
||||
version = "0.1.16"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
|
||||
@@ -398,6 +398,6 @@ yarl==1.9.2 \
|
||||
--hash=sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9 \
|
||||
--hash=sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3 \
|
||||
--hash=sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560
|
||||
yt-dlp==2023.12.30 \
|
||||
--hash=sha256:a11862e57721b0a0f0883dfeb5a4d79ba213a2d4c45e1880e9fd70f8e6570c38 \
|
||||
--hash=sha256:c00d9a71d64472ad441bcaa1ec0c3797d6e60c9f934f270096a96fe51657e7b3
|
||||
yt-dlp==2024.2.2.232707.dev0 \
|
||||
--hash=sha256:7ff05434979d89e2a7c344d6771cc60229901fe4182c19a756472bb49a50e3b8 \
|
||||
--hash=sha256:fd8d85f64842038cbdaaf7ee5812d98beed64fdcb4612f6e80b2c721bad13262
|
||||
|
||||
@@ -53,6 +53,11 @@ def main():
|
||||
dest="config",
|
||||
help="config file path",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ffmpeg_location",
|
||||
dest="ffmpeg_location",
|
||||
help="ffmpeg bin path",
|
||||
)
|
||||
|
||||
options = parser.parse_args()
|
||||
config = Config.from_options(options)
|
||||
|
||||
@@ -66,8 +66,8 @@ KEY_MATCH_ORDER = [
|
||||
]
|
||||
|
||||
SUPPORT_MUSIC_TYPE = [
|
||||
"mp3",
|
||||
"flac",
|
||||
".mp3",
|
||||
".flac",
|
||||
]
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ class Config:
|
||||
search_prefix: str = os.getenv(
|
||||
"XIAOMUSIC_SEARCH", "ytsearch:"
|
||||
) # "bilisearch:" or "ytsearch:"
|
||||
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.proxy:
|
||||
|
||||
@@ -57,6 +57,7 @@ class XiaoMusic:
|
||||
self.port = config.port
|
||||
self.proxy = config.proxy
|
||||
self.search_prefix = config.search_prefix
|
||||
self.ffmpeg_location = config.ffmpeg_location
|
||||
|
||||
# 下载对象
|
||||
self.download_proc = None
|
||||
@@ -66,6 +67,8 @@ class XiaoMusic:
|
||||
self._next_timer = None
|
||||
self._timeout = 0
|
||||
self._volume = 50
|
||||
self._all_music = {}
|
||||
self._play_list = []
|
||||
|
||||
# 关机定时器
|
||||
self._stop_timer = None
|
||||
@@ -76,20 +79,25 @@ class XiaoMusic:
|
||||
self.log.addHandler(RichHandler())
|
||||
self.log.debug(config)
|
||||
|
||||
# 启动时重新生成一次播放列表
|
||||
self.gen_all_music_list()
|
||||
|
||||
self.log.debug("ffmpeg_location: %s", self.ffmpeg_location)
|
||||
|
||||
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
|
||||
# )
|
||||
self.log.debug(
|
||||
"Listening new message, timestamp: %s", self.last_timestamp
|
||||
)
|
||||
await self.get_latest_ask_from_xiaoai(session)
|
||||
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()
|
||||
if (d := time.perf_counter() - start) < 1:
|
||||
# sleep to avoid too many request
|
||||
# self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp)
|
||||
self.log.debug("Sleep %f, timestamp: %s", d, self.last_timestamp)
|
||||
await asyncio.sleep(1 - d)
|
||||
|
||||
async def init_all_data(self, session):
|
||||
@@ -309,7 +317,7 @@ class XiaoMusic:
|
||||
"-o",
|
||||
f"{name}.mp3",
|
||||
"--ffmpeg-location",
|
||||
"./ffmpeg/bin",
|
||||
f"{self.ffmpeg_location}",
|
||||
"--no-playlist",
|
||||
)
|
||||
|
||||
@@ -319,42 +327,73 @@ class XiaoMusic:
|
||||
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||
await self.do_tts(f"正在下载歌曲{name}")
|
||||
|
||||
def get_filename(self, name):
|
||||
filename = os.path.join(self.music_path, name)
|
||||
return filename
|
||||
|
||||
# 本地是否存在歌曲
|
||||
def local_exist(self, name):
|
||||
for tp in SUPPORT_MUSIC_TYPE:
|
||||
filename = self.get_filename(f"{name}.{tp}")
|
||||
self.log.debug("try local_exist. filename:%s", filename)
|
||||
if os.path.exists(filename):
|
||||
return filename
|
||||
def get_filename(self, name):
|
||||
if name not in self._all_music:
|
||||
return ""
|
||||
filename = self._all_music[name]
|
||||
self.log.debug("try get_filename. filename:%s", filename)
|
||||
if os.path.exists(filename):
|
||||
return filename
|
||||
return ""
|
||||
|
||||
# 获取歌曲播放地址
|
||||
def get_file_url(self, filename):
|
||||
self.log.debug("get_file_url. filename:%s", filename)
|
||||
def get_file_url(self, name):
|
||||
filename = self.get_filename(name)
|
||||
self.log.debug("get_file_url. name:%s, filename:%s", name, filename)
|
||||
encoded_name = urllib.parse.quote(filename)
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}"
|
||||
|
||||
# 随机获取一首音乐
|
||||
def random_music(self):
|
||||
files = os.listdir(self.music_path)
|
||||
# 过滤音乐文件
|
||||
music_files = []
|
||||
for file in files:
|
||||
for tp in SUPPORT_MUSIC_TYPE:
|
||||
if file.endswith(f".{tp}"):
|
||||
music_files.append(file)
|
||||
# 递归获取目录下所有歌曲,生成随机播放列表
|
||||
def gen_all_music_list(self):
|
||||
self._all_music = {}
|
||||
for root, dirs, filenames in os.walk(self.music_path):
|
||||
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,
|
||||
)
|
||||
if extension not in SUPPORT_MUSIC_TYPE:
|
||||
continue
|
||||
|
||||
if len(music_files) == 0:
|
||||
# 歌曲名字相同会覆盖
|
||||
self._all_music[name] = os.path.join(root, filename)
|
||||
self._play_list = list(self._all_music.keys())
|
||||
random.shuffle(self._play_list)
|
||||
self.log.debug(self._all_music)
|
||||
|
||||
# 把下载的音乐加入播放列表
|
||||
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:
|
||||
self.log.warning(f"没有随机到歌曲")
|
||||
return ""
|
||||
# 随机选择一个文件
|
||||
music_file = random.choice(music_files)
|
||||
(filename, extension) = os.path.splitext(music_file)
|
||||
self.log.info(f"随机到歌曲{filename}{extension}")
|
||||
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
|
||||
filename = self._play_list[next_index]
|
||||
return filename
|
||||
|
||||
# 获取文件播放时长
|
||||
@@ -367,8 +406,9 @@ class XiaoMusic:
|
||||
|
||||
# 设置下一首歌曲的播放定时器
|
||||
def set_next_music_timeout(self):
|
||||
sec = int(self.get_file_duration(self.cur_music))
|
||||
self.log.info(f"歌曲{self.cur_music}的时长{sec}秒")
|
||||
filename = self.get_filename(self.cur_music)
|
||||
sec = int(self.get_file_duration(filename))
|
||||
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec} 秒")
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info(f"定时器已取消")
|
||||
@@ -449,15 +489,17 @@ class XiaoMusic:
|
||||
await self.play_next()
|
||||
return
|
||||
|
||||
filename = self.local_exist(name)
|
||||
filename = self.get_filename(name)
|
||||
if len(filename) <= 0:
|
||||
await self.download(name)
|
||||
self.log.info("正在下载中 %s", name)
|
||||
filename = self.get_filename(f"{name}.mp3")
|
||||
await self.download_proc.wait()
|
||||
# 把文件插入到播放列表里
|
||||
self.add_download_music(name)
|
||||
|
||||
self.cur_music = filename
|
||||
url = self.get_file_url(filename)
|
||||
self.cur_music = name
|
||||
self.log.info("cur_music %s", self.cur_music)
|
||||
url = self.get_file_url(name)
|
||||
self.log.info("播放 %s", url)
|
||||
await self.stop_if_xiaoai_is_playing()
|
||||
await self.mina_service.play_by_url(self.device_id, url)
|
||||
@@ -468,10 +510,10 @@ class XiaoMusic:
|
||||
# 下一首
|
||||
async def play_next(self, **kwargs):
|
||||
self.log.info("下一首")
|
||||
(name, _) = os.path.splitext(os.path.basename(self.cur_music))
|
||||
name = self.cur_music
|
||||
self.log.debug("play_next. name:%s, cur_music:%s", name, self.cur_music)
|
||||
if self.play_type == PLAY_TYPE_ALL or name == "":
|
||||
name = self.random_music()
|
||||
name = self.get_next_music()
|
||||
if name == "":
|
||||
await self.do_tts(f"本地没有歌曲")
|
||||
return
|
||||
@@ -491,6 +533,8 @@ class XiaoMusic:
|
||||
async def random_play(self, **kwargs):
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
await self.do_tts(f"已经设置为全部循环并随机播放")
|
||||
# 重新生成随机播放列表
|
||||
self.gen_all_music_list()
|
||||
await self.play_next()
|
||||
|
||||
async def stop(self, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user