1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2025-12-07 15:02:55 +08:00

Compare commits

...

23 Commits

Author SHA1 Message Date
涵曦
b9e1abff6b new version v0.1.22 2024-04-30 12:48:10 +00:00
涵曦
f962fcaa99 新增本地音乐模糊搜索 2024-04-30 12:47:57 +00:00
涵曦
eb35da595f Update README.md 2024-04-29 22:47:54 +08:00
涵曦
26a8b2412f new version v0.1.21 2024-04-08 07:48:02 +08:00
涵曦
601bff4404 修改input显示宽度 2024-04-08 07:47:57 +08:00
涵曦
1fadc9a479 new version v0.1.20 2024-04-08 07:41:16 +08:00
涵曦
69e53569ca 处理空值问题 2024-04-08 07:41:07 +08:00
涵曦
49a43fb997 new version v0.1.19 2024-04-04 20:41:15 +08:00
涵曦
5145ae399d Merge pull request #33 from zhanggaolei001/main
将搜索词分为搜索词及文件名两部分,便于更精确搜索以及实现保存下载保存文件名的自定义
2024-04-03 23:16:44 +08:00
张高磊
612eb636be 将搜索词分为搜索词及文件名两部分,便于更精确搜索以及实现保存下载保存文件名的自定义 2024-04-03 14:12:26 +08:00
涵曦
657af667ef Update README.md 2024-03-18 16:07:50 +08:00
涵曦
9541071c3a new version v0.1.18 2024-02-24 13:26:52 +08:00
涵曦
dfc78b6af5 new version script 2024-02-24 13:26:33 +08:00
涵曦
bea7b2d4eb new version v 2024-02-24 13:23:10 +08:00
涵曦
ddb3e9e03b newversion script 2024-02-24 13:23:03 +08:00
涵曦
812965f054 new version script 2024-02-24 13:22:12 +08:00
涵曦
006ea2d283 new version v0.1.16 2024-02-24 13:08:35 +08:00
涵曦
08a22ca03f 命令行支持 ffmpeg 路径参数 #15 2024-02-24 13:05:36 +08:00
涵曦
7a32917b63 启动时生成一次播放列表,修复下一首越界判断问题 2024-02-24 12:58:01 +08:00
涵曦
54b4417069 支持配置 ffmpeg 路径 #15 2024-02-24 12:49:17 +08:00
涵曦
17d7f54c20 歌曲目录支持子目录,优化随机播放列表 #18 2024-02-24 12:41:26 +08:00
涵曦
2261b5ba53 add ci docker 2024-02-04 14:11:40 +08:00
涵曦
833cb1a24a open debug log 2024-02-04 14:04:18 +08:00
12 changed files with 214 additions and 59 deletions

30
.github/workflows/ci.yml vendored Normal file
View 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 }}

1
.gitignore vendored
View File

@@ -163,3 +163,4 @@ cython_debug/
ffmpeg
music
test.sh

View File

@@ -140,7 +140,8 @@ services:
- [xiaogpt](https://github.com/yihong0618/xiaogpt)
- [MiService](https://github.com/yihong0618/MiService)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
## Star History

37
newversion.sh Normal file → Executable file
View File

@@ -1,7 +1,34 @@
version="$1"
sed -i "s/version.*/version = \"$version\"/" ./pyproject.toml
#!/bin/bash
set -e
version_file=./pyproject.toml
# 获取当前版本号
current_version=$(grep -oE "version = \"[0-9]+\.[0-9]+\.[0-9]+\"" $version_file | cut -d'"' -f2)
echo "当前版本号: "$current_version
# 将版本号分割成三部分
major=$(echo $current_version | cut -d'.' -f1)
minor=$(echo $current_version | cut -d'.' -f2)
patch=$(echo $current_version | cut -d'.' -f3)
echo "major: $major"
echo "minor: $minor"
echo "patch: $patch"
# 将补丁号加1
patch=$((patch + 1))
# 生成新版本号
new_version="$major.$minor.$patch"
# 将新版本号写入文件
sed -i "s/version.*/version = \"$new_version\"/g" $version_file
echo "新版本号:$new_version"
git diff
git add ./pyproject.toml
git commit -m "new version v$version"
git tag v$version
git add $version_file
git commit -m "new version v$new_version"
git tag v$new_version
git push -u origin main --tags

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.1.15"
version = "0.1.22"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},

View File

@@ -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)

View File

@@ -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:

View File

@@ -28,6 +28,10 @@ def getvolume():
"volume": xiaomusic.get_volume(),
}
@app.route("/searchmusic")
def searchmusic():
name = request.args.get('name')
return xiaomusic.searchmusic(name)
@app.route("/", methods=["GET"])
def redirect_to_index():

View File

@@ -39,8 +39,9 @@ $(function(){
}
$("#play").on("click", () => {
name = $("#music-name").val();
let cmd = "播放歌曲"+name;
var search_key = $("#music-name").val();
var filename=$("#music-filename").val();
let cmd = "播放歌曲"+search_key+"|"+filename;
sendcmd(cmd);
});
@@ -63,4 +64,26 @@ $(function(){
}
});
}
// 监听输入框的输入事件
$("#music-name").on('input', function() {
var inputValue = $(this).val();
// 发送Ajax请求
$.ajax({
url: "searchmusic", // 服务器端处理脚本
type: "GET",
dataType: "json",
data: {
name: inputValue
},
success: function(data) {
// 清空datalist
$("#autocomplete-list").empty();
// 添加新的option元素
$.each(data, function(i, item) {
$('<option>').val(item).appendTo("#autocomplete-list");
});
}
});
});
});

View File

@@ -25,7 +25,7 @@
}
input {
margin: 10px;
width: 200px;
width: 300px;
height: 40px;
}
</style>
@@ -44,7 +44,9 @@
</div>
<hr>
<div>
<input id="music-name" type="text" placeholder="请输入歌曲名称"></input>
<datalist id="autocomplete-list"></datalist>
<input id="music-name" type="text" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)" list="autocomplete-list"></input>
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input>
<button id="play">播放</button>
</div>
</body>

View File

@@ -7,6 +7,7 @@ import socket
from http.cookies import SimpleCookie
from typing import AsyncIterator
from urllib.parse import urlparse
import difflib
from requests.utils import cookiejar_from_dict
@@ -61,3 +62,6 @@ def validate_proxy(proxy_str: str) -> bool:
return True
# 模糊搜索
def fuzzyfinder(user_input, collection):
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1)

View File

@@ -30,6 +30,7 @@ from xiaomusic.config import (
from xiaomusic.utils import (
calculate_tts_elapse,
parse_cookie_string,
fuzzyfinder,
)
EOF = object()
@@ -57,6 +58,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 +68,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 +80,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):
@@ -291,7 +300,7 @@ class XiaoMusic:
return True
# 下载歌曲
async def download(self, name):
async def download(self, search_key, name):
if self.download_proc:
try:
self.download_proc.kill()
@@ -300,7 +309,7 @@ class XiaoMusic:
sbp_args = (
"yt-dlp",
f"{self.search_prefix}{name}",
f"{self.search_prefix}{search_key}",
"-x",
"--audio-format",
"mp3",
@@ -309,7 +318,7 @@ class XiaoMusic:
"-o",
f"{name}.mp3",
"--ffmpeg-location",
"./ffmpeg/bin",
f"{self.ffmpeg_location}",
"--no-playlist",
)
@@ -317,44 +326,76 @@ class XiaoMusic:
sbp_args += ("--proxy", f"{self.proxy}")
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
await self.do_tts(f"正在下载歌曲{search_key}")
# 本地是否存在歌曲
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:
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
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 +408,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"定时器已取消")
@@ -444,20 +486,27 @@ class XiaoMusic:
# 播放歌曲
async def play(self, **kwargs):
name = kwargs["arg1"]
if name == "":
parts = kwargs["arg1"].split("|")
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
if search_key == "" and name == "":
await self.play_next()
return
if name == "":
name = search_key
self.log.debug("play. search_key:%s name:%s", search_key, name)
filename = self.get_filename(name)
filename = self.local_exist(name)
if len(filename) <= 0:
await self.download(name)
self.log.info("正在下载中 %s", name)
filename = self.get_filename(f"{name}.mp3")
await self.download(search_key, name)
self.log.info("正在下载中 %s", search_key + ":" + name)
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 +517,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 +540,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):
@@ -523,3 +574,9 @@ class XiaoMusic:
def get_volume(self):
return self._volume
# 搜索音乐
def searchmusic(self, name):
search_list = fuzzyfinder(name, self._play_list)
self.log.debug("searchmusic. name:%s search_list:%s", name, search_list)
return search_list