1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2025-12-06 14:52:50 +08:00

Compare commits

...

214 Commits

Author SHA1 Message Date
涵曦
0d758a47f4 bump: version 0.3.66 → 0.3.67 2024-12-29 10:19:26 +08:00
涵曦
a1a025960b build: update static version 2024-12-29 10:19:26 +08:00
涵曦
2c5ffa5645 fix: 修复默认主题搜索问题 2024-12-29 10:19:02 +08:00
Issues Docs [BOT]
f78df85bb9 Auto-Generate docs 🤖 2024-12-28 14:13:02 +00:00
Issues Docs [BOT]
a771441616 Auto-Generate docs 🤖 2024-12-28 14:03:56 +00:00
Issues Docs [BOT]
0ef36d26ff Auto-Generate docs 🤖 2024-12-28 12:19:26 +00:00
Issues Docs [BOT]
589d36f98c Auto-Generate docs 🤖 2024-12-28 02:32:11 +00:00
涵曦
ad9ce10fa2 Update README.md 2024-12-28 10:30:33 +08:00
Issues Docs [BOT]
bea986f4ef Auto-Generate docs 🤖 2024-12-27 08:23:11 +00:00
涵曦
c3451fea6a Update README.md 2024-12-27 16:21:44 +08:00
Issues Docs [BOT]
6ce99a0e62 Auto-Generate docs 🤖 2024-12-27 08:16:49 +00:00
涵曦
063688f3e6 docs: 更新文档 2024-12-27 16:15:02 +08:00
涵曦
0c4b34a554 build: 修改打包镜像 2024-12-27 09:56:55 +08:00
Issues Docs [BOT]
52b6000342 Auto-Generate docs 🤖 2024-12-26 01:41:18 +00:00
涵曦
a1962cd450 Update README.md 2024-12-26 09:39:06 +08:00
Issues Docs [BOT]
2520e5d9e5 Auto-Generate docs 🤖 2024-12-26 01:35:27 +00:00
涵曦
bd0a3a1052 docs: 更新文档 2024-12-26 09:33:42 +08:00
涵曦
24218babeb feat: 简化设置,不允许修改监听端口 2024-12-26 09:18:58 +08:00
涵曦
dd803785e1 build: 打包修改 2024-12-26 09:08:44 +08:00
Issues Docs [BOT]
78ee51c8c2 Auto-Generate docs 🤖 2024-12-26 00:45:31 +00:00
涵曦
4e3e127767 bump: version 0.3.65 → 0.3.66 2024-12-26 08:43:45 +08:00
涵曦
dafd2cec05 build: update static version 2024-12-26 08:43:44 +08:00
涵曦
59e9191f70 style: 清理无效代码 2024-12-25 16:57:21 +08:00
涵曦
f7286269af fix: 修复歌曲批量重命名的问题 2024-12-25 16:20:06 +08:00
涵曦
d98652fe5d fix: 修复自定义歌单删除后没刷新歌单列表 2024-12-25 09:48:19 +08:00
Issues Docs [BOT]
497dd07030 Auto-Generate docs 🤖 2024-12-25 00:48:00 +00:00
涵曦
6cbec5bbff fix: 尝试修复更新失败问题 2024-12-25 08:38:44 +08:00
Issues Docs [BOT]
c46c652e5d Auto-Generate docs 🤖 2024-12-24 13:04:34 +00:00
涵曦
1fbbae5f1c bump: version 0.3.64 → 0.3.65 2024-12-24 21:03:03 +08:00
涵曦
ffd04fab68 build: update static version 2024-12-24 21:03:02 +08:00
Issues Docs [BOT]
51dfe70a87 Auto-Generate docs 🤖 2024-12-23 08:43:15 +00:00
涵曦
956c569b93 docs: 修改文档代理地址 2024-12-23 16:41:12 +08:00
涵曦
617a961816 fix: 处理图像报错 2024-12-22 18:14:58 +08:00
涵曦
b37b6fd57c fix: 修改歌单名字漏更新歌单列表 2024-12-22 17:59:19 +08:00
涵曦
c09baaac15 fix: 修复获取自定义歌单接口报错 2024-12-22 13:15:35 +08:00
Issues Docs [BOT]
7ded7a08dd Auto-Generate docs 🤖 2024-12-22 03:24:55 +00:00
涵曦
f4487945db bump: version 0.3.63 → 0.3.64 2024-12-22 11:23:09 +08:00
涵曦
0bdd2986f1 build: update static version 2024-12-22 11:23:08 +08:00
涵曦
a72777317e fix: 使用自己架设的 sentry 服务,解决 Cloudflare 额度超量问题 2024-12-22 11:22:42 +08:00
Issues Docs [BOT]
f2e096da38 Auto-Generate docs 🤖 2024-12-22 02:26:41 +00:00
涵曦
f42398ec9f Update static.yml 2024-12-22 10:25:12 +08:00
涵曦
5121d141b4 bump: version 0.3.62 → 0.3.63 2024-12-22 10:11:27 +08:00
涵曦
7cf9751dde build: update static version 2024-12-22 10:11:25 +08:00
Issues Docs [BOT]
990defefc9 Auto-Generate docs 🤖 2024-12-22 02:07:10 +00:00
涵曦
4e6afa0e3e Update static.yml 2024-12-22 10:05:28 +08:00
涵曦
e501097ec2 perf: 只监控报错信息 2024-12-22 00:42:46 +08:00
涵曦
ea567fd55a bump: version 0.3.61 → 0.3.62 2024-12-21 23:50:57 +08:00
涵曦
9cb1931f90 build: update static version 2024-12-21 23:50:56 +08:00
涵曦
e50db9ea59 fix: 修复首次配置时,默认主题只有一个设备的问题。 2024-12-21 23:17:09 +08:00
涵曦
5c788ccaed fix: 修复一些报错问题 2024-12-21 19:32:52 +08:00
Issues Docs [BOT]
10a63e5568 Auto-Generate docs 🤖 2024-12-20 11:16:37 +00:00
Issues Docs [BOT]
2460cd3207 Auto-Generate docs 🤖 2024-12-19 13:10:27 +00:00
涵曦
2595451280 bump: version 0.3.60 → 0.3.61 2024-12-19 21:08:49 +08:00
涵曦
2c4b131c19 build: update static version 2024-12-19 21:08:48 +08:00
涵曦
fb84f36b55 refactor: 重构更新流程 2024-12-19 19:12:59 +08:00
涵曦
811e9377f7 fix: 尝试修复更新问题 2024-12-19 18:37:15 +08:00
涵曦
00733ad669 ci: 调试 2024-12-19 16:47:04 +08:00
Issues Docs [BOT]
4cdfd68c4d Auto-Generate docs 🤖 2024-12-19 08:42:06 +00:00
涵曦
2b28bf8551 ci: 打包修改 2024-12-19 16:38:25 +08:00
涵曦
1023caf80a bump: version 0.3.59 → 0.3.60 2024-12-19 13:33:03 +08:00
涵曦
fba021d8fb build: update static version 2024-12-19 13:33:02 +08:00
涵曦
8bac3ee961 Update ci.yml 2024-12-19 13:22:23 +08:00
Issues Docs [BOT]
2f18e1935e Auto-Generate docs 🤖 2024-12-19 05:16:30 +00:00
涵曦
6c208eb1ce Update static.yml 2024-12-19 13:14:44 +08:00
涵曦
07c679c304 Update ci.yml 2024-12-19 12:38:41 +08:00
涵曦
f7551d6b3e Update ci.yml 2024-12-19 11:27:10 +08:00
涵曦
25044a8157 Update ci.yml 2024-12-19 11:07:44 +08:00
涵曦
34d33fee29 Update ci.yml 2024-12-19 10:53:30 +08:00
涵曦
772a9c4e53 Update ci.yml 2024-12-19 10:52:46 +08:00
涵曦
af06281ac3 ci: 漏了触发 2024-12-19 08:55:51 +08:00
涵曦
42a6d0fe77 bump: version 0.3.58 → 0.3.59 2024-12-19 08:42:11 +08:00
涵曦
7df7ed7110 build: update static version 2024-12-19 08:42:10 +08:00
涵曦
1b20617588 refactor: 优化代码日志级别 2024-12-19 07:10:55 +08:00
涵曦
94921eb12a feat: 新增更多的歌单编辑相关接口 2024-12-19 06:15:09 +08:00
涵曦
ce4a4149d4 refactor: 更新等消息推送到客户端再重启 2024-12-19 03:23:11 +08:00
Issues Docs [BOT]
108918ca8a Auto-Generate docs 🤖 2024-12-18 14:58:31 +00:00
涵曦
c25773e087 docs: 修改名字 2024-12-18 22:56:52 +08:00
涵曦
df6b71b47a feat: 一键更新功能 2024-12-18 22:51:53 +08:00
涵曦
b0dfb37d98 build: 调试 2024-12-18 21:51:21 +08:00
涵曦
a510f9162e style: 名字写错了 2024-12-18 20:25:47 +08:00
Issues Docs [BOT]
af1d0dc285 Auto-Generate docs 🤖 2024-12-18 09:54:07 +00:00
涵曦
7301953592 feat: 接入 sentry 平台 2024-12-18 17:51:31 +08:00
涵曦
0238982925 ci: 调试 2024-12-17 15:04:49 +08:00
涵曦
3e9f2aac1b ci: 调试 2024-12-17 14:54:34 +08:00
涵曦
b32aca16bc ci: 调试 2024-12-17 14:47:43 +08:00
涵曦
9bae1397ba ci: 调试 2024-12-17 14:23:02 +08:00
涵曦
6693272da5 ci: 调试 2024-12-17 14:17:36 +08:00
涵曦
136d0efde4 ci: 调试 2024-12-17 13:32:37 +08:00
涵曦
fa67b6deb1 ci: 调试 2024-12-17 12:45:18 +08:00
涵曦
998e13c2fe ci: 优化流程 2024-12-17 11:05:34 +08:00
涵曦
93ea4c9736 ci: 调试 2024-12-17 10:25:43 +08:00
涵曦
42eea14335 Update ci.yml 2024-12-17 08:59:22 +08:00
涵曦
558f00a5a4 Fix code scanning alert no. 38: Uncontrolled data used in path expression (#317)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2024-12-17 08:41:26 +08:00
涵曦
78bf7d30f5 Update static.yml 2024-12-17 02:54:54 +08:00
涵曦
22f8711818 fix: 解决飞牛平台报错问题 2024-12-17 02:48:58 +08:00
涵曦
740b24131f ci: 自动生成 versions.json 文件 2024-12-17 02:24:36 +08:00
涵曦
9079a7d79a build: reset log path 2024-12-17 01:45:59 +08:00
涵曦
c5ec98a527 refactor: 更新接口修改 2024-12-17 01:21:51 +08:00
涵曦
569ae234ca feat: 实现更新接口 2024-12-17 01:04:19 +08:00
Formatter [BOT]
8c021e5889 Auto-format code 🧹🌟🤖 2024-12-16 12:21:40 +00:00
涵曦
d30cb67bc1 feat: 下载完成之后修改文件权限为755 close #316 2024-12-16 20:20:42 +08:00
Issues Docs [BOT]
8e0fc0bf03 Auto-Generate docs 🤖 2024-12-16 02:54:25 +00:00
涵曦
3dfc6b4bb4 ci(调试): 调试 (#313) 2024-12-15 21:26:32 +08:00
涵曦
87da35ca4c Update ci.yml 2024-12-15 20:15:01 +08:00
涵曦
e3c0586b10 Update ci.yml 2024-12-15 20:03:47 +08:00
涵曦
b903a14304 ci: 调试 2024-12-15 19:53:57 +08:00
涵曦
3691dbbcf3 ci: 调试ci 2024-12-15 18:43:53 +08:00
涵曦
d8e68f7492 ci: 调试 2024-12-15 14:08:48 +08:00
涵曦
b617f08032 Update ci.yml 2024-12-15 13:15:32 +08:00
涵曦
f46d2f60dc ci: 调试 2024-12-15 13:11:23 +08:00
涵曦
acad787c9f ci: 调试 2024-12-15 13:08:32 +08:00
涵曦
bf2a25e7c5 ci: 调试 2024-12-15 12:41:32 +08:00
涵曦
6987ff35f7 ci: 调试 2024-12-15 12:28:05 +08:00
涵曦
c256857fe6 ci: 调试 2024-12-15 11:33:35 +08:00
Issues Docs [BOT]
c5708fc5f5 Auto-Generate docs 🤖 2024-12-15 03:26:07 +00:00
涵曦
ea665b0f81 ci: 修改ci 2024-12-15 11:23:40 +08:00
涵曦
dd6d2efddd bump: version 0.3.57 → 0.3.58 2024-12-15 09:01:27 +08:00
涵曦
f0e344649e build: update static version 2024-12-15 09:01:26 +08:00
涵曦
21296967bf Update ci.yml 2024-12-15 08:44:13 +08:00
涵曦
71af5f8d0e ci: 上传 tar.gz 包 2024-12-15 08:09:28 +08:00
涵曦
14e7a151a3 fix: 尝试解决七牛设备问题 2024-12-15 08:05:30 +08:00
Issues Docs [BOT]
24a40882df Auto-Generate docs 🤖 2024-12-14 23:17:17 +00:00
涵曦
1d8732d3da build: 基础镜像修改 2024-12-15 02:07:48 +08:00
涵曦
499728df0b build: 打包更新文件 2024-12-15 01:55:30 +08:00
涵曦
62cf39a959 fix: 修复 umami 统计函数报错,解决七牛环境问题 2024-12-15 00:36:02 +08:00
涵曦
7c4af5d548 build: 更新 runtime 镜像 2024-12-14 22:13:07 +08:00
Issues Docs [BOT]
93c20f2f94 Auto-Generate docs 🤖 2024-12-14 10:24:26 +00:00
Issues Docs [BOT]
ffb79f0fc3 Auto-Generate docs 🤖 2024-12-14 00:37:34 +00:00
涵曦
161c0d738b Update static.yml 2024-12-14 07:39:47 +08:00
涵曦
52a5574220 Update static.yml 2024-12-14 07:37:03 +08:00
涵曦
8e0f7a4b46 bump: version 0.3.56 → 0.3.57 2024-12-14 03:51:03 +08:00
涵曦
f060665027 build: update static version 2024-12-14 03:51:03 +08:00
涵曦
dfc79e1b5f build: 文档生成问题 2024-12-13 21:09:10 +08:00
涵曦
3737c7359f docs: changelog 页面问题 2024-12-13 20:52:26 +08:00
涵曦
96083bee89 docs: 修改链接 2024-12-13 20:23:47 +08:00
涵曦
92ed00f7cc Update static.yml 2024-12-13 18:52:47 +08:00
Issues Docs [BOT]
37a82e8c98 Auto-Generate docs 🤖 2024-12-13 10:23:54 +00:00
涵曦
361d25786b Update static.yml 2024-12-13 18:21:30 +08:00
涵曦
3fe5215f59 Update static.yml 2024-12-13 18:12:25 +08:00
涵曦
5163f22f7d docs: 更新 github 代理地址 2024-12-13 17:46:15 +08:00
涵曦
971b8efb3f docs: 替换 github 资源链接为代理链接 2024-12-13 17:02:47 +08:00
涵曦
681b65815a docs: 自动提交抓取的 issues md 文件 2024-12-13 16:30:05 +08:00
涵曦
1015736635 docs: 替换 GitHub 图片资源链接为代理链接 2024-12-13 16:23:27 +08:00
涵曦
1e0949703f docs: 自动构建 docs 2024-12-13 13:21:56 +08:00
涵曦
8dc18ad208 Delete docs/.vitepress/vitepress-plugin-replace-markdown-links.mts 2024-12-13 04:41:30 +08:00
涵曦
74d772e242 build: 更新docs目录 2024-12-13 01:03:50 +08:00
涵曦
2815f058ec docs: update 2024-12-13 01:02:53 +08:00
涵曦
31eeb0a1cf Create static.yml 2024-12-13 00:55:49 +08:00
涵曦
3468e4a979 docs: 将 issues 转成 vitepress 网站 2024-12-13 00:53:32 +08:00
涵曦
9847f7afc2 Update CNAME 2024-12-13 00:49:14 +08:00
涵曦
d7ee021dc9 Create CNAME 2024-12-13 00:45:28 +08:00
涵曦
fd6d1ca66a fix: 默认主题刷新时选中当前播放歌曲 2024-12-12 07:31:52 +08:00
涵曦
23882cf85c feat: 优化批量下载工具命名和下载高码率音频 2024-12-12 00:43:30 +08:00
涵曦
73df9dec48 refactor: 修改默认主题 2024-12-12 00:16:01 +08:00
涵曦
accc449824 docs: 更新文档 2024-12-11 13:39:50 +08:00
涵曦
59e096b2ce feat: 新增搜索播放口令用于生成临时播放列表 2024-12-11 13:39:25 +08:00
涵曦
12532b48d6 feat: 新增设置项 ignore_tag_dirs 用于忽略读取目录下的标签信息,解决挂载 alist 目录的问题 2024-12-11 09:58:16 +08:00
涵曦
9ed39d0c02 refactor: 后端也加入 umami 统计 2024-12-10 22:19:57 +08:00
涵曦
c7045e4071 refactor: 新增自托管 umami 统计 2024-12-10 19:11:34 +08:00
涵曦
2a443915d5 Update README.md 2024-12-10 18:49:16 +08:00
52fisher
5fbaf89d1b refactor: XIAOMUSIC_HOSTNAME 携带端口号友好提醒并处理 (#303)
* refactor: XIAOMUSIC_HOSTNAME 携带端口号友好提醒并处理

* refactor: input/select宽度对齐
2024-12-09 23:19:32 +08:00
涵曦
c1994b5bc6 refactor: 修改标题 2024-12-09 19:48:41 +08:00
涵曦
4737a4c2e6 fix: 修复当前播放列表没更新的问题 2024-12-09 19:27:59 +08:00
涵曦
40c0b740ae refactor: 冲突解决错误 2024-12-08 16:23:07 +08:00
涵曦
0ba125c63c style: 修改标题 2024-12-08 16:19:37 +08:00
涵曦
f44ec11a53 fix: 修复搜索时不显示保存输入框问题 2024-12-08 16:16:38 +08:00
52fisher
baedee5638 fix: 收藏 (#301) 2024-12-08 16:13:42 +08:00
涵曦
e434faf684 fix: 修复收藏歌曲失败的问题 2024-12-08 15:00:39 +08:00
52fisher
8b746b32dc fix: 小屏幕设备主页显示问题 (#300) 2024-12-08 14:55:02 +08:00
52fisher
977ff684eb feat: 新主题 Material (#299)
* feat: 新主题 Material

* refactor: 首页
2024-12-08 14:07:07 +08:00
涵曦
c552a34ec1 bump: version 0.3.55 → 0.3.56 2024-12-07 17:31:00 +08:00
涵曦
e4144aa568 build: update static version 2024-12-07 17:30:59 +08:00
涵曦
fe54ad29e2 fix: 播放失败设置重试次数10次,解决设备失联后无限重试的问题 2024-12-07 15:54:19 +08:00
涵曦
48a8bf3325 feat: tag 信息支持写入到歌曲文件 see #266 2024-12-06 22:55:14 +08:00
涵曦
2c5b7b7f26 Update README.md 2024-12-06 00:22:27 +08:00
涵曦
46168315d0 feat: 开启gzip压缩 2024-12-05 21:18:51 +08:00
涵曦
b385bfab54 fix: 修复最近新增歌单问题 2024-12-04 16:46:19 +08:00
徒言
4e2e3e1f74 fix: 小程序码移动端布局兼容 (#293) 2024-12-04 12:21:49 +08:00
涵曦
0db3e4cada bump: version 0.3.54 → 0.3.55 2024-12-04 10:22:27 +08:00
涵曦
fa21d02e1d build: update static version 2024-12-04 10:22:26 +08:00
涵曦
be48fe9f54 fix: 修复播放接口报错问题 2024-12-04 10:17:06 +08:00
徒言
042a91336e chore: 引导页新增小程序码 (#290) 2024-12-04 09:54:38 +08:00
涵曦
790a4cd808 bump: version 0.3.53 → 0.3.54 2024-12-04 01:41:29 +08:00
涵曦
70d029f4d1 build: update static version 2024-12-04 01:41:28 +08:00
52fisher
0472ad3188 fix: 安卓低版本webview对audio的src为空值的报错 (#289) 2024-12-04 01:30:07 +08:00
涵曦
733c44d12f feat: 新增最近新增歌单 close #273 2024-12-03 21:44:48 +08:00
涵曦
8c92afd09b fix: 修复M01语音播放问题,X08C X08E X8F 型号默认采用型号兼容模式 see #30 2024-12-03 21:25:51 +08:00
涵曦
742929b9d5 Update README.md 2024-12-03 21:18:26 +08:00
涵曦
4766e08ff4 Update README.md 2024-12-03 21:17:11 +08:00
涵曦
99c4296c7a bump: version 0.3.52 → 0.3.53 2024-12-03 12:15:09 +08:00
涵曦
0ae24f4810 build: update static version 2024-12-03 12:15:08 +08:00
涵曦
77cca0d18c fix: 解决播放接口修改后播放失败的问题 2024-12-03 12:14:57 +08:00
涵曦
ddc24595df bump: version 0.3.51 → 0.3.52 2024-12-03 12:02:39 +08:00
涵曦
ce1bfb164c build: update static version 2024-12-03 12:02:39 +08:00
涵曦
78dbba6c1f fix: 修复播放接口参数错误的问题 2024-12-03 12:02:14 +08:00
涵曦
7eb0ab1e46 bump: version 0.3.50 → 0.3.51 2024-12-03 09:06:51 +08:00
涵曦
0a2e3cc579 build: update static version 2024-12-03 09:06:50 +08:00
涵曦
75ec336285 fix: 修复空配置启动失败问题 close #284 2024-12-03 09:02:15 +08:00
涵曦
a19d53f000 bump: version 0.3.49 → 0.3.50 2024-12-03 07:11:40 +08:00
涵曦
cdb6190e7a build: update static version 2024-12-03 07:11:40 +08:00
涵曦
1aa9b58561 style: 更新 gitignore 2024-12-02 23:17:54 +08:00
涵曦
6a990f48d0 fix: 更新 yt-dlp ,解决 B 站下载问题 close #279 2024-12-02 23:17:12 +08:00
涵曦
82e92a0380 Update README.md 2024-12-02 21:43:56 +08:00
涵曦
ecce5c8848 feat: 修改日志文件的默认值 2024-12-02 21:38:31 +08:00
涵曦
87fb34e5c9 feat: 新增修改tag缓存信息的接口 close #266 2024-12-02 12:27:09 +08:00
涵曦
df3c4b7fa9 feat: 新增专用的播放歌曲和播放歌单接口,解决默认口令提示词被修改了导致后台失效的问题 2024-12-02 11:40:26 +08:00
涵曦
cb2af0ee9f feat: 统计设备型号 2024-12-02 11:08:19 +08:00
涵曦
8f9bba0ca3 refactor: 调整设置页面 2024-12-01 22:47:56 +08:00
涵曦
b86e8d3196 Update README.md 2024-12-01 22:41:47 +08:00
52fisher
65067346f3 feat: 页面与设置中的HOST不一致时弹窗提醒 (#281) 2024-12-01 22:23:50 +08:00
52fisher
865b412fb7 fix: 网页播放audio获取到错误url无法播放时提醒用户 (#280) 2024-12-01 12:33:05 +08:00
涵曦
441fffd59e Update README.md 2024-11-29 10:02:51 +08:00
52fisher
2ee7b956cf feat: 未发现小爱设备时给予提示 (#278)
fix: input标签自闭合
2024-11-29 07:23:57 +08:00
涵曦
126bfa43a2 feat: 优化设置页面提示 2024-11-28 23:29:06 +08:00
75 changed files with 13698 additions and 530 deletions

View File

@@ -2,6 +2,7 @@ name: Build Docker Base Image
on:
push:
branches: ["main"]
paths:
- 'Dockerfile.builder'
- 'Dockerfile.runtime'

View File

@@ -1,18 +1,31 @@
name: ci
name: CI Workflow
on:
push:
branches:
- "*"
- "*" # 所有分支触发
tags:
- 'v*'
workflow_dispatch:
env:
TEST_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
LATEST_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest
STABLE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable
permissions:
contents: write
id-token: write
jobs:
build-test-publish:
# Job 构建镜像
build:
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -26,6 +39,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build Docker image (linux/amd64)
id: build_amd64
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
@@ -37,6 +51,7 @@ jobs:
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Build Docker image (linux/arm64)
id: build_arm64
uses: docker/build-push-action@v6
with:
platforms: linux/arm64
@@ -48,6 +63,7 @@ jobs:
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Build Docker image (linux/arm/v7)
id: build_armv7
uses: docker/build-push-action@v6
with:
platforms: linux/arm/v7
@@ -58,14 +74,89 @@ jobs:
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
# We test all the images on amd64 host here. This uses QEMU to emulate
# the other platforms.
- run: docker run --rm ${TEST_TAG}-linux-amd64 -h
- run: docker run --rm ${TEST_TAG}-linux-arm64 -h
- run: docker run --rm ${TEST_TAG}-linux-arm-v7 -h
- name: List Docker images
run: docker images
# This will only push the previously built images.
- name: Publish to Docker Hub
- name: Test amd64 image
run: |
docker run --rm ${{ env.TEST_TAG }}-linux-amd64 /app/.venv/bin/python3 /app/xiaomusic.py -h
- name: Test arm64 image
run: |
docker run --rm ${{ env.TEST_TAG }}-linux-arm64 /app/.venv/bin/python3 /app/xiaomusic.py -h
- name: Test armv7 image
run: |
docker run --rm ${{ env.TEST_TAG }}-linux-arm-v7 /app/.venv/bin/python3 /app/xiaomusic.py -h
- name: Docker Hub Description
if: github.ref == 'refs/heads/main'
uses: peter-evans/dockerhub-description@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: hanxi/xiaomusic
# 发布 PyPI 版本
# 仅在 ref 为以 v 开头的标签时执行
- uses: actions/setup-node@v4
if: startsWith(github.ref, 'refs/tags/v')
with:
registry-url: https://registry.npmjs.org/
node-version: lts/*
- run: npx changelogithub
if: startsWith(github.ref, 'refs/tags/v')
continue-on-error: true
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- uses: pdm-project/setup-pdm@v3
if: startsWith(github.ref, 'refs/tags/v')
- name: Publish package distributions to PyPI
if: startsWith(github.ref, 'refs/tags/v')
run: pdm publish
continue-on-error: true
# Job 打包应用, 发布镜像和 Release
# 仅在 main 分支或以 v 开头的标签运行
- name: Package /app for amd64
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: |
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-amd64 tar czf /workspace/app-amd64.tar.gz -C / app
- name: Package /app (lite) for amd64
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: |
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-amd64 bash -c \
"cd /app && tar --exclude='ffmpeg' -czf /workspace/app-amd64-lite.tar.gz .[!.]* *"
- name: Package /app for arm64
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: |
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm64 tar czf /workspace/app-arm64.tar.gz -C / app
- name: Package /app (lite) for arm64
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: |
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm64 bash -c \
"cd /app && tar --exclude='ffmpeg' -czf /workspace/app-arm64-lite.tar.gz .[!.]* *"
- name: Package /app for arm/v7
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: |
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm-v7 tar czf /workspace/app-arm-v7.tar.gz -C / app
- name: Package /app (lite) for arm/v7
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: |
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm-v7 bash -c \
"cd /app && tar --exclude='ffmpeg' -czf /workspace/app-arm-v7-lite.tar.gz .[!.]* *"
- name: Publish to Docker Hub main
if: github.ref == 'refs/heads/main'
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
@@ -75,14 +166,97 @@ jobs:
cache-from: type=local,src=/tmp/.buildx-cache-new
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v4
# 仅在 ref 为以 v 开头的标签时执行
- name: Publish to Docker Hub latest and stable
if: startsWith(github.ref, 'refs/tags/v')
uses: docker/build-push-action@v6
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: hanxi/xiaomusic
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
push: true
tags: ${{ env.TEST_TAG }}, ${{ env.LATEST_TAG }}, ${{ env.STABLE_TAG }}
cache-from: type=local,src=/tmp/.buildx-cache-new
cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Move cache to limit growth
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
# 上传文件到 release 页面
- name: Install GitHub CLI
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: |
sudo apt update
sudo apt install -y gh
# 创建或更新 Release
- name: Create or update Release main
if: github.ref == 'refs/heads/main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
run: |
RELEASE_NAME=test
RELEASE_BODY="This release is automatically updated from the main branch."
# 检查是否已有同名 Release
EXISTING_RELEASE=$(gh release view "${RELEASE_NAME}" --json id --jq .id || echo "")
if [[ -n "${EXISTING_RELEASE}" ]]; then
echo "release exist"
else
# 创建新的 Release
gh release create "${RELEASE_NAME}" \
--prerelease=false \
--title "${RELEASE_NAME}" \
--notes "${RELEASE_BODY}"
fi
- name: Create or update Release tag
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
run: |
RELEASE_NAME=${{ github.ref_name }}
RELEASE_BODY="This release is automatically updated from the ${RELEASE_NAME} branch."
# 检查是否已有同名 Release
EXISTING_RELEASE=$(gh release view "${RELEASE_NAME}" --json id --jq .id || echo "")
if [[ -n "${EXISTING_RELEASE}" ]]; then
echo "release exist"
else
# 创建新的 Release
gh release create "${RELEASE_NAME}" \
--prerelease=false \
--title "${RELEASE_NAME}" \
--notes "${RELEASE_BODY}"
fi
# 上传多个文件并覆盖
- name: Upload assets to Release main
if: github.ref == 'refs/heads/main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
run: |
RELEASE_NAME=test
FILES=$(find . -type f -name "app-*.tar.gz")
for FILE in ${FILES}; do
echo "type upload ${FILE}"
gh release upload "${RELEASE_NAME}" "${FILE}" --clobber
done
- name: Upload assets to Release tag
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
run: |
RELEASE_NAME=${{ github.ref_name }}
FILES=$(find . -type f -name "app-*.tar.gz")
for FILE in ${FILES}; do
echo "type upload ${FILE}"
gh release upload "${RELEASE_NAME}" "${FILE}" --clobber
done

View File

@@ -24,7 +24,7 @@ jobs:
- name: install ruff
run: pip install ruff
- name: Format code
run: pdm fmt && pdm lint --fix
run: pdm lintfmt
- name: Check for changes
id: check_changes

View File

@@ -1,60 +0,0 @@
name: Release and Build Docker Image
permissions:
contents: write
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
pypi-publish:
name: upload release to PyPI
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
registry-url: https://registry.npmjs.org/
node-version: lts/*
- run: npx changelogithub
continue-on-error: true
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- uses: pdm-project/setup-pdm@v3
- name: Publish package distributions to PyPI
run: pdm publish
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, linux/arm/v7
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable

104
.github/workflows/static.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
paths:
- 'docs/**'
- 'README.md'
- 'CHANGELOG.md'
- '.github/workflows/static.yml'
# Runs on issue events
issues:
types: [opened, edited, reopened] # Specify events you're interested in
release:
types:
- uploaded
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: write
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Single deploy job since we're just deploying
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '20'
- name: Install dependencies
working-directory: ./docs # 指定工作目录为 docs
run: |
npm install
- name: Build VitePress
env:
VITE_GITHUB_ISSUES_TOKEN: ${{ secrets.VITE_GITHUB_ISSUES_TOKEN }}
working-directory: ./docs # 指定工作目录为 docs
run: | # 有点小问题得执行2次
npm run docs:build
npm run docs:build
- uses: pdm-project/setup-pdm@v3
- name: Install dependencies
run: pdm install
- name: generate versions.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pdm run get_release.py
- name: Check for changes
id: check_changes
run: |
if [ -n "$(git diff docs)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
continue-on-error: true
# Optionally, customize the user name and commit message, and can add an email as well such as Github Actions' email
- name: Set up Git and Commit Changes
run: |
if [ "${{ steps.check_changes.outputs.changed }}" == "true" ]; then
git config --local user.name "Issues Docs [BOT]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "Auto-Generate docs 🤖"
git push
fi
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: './docs/.vitepress/dist'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

2
.gitignore vendored
View File

@@ -169,3 +169,5 @@ setting.json
.DS_Store
cache
tmp/
xiaomusic.log.txt
node_modules

View File

@@ -1,3 +1,184 @@
## v0.3.67 (2024-12-29)
### Feat
- 简化设置,不允许修改监听端口
### Fix
- 修复默认主题搜索问题
## v0.3.66 (2024-12-26)
### Fix
- 修复歌曲批量重命名的问题
- 修复自定义歌单删除后没刷新歌单列表
- 尝试修复更新失败问题
## v0.3.65 (2024-12-24)
### Fix
- 处理图像报错
- 修改歌单名字漏更新歌单列表
- 修复获取自定义歌单接口报错
## v0.3.64 (2024-12-22)
### Fix
- 使用自己架设的 sentry 服务,解决 Cloudflare 额度超量问题
## v0.3.63 (2024-12-22)
### Perf
- 只监控报错信息
## v0.3.62 (2024-12-21)
### Fix
- 修复首次配置时,默认主题只有一个设备的问题。
- 修复一些报错问题
## v0.3.61 (2024-12-19)
### Fix
- 尝试修复更新问题
### Refactor
- 重构更新流程
## v0.3.60 (2024-12-19)
## v0.3.59 (2024-12-19)
### Feat
- 新增更多的歌单编辑相关接口
- 一键更新功能
- 接入 sentry 平台
- 实现更新接口
- 下载完成之后修改文件权限为755 close #316
### Fix
- 解决飞牛平台报错问题
### Refactor
- 优化代码日志级别
- 更新等消息推送到客户端再重启
- 更新接口修改
## v0.3.58 (2024-12-15)
### Fix
- 尝试解决七牛设备问题
- 修复 umami 统计函数报错,解决七牛环境问题
## v0.3.57 (2024-12-14)
### Feat
- 优化批量下载工具命名和下载高码率音频
- 新增搜索播放口令用于生成临时播放列表
- 新增设置项 ignore_tag_dirs 用于忽略读取目录下的标签信息,解决挂载 alist 目录的问题
- 新主题 Material (#299)
### Fix
- 默认主题刷新时选中当前播放歌曲
- 修复当前播放列表没更新的问题
- 修复搜索时不显示保存输入框问题
- 收藏 (#301)
- 修复收藏歌曲失败的问题
- 小屏幕设备主页显示问题 (#300)
### Refactor
- 修改默认主题
- 后端也加入 umami 统计
- 新增自托管 umami 统计
- XIAOMUSIC_HOSTNAME 携带端口号友好提醒并处理 (#303)
- 修改标题
- 冲突解决错误
## v0.3.56 (2024-12-07)
### Feat
- tag 信息支持写入到歌曲文件 see #266
- 开启gzip压缩
### Fix
- 播放失败设置重试次数10次解决设备失联后无限重试的问题
- 修复最近新增歌单问题
- 小程序码移动端布局兼容 (#293)
## v0.3.55 (2024-12-04)
### Fix
- 修复播放接口报错问题
## v0.3.54 (2024-12-04)
### Feat
- 新增最近新增歌单 close #273
### Fix
- 安卓低版本webview对audio的src为空值的报错 (#289)
- 修复M01语音播放问题X08C X08E X8F 型号默认采用型号兼容模式 see #30
## v0.3.53 (2024-12-03)
### Fix
- 解决播放接口修改后播放失败的问题
## v0.3.52 (2024-12-03)
### Fix
- 修复播放接口参数错误的问题
## v0.3.51 (2024-12-03)
### Fix
- 修复空配置启动失败问题 close #284
## v0.3.50 (2024-12-03)
### Feat
- 修改日志文件的默认值
- 新增修改tag缓存信息的接口 close #266
- 新增专用的播放歌曲和播放歌单接口,解决默认口令提示词被修改了导致后台失效的问题
- 统计设备型号
- 页面与设置中的HOST不一致时弹窗提醒 (#281)
- 未发现小爱设备时给予提示 (#278)
- 优化设置页面提示
### Fix
- 更新 yt-dlp ,解决 B 站下载问题 close #279
- 网页播放audio获取到错误url无法播放时提醒用户 (#280)
- input标签自闭合
### Refactor
- 调整设置页面
## v0.3.49 (2024-11-28)
### Feat

View File

@@ -11,15 +11,19 @@ RUN pdm install --prod --no-editable
FROM hanxi/xiaomusic:runtime
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/.venv ./.venv
COPY --from=builder /app/xiaomusic/ ./xiaomusic/
COPY --from=builder /app/plugins/ ./plugins/
COPY --from=builder /app/xiaomusic.py .
ENV XIAOMUSIC_HOSTNAME=192.168.2.5
ENV XIAOMUSIC_PORT=8090
COPY --from=builder /app/xiaomusic/__init__.py /base_version.py
RUN touch /app/.dockerenv
COPY supervisor.conf /etc/supervisor.conf
VOLUME /app/conf
VOLUME /app/music
EXPOSE 8090
ENV TZ=Asia/Shanghai
ENV PATH=/app/.venv/bin:$PATH
ENTRYPOINT [".venv/bin/python3","xiaomusic.py"]
CMD ["/bin/sh", "-c", "/usr/bin/supervisord -c /etc/supervisor.conf && tail -F /app/supervisord.log /app/xiaomusic.log.txt"]

View File

@@ -7,6 +7,8 @@ RUN apt-get update && apt-get install -y \
libtiff6 \
libopenjp2-7 \
libxcb1 \
supervisor \
vim \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app

130
README.md
View File

@@ -1,4 +1,5 @@
# XiaoMusic: 无限听歌,解放小爱音箱
[![GitHub License](https://img.shields.io/github/license/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic)
[![Docker Image Version](https://img.shields.io/docker/v/hanxi/xiaomusic?sort=semver&label=docker%20image)](https://hub.docker.com/r/hanxi/xiaomusic)
[![Docker Pulls](https://img.shields.io/docker/pulls/hanxi/xiaomusic)](https://hub.docker.com/r/hanxi/xiaomusic)
@@ -6,8 +7,8 @@
[![PyPI - Downloads](https://img.shields.io/pypi/dm/xiaomusic)](https://pypi.org/project/xiaomusic/)
[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fhanxi%2Fxiaomusic%2Fmain%2Fpyproject.toml)](https://pypi.org/project/xiaomusic/)
[![GitHub Release](https://img.shields.io/github/v/release/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic/releases)
[![Visitors](https://api.visitorbadge.io/api/daily?path=hanxi%2Fxiaomusic&label=daily%20visitor&countColor=%232ccce4&style=flat)](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
[![Visitors](https://api.visitorbadge.io/api/visitors?path=hanxi%2Fxiaomusic&label=total%20visitor&countColor=%232ccce4&style=flat)](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
@@ -21,13 +22,13 @@
已经支持在 web 页面配置其他参数docker 启动命令如下:
```bash
docker run -p 8090:8090 -v /xiaomusic/music:/app/music -v /xiaomusic/conf:/app/conf hanxi/xiaomusic
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf hanxi/xiaomusic
```
🔥 国内:
```bash
docker run -p 8090:8090 -v /xiaomusic/music:/app/music -v /xiaomusic/conf:/app/conf m.daocloud.io/docker.io/hanxi/xiaomusic
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf m.daocloud.io/docker.io/hanxi/xiaomusic
```
对应的 docker compose 配置如下:
@@ -39,10 +40,12 @@ services:
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
- 58090:8090
environment:
XIAOMUSIC_PUBLIC_PORT: 58090
volumes:
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
- /xiaomusic_music:/app/music
- /xiaomusic_conf:/app/conf
```
🔥 国内:
@@ -54,48 +57,34 @@ services:
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
- 58090:8090
environment:
XIAOMUSIC_PUBLIC_PORT: 58090
volumes:
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
- /xiaomusic_music:/app/music
- /xiaomusic_conf:/app/conf
```
其中 conf 目录为配置文件存放目录music 目录为音乐存放目录,建议分开配置为不同的目录。
- 其中 conf 目录为配置文件存放目录music 目录为音乐存放目录,建议分开配置为不同的目录。
- /xiaomusic_music 和 /xiaomusic_conf 是 docker 主机里的目录,可以修改为其他目录。如果报错找不到 /xiaomusic_music 目录,可以先执行 `mkdir -p /xiaomusic_{music,conf}` 命令新建目录。
- XIAOMUSIC_PUBLIC_PORT 是用来配置 NAS 本地端口的。8090 是容器端口,不要去修改。
- 后台访问地址为: http://NAS_IP:58090
> [!NOTE]
> 上面配置的 /xiaomusic/music 和 /xiaomusic/conf 是 docker 主机里的 /xiaomusic 目录下的,可以修改为其他目录。如果报错找不到 /xiaomusic/music 目录,可以先执行 `mkdir -p /xiaomusic/{music,conf}` 命令新建目录
docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
> docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表
> [!TIP]
> 目前安装步骤已经是最简化了,如果还是嫌安装麻烦,可以微信或者 QQ 约我远程安装,我一般周末和晚上才有时间,收个辛苦费 :moneybag: 50 元一次,安装失败不收费。
### 🔥 修改默认8090端口映射
如果需要修改 8090 端口为其他端口,比如 5678需要这样配3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 5678:5678
volumes:
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
environment:
XIAOMUSIC_PORT: 5678
```
如果不是首次修改端口,还需要修改 /xiaomusic/conf/setting.json 文件里的端口(也可以在后台修改监听端口后重启)。
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
> [!IMPORTANT]
> XIAOMUSIC_PORT 也可以在后台配置,对应的是监听端口
> [!TIP]
> 海外 RackNerd VPS 机器推荐,可支付宝付款
> - [🔥1 GB KVM VPS $11.29/年](https://my.racknerd.com/aff.php?aff=1177&pid=903)
> - [2 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=904)
> - [3.5 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=905)
> - [4 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=906)
> - [6 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=907)
### 🤐 支持语音口令
@@ -114,7 +103,8 @@ services:
- 【播放列表收藏】,这个用于播放收藏歌单。
- 【播放本地歌曲+歌名】,这个口令和播放歌曲的区别是本地找不到也不会去下载。
- 【播放列表第几个+列表名】,具体见: <https://github.com/hanxi/xiaomusic/issues/158>
- 【播放歌曲+关键词】,会搜索关键词作为临时搜索列表播放,比如说【播放歌曲林俊杰】,会播放所有林俊杰的歌。
-搜索播放+关键词】,会搜索关键词作为临时搜索列表播放,比如说【搜索播放林俊杰】,会播放所有林俊杰的歌。
- 【本地搜索播放+关键词】,跟搜索播放的区别是本地找不到也不会去下载。
> [!TIP]
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会先下载小猪佩奇的故事,然后再播放小猪佩奇的故事。
@@ -129,7 +119,7 @@ services:
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.3.37 by: github.com/hanxi
XiaoMusic v0.3.65 by: github.com/hanxi
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
@@ -171,8 +161,7 @@ pdm run xiaomusic.py
提交前请执行
```
pdm fmt
pdm lint --fix
pdm lintfmt
```
用于检查代码和格式化代码。
@@ -183,6 +172,13 @@ pdm lint --fix
docker build -t xiaomusic .
```
### 技术栈
- 后端代码使用 Python 语言编写。
- HTTP 服务使用的是 FastAPI 框架,~~早期版本使用的是 Flask~~。
- 使用了 Docker ,在 NAS 上安装更方便。
- 默认的前端主题使用了 jQuery 。
## 已测试支持的设备
| 型号 | 名称 |
@@ -201,7 +197,8 @@ docker build -t xiaomusic .
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
| L09A | [小米音箱Art](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l09a) |
| LX04 X10A X08A | 已经支持的触屏版 |
| M01/XMYX01JY | 小米小爱音箱HD (获取对话记录的接口比较特殊) |
| X08C X08E X8F | 需要设置【型号兼容模式】选项为 true |
| M01/XMYX01JY | 小米小爱音箱HD 需要设置【特殊型号获取对话记录】选项为 true 才能语音播放|
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
@@ -224,24 +221,6 @@ docker build -t xiaomusic .
> 已知 L05B L05C LX06 L16A 不支持 flac 格式。
> 如果格式不能播放可以打开【转换为MP3】和【型号兼容模式】选项。具体见 <https://github.com/hanxi/xiaomusic/issues/153#issuecomment-2328168689>
## 💡 简易的控制面板
浏览器进入 <http://192.168.2.5:8090>
- ip 是 XIAOMUSIC_HOSTNAME 设置的
- 8090 是默认端口
- 支持功能
- 显示正在播放的歌曲
- 模糊搜索本地歌曲
- 播放列表
- 删除歌曲
- 设置页面
- 配置网络歌单
- 日志文件下载
采用新的设置页面之后,没有必须在启动前配置的环境变量了,除非是改默认的 8090 端口才需要配置环境变量。
## 🌏 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
@@ -251,30 +230,14 @@ docker build -t xiaomusic .
## 🍺 更多其他可选配置
- XIAOMUSIC_ACTIVE_CMD 环境变量,对应后台的 【允许唤醒的命令】,用于唤醒口令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体<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 用来存放配置文件的目录,对应后台的 【配置文件目录】,记得把目录映射到主机,默认为 `/app/config` ,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
- XIAOMUSIC_CACHE_DIR 用来音乐 tag 缓存,默认为 `/app/cache`,对应后台的 【缓存文件目录】。
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,对应后台的 【关闭下载功能】,见 <https://github.com/hanxi/xiaomusic/issues/82>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,对应后台的 【型号兼容模式】,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 <https://github.com/hanxi/xiaomusic/issues/30>
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,对应后台的 【播放歌曲口令】,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,对应后台的 【停止口令】,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,对应后台的 【播放本地歌曲口令】,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,对应后台的 【开启模糊搜索】,支持模糊匹配歌名和歌单名。 具体见 <https://github.com/hanxi/xiaomusic/issues/52>
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值默认0.6可以配0到1直接的小数越小越模糊越大越精准对应后台的 【模糊匹配阈值】。具体见 <https://github.com/hanxi/xiaomusic/issues/52>
- XIAOMUSIC_PUBLIC_PORT 用于设置外网端口,对应后台的 【外网访问端口】当使用反向代理时可以设置为外网端口XIAOMUSIC_HOSTNAME 设为外网IP或者域名即可。
- XIAOMUSIC_DOWNLOAD_PATH 变量可以配置下载目录,默认为空,表示使用 music 目录为下载目录,对应后台的 【音乐下载目录】。设置这个目录必须是 music 的子目录,否则刷新列表后会找不到歌曲。具体见 <https://github.com/hanxi/xiaomusic/issues/98>
- XIAOMUSIC_PROXY 用于配置国内使用 youtube 源下载歌曲时使用的代理,参数格式参考 yt-dlp 文档说明。 见 <https://github.com/hanxi/xiaomusic/issues/2><https://github.com/hanxi/xiaomusic/issues/11>
- MIIO_TTS_CMD 用于部分机型(如:`L05C`)使用 MiIO 支持 tts 能力,默认为空,命令选择见 [MiService-fork 文档](https://github.com/yihong0618/MiService)
<https://github.com/hanxi/xiaomusic/issues/333>
### ⚠️ 安全提醒
## ⚠️ 安全提醒
> [!IMPORTANT]
> 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
>
> 1. 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
> 2. 强烈不建议将小爱音箱的小米账号绑定摄像头,代码难免会有 bug ,一旦小米账号密码泄露,可能监控录像也会泄露。
## 🤔 高级篇
@@ -301,6 +264,8 @@ docker build -t xiaomusic .
- [pure 主题 xiaomusicUI](https://github.com/52fisher/xiaomusicUI)
- [移动端的播放器主题](https://github.com/52fisher/XMusicPlayer)
- [一个第三方的主题](https://github.com/DarrenWen/xiaomusicui)
- [Umami 统计](https://github.com/umami-software/umami)
- [Sentry 报错监控](https://github.com/getsentry/sentry)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
@@ -329,4 +294,3 @@ docker build -t xiaomusic .
## License
[MIT](https://github.com/hanxi/xiaomusic/blob/main/LICENSE) License © 2024 涵曦

View File

@@ -0,0 +1,70 @@
import { loadEnv, defineConfig } from 'vitepress'
import AutoSidebar from 'vite-plugin-vitepress-auto-sidebar';
import GitHubIssuesPlugin from './vitepress-plugin-github-issues.mts';
export default async ({ mode }) => {
const env = loadEnv(mode || '', process.cwd())
return defineConfig({
title: "XiaoMusic",
description: "XiaoMusic doc",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: 'Guide', link: '/issues' },
{ text: 'Admin', link: 'https://x.hanxi.cc' },
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/hanxi/xiaomusic' }
],
footer: {
message: '基于 MIT 许可发布',
copyright: `版权所有 © 2023-${new Date().getFullYear()} 涵曦`
},
},
sitemap: {
hostname: 'https://xdocs.hanxi.cc'
},
head: [
['script', { defer: true, src: 'https://umami.hanxi.cc/script.js', 'data-website-id': '29cca3f5-e420-432b-adc7-8a1325d31c68' }]
],
lastUpdated: true,
markdown: {
lineNumbers: false, // 关闭代码块行号显示
// 自定义 markdown-it 插件
config: (md) => {
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const aIndex = tokens[idx].attrIndex('target');
if (aIndex < 0) {
tokens[idx].attrPush(['target', '_self']); // 将默认行为改为不使用 _blank
} else {
tokens[idx].attrs![aIndex][1] = '_self'; // 替换 _blank 为 _self
}
return self.renderToken(tokens, idx, options);
};
},
},
logLevel: 'warn',
vite: {
plugins: [
AutoSidebar({
path: '.',
collapsed: true,
titleFromFile: true,
}),
GitHubIssuesPlugin({
repo: 'hanxi/xiaomusic',
token: env.VITE_GITHUB_ISSUES_TOKEN,
replaceRules: [
{
baseUrl: 'https://github.com/hanxi/xiaomusic/issues',
targetUrl: '/issues',
},
],
githubProxy: 'https://gproxy.hanxi.cc/proxy',
}),
],
}
})
}

View File

@@ -0,0 +1,259 @@
// vitepress-plugin-github-issues.mts
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import type { Plugin } from 'vitepress';
interface ReplaceRule {
baseUrl: string; // 要匹配的基地址
targetUrl: string; // 替换后的目标地址
}
interface GitHubIssuesPluginOptions {
repo: string; // GitHub repository info in the format 'owner/repo'
token: string;
replaceRules: ReplaceRule[];
githubProxy: string;
}
async function fetchAllIssues(repo: string, token: string): Promise<any[]> {
const maxRetries = 3; // 最大重试次数
let attempt = 0;
const allIssues: any[] = [];
let page = 1;
while (true) {
while (attempt < maxRetries) {
try {
const response = await axios.get(`https://api.github.com/repos/${repo}/issues`, {
headers: {
Authorization: `token ${token}`
},
params: {
page: page,
per_page: 100 // 每页最多返回100条记录
}
});
// 如果没有更多问题了,退出循环
if (response.data.length === 0) {
return allIssues;
}
allIssues.push(...response.data);
page++; // 下一页
attempt = 0; // 重置尝试次数
break; // 退出尝试循环
} catch (error) {
if (error.response && error.response.status === 503) {
console.error(`服务不可用, 正在重试...`);
attempt++;
const waitTime = Math.pow(2, attempt) * 1000; // 指数等待时间
await new Promise(resolve => setTimeout(resolve, waitTime));
} else {
throw error; // 如果不是503错误抛出错误并停止重试
}
}
}
if (attempt >= maxRetries) {
throw new Error('最大重试次数已达,请检查 API 状态(可能是请求过于频繁)');
}
}
}
async function fetchIssueComments(repo: string, issueNumber: number, token: string): Promise<any[]> {
const maxRetries = 3;
let attempt = 0;
const allComments: any[] = [];
let page = 1;
while (true) {
while (attempt < maxRetries) {
try {
const response = await axios.get(
`https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`,
{
headers: {
Authorization: `token ${token}`,
},
params: {
page: page,
per_page: 100,
},
}
);
if (response.data.length === 0) {
return allComments; // 如果没有更多评论,退出循环
}
allComments.push(...response.data);
page++;
attempt = 0;
break; // 成功获取评论数据,退出重试
} catch (error: any) {
if (error.response && error.response.status === 503) {
console.error('服务不可用,正在重试...');
attempt++;
const waitTime = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, waitTime));
} else {
throw error;
}
}
}
if (attempt >= maxRetries) {
throw new Error('最大重试次数已达,请检查 API 状态(可能是请求过于频繁)');
}
}
}
function clearDirectory(dir: string) {
if (fs.existsSync(dir)) {
fs.readdirSync(dir).forEach((file) => {
const filePath = path.join(dir, file);
if (fs.lstatSync(filePath).isDirectory()) {
clearDirectory(filePath); // 递归清理子目录
fs.rmdirSync(filePath);
} else {
fs.unlinkSync(filePath); // 删除文件
}
});
console.log(`Cleared directory: ${dir}`);
}
}
function copyFile(source: string, destination: string) {
if (fs.existsSync(source)) {
fs.copyFileSync(source, destination);
console.log(`Copied file from ${source} to ${destination}`);
} else {
console.error(`file not found at ${source}`);
}
}
// 在文件开头插入内容
function prependToFile(filePath: string, text: string) {
const content = fs.readFileSync(filePath, 'utf-8');
const updatedContent = `${text}\n\n${content}`;
fs.writeFileSync(filePath, updatedContent, 'utf-8');
console.log(`Prepended text to ${filePath}`);
}
function replaceGithubAssetUrls(content: string, githubProxy: string): string {
const pattern1 = /https:\/\/github\.com\/[^\/]+\/[^\/]+\/assets\/[\w-]+/g;
const pattern2 = /https:\/\/github\.com\/user-attachments\/assets\/[\w-]+/g;
// 使用正则表达式替换符合条件的链接
const transformedContent = content.replace(pattern1, (match) => {
return match.replace("https://github.com", githubProxy);
}).replace(pattern2, (match) => {
return match.replace("https://github.com", githubProxy);
});
return transformedContent;
}
export default function GitHubIssuesPlugin(options: GitHubIssuesPluginOptions): Plugin {
const { repo, token, replaceRules, githubProxy } = options;
return {
name: 'vitepress-plugin-github-issues',
async buildStart() {
try {
const issues = await fetchAllIssues(repo, token);
console.log(`Fetched ${issues.length} issues from GitHub`); // Log the number of issues fetched
const docsDir = path.join(process.cwd(), 'issues');
// 清空 issues 目录
clearDirectory(docsDir);
// Create a directory to store markdown files if it doesn't exist
if (!fs.existsSync(docsDir)) {
fs.mkdirSync(docsDir);
console.log(`Created docs directory: ${docsDir}`);
}
// 拷贝 ../README.md 文件到当前目录
const readmeSource = path.join(process.cwd(), '../README.md');
const readmeDestination = path.join(docsDir, 'index.md');
copyFile(readmeSource, readmeDestination);
// 拷贝 ../CHANGELOG.md 文件到当前目录
const changelogSource = path.join(process.cwd(), '../CHANGELOG.md');
const changelogDestination = path.join(docsDir, 'changelog.md');
copyFile(changelogSource, changelogDestination);
prependToFile(changelogDestination, '# 版本日志');
for (const issue of issues) {
// 仅处理包含 "文档" 标签的 issue
const hasDocumentLabel = issue.labels.some(label => label.name === '文档');
if (hasDocumentLabel) {
const title = issue.title.replace(/[\/\\?%*:|"<>]/g, '-');
const fileName = `${issue.number}.md`;
// 获取评论数据
const comments = await fetchIssueComments(repo, issue.number, token);
let content =
`---
title: ${issue.title}
---
# ${title}
${issue.body}
## 评论
`;
// 插入评论
if (comments.length > 0) {
comments.forEach((comment, index) => {
content += `
### 评论 ${index + 1} - ${comment.user.login}
${comment.body}
---
`;
});
} else {
content += "没有评论。\n";
}
replaceRules.forEach(({ baseUrl, targetUrl }) => {
// 将 baseUrl 转换为正则表达式,匹配后接的路径部分
const pattern = new RegExp(`${baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}(/\\d+)`, 'g');
// 替换为目标 URL
content = content.replace(pattern, `${targetUrl}$1.html`);
});
content = replaceGithubAssetUrls(content, githubProxy);
content += `[链接到 GitHub Issue](${issue.html_url})\n`;
const filePath = path.join(docsDir, fileName);
fs.writeFileSync(filePath, content, { encoding: 'utf8' });
console.log(`Created file: ${filePath}`); // Log each created file
} else {
console.log(`Skipped issue: ${issue.title}`); // Log skipped issues
}
}
console.log(`Successfully created markdown files from GitHub issues.`);
} catch (error) {
console.error('Error fetching GitHub issues:', error);
}
},
};
}

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
xdocs.hanxi.cc

28
docs/index.md Normal file
View File

@@ -0,0 +1,28 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: "XiaoMusic"
text: "无限听歌<br>解放小爱音箱"
tagline: 使用小爱音箱播放音乐,音乐使用 yt-dlp 下载
actions:
- theme: brand
text: 快速开始
link: /issues/index
- theme: alt
text: 文档汇总
link: /issues/211
- theme: alt
text: GitHub
link: https://github.com/hanxi/xiaomusic
features:
- title: MIT 开源
details: 完全开源,自主可控
- title: 一键部署
details: 支持 Docker 部署,兼容各大 NAS 平台
- title: 口令自定义
details: 可以完全自定义语音口令,可以写自己的插件
---

85
docs/issues/101.md Normal file
View File

@@ -0,0 +1,85 @@
---
title: 群晖docker安装 xiaomusic
---
# 群晖docker安装 xiaomusic
由于现在群晖已经无法正常下载 docker 里的镜像了,绕了好多弯;现在用 ssh 服务命令拉取镜像来创建容器;
## 1. ssh 输入账号密码进入群晖
## 2. 输入 sudo -i 再次输入密码进入 root 权限
## 3. 输入 docker search xiaomusic来查找到该镜像名
## 4. 然后输入 docker pull xiaomusic 试试是否能安装,如果不能;就得在命令前加个代理地址;下面列了一些代理地址可以一个个的试
```
docker.fxxk.dedyn.iodocker.io
registry-docker-hub-latest-9vgc.onrender.com
docker.chenby.cn
dockerproxy.com
hub.uuuadc.top
docker.jsdelivr.fyi
docker.registry.cyou
dockerhub.anzu.vip
```
我是用的最下面这个成功的
```
docker pull dockerhub.anzu.vip/xiaomusic:latest
```
## 5. 安装完成后就进入群晖 DOCKER 配置 xiaomusic
<img width="491" alt="image" src="https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/38914725/e318062b-bd70-464c-a8df-8ce3635f2d84">
- MI_HARDWARE=型号 前面第4 步骤获取的
- XIAOMUSIC_SEARCH=搜索方式我填写的bilisearch: 意思是通过 bilibili 搜索
- MI_DID=前面第4 步骤获取的
- MI_USER=小米账号
- MI_PASS=小米密码
- XIAOMUSIC_FUZZY_MATCH_CUTOFF=模糊匹配,最小为 0.1 最大为 1越小越模糊越大越精准
## 6. 配置端口
<img width="757" alt="image (1)" src="https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/38914725/2b6b9283-296f-4845-a3ff-0ebb11f548b4">
## 7. 映射路径
<img width="737" alt="image (2)" src="https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/38914725/593718dd-8302-4a69-bec9-36e70f3f0407">
## 评论
### 评论 1 - kiwi5656
MI_DID=前面第4 步骤获取的第4步骤在哪
---
### 评论 2 - hanxi
> MI_DID=前面第4 步骤获取的第4步骤在哪
不用设置 MI_DID
---
### 评论 3 - hanxi
国内 docker 镜像
```
docker pull m.daocloud.io/docker.io/hanxi/xiaomusic:latest
docker tag m.daocloud.io/docker.io/hanxi/xiaomusic:latest hanxi/xiaomusic:latest
```
---
### 评论 4 - ginitaimeiyty
如果手头上有能科学上网的机器直接把群辉的代理服务器IP填写成可以科学上网的机器IP+端口,翻墙软件打开允许局域网连接就可以
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/101)

404
docs/issues/105.md Normal file
View File

@@ -0,0 +1,404 @@
---
title: 【插件】自定义口令功能
---
# 【插件】自定义口令功能
自定义口令配置需要配置到 config.json 文件里,使用 config.json 方式启动。参考 </issues/94.html> 。
口令的配置方式见 config-example.json 文件。口令对应的代码需要写到 `plugins/` 目录下面,如果是容器启动,则需要把这个目录挂载出来。
config.json 格式是下面这样的。
```json
{
"hardware": "L07A",
"account": "",
"password": "",
"mi_did": "",
"cookie": "",
"verbose": false,
"music_path": "music",
"conf_path": null,
"hostname": "192.168.2.5",
"port": 8090,
"public_port": 0,
"proxy": null,
"search_prefix": "bilisearch:",
"ffmpeg_location": "./ffmpeg/bin",
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
"exclude_dirs": "@eaDir",
"music_path_depth": 10,
"disable_httpauth": true,
"httpauth_username": "admin",
"httpauth_password": "admin",
"music_list_url": "",
"music_list_json": "",
"disable_download": false,
"key_word_dict": {
"播放歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume",
"get_volume#": "get_volume",
"本地播放歌曲": "playlocal",
"放歌曲": "play",
"暂停": "stop",
"停止": "stop",
"停止播放": "stop",
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
},
"key_match_order": [
"set_volume#",
"get_volume#",
"分钟后关机",
"播放歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表",
"播放本地歌曲",
"本地播放歌曲",
"放歌曲",
"暂停",
"停止",
"停止播放",
"测试自定义口令",
"测试链接"
],
"use_music_api": false,
"use_music_audio_id": "1582971365183456177",
"use_music_id": "355454500",
"log_file": "/tmp/xiaomusic.txt",
"fuzzy_match_cutoff": 0.6,
"enable_fuzzy_match": true,
"stop_tts_msg": "收到,再见",
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
"keywords_play": "播放歌曲,放歌曲",
"keywords_stop": "关机,暂停,停止,停止播放",
"user_key_word_dict": {
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
}
}
```
配置自定义口令时,只需要配置 user_key_word_dict 即可,会自动插入到 key_word_dict 里的。配置格式是:
```
"测试自定义口令": "exec#code1(\"hello\")",
```
其中 "测试自定义口令" 就是对小爱音箱说的,`"exec#code1(\"hello\")"` 就是要执行的插件代码,代码以 `exec#` 开头,后面紧跟着执行代码。这里 code1 是一个插件函数,插件函数需要在 plugin 目录里实现,一个文件只会导出一个与文件名相同的插件函数。所以 code1 函数是在 plugin/code1.py 里实现的。
```
async def code1(arg1):
global log, xiaomusic
log.info(f"code1:{arg1}")
await xiaomusic.do_tts("你好,我是自定义的测试口令")
```
这里只是演示了打印日志和让小爱音箱说话。还有一个示例插件是 httpget ,可以用来访问 url 。
比如下面这样配置的话,当对小爱音箱说测试链接时,会去访问 url ,可以用来很多其他的事情。
```
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")
```
最后还需要在 `active_cmd` 中配上口令用于唤醒:
```
"active_cmd": "play,set_random_play,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,测试自定义口令",
```
感兴趣的可以体验一下,写了有什么好玩的插件也可以在这里分享,或者提 pr 合并进官方库里作为自带插件。
## 评论
### 评论 1 - carson512
如果开启服务的状态下 如何唤醒才能调用原有的播放QQ 网易云而特定唤醒词调用xiaoai播放
---
### 评论 2 - hanxi
> 如果开启服务的状态下 如何唤醒才能调用原有的播放QQ 网易云而特定唤醒词调用xiaoai播放
不使用 xiaomusic 的唤醒词就会调用音箱自带的,比如说播放音乐
---
### 评论 3 - shellingford37
```
[23:26:12] [0.3.30] [INFO] xiaomusic.py:531: 收到消息:测试自定义口令 控制面板:False did:290874427
[23:26:12] [0.3.30] [INFO] xiaomusic.py:577: 完全匹配指令. query:测试自定义口令 opvalue:exec#code1("hello")
[23:26:12] [0.3.30] [INFO] code1.py:3: code1:hello
[23:26:12] [0.3.30] [ERROR] xiaomusic.py:542: Execption XiaoMusic.do_tts() missing 1 required positional argument: 'value'
Traceback (most recent call last):
File "/app/xiaomusic/xiaomusic.py", line 540, in do_check_cmd
await func(did=did, arg1=oparg)
File "/app/xiaomusic/xiaomusic.py", line 890, in exec
await self.plugin_manager.execute_plugin(code)
File "/app/xiaomusic/plugin.py", line 66, in execute_plugin
await coroutine
File "/app/plugins/code1.py", line 4, in code1
await xiaomusic.do_tts("你好,我是自定义的测试口令")
TypeError: XiaoMusic.do_tts() missing 1 required positional argument: 'value'
```
我用code1的代码执行报错有大佬知道为什么吗
---
### 评论 4 - hanxi
@shellingford37 重构后漏改了,修复了。
---
### 评论 5 - guoxiangke
先说播放歌曲,再说 测试自定义口令 就行
---
### 评论 6 - CZJCC
想请教下插件那个功能,如何把用户的语音输入作为参数内容传到自定义函数里
---
### 评论 7 - hanxi
> 想请教下插件那个功能,如何把用户的语音输入作为参数内容传到自定义函数里
现在获取不到,等我加个接口获取吧。
---
### 评论 8 - CZJCC
666,支持以后我可以贡献一个接入通义模型的插件
---
### 评论 9 - hanxi
@CZJCC 你可以更新看看 plugins/code1.py 的测试代码,我测试了是可以拿到语音输入的原始内容的。
---
### 评论 10 - hanxi
文档更新了下active_cmd 也需要配置一下才能正常唤醒。
---
### 评论 11 - CZJCC
> @CZJCC 你可以更新看看 plugins/code1.py 的测试代码,我测试了是可以拿到语音输入的原始内容的。
我原先设想的事用户的话术是”通义提问为什么地球是圆的“指令匹配的时候通义提问前缀匹配到类似于code1方法为什么地球是圆的作为参数传入这个函数但我看现在是拿历史记录实现的
---
### 评论 12 - hanxi
是的插件函数里面再切割一下前缀就行。last_record就是当前的那条语音数据。
---
### 评论 13 - hanxi
> > @CZJCC 你可以更新看看 plugins/code1.py 的测试代码,我测试了是可以拿到语音输入的原始内容的。
>
> 我原先设想的事用户的话术是”通义提问为什么地球是圆的“指令匹配的时候通义提问前缀匹配到类似于code1方法为什么地球是圆的作为参数传入这个函数但我看现在是拿历史记录实现的
是的,这样比较简单,交给插件里面处理也比较自由。
---
### 评论 14 - mogeqian
key_word_dict中的“播放歌曲”口令是不能修改的是吧因为以前用小爱同学播放歌曲说习惯了总是触发xiaomusic自动下载歌曲我想把口令改成“查找歌曲”这样我当说播放歌曲的时候就调用网易云音乐或者QQ音乐当我说查找歌曲的时候就先看本地有没有歌曲没有话就自动下载到本地。
当我先按照config-example.json的模板写好如下配置并重命名为config.json
```
"key_word_dict": {
"查找歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "set_random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"本地播放歌曲": "playlocal",
"下载歌曲": "play",
"暂停": "stop",
"停止": "stop",
"停止播放": "stop",
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
},
"key_match_order": [
"分钟后关机",
"查找歌曲",
"下一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表",
"播放本地歌曲",
"本地播放歌曲",
"下载歌曲",
"暂停",
"停止",
"停止播放",
"测试自定义口令",
"测试链接"
],
```
用以下命令安装docker
`docker run --name xiaomusic -p 5488:5488 -v /mnt/sharedata/audiodata/musci/xiaomusic:/app/music -v /mnt/data_sdb1/docker/xiaomusic/config.json:/app/config.json -e XIAOMUSIC_PORT=5488 hanxi/xiaomusic --config /app/config.json`
日志里提示的依然是:
` key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接']`
似乎自定义的口令只能以插入的方式添加上去,并不能替换掉原来的口令
---
### 评论 15 - hanxi
> key_word_dict中的“播放歌曲”口令是不能修改的是吧因为以前用小爱同学播放歌曲说习惯了总是触发xiaomusic自动下载歌曲我想把口令改成“查找歌曲”这样我当说播放歌曲的时候就调用网易云音乐或者QQ音乐当我说查找歌曲的时候就先看本地有没有歌曲没有话就自动下载到本地。 当我先按照config-example.json的模板写好如下配置并重命名为config.json
>
> ```
> "key_word_dict": {
> "查找歌曲": "play",
> "播放本地歌曲": "playlocal",
> "关机": "stop",
> "下一首": "play_next",
> "单曲循环": "set_play_type_one",
> "全部循环": "set_play_type_all",
> "随机播放": "set_random_play",
> "分钟后关机": "stop_after_minute",
> "播放列表": "play_music_list",
> "刷新列表": "gen_music_list",
> "本地播放歌曲": "playlocal",
> "下载歌曲": "play",
> "暂停": "stop",
> "停止": "stop",
> "停止播放": "stop",
> "测试自定义口令": "exec#code1(\"hello\")",
> "测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
> },
> "key_match_order": [
> "分钟后关机",
> "查找歌曲",
> "下一首",
> "单曲循环",
> "全部循环",
> "随机播放",
> "关机",
> "刷新列表",
> "播放列表",
> "播放本地歌曲",
> "本地播放歌曲",
> "下载歌曲",
> "暂停",
> "停止",
> "停止播放",
> "测试自定义口令",
> "测试链接"
> ],
> ```
>
> 用以下命令安装docker `docker run --name xiaomusic -p 5488:5488 -v /mnt/sharedata/audiodata/musci/xiaomusic:/app/music -v /mnt/data_sdb1/docker/xiaomusic/config.json:/app/config.json -e XIAOMUSIC_PORT=5488 hanxi/xiaomusic --config /app/config.json` 日志里提示的依然是:
>
> ` key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接']`
>
> 似乎自定义的口令只能以插入的方式添加上去,并不能替换掉原来的口令
可以在网页后台设置页面改。
---
### 评论 16 - mogeqian
不行,后台设置如图
![QQ截图20241111181411](https://gproxy.hanxi.cc/proxy/user-attachments/assets/cc89512f-cab9-488d-b0d6-5b2a3a720ac2)
日志如下:
```
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1130: update_config_from_setting ok. data:Config(account='**', password='**', mi_did='726577518,570867755', miio_tts_command='', cookie='', verbose=False, music_path='music', download_path='music/download', conf_path='conf', cache_dir='cache', hostname='192.168.22.4', port=8090, public_port=0, proxy='', search_prefix='bilisearch:', ffmpeg_location='./ffmpeg/bin', active_cmd='play,set_random_play,playlocal,play_music_list,stop', exclude_dirs='@eaDir', music_path_depth=10, disable_httpauth=True, httpauth_username='******', httpauth_password='******', music_list_url='', music_list_json='', custom_play_list_json='', disable_download=False, key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接'], use_music_api=False, use_music_audio_id='1582971365183456177', use_music_id='355454500', log_file='/tmp/xiaomusic.txt', fuzzy_match_cutoff=0.6, enable_fuzzy_match=True, stop_tts_msg='收到,再见', enable_config_example=False, keywords_playlocal='播放本地歌曲,本地播放歌曲', keywords_play='查找歌曲,下载歌曲', keywords_stop='关机,暂停,停止,停止播放', keywords_playlist='播放列表,播放歌单', user_key_word_dict={'测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, enable_force_stop=False, devices={'726577518': Device(did='726577518', device_id='******', hardware='LX06', name='小爱音箱Pro', play_type='', cur_music='', cur_playlist='全部'), '570867755': Device(did='570867755', device_id='*********', hardware='L15A', name='小米AI音箱(第二代)', play_type='', cur_music='', cur_playlist='全部')}, group_list='', remove_id3tag=False, convert_to_mp3=False, delay_sec=3, continue_play=False, pull_ask_sec=1, crontab_json='', enable_yt_dlp_cookies=False, get_ask_by_mina=False)
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1133: 语音控制已启动, 用【分钟后关机/播放歌曲/下一首/上一首/单曲循环/全部循环/随机播放/关机/刷新列表/播放列表第/播放列表/加入收藏/收藏歌曲/取消收藏/播放本地歌曲/本地播放歌曲/查找歌曲/下载歌曲/暂停/停止/停止播放/播放歌单/测试自定义口令/测试链接】开头来控制
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:543: 协程时间循环未启动
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1250: 没打乱 全部 ['乌兰托娅 火红的萨日朗', '凤凰传奇麝香夫人'] ... ['罗大佑童年', '阿嬷'] with len: 7
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1250: 没打乱 全部 ['乌兰托娅 火红的萨日朗', '凤凰传奇麝香夫人'] ... ['罗大佑童年', '阿嬷'] with len: 7
[2024-11-11 18:08:04] [0.3.46] [INFO] analytics.py:28: analytics init ok
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:104: Startup OK. Config(account='***', password='***', mi_did='726577518,570867755', miio_tts_command='', cookie='', verbose=False, music_path='music', download_path='music/download', conf_path='conf', cache_dir='cache', hostname='192.168.22.4', port=8090, public_port=0, proxy='', search_prefix='bilisearch:', ffmpeg_location='./ffmpeg/bin', active_cmd='play,set_random_play,playlocal,play_music_list,stop', exclude_dirs='@eaDir', music_path_depth=10, disable_httpauth=True, httpauth_username='******', httpauth_password='******', music_list_url='', music_list_json='', custom_play_list_json='', disable_download=False, key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接'], use_music_api=False, use_music_audio_id='1582971365183456177', use_music_id='355454500', log_file='/tmp/xiaomusic.txt', fuzzy_match_cutoff=0.6, enable_fuzzy_match=True, stop_tts_msg='收到,再见', enable_config_example=False, keywords_playlocal='播放本地歌曲,本地播放歌曲', keywords_play='查找歌曲,下载歌曲', keywords_stop='关机,暂停,停止,停止播放', keywords_playlist='播放列表,播放歌单', user_key_word_dict={'测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, enable_force_stop=False, devices={'726577518': Device(did='726577518', device_id='*****', hardware='LX06', name='小爱音箱Pro', play_type='', cur_music='', cur_playlist='全部'), '570867755': Device(did='570867755', device_id='*********', hardware='L15A', name='小米AI音箱(第二代)', play_type='', cur_music='', cur_playlist='全部')}, group_list='', remove_id3tag=False, convert_to_mp3=False, delay_sec=3, continue_play=False, pull_ask_sec=1, crontab_json='', enable_yt_dlp_cookies=False, get_ask_by_mina=False)
[2024-11-11 18:08:04] [0.3.46] [INFO] httpserver.py:111: disable_httpauth:True
[18:08:04] [0.3.46] [INFO] Started server process [1]
[18:08:04] [0.3.46] [INFO] Waiting for application startup.
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:541: 启动后台构建 tag cache
[18:08:04] [0.3.46] [INFO] Application startup complete.
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:513: 已从【cache/tag_cache.json】加载 tag cache
[18:08:04] [0.3.46] [INFO] Uvicorn running on http://['0.0.0.0', '::']:8090 (Press CTRL+C to quit)
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:527: 保存tag cache 已保存到【cache/tag_cache.json】
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:577: tag 更新完成
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:248: 选中的设备: {'726577518': Device(did='726577518', device_id='******', hardware='LX06', name='小爱音箱Pro', play_type='', cur_music='', cur_playlist='全部'), '570867755': Device(did='570867755', device_id='*********', hardware='L15A', name='小米AI音箱(第二代)', play_type='', cur_music='', cur_playlist='全部')}
[18:08:34] [0.3.46] [INFO] 172.20.0.1:35058 - "GET /static/default/setting.html HTTP/1.1" 304
[18:08:34] [0.3.46] [INFO] 172.20.0.1:35058 - "GET /getversion HTTP/1.1" 200
```
使用的docker-compose命令安装
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- /mnt/sharedata/audiodata/musci/xiaomusic:/app/music
- /mnt/data_sdb1/docker/xiaomusic/config.json:/app/config.json
command: ['--config', '/app/config.json']
根据日志的提示,'播放歌曲': 'play'依然存在,只是增加了 '查找歌曲': 'play', '下载歌曲': 'play', 这两个关于play的自定义口令所以实际上play有三条口令 “播放歌曲、查找歌曲、下载歌曲”,能否删除掉'播放歌曲': 'play'这个系统默认的口令?只使用 '查找歌曲': 'play', '下载歌曲': 'play', 这两个关于play的自定义口令
---
### 评论 17 - hanxi
@mogeqian 另外提个 issue 吧,现在应该是不支持删除默认的口令。
---
### 评论 18 - mogeqian
好的已经重开了一个issue #259
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/105)

111
docs/issues/182.md Normal file
View File

@@ -0,0 +1,111 @@
---
title: 定时任务配置格式
---
# 定时任务配置格式
支持采用 crontab 的格式配置定时任务,已经支持下面的任务类型:
- stop 关机
- play 播放歌曲
- play_music_list 播放列表
- tts 文字转语音
- refresh_music_list 刷新播放列表
- set_volume 设置音量
- set_play_type 设置播放类型,单曲循环 0 , 全部循环 1 , 随机播放 2 , 单曲播放 3 , 顺序播放 4
### 示例
```json
[
{
"expression": "0 8 * * 0-4",
"name": "play",
"did": "123456789",
"arg1": "周杰伦晴天"
},
{
"expression": "10 8 * * 0-4",
"name": "stop",
"did": "123456789"
},
{
"expression": "0 9 * * *",
"name": "play",
"did": "123456789",
"arg1": "周杰伦晴天"
},
{
"expression": "0 10 * * *",
"name": "play_music_list",
"did": "123456789",
"arg1": "周杰伦"
},
{
"expression": "30 10 * * *",
"name": "play_music_list",
"did": "123456789",
"arg1": "周杰伦|晴天"
},
{
"expression": "0 7 * * *",
"name": "tts",
"did": "123456789",
"arg1": "早上好!该起床了!"
},
{
"expression": "0 3 * * *",
"name": "refresh_music_list"
},
{
"expression": "* * * * *",
"name": "set_volume",
"did": "123456789",
"arg1": "25"
},
{
"expression": "* * * * *",
"name": "set_play_type",
"did": "123456789",
"arg1": "2"
}
]
```
示例中的意思是:
- 周一到周五每天 8 点播放歌曲 "周杰伦晴天"
- 周一到周五每天 8 点 10 分执行关机指令
- 每天 9 点播放歌曲 "周杰伦晴天"
- 每天 10 点播放列表 "周杰伦"
- 每天 10 点 30 分播放列表 "周杰伦" 里的 "晴天"
- 每天 7 点发出语音 "早上好!该起床了!"
- 每天 3 点刷新播放列表,用于自动更新目录下的歌曲到播放列表里。
- 每分钟设置音量为 25
- 每分钟设置为随机播放
> 注意星期一是0星期二是1星期日是6。
> (0-6 or mon,tue,wed,thu,fri,sat,sun)
> The first weekday is always monday.
### 参数意思
- expression 的格式是标准的 crontab 的格式,用于配置任务的执行时机,如何配置可以直接用下面的 crontab ai 工具生成
- <https://cronly.app/ai>
- <https://cronify.zimo.li/>
- name 是任务名,目前只支持上面那几种。
- did 是小爱音箱的设备ID就是设置页面的音箱型号后面的那串数字。
- arg1 根据任务不同而不同。
- play 的 arg1 表示要播放的歌曲名。
- play_music_list 的 arg1 表示要播放的播放目录名,可以加 `|` 符合加上目录下面的歌曲名,也可不加。
- tts 的 arg1 表示要说的语音文字。
## 评论
### 评论 1 - hanxi
0.3.38版本功能。
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/182)

130
docs/issues/19.md Normal file
View File

@@ -0,0 +1,130 @@
---
title: 如何修改默认的8090端口
---
# 如何修改默认的8090端口
docker-compose 修改映射端口会播放失败
```
ports:
- 80:8090
```
从日志看继续调用了 http://10.0.0.4:8090 而不是修改映射的80还原成
```
ports:
- 8090:8090
```
则一切正常
```
xiaomusic | [BiliBiliSearch] Playlist 安河桥北: Downloading 1 items of 1
xiaomusic | [download] Downloading item 1 of 1
xiaomusic | [BiliBili] Extracting URL: http://www.bilibili.com/video/av319943893
xiaomusic | [BiliBili] 319943893: Downloading webpage
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | [BiliBili] BV1tw411X7Rr: Extracting videos in anthology
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | [BiliBili] 319943893: Extracting chapters
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | [BiliBili] Format(s) 1080P 高码率, 1080P 高清, 720P 高清, 4K 超清 are missing; you have to login or become premium member to download them. Use --cookies-from-browser or --cookies for the authentication. See https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how to manually pass cookies
xiaomusic | [info] BV1tw411X7Rr: Downloading 1 format(s): 30280
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
xiaomusic | [download] Destination: music/安河桥北.m4a
[download] 0.3% of 5.61MiB at 6.24MiB/s ETA 00:0010.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
[download] 0.5% of 5.61MiB at 2.42MiB/s ETA 00:0210.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
[download] 1.1% of 5.61MiB at 1.50MiB/s ETA 00:0310.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
[download] 4.4% of 5.61MiB at 2.35MiB/s ETA 00:0210.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
[download] 8.9% of 5.61MiB at 3.59MiB/s ETA 00:0110.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
[download] 100% of 5.61MiB in 00:00:00 at 10.83MiB/s
xiaomusic | [ExtractAudio] Destination: music/安河桥北.mp3
xiaomusic | Deleting original file music/安河桥北.m4a (pass -k to keep)
xiaomusic | [download] Finished downloading playlist: 安河桥北
xiaomusic | [02/21/24 15:29:29] INFO 播放 xiaomusic.py:461
xiaomusic | http://10.0.0.4:8090/music/%E5%AE%
xiaomusic | 89%E6%B2%B3%E6%A1%A5%E5%8C%97.mp3
xiaomusic | INFO 已经开始播放了 xiaomusic.py:464
xiaomusic | INFO 歌曲music/安河桥北.mp3的时长251秒 xiaomusic.py:371
xiaomusic | INFO 251秒后将会播放下一首 xiaomusic.py:385
xiaomusic | INFO 匹配到指令. opkey:set_volume# xiaomusic.py:441
xiaomusic | opvalue:set_volume oparg:24
```
## 评论
### 评论 1 - hanxi
需要添加环境变量
```
environment:
XIAOMUSIC_PORT:80
ports:
- 80:80
```
---
### 评论 2 - newrookie001
> 需要添加环境变量
>
> ```
> environment:
> XIAOMUSIC_PORT:80
> ports:
> - 80:80
> ```
自己走了点弯路,半天才搞明白。补充说明:
> environment:
> XIAOMUSIC_PORT: 5678 #就是“5678”可以根据自己要求设置但要求上下的5678都设置成一个
> ports:
> - 5678:5678
---
### 评论 3 - hanxi
如果换端口需要3个数字一致比如
```
environment:
XIAOMUSIC_PORT:6874
ports:
- 6874:6874
```
---
### 评论 4 - hanxi
文档类型的我都打开下,方便其他人看到。
---
### 评论 5 - flymin
docker-compose 中对应关系应该是
```yaml
ports:
- aaaa:bbbb
environment:
XIAOMUSIC_PORT: bbbb # 配置文件中的 port后台监听端口(修改后需要重启)
XIAOMUSIC_PUBLIC_PORT: aaaa # 配置文件中的 public_port后台外网访问端口(0表示跟监听端口一致)
```
以上docker 环境中基本不存在需要修改 bbbb 的情况,也就是不用设置 XIAOMUSIC_PORT。如果需要修改端口只需要修改两处 aaaa
如果使用反向代理,则转发 localhost:aaaaXIAOMUSIC_PUBLIC_PORT 设置成代理的监听端口 cccc
另外setting 文件存在会覆盖环境变量。启动过之后需要直接修改 settings.json 或者在后台修改
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/19)

73
docs/issues/210.md Normal file
View File

@@ -0,0 +1,73 @@
---
title: yt-dlp cookies 文件上传功能
---
# yt-dlp cookies 文件上传功能
此功能用于解决 yt-dlp 下载资源失败时使用,比如 ip 被 B站或者 youtube 加入黑名单后才需要使用。
上传的文件用于 yt-dlp 的 `--cookies` 参数。
```
--cookies FILE Netscape formatted file to read cookies from
and dump cookie jar in
```
## 获取 cookies.txt 文件
1. 下载插件 [Get cookies.txt LOCALLY](https://chromewebstore.google.com/detail/cclelndahbckbenkjhflpdbgdldlbecc)
2. 给予插件访问权限和无痕模式允许使用
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/89f6ce94-bb51-4805-8c16-a867ba41e5d2)
3. 打开无痕窗口
4. 打开 youtube.com
5. 登陆 youtube.com
6. 打开新标签页
7. 关闭 youtube.com 的标签页
8. 保存 cookies.txt
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/64242595-7b5c-4159-a8bc-4fc922d5de9e)
原因见 https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies
## 上传 cookies.txt
1. 打开设置页面
2. 设置启用yt-dlp-cookies 选项为 true
![Screenshot_2024-09-29-22-31-40-134_com android chrome-edit](https://gproxy.hanxi.cc/proxy/user-attachments/assets/49760905-475b-493c-9ff4-271c5e797b2f)
3. 点击保存
4. 再点击选择文件,选择前面保存好的 cookies.txt 文件,点击上传。
![Screenshot_2024-09-29-22-33-21-361_com android chrome-edit](https://gproxy.hanxi.cc/proxy/user-attachments/assets/838bfd1c-f19f-4690-86b0-8208d596fbf1)
## 后续用途
1. 语音下载歌曲都会带上前面上传的 cookies.txt 文件去搜索下载歌曲。
2. 歌曲批量下载工具下载歌曲或者歌单时也会带上 cookies.txt 文件。
## 评论
### 评论 1 - kingfly2016
0.3.37的版本并没有发现可以开启yt-dlp-cookies 并上传cookies文件的地方,尝试把导出的cookies.txt手工上传到conf目录下,没有生效.
![屏幕截图_11-10-2024_183725_192 168 6 202](https://gproxy.hanxi.cc/proxy/user-attachments/assets/9b8b9750-b616-4fd3-8a3c-216b2f99d02c)
---
### 评论 2 - hanxi
需要等38版本或者用测试版本镜像名后面加 :main
---
### 评论 3 - kingfly2016
> 需要等38版本或者用测试版本镜像名后面加 :main
谢谢
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/210)

112
docs/issues/211.md Normal file
View File

@@ -0,0 +1,112 @@
---
title: 📝 文档汇总
---
# 📝 文档汇总
## 1⃣ 基础文档
- [💬 FAQ问题集合](/issues/99.html)
- [如何修改默认的8090端口](/issues/19.html)
- [如何配置网络歌单](/issues/78.html)
- [如何添加m3u格式文件的电台](/issues/88.html)
- [xiaomusic极空间安装教程](/issues/297.html)
## 2⃣ 进阶文档
- [设置项功能介绍](/issues/333.html)
- [采用config.json配置方式](/issues/94.html)
- [ios系统上的捷径配置](/issues/96.html)
- [【插件】自定义口令功能](/issues/105.html)
- [定时任务配置格式](/issues/182.html)
- [yt-dlp cookies 文件上传功能](/issues/210.html)
- [如何批量下载歌曲](/issues/212.html)
- [设备分组播放](/issues/65.html#issuecomment-2215736529)
- [如何播放小雅alist里的歌曲](/issues/128.html#issuecomment-2232867180)
- [如何添加 网易云音乐playlist](/issues/269.html)
- [相关工具推荐](/issues/285.html)
## 3⃣ 其他安装文档
> [!NOTE]
> 下面教程可能比较旧,只供参考
- [群晖docker安装 xiaomusic](/issues/101.html)
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
- [QNAS部署教程](https://post.smzdm.com/p/a5xz5x63/)
- [视频教程-群晖1](https://www.bilibili.com/video/BV1ZZpweHEtT/)
- [视频教程-群晖2](https://www.bilibili.com/video/BV1JXxXeBEdY/)
- [视频教程-拾光坞N3](https://www.bilibili.com/video/BV1q629YMEG6/)
- [TechHive](https://mp.weixin.qq.com/s/4a41muFtPaFKtHeZYu795w)
- [弹个AI](https://mp.weixin.qq.com/s/sIsKxB7Y8b83AhnvaWiMog)
- [简单免费教你用绿联NAS联动小爱音箱私人音乐库也能语音点播](https://post.m.smzdm.com/p/a8pldgg7/)
- [飞牛教程](https://mp.weixin.qq.com/s?t=pages/image_detail&__biz=MzkxODc1NDMwOA==&mid=2247483725&idx=1&sn=2d615f14733b9bf989557fa766b4e1fc)
## 评论
### 评论 1 - sghuenn
redmi小爱触屏音箱8仍然需要打开“型号兼容模式”才能播放
打开兼容模式后的问题有每首歌曲播放完毕后都要再从头播放4、5秒才播放下一曲语音命令小爱同学播放下一曲它会从头开始播放当前歌曲。
---
### 评论 2 - hanxi
> redmi小爱触屏音箱8仍然需要打开“型号兼容模式”才能播放 打开兼容模式后的问题有每首歌曲播放完毕后都要再从头播放4、5秒才播放下一曲语音命令小爱同学播放下一曲它会从头开始播放当前歌曲。
播放歌曲的接口应该是有点问题,等有设备有开发能力的人来搞吧。
---
### 评论 3 - zhoukk37
想问下如何利用反向代理完成使得小爱外网访问nas呢您能提供一下关键词我自己去检索学下一下吗
---
### 评论 4 - hanxi
> 想问下如何利用反向代理完成使得小爱外网访问nas呢您能提供一下关键词我自己去检索学下一下吗
内网穿透frp能实现就是把局域网的端口映射成公网的端口。
---
### 评论 5 - Justlook99
按照飞牛的教程,部署成功了,一直没有设备显示出来,然后我也按照相应的问题集去处理:关闭本地代理。
如果是nas运行的网络由bridge改为host。
米家app重新登陆。
mi.com官网重新登陆。
但是还是没有办法显示设备出来请问到底是什么原因最新的37版本。
---
### 评论 6 - hanxi
> 按照飞牛的教程,部署成功了,一直没有设备显示出来,然后我也按照相应的问题集去处理:关闭本地代理。 如果是nas运行的网络由bridge改为host。 米家app重新登陆。 mi.com官网重新登陆。 但是还是没有办法显示设备出来请问到底是什么原因最新的37版本。
目前反馈的都是飞牛的用户,可能是飞牛有问题。
---
### 评论 7 - 3794313569
在同一个容器内前后分别启动了mi-gpt和xiaomusic两个应用现在通过日志发现mi-gpt的日志一直在记录语音需求基本都在mi-gpt这个应用响应了请问下按照您现在设计的框架内有没有办法可以实现这两个应用同时生效或者稍后类似应用会有专用的通讯协议保证多项应用在同一台机器上的响应。
类似:语音命令-“播放本地歌曲”触发xiaomusic“召唤”mi-gpt配置的唤醒词触发mi-gpt等等。。。。。。
暂时的办法就是买两个小爱音箱不同的命名然后一个应用配置一个did。
---
### 评论 8 - hanxi
> 在同一个容器内前后分别启动了mi-gpt和xiaomusic两个应用现在通过日志发现mi-gpt的日志一直在记录语音需求基本都在mi-gpt这个应用响应了请问下按照您现在设计的框架内有没有办法可以实现这两个应用同时生效或者稍后类似应用会有专用的通讯协议保证多项应用在同一台机器上的响应。 类似:语音命令-“播放本地歌曲”触发xiaomusic“召唤”mi-gpt配置的唤醒词触发mi-gpt等等。。。。。。 暂时的办法就是买两个小爱音箱不同的命名然后一个应用配置一个did。
可以分别部署到两个不同的容器里,两个应用的唤醒词是不同的,不会互相干扰。
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/211)

51
docs/issues/212.md Normal file
View File

@@ -0,0 +1,51 @@
---
title: 如何批量下载歌曲
---
# 如何批量下载歌曲
批量下载歌曲依赖的是 yt-dlp 批量下载播放列表里的视频并转为 mp3 实现的。
先进入到歌曲下载工具页面:
> 默认主题 => 设置 => 歌曲下载工具
![Screenshot_2024-09-29-22-36-12-178_com android chrome-edit](https://gproxy.hanxi.cc/proxy/user-attachments/assets/ddd2af00-cd9e-4938-9450-56503453807c)
已经测试过 B 站和 youtube 两种播放列表,播放列表的链接是有要求,不能有其他多余参数。
比如 B 站的是这样的
https://m.bilibili.com/video/BV1WUsDezE88
youtube 的是这样的
https://m.youtube.com/playlist?list=PLUD2d-pqyvT6_ztf31hx-5SsUUvY5UsQn
输入歌单名字是用于保存的文件夹名字,最好不是已经存在的名字,每次下载歌单都取个新名字比较合适。
已知 youtube 需要上传无痕模式下的 cookies.txt 文件才能正常下载。具体步骤见 /issues/210.html 。
也支持单独下载一个链接只有一首歌曲的。
## 评论
### 评论 1 - lazybabyz
默认主题 => 设置 =>没有显示找到 歌曲下载工具,
有一个 歌单地址 歌单内容: 输入B站测试地址显示返回无效
![aaa](https://gproxy.hanxi.cc/proxy/user-attachments/assets/31e224cb-fcbd-4841-b545-bfbd2496061b)
---
### 评论 2 - hanxi
等0.3.38版本。
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/212)

23
docs/issues/235.md Normal file
View File

@@ -0,0 +1,23 @@
---
title: xiaomusic立体声
---
# xiaomusic立体声
有多个不同版本的小爱,怎么能选择多个音箱一起播放?
## 评论
### 评论 1 - hanxi
参考这个文档,配到一个组里就能同时播放,但是会有播放进度不一致的情况。 /issues/65.html#issuecomment-2215736529
---
### 评论 2 - F-loat
我这边先用一个音箱播放,然后米家里设置全屋播放,就能多个音箱同时播了,进度也同步,而且后续会自动全屋播放
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/235)

532
docs/issues/269.md Normal file
View File

@@ -0,0 +1,532 @@
---
title: 如何添加 网易云音乐playlist
---
# 如何添加 网易云音乐playlist
利用 NeteaseCloudMusicApi 获取歌单和播放地址
我在你基础上改了一下,但是我的逻辑不合理
https://github.com/dissipator/xiaomusic
## 评论
### 评论 1 - hanxi
建议通过插件实现或者新增一个页面工具把歌单导出 json 。
歌单的 json 格式见 /issues/78.html
---
### 评论 2 - qiujie8092916
> 利用 NeteaseCloudMusicApi 获取歌单和播放地址 我在你基础上改了一下,但是我的逻辑不合理 [dissipator/xiaomusic](https://github.com/dissipator/xiaomusic)
老哥在实现插件了吗?我也急需播放歌单
我诉求是:我用网易云音乐单独新建了一个歌单,我往里面扔歌曲,以更新歌单。希望创建一个自定义的语音命令,让小爱同学随机播放这个歌单里的音乐。然后我通过在米家里执行,比如触发了「我回来了」的智能场景时,就让小爱音箱执行这个自定义的语音命令,就会自动播放我新建的这个歌单里的音乐了。
现在的小爱音箱虽然能勉强实现,但是很垃圾。随机播放的随机性有问题,并且只能选择歌单里的 30 首歌。
(老哥如果没实现的话,我可以尝试搞搞)
---
### 评论 3 - dissipator
我都能直接利用这个直接播放NeteaseCloudMusicApi这上的歌了。在配合unblk,无敌。我就是没设备,还在路上
通过插件实现,非常好,就是不知道怎么开发,有文档我可以尝试一下。
---
### 评论 4 - dissipator
> 建议通过插件实现或者新增一个页面工具把歌单导出 json 。 歌单的 json 格式见 #78
配置处直接填了api的接口
http://127.0.0.1:3000/playlist/detail?id=12758992226
我是直接再你歌单保存上强改的。
```python
async def downloadjson(data: UrlInfo, Verifcation=Depends(verification)):
log.info(data)
url = data.url
content = "[{"
host = f"{url.split('/')[0]}//{url.split('/')[2]}"
try:
ret = "OK"
jsons = await downloadfile(url,"json")
# print(jsons)
list_name = jsons['playlist']['name']
content += '"name":"'+list_name+'","musics":['
for song in jsons['playlist']['tracks']:
content += f"{{\"name\":\"{song['name']}\",\"url\": \"{host}/song/url?br=999000&proxy=http:%2F%2F127.0.0.1:8080&realIP=211.161.244.70&id={song['id']}\"}}"
except Exception as e:
log.exception(f"Execption {e}")
ret = "Download JSON file failed."
content = content[:-1] + "]}]"
```
照着你的说明。而且能成功播发
```python
@app.get("/musicinfo")
async def musicinfo(
name: str, musictag: bool = False, Verifcation=Depends(verification)
):
url = xiaomusic.get_music_url(name)
if("song/url" in url):
jsons = await downloadfile(url,"json")
url = jsons['data'][0]['url']
```
播放处加了一个判断
---
### 评论 5 - hanxi
> > 建议通过插件实现或者新增一个页面工具把歌单导出 json 。 歌单的 json 格式见 #78
你的修改我看了不太通用。生成json再用现有的接口提交json更通用。
---
### 评论 6 - dissipator
是的,不通用。最好是用插件实现。
1. 就是不知道你插件的逻辑。
2. 如果用插件就考虑直接读取网易账号下所有歌单。然后选择一个导入,或者全部导入。
3. 等你完善文档后我可以尝试写一个。同理qq等其他平台的歌单也就都可以弄了
---
### 评论 7 - hanxi
等有空我写个修改歌单内容的插件示例吧。
---
### 评论 8 - dissipator
```
import requests
def getmy_playlist(type="netease",api_host="http://127.0.0.1/api", playlist_id=None,uid=None):
"""
Purpose:
"""
global log, xiaomusic
if type == "netease":
if uid:
api_url = f"{api_host}/user/playlist?uid={uid}"
# 发起请求
response = requests.get(api_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
music_list = response.json()
for item in music_list['playlist']:
list_name = item.get("name")
log.info(f"getmy_playlist name:{list_name}")
# if item.get("id") in [12709941656,]:
songs_url = f"{api_host}/playlist/detail?id={item['id']}"
# 发起请求
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
musics = response.json()
one_music_list = []
for music in musics['playlist']['tracks']:
if (not music):
continue
# try:
name = music['name']
picUrl = music['al']['picUrl']
artist = music['ar'][0]['name']
album = music['al']['name']
name = music.get("name")
url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"
if (not name) or (not url):
continue
xiaomusic.all_music[name] = url
xiaomusic.all_music_tags[name] = {
"title": name,
"artist": artist,
"album": album,
"year": "",
"genre": "",
"picture": picUrl,
"lyrics": ""
}
one_music_list.append(name)
log.debug(f"getmy_playlist name:{list_name}")
log.debug(one_music_list)
# 歌曲名字相同会覆盖
xiaomusic.music_list[list_name] = one_music_list
xiaomusic.try_save_tag_cache()
log.debug(xiaomusic.all_music)
log.debug(xiaomusic.music_list)
return
if playlist_id:
songs_url = f"{api_host}/playlist/detail?id={playlist_id}"
# 发起请求
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
musics = response.json()
list_name = musics['playlist']['name']
one_music_list = []
for music in musics['playlist']['tracks']:
if (not music):
continue
# try:
name = music['name']
picUrl = music['al']['picUrl']
artist = music['ar'][0]['name']
album = music['al']['name']
name = music.get("name")
url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"
if (not name) or (not url):
continue
xiaomusic.all_music[name] = url
xiaomusic.all_music_tags[name] = {
"title": name,
"artist": artist,
"album": album,
"year": "",
"genre": "",
"picture": picUrl,
"lyrics": ""
}
one_music_list.append(name)
log.debug(f"getmy_playlist name:{list_name}")
log.debug(one_music_list)
# 歌曲名字相同会覆盖
xiaomusic.music_list[list_name] = one_music_list
xiaomusic.try_save_tag_cache()
log.debug(xiaomusic.all_music)
log.debug(xiaomusic.music_list)
return
else:
log.error(f"getmy_playlist type:{type} not support")
```
---
### 评论 9 - dissipator
> 等有空我写个修改歌单内容的插件示例吧。
不用拉,插件我已经写出来了
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/53e593e7-1439-4968-9549-8c84b2fee42c)
---
### 评论 10 - hanxi
发下你的 setting.json 配置吧,方便他人知道怎么配。
---
### 评论 11 - dissipator
我还在测试,设备到了能用了再发吧
---
### 评论 12 - guitarbug
也需要网易歌单功能, 坐等教程
---
### 评论 13 - dissipator
# 成功了
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/33e22665-b1cc-4069-9e8c-46e466679b30)
# 最好是在setting.json里去配置我测试老是不匹配在setting.json里去配置终于成功。可以直接调用 [NeteaseCloudMusicApi](http://localhost:3000/docs/#/?id=neteasecloudmusicapi) 接口甚至使用UnblockNeteaseMusic 解锁灰色,但是代码需要小调整。
setting.json
```json
{
"account": "",
"password": "",
"mi_did": "",
"miio_tts_command": "",
"cookie": "",
"verbose": false,
"music_path": "music",
"download_path": "music/download",
"conf_path": "conf",
"cache_dir": "cache",
"hostname": "192.168.2.5",
"port": 8090,
"public_port": 0,
"proxy": "",
"search_prefix": "bilisearch:",
"ffmpeg_location": "./ffmpeg/bin",
"active_cmd": "play,set_random_play,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,获取歌单",
"exclude_dirs": "@eaDir,tmp",
"music_path_depth": 10,
"disable_httpauth": true,
"httpauth_username": "",
"httpauth_password": "",
"music_list_url": "",
"music_list_json": "",
"custom_play_list_json": "",
"disable_download": false,
"key_word_dict": {
"播放歌曲": "play",
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"上一首": "play_prev",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "set_random_play",
"分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"加入收藏": "add_to_favorites",
"收藏歌曲": "add_to_favorites",
"取消收藏": "del_from_favorites",
"播放列表第": "play_music_list_index",
"本地播放歌曲": "playlocal",
"放歌曲": "play",
"暂停": "stop",
"停止": "stop",
"停止播放": "stop",
"播放歌单": "play_music_list",
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")",
"获取歌单": "exec#getmy_playlist(playlist_id=12758992225)"
},
"key_match_order": [
"分钟后关机",
"播放歌曲",
"下一首",
"上一首",
"单曲循环",
"全部循环",
"随机播放",
"关机",
"刷新列表",
"播放列表第",
"播放列表",
"加入收藏",
"收藏歌曲",
"取消收藏",
"播放本地歌曲",
"本地播放歌曲",
"放歌曲",
"暂停",
"停止",
"停止播放",
"播放歌单",
"测试自定义口令",
"测试链接",
"获取歌单"
],
"use_music_api": false,
"use_music_audio_id": "1582971365183456177",
"use_music_id": "355454500",
"log_file": "/tmp/xiaomusic.txt",
"fuzzy_match_cutoff": 0.6,
"enable_fuzzy_match": true,
"stop_tts_msg": "收到,再见",
"enable_config_example": false,
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
"keywords_play": "播放歌曲,放歌曲",
"keywords_stop": "关机,暂停,停止,停止播放",
"keywords_playlist": "播放列表,播放歌单",
"user_key_word_dict": {
"测试自定义口令": "exec#code1(\"hello\")",
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")",
"获取歌单": "exec#getmy_playlist(playlist_id=12758992225)"
},
"enable_force_stop": false,
"devices": {
" ": {
"did": " ",
"device_id": " -17c6-4204- - ",
"hardware": "L05C",
"name": "小黑你好",
"play_type": "",
"cur_music": "",
"cur_playlist": ""
}
},
"group_list": "",
"remove_id3tag": false,
"convert_to_mp3": false,
"delay_sec": 3,
"continue_play": false,
"pull_ask_sec": 1,
"crontab_json": "",
"enable_yt_dlp_cookies": false,
"get_ask_by_mina": false
}
```
getmy_playlist.py
```python
import requests
async def getmy_playlist(type="netease",api_host="http://127.0.0.1/api", playlist_id=None,uid=None):
"""
Purpose:
"""
global log, xiaomusic
if type == "netease":
if uid:
api_url = f"{api_host}/user/playlist?uid={uid}"
# 发起请求
response = requests.get(api_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
music_list = response.json()
for item in music_list['playlist']:
list_name = item.get("name")
log.info(f"getmy_playlist name:{list_name}")
# if item.get("id") in [12709941656,]:
songs_url = f"{api_host}/playlist/detail?id={item['id']}"
# 发起请求
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
musics = response.json()
one_music_list = []
for music in musics['playlist']['tracks']:
if (not music):
continue
# try:
name = music['name']
picUrl = music['al']['picUrl']
artist = music['ar'][0]['name']
album = music['al']['name']
name = music.get("name")
url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"
if (not name) or (not url):
continue
xiaomusic.all_music[name] = url
xiaomusic.all_music_tags[name] = {
"title": name,
"artist": artist,
"album": album,
"year": "",
"genre": "",
"picture": picUrl,
"lyrics": ""
}
one_music_list.append(name)
log.debug(f"getmy_playlist name:{list_name}")
log.debug(one_music_list)
# 歌曲名字相同会覆盖
xiaomusic.music_list[list_name] = one_music_list
xiaomusic.try_save_tag_cache()
log.debug(xiaomusic.all_music)
log.debug(xiaomusic.music_list)
return
if playlist_id:
songs_url = f"{api_host}/playlist/detail?id={playlist_id}"
# 发起请求
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
musics = response.json()
list_name = musics['playlist']['name']
log.info(f"getmy_playlist list_name:{list_name} ")
one_music_list = []
for music in musics['playlist']['tracks']:
if (not music):
continue
# try:
name = music['name']
picUrl = music['al']['picUrl']
artist = music['ar'][0]['name']
album = music['al']['name']
name = music.get("name")
url = f"{api_host}/song/url?id={music['id']}&br=350000&proxy=HTTP:%2F%2F127.0.0.1:8080"
if (not name) or (not url):
continue
xiaomusic.all_music[name] = url
xiaomusic.all_music_tags[name] = {
"title": name,
"artist": artist,
"album": album,
"year": "",
"genre": "",
"picture": picUrl,
"lyrics": ""
}
one_music_list.append(name)
log.debug(f"getmy_playlist name:{list_name}")
log.debug(one_music_list)
# 歌曲名字相同会覆盖
xiaomusic.music_list[list_name] = one_music_list
xiaomusic.try_save_tag_cache()
return
else:
log.error(f"getmy_playlist type:{type} not support")
```
---
### 评论 14 - dissipator
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/7412eec4-f7d3-4a86-b186-0118d6f331ff)
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/2215e520-0d40-4c2c-8d46-d3106d65fc51)
---
### 评论 15 - dludream
NeteaseCloudMusicApi似乎获取不到播放地址其他信息倒是有。
![2024-11-28_151952](https://gproxy.hanxi.cc/proxy/user-attachments/assets/c7e7a748-aa82-47f0-8784-f6469cc3e99b)
---
### 评论 16 - hanxi
看代码是另一个接口获取url的 `url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"`
---
### 评论 17 - dludream
> 看代码是另一个接口获取url的 `url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"`
是的我用的是这个接口https://registry.hub.docker.com/r/gnehs/neteasecloudmusicapi-docker/
可能网易修改了api这些获取不了播放地址了。
我倒不是要这个功能只是感兴趣看看。我不获取我直接用yt把歌全下载下来播放。
---
### 评论 18 - dissipator
这个不是网易的接口,是[NeteaseCloudMusicApi](http://localhost:3000/docs/#/?id=neteasecloudmusicapi) 接口。proxy=HTTP:%2F%2F127.0.0.1:8080"是UnblockNeteaseMusic 解锁灰色;
完整的使用方式和docker 可以到 https://github.com/dissipator/xiaomusic/tree/dev 看README.md目前没有教程。本人就在群2有问题可以找我。
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/269)

45
docs/issues/285.md Normal file
View File

@@ -0,0 +1,45 @@
---
title: 相关工具推荐
---
# 相关工具推荐
- [xhongc/music-tag-web 刮削音乐歌词图片](https://github.com/xhongc/music-tag-web)
- [onlyLTY/dockerCopilot 一键更新容器](https://github.com/onlyLTY/dockerCopilot)
## 评论
### 评论 1 - F-loat
借楼加个小程序的图 :partying_face: https://github.com/F-loat/xiaoplayer
### 小程序码
<p>
<img alt="weapp" src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/weappcode.jpg" width="24%" />
</p>
### 截图
<p>
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/1.jpg" width="24%" />
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/2.jpg" width="24%" />
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/4.jpg" width="24%" />
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/3.jpg" width="24%" />
</p>
---
### 评论 2 - hanxi
@F-loat 可以在欢迎页加个链接显示小程序码。
---
### 评论 3 - F-loat
@hanxi 可以的,这样还能自动把 ip 用参数带过来,我有空搞一下
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/285)

62
docs/issues/294.md Normal file
View File

@@ -0,0 +1,62 @@
---
title: 关于M01型号的注意事项
---
# 关于M01型号的注意事项
M01:在0.3.55版本【型号兼容模式】与【特殊型号获取对话记录】都设为false或true都可以语音了。
如果【型号兼容模式】为 true默认UI显示播放中但音箱没声音。
型号:S12A、LX04、S12 在米家APP可以联动比如客厅有人自定义指令:播放歌曲、关机...等
而M01无论【型号兼容模式】与【特殊型号获取对话记录】设为false或true都无法执行任何自定义指令…
![IMG_6460](https://gproxy.hanxi.cc/proxy/user-attachments/assets/0913e3fa-1f1a-47b0-b8b9-d308bd7793df)
## 评论
### 评论 1 - hanxi
M01 保持默认设置应该是能语音和播放的吧。自定义指令的功能应该是 M01 本身就不支持,可能属于放弃维护的产品吧。
---
### 评论 2 - bj803
> M01 保持默认设置应该是能语音和播放的吧。自定义指令的功能应该是 M01 本身就不支持,可能属于放弃维护的产品吧。
刚试了用:pause是可以暂停(不过只暂2分钟左右又自己播放了)
---
### 评论 3 - hanxi
只能网页里点关机按钮,或者语音关机。所有型号都一样。
---
### 评论 4 - bj803
> 型号:S12A、LX04、S12
> 只能网页里点关机按钮,或者语音关机。所有型号都一样。
在型号:S12A、LX04、S12 除了能网页里点关机按钮或者语音开关机外能在米家APP自定义指令进行播放与关机(例如当客厅客有人自定义指令"播放歌曲",无人自定义指令"关机"。M01自定义指令"pause"可以暂停,其他口令不行。
---
### 评论 5 - bj803
刚又试了一下用自定义play stop、power off可以停止播放关机用自定义play music可以播放音乐可能M01不需要下划线
---
### 评论 6 - hanxi
感谢你的反馈。
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/294)

250
docs/issues/297.md Normal file
View File

@@ -0,0 +1,250 @@
---
title: xiaomusic极空间安装教程2024/12/28更新
---
# xiaomusic极空间安装教程2024-12-28更新
> 本教程同步更新于最新版的xiaomusic
<s>看不懂/嫌麻烦/懒 但有点小钱,找 hanxi 预约微信或者 QQ 远程安装他便宜收费50一次作法不成功不要钱</s>
**ARM架构自己想办法获取镜像 点名Z2PRO**
# 获取镜像
## 科学环境:
1.**搜索框** 中输入 `hanxi/xiaomusic`,在搜索的结果中直接选择第一个,点击**下载**
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/ef18b479-aef3-4e76-a299-6f220fc9e549)
2. 在新弹出的版本选择窗口中,根据你的情况选择。
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/20dd0b64-9223-47ae-a3e4-0b0af05796f8)
### 版本说明
- 获取 **最新版** 直接点击 **下载** 即可,建议使用默认的 `latest`
- 获取 **特定版本** [点击此处可查看](https://github.com/hanxi/xiaomusic/releases) 用于回退出现功能不兼容、恶性bug等情况一般建议反馈开发者修复很及时尽量不要回退版本 请输入如 `v0.3.55`
- 获取 **实验版本**已修复部分bug但未推送请输入 `main`
3. 接着弹出如图所示的页面,耐心等待下载完成。
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/5ea2621a-bba5-4269-b896-0e6ca323beb8)
4. 下载完成后切换到 **本地镜像** 选项卡
剩余步骤与国内环境相同,见 [部署镜像](#部署镜像)
## 国内环境:
1. 打开docker在左侧的菜单中选择 **镜像** 切换到 **仓库** 选项卡,点击 **自定义拉取** 按钮
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/bf4ac64e-ce50-4456-bc7b-05530b5abc1b)
2. 在弹出的对话框中输入 ` m.daocloud.io/docker.io/hanxi/xiaomusic ` ,点击 **拉取** 按钮
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/a8f91db7-eac9-472d-a920-a77c280bbc5e)
3. 下载完成后切换到 **本地镜像** 选项卡
# 部署镜像
1. 找到刚才已经拉取好的镜像,*单击选中*,点击 **添加到容器**
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/6e0bcf60-d1aa-46ac-9053-5ad957f3d509)
2. 在弹出的 **创建容器** 菜单中,切换到 **文件夹路径** 选项卡中,按图中的提示进行配置。
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/cca8bc1c-de8f-4d03-81eb-a0f2b06c121e)
**注意:**
* 装载路径中的 **配置文件目录** 和 **音乐目录** 必须进行配置,**其他目录非必要请勿配置**
* 主题目录为方便开发主题调试时的配置选项,普通用户不能理解明确用途请**不要配置主题目录**,否则会报**HTTP Status 500 Internal Server Error** 错误
* 如有多个音乐目录,请按照下面的格式进行配置
| 文件/文件夹 | 装载路径 |
| :----------: | :----------: |
| /data/music1 | /app/music/music1 |
| /data/music2 | /app/music/music2 |
3. 切换到 **端口** 选项卡,修改成与你的极空间 *不冲突* 的本地端口号,如 `5678` 示例按照本地端口号5678来进行配置下同
> 友情提醒: 尽量不要修改容器端口号,否则要到配置文件目录修改对应的`setting.json`文件中的配置,会增加很多麻烦
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/182b32ef-d91f-47c5-ba9d-7ef3542ebf40)
5. 切换到 **环境** 选项卡,将`XIAOMUSIC_HOSTNAME` 修改为你的 **极空间的IP地址**
> 友情提醒:
> 1. 此处不可忽略,否则后续播放音乐会出现问题
> 2. 不要尝试修改XIAOMUSIC_PORT除非你没有看上一条的友情提醒
> 3. 不要在此处配置`ACCOUNT`和`PASSWORD`,没有过风控仍然无法使用!上古时代的教程不要再看了,容易走火入魔!
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/b41b7359-f2b9-4ad8-b2a2-0ce2a1601739)
6. 点击 **应用**按钮,此时容器已经配置完成了,切换到左侧的 **容器概况** 菜单,可查看容器详情
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/5c5b3497-49d8-4f17-9acb-6d1e551caf4f)
# 进入xiaomusic网页端进行配置
1.请关闭代理,打开浏览器,地址栏输入 **极空间IP:本地端口号**`192.168.2.5:5678`,打开网页后点击 **默认主题**
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/1b286d0f-f10e-46ff-89a0-cdff9e192f9b)
**注意:**
* 不要复制此处的地址必须输入极空间的IP地址。不知道的建议上咸鱼50块换个不锈钢盆
* 不要输入容器的端口号8090极空间不能使用这个端口号。
2. 点击 **设置** 按钮进入设置页面
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/f3aca07a-3663-4bb6-bb93-0329b2d4c433)
3. 输入**小米账号**、**小米密码**、**XIAOMUSIC_HOSTNAME(IP或域名):**、**外网访问端口**,滑到页面最下方点击 **保存**
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/db03af0c-851a-4185-a7fd-b5b02b6d8d2b)
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/dd42a653-d364-4eec-9f87-efb3c52e57a7)
**注意:**
* 小米账号非手机号,请在手机设置-个人中心中查看小米ID
* 密码不要输错,账号密码错误在上面会弹出提醒,不要假装看不见上面的提醒文字
* XIAOMUSIC_HOSTNAME(IP或域名): 可以输入当前页面的IP地址在地址栏**不要在此处输入端口号!!!**如果域名需要使用https协议请加上https://
4.如果以上步骤没错,你将在设置中心看见设备列表
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/04074894-6599-4b35-95ea-0618ed906d15)
5. 回到首页,出现设备列表,切换对应设备即可畅享
![图片](https://gproxy.hanxi.cc/proxy/user-attachments/assets/1ce4791f-7ae7-40b4-8c90-eca6b0799e19)
## 评论
### 评论 1 - xiaohuobanhahaha
[xiaomusic.txt](https://github.com/user-attachments/files/18011572/xiaomusic.txt)
<img width="559" alt="截屏2024-12-05 00 43 24" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/160aeacc-e1c0-40fa-b219-6b6f5183c43c">
an'zh
无法使用语音播放歌曲小爱s12a。极空间z4pro。
1. 按照教程点击播放本地歌曲提示hostname和设置的端口映射不匹配。映射5678容器端口8090.
2.网络host模式能够本地播放无法使用语音控制提示“下载app”。日志已上传
---
### 评论 2 - 52fisher
> [xiaomusic.txt](https://github.com/user-attachments/files/18011572/xiaomusic.txt)
> <img alt="截屏2024-12-05 00 43 24" width="559" src="https://private-user-images.githubusercontent.com/20666294/392485798-160aeacc-e1c0-40fa-b219-6b6f5183c43c.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzE3NzIsIm5iZiI6MTczMzMzMTQ3MiwicGF0aCI6Ii8yMDY2NjI5NC8zOTI0ODU3OTgtMTYwYWVhY2MtZTFjMC00MGZhLWIyMTktNmI2ZjUxODNjNDNjLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE2NTc1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJhMmZjMzFhMzdmYmI1NGJjZTg1ZGVhNGI2Njc1YjYwYmQxZjVmMzYyYjg3YWNlNzdhNmEwMzE4Y2UyMTRlNjUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._OSLH4Bc0stzG4tLdPalW-mB-ASKlM9uztZ1icefPFU"> an'zh 无法使用语音播放歌曲小爱s12a。极空间z4pro。 1. 按照教程点击播放本地歌曲提示hostname和设置的端口映射不匹配。映射5678容器端口8090. 2.网络host模式能够本地播放无法使用语音控制提示“下载app”。日志已上传
既然你映射的5678为什么你又在那把监听端口改成11999? 我的教程里面全程没有写要修改监听端口
---
### 评论 3 - xiaohuobanhahaha
我没讲清楚。我试了两种极空间的桥接和host模式。桥接模式。我按照教程走的。报错如图
<img width="847" alt="截屏2024-12-05 01 46 52" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/e7f58907-a216-41e8-bafa-5d49db8eca45">
<img width="516" alt="截屏2024-12-05 01 49 11" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/4261c2e2-fe0c-4ff6-ae06-ead7f928af57">
<img width="647" alt="截屏2024-12-05 01 47 02" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/35b195d1-9512-40bb-b336-847e0bb2e6c9">
<img width="667" alt="截屏2024-12-05 01 47 15" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/b917a977-38cf-4126-8754-c46abe9360a2">
提到的第二个问题和日志是我将网络模式改为host的情况能连上音箱但是没法使用语音控制。
---
### 评论 4 - 52fisher
> 我没讲清楚。我试了两种极空间的桥接和host模式。桥接模式。我按照教程走的。报错如图 <img alt="截屏2024-12-05 01 46 52" width="847" src="https://private-user-images.githubusercontent.com/20666294/392507064-e7f58907-a216-41e8-bafa-5d49db8eca45.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDcwNjQtZTdmNTg5MDctYTIxNi00MWU4LWJhZmEtNWQ0OWRiOGVjYTQ1LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBlMTYxZGI0ZjRiYTkyOGIwZWQ4YWZlZWJiY2U3MzljZTg5NTNhZjJkNzlkNzYyYzlmMWJkZjlkMGYwNWEzNzgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.XSsE7pZj7pwj4kaZBQ02fyH13bVXLzGuJcjRv98WWiQ"> <img alt="截屏2024-12-05 01 49 11" width="516" src="https://private-user-images.githubusercontent.com/20666294/392507855-4261c2e2-fe0c-4ff6-ae06-ead7f928af57.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDc4NTUtNDI2MWMyZTItZmUwYy00ZmY2LWFlMDYtZWFkN2Y5MjhhZjU3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk2NzFjYTRmNzAyMTJlYzYwZDI4NThlMWQ1MDViZGU5MDI3YThjNzExZTAyNWJmM2NlYWQzZDIzYzRhMjc1MTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.NIBWfzpZ5OTIic6Gv83_PrOdUX27o19Vo1zDvFyrILE"> <img alt="截屏2024-12-05 01 47 02" width="647" src="https://private-user-images.githubusercontent.com/20666294/392507111-35b195d1-9512-40bb-b336-847e0bb2e6c9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDcxMTEtMzViMTk1ZDEtOTUxMi00MGJiLWIzMzYtODQ3ZTBiYjJlNmM5LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI5YTliOWQzNDUyYThjODYwZmY4NTJiMTNkYmFmNmY3ZWE5ZDBlMmQ5OGQxMTIzM2JlMmIxZTcwNTNlMmYwZTEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.f9czCn43Yzm7sn6-cpJykaWIng4WJf9aoE52kbVeASY"> <img alt="截屏2024-12-05 01 47 15" width="667" src="https://private-user-images.githubusercontent.com/20666294/392507187-b917a977-38cf-4126-8754-c46abe9360a2.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDcxODctYjkxN2E5NzctMzhjZi00MTI2LTg3NTQtYzQ2YWJlOTM2MGEyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ3MDE1NjVhZjMzMTRkNjg5ZTA5NDc1MDU3OTFiODc3NTYyMTg3Y2FjNjg2NGM3MjE0N2VlNjg0YzFmZTgwZGImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.tX8iIlpvE43Krq4q__7dQ3Wuz8kYQuzmlf-XXNHO1Ws">
>
> 提到的第二个问题和日志是我将网络模式改为host的情况能连上音箱但是没法使用语音控制。
把外网访问端口改成5678
---
### 评论 5 - xiaohuobanhahaha
可以连接上网页控制了但是语音控制仍然不行。已经按照FAQ问题集合 #99 重启docker。
这是日志,
[xiaomusic.txt](https://github.com/user-attachments/files/18012369/xiaomusic.txt)
---
### 评论 6 - 52fisher
> 可以连接上网页控制了但是语音控制仍然不行。已经按照FAQ问题集合 #99 重启docker。 这是日志, [xiaomusic.txt](https://github.com/user-attachments/files/18012369/xiaomusic.txt)
你的设置 hostname='192.168.31.165', port=8090, public_port=5678,
后台的ip 192.168.31.143 检查一下你的地址 有可能是你的ip地址改变了
---
### 评论 7 - xiaohuobanhahaha
> > 可以连接上网页控制了但是语音控制仍然不行。已经按照FAQ问题集合 #99 重启docker。 这是日志, [xiaomusic.txt](https://github.com/user-attachments/files/18012369/xiaomusic.txt)
>
> 你的设置 hostname='192.168.31.165', port=8090, public_port=5678, 后台的ip 192.168.31.143 检查一下你的地址 有可能是你的ip地址改变了
确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
---
### 评论 8 - 52fisher
> 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
---
### 评论 9 - xiaohuobanhahaha
<img width="660" alt="截屏2024-12-05 02 23 53" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/b9d26de9-3dcf-4e65-9460-36603735c887">
<img width="780" alt="截屏2024-12-05 02 24 49" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/6a204cdb-bb10-4f35-822d-613aeed0fae0">
> > 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
>
> 容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
辛苦了这么晚还在回复。我一直用的bridge。大佬群号多少不行我明天群里问吧。
---
### 评论 10 - 52fisher
> <img alt="截屏2024-12-05 02 23 53" width="660" src="https://private-user-images.githubusercontent.com/20666294/392519509-b9d26de9-3dcf-4e65-9460-36603735c887.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk1MDktYjlkMjZkZTktM2RjZi00ZTY1LTk0NjAtMzY2MDM3MzVjODg3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWFhN2I4NzQyYjA4YzRiOTk4OTU3NmVkNjU2MjM1ODhjMmNlOWU0YTg5OTFlMzA1NTcxMTA5OTk1YjgwNThhOTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.yrC-sn2T6PKwBJb_mal1T2yxcSz008Hb7KWVmOe6zaA"> <img alt="截屏2024-12-05 02 24 49" width="780" src="https://private-user-images.githubusercontent.com/20666294/392519852-6a204cdb-bb10-4f35-822d-613aeed0fae0.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk4NTItNmEyMDRjZGItYmIxMC00ZjM1LTgyMmQtNjEzYWVlZDBmYWUwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTE4MzlhODM1NDY4ZjFmN2I2Y2JkZWU5ZGFkNTNjYTNmZDg2OTU3YzA0MjRkMDA2MzRmOTk2ZGE3NmE2NjViZmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._I8Rll2a96i-cx6WBvEwOclNInfOmZkD5HmcorzT-KI">
>
> > > 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
> >
> >
> > 容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
>
> 辛苦了这么晚还在回复。我一直用的bridge。大佬群号多少不行我明天群里问吧。
[readme](https://github.com/hanxi/xiaomusic?tab=readme-ov-file#-%E8%AE%A8%E8%AE%BA%E5%8C%BA)
---
### 评论 11 - xiaohuobanhahaha
> > <img alt="截屏2024-12-05 02 23 53" width="660" src="https://private-user-images.githubusercontent.com/20666294/392519509-b9d26de9-3dcf-4e65-9460-36603735c887.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk1MDktYjlkMjZkZTktM2RjZi00ZTY1LTk0NjAtMzY2MDM3MzVjODg3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWFhN2I4NzQyYjA4YzRiOTk4OTU3NmVkNjU2MjM1ODhjMmNlOWU0YTg5OTFlMzA1NTcxMTA5OTk1YjgwNThhOTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.yrC-sn2T6PKwBJb_mal1T2yxcSz008Hb7KWVmOe6zaA"> <img alt="截屏2024-12-05 02 24 49" width="780" src="https://private-user-images.githubusercontent.com/20666294/392519852-6a204cdb-bb10-4f35-822d-613aeed0fae0.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk4NTItNmEyMDRjZGItYmIxMC00ZjM1LTgyMmQtNjEzYWVlZDBmYWUwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTE4MzlhODM1NDY4ZjFmN2I2Y2JkZWU5ZGFkNTNjYTNmZDg2OTU3YzA0MjRkMDA2MzRmOTk2ZGE3NmE2NjViZmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._I8Rll2a96i-cx6WBvEwOclNInfOmZkD5HmcorzT-KI">
> > > > 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
> > >
> > >
> > > 容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
> >
> >
> > 辛苦了这么晚还在回复。我一直用的bridge。大佬群号多少不行我明天群里问吧。
>
> [readme](https://github.com/hanxi/xiaomusic?tab=readme-ov-file#-%E8%AE%A8%E8%AE%BA%E5%8C%BA)
已自查解决。问题是账号问题。绑定设备的一定是创建者,不能是管理员。
---
### 评论 12 - McCree2020
这个主题目录不能设置吧没人遇到这个issue我原来用的张大妈平台的教程设置的能用后来看到这个教程后就修改了后台的路径映射但是dockers启动正常网页不能打开提示internal sever error后来ssh进docker看了日志文件 提示static那个路径有问题下边的index什么的文件找不到 删除主题映射以后重启docker后网页正常显示了
---
### 评论 13 - 52fisher
> 这个主题目录不能设置吧没人遇到这个issue我原来用的张大妈平台的教程设置的能用后来看到这个教程后就修改了后台的路径映射但是dockers启动正常网页不能打开提示internal sever error后来ssh进docker看了日志文件 提示static那个路径有问题下边的index什么的文件找不到 删除主题映射以后重启docker后网页正常显示了
要注意看提示:
装载路径中的 配置文件目录 和 音乐目录 必须进行配置。
其他的路径非必要不要配置,主题目录路径是方便开发调试的时候用的,普通用户不要映射主题目录
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/297)

31
docs/issues/333.md Normal file
View File

@@ -0,0 +1,31 @@
---
title: 设置项功能介绍
---
# 设置项功能介绍
- XIAOMUSIC_ACTIVE_CMD 环境变量,对应后台的 【允许唤醒的命令】,用于唤醒口令,配置成'play,random_play'在非播放状态下只有这两个指令播放歌曲和随机播放可以触发触发后xiaomusic进入playing状态其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录,对应后台的 【忽略目录】
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,对应后台的 【目录深度】,具体见 </issues/76.html>
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台对应后台的 【关闭密码验证】,具体见 </issues/47.html>
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户,对应后台的 【控制台账户】
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码,对应后台的 【控制台密码】
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,对应后台的 【配置文件目录】,记得把目录映射到主机,默认为 `/app/config` ,具体见 </issues/74.html>
- XIAOMUSIC_CACHE_DIR 用来音乐 tag 缓存,默认为 `/app/cache`,对应后台的 【缓存文件目录】。
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,对应后台的 【关闭下载功能】,见 </issues/82.html>
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,对应后台的 【型号兼容模式】,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 </issues/30.html>
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,对应后台的 【播放歌曲口令】,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,对应后台的 【停止口令】,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,对应后台的 【播放本地歌曲口令】,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,对应后台的 【开启模糊搜索】,支持模糊匹配歌名和歌单名。 具体见 </issues/52.html>
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值默认0.6可以配0到1直接的小数越小越模糊越大越精准对应后台的 【模糊匹配阈值】。具体见 </issues/52.html>
- XIAOMUSIC_PUBLIC_PORT 用于设置外网端口,对应后台的 【外网访问端口】当使用反向代理时可以设置为外网端口XIAOMUSIC_HOSTNAME 设为外网IP或者域名即可。
- XIAOMUSIC_DOWNLOAD_PATH 变量可以配置下载目录,默认为空,表示使用 music 目录为下载目录,对应后台的 【音乐下载目录】。设置这个目录必须是 music 的子目录,否则刷新列表后会找不到歌曲。具体见 </issues/98.html>
- XIAOMUSIC_PROXY 用于配置国内使用 youtube 源下载歌曲时使用的代理,参数格式参考 yt-dlp 文档说明。 见 </issues/2.html> 和 </issues/11.html>
- MIIO_TTS_CMD 用于部分机型(如:`L05C`)使用 MiIO 支持 tts 能力,默认为空,命令选择见 [MiService-fork 文档](https://github.com/yihong0618/MiService)
## 评论
没有评论。
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/333)

392
docs/issues/78.md Normal file
View File

@@ -0,0 +1,392 @@
---
title: 已支持配置自定义网络歌单,在这里分享你的歌单
---
# 已支持配置自定义网络歌单,在这里分享你的歌单
设置页面新增一个输入框配置json格式可以定义配置音乐源可以是电台或者其他的m3u8格式的。
再加一个输入框配置这个json文件的url点击获取按钮把url对应的json内容填充到json输入框方便直接使用别人分享的歌单。
比如这样的链接
- https://lhttp.qtfm.cn/live/4915/64k.mp3
- http://ngcdn001.cnr.cn/live/zgzs/index.m3u8
已经测试能播放出来:
```
python3 micli.py play http://ngcdn001.cnr.cn/live/zgzs/index.m3u8
```
预计歌单格式是这样的, type 为 radio 作为电台的设定,会一直播放当前电台,不会播放下一首。
```json
[
{
"name":"歌单1",
"musics":[
{
"name":"歌名1",
"url":"http://ngcdn001.cnr.cn/live/zgzs/index.m3u8",
"type":"radio"
},
{
"name":"歌名2",
"url":"https://lhttp.qtfm.cn/live/4915/64k.mp3"
}
]
},
{
"name":"歌单2",
"musics":[
{
"name":"歌名3",
"url":"https://lhttp.qtfm.cn/live/4915/64k.mp3"
},
{
"name":"歌名4",
"url":"https://lhttp.qtfm.cn/live/4915/64k.mp3"
}
]
}
]
```
这里分享一个让 chatgpt 写 python 脚本来生成歌单的例子 <https://chatgpt.com/share/6751c019-74c0-800a-a978-a20c636d4464>
## 评论
### 评论 1 - hanxi
可以使用 gist 来配置和分享 json 文件,比如 https://gist.github.com/hanxi/dda82d964a28f8110f8fba81c3ff8314
点击 raw 得到 json 文件的链接 https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/8787844d81c39dbfaad4e37954dd449d8bba5728/example.json
当然还可以用其他工具分享json文件比如 github 和国内的 gitee 。
---
### 评论 2 - hanxi
已经有工具支持将 m3u 格式的电台文件转为网络歌单格式,见 /issues/88.html
欢迎有兴趣的朋友制作其他格式转换工具,比如网易歌单那一类的。
---
### 评论 3 - lazybabyz
按照教程设置后 播放列表选择m3u电台 再选择播放列表 歌曲,结果显示播放中 不断在转台
stdout: [08:58:02] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /static/default/index.html HTTP/1.1" 304
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /getsetting HTTP/1.1" 200
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24022 - "GET /getversion HTTP/1.1" 200
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /musiclist HTTP/1.1" 200
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /static/default/curplaylist?did=566731712 HTTP/1.1" 404
stdout: [08:58:05] [0.3.37] [INFO] 10.0.80.191:24022 - "GET /getvolume?did=566731712 HTTP/1.1" 500
stderr: [08:58:05] [0.3.37] [ERROR] Exception in ASGI application
stderr: Traceback (most recent call last):
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 406, in run_asgi
stderr: result = await app( # type: ignore[func-returns-value]
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 70, in __call__
stderr: return await self.app(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
stderr: await super().__call__(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/applications.py", line 113, in __call__
stderr: await self.middleware_stack(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 187, in __call__
stderr: raise exc
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 165, in __call__
stderr: await self.app(scope, receive, _send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/cors.py", line 85, in __call__
stderr: await self.app(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
stderr: await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
stderr: raise exc
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
stderr: await app(scope, receive, sender)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 715, in __call__
stderr: await self.middleware_stack(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 735, in app
stderr: await route.handle(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 288, in handle
stderr: await self.app(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 76, in app
stderr: await wrap_app_handling_exceptions(app, request)(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
stderr: raise exc
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
stderr: await app(scope, receive, sender)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 73, in app
stderr: response = await f(request)
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 301, in app
stderr: raw_response = await run_endpoint_function(
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
stderr: return await dependant.call(**values)
stderr: File "/app/xiaomusic/httpserver.py", line 124, in getvolume
stderr: volume = await xiaomusic.get_volume(did=did)
stderr: File "/app/xiaomusic/xiaomusic.py", line 809, in get_volume
stderr: return await self.devices[did].get_volume()
stderr: File "/app/xiaomusic/xiaomusic.py", line 1400, in get_volume
stderr: playing_info = await self.xiaomusic.mina_service.player_get_status(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 103, in player_get_status
stderr: return await self.ubus_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
stderr: result = await self.mina_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
stderr: return await self.account.mi_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
stderr: raise Exception(f"Error {url}: {resp}")
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_YfG6nlPVAtwDHEgMeJXxcOiZQLrR57'}}
stderr: [08:58:05] [0.3.37] [ERROR] h11_impl.py:411: Exception in ASGI application
stderr: Traceback (most recent call last):
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 406, in run_asgi
stderr: result = await app( # type: ignore[func-returns-value]
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 70, in __call__
stderr: return await self.app(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
stderr: await super().__call__(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/applications.py", line 113, in __call__
stderr: await self.middleware_stack(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 187, in __call__
stderr: raise exc
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 165, in __call__
stderr: await self.app(scope, receive, _send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/cors.py", line 85, in __call__
stderr: await self.app(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
stderr: await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
stderr: raise exc
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
stderr: await app(scope, receive, sender)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 715, in __call__
stderr: await self.middleware_stack(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 735, in app
stderr: await route.handle(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 288, in handle
stderr: await self.app(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 76, in app
stderr: await wrap_app_handling_exceptions(app, request)(scope, receive, send)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
stderr: raise exc
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
stderr: await app(scope, receive, sender)
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 73, in app
stderr: response = await f(request)
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 301, in app
stderr: raw_response = await run_endpoint_function(
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
stderr: return await dependant.call(**values)
stderr: File "/app/xiaomusic/httpserver.py", line 124, in getvolume
stderr: volume = await xiaomusic.get_volume(did=did)
stderr: File "/app/xiaomusic/xiaomusic.py", line 809, in get_volume
stderr: return await self.devices[did].get_volume()
stderr: File "/app/xiaomusic/xiaomusic.py", line 1400, in get_volume
stderr: playing_info = await self.xiaomusic.mina_service.player_get_status(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 103, in player_get_status
stderr: return await self.ubus_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
stderr: result = await self.mina_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
stderr: return await self.account.mi_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
stderr: raise Exception(f"Error {url}: {resp}")
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_YfG6nlPVAtwDHEgMeJXxcOiZQLrR57'}}
stdout: [08:58:05] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stdout: [08:58:05] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stdout: [08:58:07] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stdout: [08:58:08] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stdout: [08:58:08] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
stderr: [08:58:09] [0.3.37] [INFO] httpserver.py:177: docmd. did:566731712 cmd:播放列表m3u电台|80后音悦台
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:582: cancel_all_tasks no task
stdout: [08:58:09] [0.3.37] [INFO] 10.0.80.191:24020 - "POST /cmd HTTP/1.1" 200
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:560: 收到消息:播放列表m3u电台|80后音悦台 控制面板:True did:566731712
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:648: 匹配到指令. opkey:播放列表 opvalue:play_music_list oparg:m3u电台|80后音悦台
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:716: 根据【m3u电台】找到播放列表【m3u电台】
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:991: 没打乱 m3u电台 ['80后音悦台', 'BBC Radio 1', 'BBC Radio 2', 'BBC Radio 3', 'BBC Radio 4', 'CNA938', 'FM 988', ' AFO Radio', 'BBC News', 'BBC Radio 1 Dance', 'BBC Radio 1 Relax', 'BBC Radio 1Xtra', 'BBC Radio 4 Extra', 'BBC Radio 5 Live', 'BBC Radio 6 Music', 'BCC中广新闻', 'BCC中广流行', 'BCC中广音乐', 'CNA亚洲新闻', 'CNR中国之声', 'CNR乡村之声', 'CNR交通广播', 'CNR台海之声', 'CNR文艺之声', 'CNR湾区之声', 'CNR神州之声', 'CNR经典音乐', 'CNR经济之声', 'CNR老年之声', 'CNR阅读之声', 'CNR音乐之声', 'CRI世界华声', 'CRI华语环球', 'CRI南海之声', 'CRI环球资讯', 'CRI英语资讯', 'CRI轻松调频', 'Capital FM', 'CityFM台之音', 'Cool Radio', 'Gold FM', 'Hit FM劲曲', 'Kiss FM', 'LBC News', 'LBC UK', 'Love FM', 'Love Radio', 'Money FM', 'NPR News', 'News Radio UK', 'One FM', 'Power FM', 'RFA', 'RFI', 'RTI中央广播', 'Times Radio', 'VOA环球英语', 'Yes FM', '上海故事广播', '上海流行音乐', '上海音乐广播', '上海音乐电台', '中文流行', '全国广播', '北京交通广播', '北京体育广播', '北京城市广播', '北京文艺广播', '北京新闻广播', '北京经典调频', '北京音乐广播', '南京音乐广播', '台中广播电台', '四川文艺广播', '国乐悠扬', '天籁之音 Hi-Fi', '天籁古典', '天籁国风', '安全百科', '山西文艺广播', '广东音乐之声', '广州汽车音乐', '摇滚天空', '有声文摘', '民谣蓝调', '民谣音乐台', '江苏故事广播', '江苏文艺广播', '江苏音乐广播', '河北文艺广播', '河北汽车音乐', '深圳音乐广播', '湖南文艺广播', '湖南旅游广播', '湖南音乐之声', '潮流音悦台', '经典FM', '美国之音', '陕西故事广播', '陕西音乐广播']
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1422: 开始播放列表m3u电台
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1005: play. search_key: name:80后音悦台
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:663: 根据【80后音悦台】找到歌曲【80后音悦台】
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1088: cur_music 80后音悦台
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:388: get_music_url web music. name:80后音悦台, url:http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:360: get_music_sec_url. name:80后音悦台 url:http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:362: 电台不会有播放时长
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1437: group_force_stop_xiaoai ['be1daeaa-df03-4a27-8aff-404356abfa9a']
stderr: [08:58:09] [0.3.37] [ERROR] xiaomusic.py:1137: Execption Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_10DxZOintrGWhyETo4q9auU3VMNH6A'}}
stderr: Traceback (most recent call last):
stderr: File "/app/xiaomusic/xiaomusic.py", line 1131, in force_stop_xiaoai
stderr: ret = await self.xiaomusic.mina_service.player_pause(device_id)
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 79, in player_pause
stderr: return await self.ubus_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
stderr: result = await self.mina_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
stderr: return await self.account.mi_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
stderr: raise Exception(f"Error {url}: {resp}")
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_10DxZOintrGWhyETo4q9auU3VMNH6A'}}
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1440: group_force_stop_xiaoai ['be1daeaa-df03-4a27-8aff-404356abfa9a'] [None]
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1091: 播放 http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8
stderr: [08:58:10] [0.3.37] [ERROR] xiaomusic.py:1332: Execption Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_Bbkf4HLQiN7tjl1rGD2gpXuJ8yI0nP'}}
stderr: Traceback (most recent call last):
stderr: File "/app/xiaomusic/xiaomusic.py", line 1327, in play_one_url
stderr: ret = await self.xiaomusic.mina_service.play_by_url(device_id, url)
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 125, in play_by_url
stderr: return await self.ubus_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
stderr: result = await self.mina_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
stderr: return await self.account.mi_request(
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
stderr: raise Exception(f"Error {url}: {resp}")
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_Bbkf4HLQiN7tjl1rGD2gpXuJ8yI0nP'}}
stderr: [08:58:10] [0.3.37] [INFO] xiaomusic.py:1305: group_player_play http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8 ['be1daeaa-df03-4a27-8aff-404356abfa9a'] [None]
stderr: [08:58:10] [0.3.37] [INFO] xiaomusic.py:1094: 播放 80后音悦台 失败
---
### 评论 4 - hanxi
设备掉线了
---
### 评论 5 - 201692929
怎么获取 他正在播放什么?或者是播放进度 ?播放列表?我想给他加进去
![233333](https://gproxy.hanxi.cc/proxy/user-attachments/assets/013cd952-69e9-4754-870f-2d5321865179)
---
### 评论 6 - hanxi
> 怎么获取 他正在播放什么?或者是播放进度 ?播放列表?我想给他加进去 ![233333](https://private-user-images.githubusercontent.com/100142519/372926296-013cd952-69e9-4754-870f-2d5321865179.jpg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mjc4ODY3MzUsIm5iZiI6MTcyNzg4NjQzNSwicGF0aCI6Ii8xMDAxNDI1MTkvMzcyOTI2Mjk2LTAxM2NkOTUyLTY5ZTktNDc1NC04NzBmLTJkNTMyMTg2NTE3OS5qcGc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjQxMDAyJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI0MTAwMlQxNjI3MTVaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0wNzI5OTdhOTAxMmIwMDkxZTBjOGNhYTZkOWVjY2MwZTRmNGE0YTYzNDFhNGY1YzNjNTI4ZWY0YzYzYzc0Nzk3JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.4-5nGDdkDv9FRp9bAnwN4dzmf4wqKHnG4bW44BhVyRQ)
这个接口 `/playingmusic`
---
### 评论 7 - 114514thD
加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
---
### 评论 8 - hanxi
> 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
发出来看看?
---
### 评论 9 - 114514thD
> > 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
>
> 发出来看看?
~~本地开服务器生成的m3u列表
格式如下
`#EXTINF:247,周传雄 - 临别一眼.mp3
http://192.168.1.147:8000/%E5%91%A8%E4%BC%A0%E9%9B%84%20-%20%E4%B8%B4%E5%88%AB%E4%B8%80%E7%9C%BC.mp3`
包含了时长信息
版本是0.3.46
potplayer里播放完全正常~~
仔细研究了一下,发现确实存在问题,不过是另一种情况,下面单说
---
### 评论 10 - 114514thD
> > 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
>
> 发出来看看?
经过实验发现本地生成的m3u用potplayer播放正常
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/754e4344-9262-4ad1-bf17-dd83f5e3b6e5)
转换为json去掉"type":"radio")后用小爱播放也正常
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/9f1a9f02-6cf1-4536-91bd-e5e3677d6513)
但是alist链接就不正常alist生成的m3u格式如下
`#EXTM3U
#EXTINF:-1,Let Me Hear.mp3
http://192.168.1.198:5244/d/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%20%E9%9F%B3%E4%B9%90%E4%BA%91%E7%9B%98/Let%20Me%20Hear.mp3?sign=xxxx=:0`
没有时长信息但是用potplayer一播放就出现时长了
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/34ac4b9f-8b7f-40d7-9ac9-aa4621b59aa3)
而用小爱播放就始终没有时长(切歌、等待都试过了)
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/a42b62e4-7a48-46b8-b83b-a4c8cb219c0b)
大佬你的示例链接gist.github.com/hanxi/dda82d964a28f8110f8fba81c3ff8314里的又是正常的感觉可能是alist的流比较特殊。。
![image](https://gproxy.hanxi.cc/proxy/user-attachments/assets/f84ab805-54a6-40f1-937c-67832ff0b9d6)
---
### 评论 11 - 114514thD
> > 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
>
> 发出来看看?
这几天再仔细研究了一下发现一个可能的原因这样获取到的是m4a文件我尝试着在json里配置获取到的m4a链接发现播放同样也是无时长
---
### 评论 12 - hanxi
获取歌曲时长确实有些格式获取不到。
---
### 评论 13 - 114514thD
> 获取歌曲时长确实有些格式获取不到。
http://m7.music.126.net/20241216093525/75c9080afa2929d7eec8e1cdbcbc0a92/yyaac/0709/535a/5358/0c6e2dcac3d0e9fa4415d22e1eca1337.m4a
以这个文件为例我用ffmpeg可以获取时长等元数据
`
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'http://m7.music.126.net/20241216093525/75c9080afa2929d7eec8e1cdbcbc0a92/yyaac/0709/535a/5358/0c6e2dcac3d0e9fa4415d22e1eca1337.m4a':
Metadata:
major_brand : mp42
minor_version : 0
compatible_brands: M4A mp42isom
creation_time : 2019-02-21T02:51:36.000000Z
iTunSMPB : 00000000 00000920 000003E8 00000000004BE2F8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
encoder : Nero AAC codec / 1.5.4.0
Duration: 00:03:45.70, start: 0.052971, bitrate: 64 kb/s
Chapters:
Chapter #0:0: start 0.105941, end 225.750930
Metadata:
title :
Stream #0:0[0x1](und): Audio: aac (HE-AAC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 63 kb/s (default)
Metadata:
creation_time : 2019-02-21T02:51:36.000000Z
handler_name : Sound Media Handler
vendor_id : [0][0][0][0]`
那是为什么播放就不行呢
---
### 评论 14 - hanxi
因为代码有问题。
---
### 评论 15 - 114514thD
> 因为代码有问题。
好吧😂😂,大佬真是直接😁
---
### 评论 16 - hanxi
重构方案 #314
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/78)

31
docs/issues/86.md Normal file
View File

@@ -0,0 +1,31 @@
---
title: 微信交流群二维码
---
# 微信交流群二维码
![mmqrcode1734721614705](https://gproxy.hanxi.cc/proxy/user-attachments/assets/4113e68b-17a8-4067-ba89-e809b3ae817b)
## 评论
### 评论 1 - DarrenWen
群满200人没法加入了
---
### 评论 2 - hanxi
没想到这么快就满了,新建了一个。
---
### 评论 3 - hanxi
![mm_reward_qrcode_1726365700471](https://gproxy.hanxi.cc/proxy/user-attachments/assets/7863e361-7e61-48a7-bd71-8f8f609f11b4)
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/86)

74
docs/issues/88.md Normal file
View File

@@ -0,0 +1,74 @@
---
title: 如何添加m3u格式文件的电台
---
# 如何添加m3u格式文件的电台
比如可以找到这样的 m3u 电台文件: https://github.com/YueChan/Live/blob/main/Radio.m3u
1. 复制文件内容,粘贴到 m3u 转换工具里,点击转换为 json 格式:
![Screenshot_2024-06-29-11-28-58-904_com android chrome](https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/1185757/bb812a47-17c5-4483-9234-4cf33367b181)
2. 然后复制 json 内容,粘贴到歌单内容里,点击保存,再返回首页:
![Screenshot_2024-06-29-11-29-22-248_com android chrome](https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/1185757/2fb4ca44-6b79-4438-9bc6-cfbd01272f20)
3. 在首页点击刷新列表,选择所有电台,再点击播放列表歌曲:
![Screenshot_2024-06-29-11-29-55-621_com android chrome](https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/1185757/c94e4667-f83e-4cd5-9662-e680316cb5b4)
4. 也可以用口令播放电台: `播放列表所有电台` ,或者口令: `播放歌曲北京城市广播`
## 评论
### 评论 1 - guoxiangke
转换m3u链接 http://127.0.0.1:8090/static/m3u.html
---
### 评论 2 - guoxiangke
http://127.0.0.1:8090/playurl?did=mydid&url=https://a.com/test.m3u 如果能支持 这么播放m3u 就太完美了
希望能够支持,谢谢作者。
---
### 评论 3 - hanxi
> http://127.0.0.1:8090/playurl?did=mydid&url=https://a.com/test.m3u 如果能支持 这么播放m3u 就太完美了 希望能够支持,谢谢作者。
不想破坏现有接口,可以考虑用插件的方式来实现。
---
### 评论 4 - lazybabyz
potplayer 测试 https://raw.githubusercontent.com/YueChan/Live/refs/heads/main/Radio.m3u 部分可以听
xiaomusic测试 https://github.com/YueChan/Live/blob/main/Radio.m3u 复制raw文件转换 全部失败不停转台
以下potplayer测试 可以听xiaomusic测试 复制raw文件转换 全部失败不停转台
https://raw.githubusercontent.com/kaige-cai/live/refs/heads/main/radio.m3u
https://raw.githubusercontent.com/imDazui/Tvlist-awesome-m3u-m3u8/master/m3u/%E5%B9%BF%E6%92%AD%E7%94%B5%E5%8F%B02021.m3u
---
### 评论 5 - hanxi
检查一下是不是 ipv6 的地址?小米音箱不支持 ipv6
---
### 评论 6 - lazybabyz
> 检查一下是不是 ipv6 的地址?小米音箱不支持 ipv6
是我硬件设置的问题 重新安装了 解决!
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/88)

101
docs/issues/94.md Normal file
View File

@@ -0,0 +1,101 @@
---
title: 采用config.json配置方式
---
# 采用config.json配置方式
docker 方式部署默认推荐使用环境变量的方式来配置参数,如果是自己用命令行启动,目前支持的参数配置比较少,但是是支持 `--config` 参数。
使用 pip 安装 xiaomusic 【0.1.83版本才支持 pip 安装】
```shell
pip install xiaomusic
```
依赖的 ffmpeg 需要自己安装。
![image](https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/1185757/6912e3ec-c42f-42de-b027-a296f5a26ba1)
仓库中有个 config-example.json 文件,可以把这个文件拷贝为 config.json 然后修改 config.json 里的配置,再用下面的命令启动。
```shell
xiaomusic --config ./config.json
```
默认的 config.json 模板如下:
```json
{
"hardware": "L07A",
"account": "",
"password": "",
"mi_did": "",
"cookie": "",
"verbose": false,
"music_path": "music",
"conf_path": null,
"hostname": "192.168.2.5",
"port": 8090,
"proxy": null,
"search_prefix": "ytsearch:",
"ffmpeg_location": "./ffmpeg/bin",
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
"exclude_dirs": "@eaDir",
"music_path_depth": 10,
"disable_httpauth": true,
"httpauth_username": "admin",
"httpauth_password": "admin",
"music_list_url": "",
"music_list_json": "",
"disable_download": false,
"use_music_api": false,
"log_file": "/tmp/xiaomusic.txt",
"fuzzy_match_cutoff": 0.6,
"enable_fuzzy_match": true
}
```
如果采用 docker compose 启动想用 config.json 的配置方式,可以这样配:
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
- ./config.json:/app/config.json
command: ['--config', '/app/config.json']
```
主要就是把 config.json 文件映射进容器和传 `--config` 参数。
## 评论
### 评论 1 - alitime
正需要,有配置文件方便多了
---
### 评论 2 - hanxi
ffmpeg 如果是用 apt install 这类系统工具安装的,默认会在 /usr/bin 目录下ffmpeg_location 这个参数就需要设置为 /usr/bin .
目前 armv7 cpu 的 docker 镜像里的 ffmpeg 有问题,最好是用 pip 方式安装运行。
---
### 评论 3 - hanxi
> ffmpeg 如果是用 apt install 这类系统工具安装的,默认会在 /usr/bin 目录下ffmpeg_location 这个参数就需要设置为 /usr/bin .
>
> 目前 armv7 cpu 的 docker 镜像里的 ffmpeg 有问题,最好是用 pip 方式安装运行。
armv7 问题已经解决。
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/94)

17
docs/issues/96.md Normal file
View File

@@ -0,0 +1,17 @@
---
title: ios系统上的捷径配置
---
# ios系统上的捷径配置
下面是播放音乐和关机两个示例。只要在 web 页面上能看到的功能,都有对应的 http 请求接口,都可以用来配置捷径。
![mmexport1719767452647](https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/1185757/db0a1856-e1ed-47cb-972d-d997f71bf92b)
![mmexport1719767449742](https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/1185757/92b7bc4b-9699-49cc-956a-4bddb6bd50fa)
## 评论
没有评论。
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/96)

1360
docs/issues/99.md Normal file

File diff suppressed because it is too large Load Diff

1089
docs/issues/changelog.md Normal file

File diff suppressed because it is too large Load Diff

296
docs/issues/index.md Normal file
View File

@@ -0,0 +1,296 @@
# XiaoMusic: 无限听歌,解放小爱音箱
[![GitHub License](https://img.shields.io/github/license/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic)
[![Docker Image Version](https://img.shields.io/docker/v/hanxi/xiaomusic?sort=semver&label=docker%20image)](https://hub.docker.com/r/hanxi/xiaomusic)
[![Docker Pulls](https://img.shields.io/docker/pulls/hanxi/xiaomusic)](https://hub.docker.com/r/hanxi/xiaomusic)
[![PyPI - Version](https://img.shields.io/pypi/v/xiaomusic)](https://pypi.org/project/xiaomusic/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/xiaomusic)](https://pypi.org/project/xiaomusic/)
[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fhanxi%2Fxiaomusic%2Fmain%2Fpyproject.toml)](https://pypi.org/project/xiaomusic/)
[![GitHub Release](https://img.shields.io/github/v/release/hanxi/xiaomusic)](https://github.com/hanxi/xiaomusic/releases)
[![Visitors](https://api.visitorbadge.io/api/daily?path=hanxi%2Fxiaomusic&label=daily%20visitor&countColor=%232ccce4&style=flat)](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
[![Visitors](https://api.visitorbadge.io/api/visitors?path=hanxi%2Fxiaomusic&label=total%20visitor&countColor=%232ccce4&style=flat)](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
<https://github.com/hanxi/xiaomusic>
> [!TIP]
> 初次安装遇到问题请查阅 [💬 FAQ问题集合](https://github.com/hanxi/xiaomusic/issues/99) ,一般遇到的问题都已经有解决办法。
## 👋 最简配置运行
已经支持在 web 页面配置其他参数docker 启动命令如下:
```bash
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf hanxi/xiaomusic
```
🔥 国内:
```bash
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf m.daocloud.io/docker.io/hanxi/xiaomusic
```
对应的 docker compose 配置如下:
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 58090:8090
environment:
XIAOMUSIC_PUBLIC_PORT: 58090
volumes:
- /xiaomusic_music:/app/music
- /xiaomusic_conf:/app/conf
```
🔥 国内:
```yaml
services:
xiaomusic:
image: m.daocloud.io/docker.io/hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 58090:8090
environment:
XIAOMUSIC_PUBLIC_PORT: 58090
volumes:
- /xiaomusic_music:/app/music
- /xiaomusic_conf:/app/conf
```
- 其中 conf 目录为配置文件存放目录music 目录为音乐存放目录,建议分开配置为不同的目录。
- /xiaomusic_music 和 /xiaomusic_conf 是 docker 主机里的目录,可以修改为其他目录。如果报错找不到 /xiaomusic_music 目录,可以先执行 `mkdir -p /xiaomusic_{music,conf}` 命令新建目录。
- XIAOMUSIC_PUBLIC_PORT 是用来配置 NAS 本地端口的。8090 是容器端口,不要去修改。
- 后台访问地址为: http://NAS_IP:58090
> [!NOTE]
> docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
> [!TIP]
> 目前安装步骤已经是最简化了,如果还是嫌安装麻烦,可以微信或者 QQ 约我远程安装,我一般周末和晚上才有时间,收个辛苦费 :moneybag: 50 元一次,安装失败不收费。
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
> [!TIP]
> 海外 RackNerd VPS 机器推荐,可支付宝付款。
> - [🔥1 GB KVM VPS $11.29/年](https://my.racknerd.com/aff.php?aff=1177&pid=903)
> - [2 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=904)
> - [3.5 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=905)
> - [4 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=906)
> - [6 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=907)
### 🤐 支持语音口令
- 【播放歌曲】,播放本地的歌曲
- 【播放歌曲+歌名】,比如:播放歌曲周杰伦晴天
- 【上一首】
- 【下一首】
- 【单曲循环】
- 【全部循环】
- 【随机播放】
- 【关机】,【停止播放】,两个效果是一样的。
- 【刷新列表】,当复制了歌曲进 music 目录后,可以用这个口令刷新歌单。
- 【播放列表+列表名】,比如:播放列表其他。
- 【加入收藏】,把当前播放的歌曲加入收藏歌单。
- 【取消收藏】,把当前播放的歌曲从收藏歌单里移除。
- 【播放列表收藏】,这个用于播放收藏歌单。
- 【播放本地歌曲+歌名】,这个口令和播放歌曲的区别是本地找不到也不会去下载。
- 【播放列表第几个+列表名】,具体见: <https://github.com/hanxi/xiaomusic/issues/158>
- 【搜索播放+关键词】,会搜索关键词作为临时搜索列表播放,比如说【搜索播放林俊杰】,会播放所有林俊杰的歌。
- 【本地搜索播放+关键词】,跟搜索播放的区别是本地找不到也不会去下载。
> [!TIP]
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会先下载小猪佩奇的故事,然后再播放小猪佩奇的故事。
## 🛠️ pip 方式安装运行
```shell
> pip install -U xiaomusic
> xiaomusic --help
__ __ _ __ __ _
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.3.65 by: github.com/hanxi
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
[--config CONFIG] [--ffmpeg_location FFMPEG_LOCATION]
options:
-h, --help show this help message and exit
--port PORT 监听端口
--hardware HARDWARE 小爱音箱型号
--account ACCOUNT xiaomi account
--password PASSWORD xiaomi password
--cookie COOKIE xiaomi cookie
--verbose show info
--config CONFIG config file path
--ffmpeg_location FFMPEG_LOCATION
ffmpeg bin path
> xiaomusic --config config.json
```
其中 `config.json` 文件可以参考 `config-example.json` 文件配置。见 <https://github.com/hanxi/xiaomusic/issues/94>
不修改默认端口 8090 的情况下,只需要执行 `xiaomusic` 即可启动。
## 🔩 开发环境运行
- 使用 install_dependencies.sh 下载依赖
- 使用 pdm 安装环境
- 默认监听了端口 8090 , 使用其他端口自行修改。
```shell
pdm run xiaomusic.py
````
如果是开发前端界面,可以通过 <http://localhost:8090/docs>
查看有什么接口。目前的 web 控制台非常简陋,欢迎有兴趣的朋友帮忙实现一个漂亮的前端,需要什么接口可以随时提需求。
### 🚦 代码提交规范
提交前请执行
```
pdm lintfmt
```
用于检查代码和格式化代码。
### 本地编译 Docker Image
```shell
docker build -t xiaomusic .
```
### 技术栈
- 后端代码使用 Python 语言编写。
- HTTP 服务使用的是 FastAPI 框架,~~早期版本使用的是 Flask~~。
- 使用了 Docker ,在 NAS 上安装更方便。
- 默认的前端主题使用了 jQuery 。
## 已测试支持的设备
| 型号 | 名称 |
| ---- | ---------------------------------------------------------------------------------------------- |
| L06A | [小爱音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l06a) |
| L07A | [Redmi小爱音箱 Play](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l7a) |
| S12/S12A/MDZ-25-DA | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
| LX5A | [小爱音箱 万能遥控版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx5a) |
| LX05 | [小爱音箱Play2019款](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
| L15A | [小米AI音箱第二代](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l15a#/) |
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
| L17A | [Xiaomi Sound Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l17a) |
| LX06 | [小爱音箱Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx06) |
| LX01 | [小爱音箱mini](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx01) |
| L05B | [小爱音箱Play](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05b) |
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
| L09A | [小米音箱Art](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l09a) |
| LX04 X10A X08A | 已经支持的触屏版 |
| X08C X08E X8F | 需要设置【型号兼容模式】选项为 true |
| M01/XMYX01JY | 小米小爱音箱HD 需要设置【特殊型号获取对话记录】选项为 true 才能语音播放|
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> [!NOTE]
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
> 其他触屏版不能播放可以设置【型号兼容模式】选项为 true 试试。见 <https://github.com/hanxi/xiaomusic/issues/30>
## 🎵 支持音乐格式
- mp3
- flac
- wav
- ape
- ogg
- m4a
> [!NOTE]
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
> 已知 L05B L05C LX06 L16A 不支持 flac 格式。
> 如果格式不能播放可以打开【转换为MP3】和【型号兼容模式】选项。具体见 <https://github.com/hanxi/xiaomusic/issues/153#issuecomment-2328168689>
## 🌏 网络歌单功能
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
> [!NOTE]
> 欢迎有想法的朋友们制作更多的歌单转换工具。
## 🍺 更多其他可选配置
<https://github.com/hanxi/xiaomusic/issues/333>
## ⚠️ 安全提醒
> [!IMPORTANT]
>
> 1. 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
> 2. 强烈不建议将小爱音箱的小米账号绑定摄像头,代码难免会有 bug ,一旦小米账号密码泄露,可能监控录像也会泄露。
## 🤔 高级篇
- 自定义口令功能 <https://github.com/hanxi/xiaomusic/issues/105>
- [ ] 缺少一篇教程 [如何写自定义插件](https://github.com/hanxi/xiaomusic/issues/105)
## 📢 讨论区
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
- [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973)
- <https://github.com/hanxi/xiaomusic/issues>
- [微信群二维码](https://github.com/hanxi/xiaomusic/issues/86)
## ❤️ 感谢
- [xiaomi](https://www.mi.com/)
- [PDM](https://pdm.fming.dev/latest/)
- [xiaogpt](https://github.com/yihong0618/xiaogpt)
- [MiService](https://github.com/yihong0618/MiService)
- [实现原理](https://github.com/yihong0618/gitblog/issues/258)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [awesome-xiaoai](https://github.com/zzz6519003/awesome-xiaoai)
- [微信小程序: XIAO晓音](https://github.com/F-loat/xiaoplayer)
- [pure 主题 xiaomusicUI](https://github.com/52fisher/xiaomusicUI)
- [移动端的播放器主题](https://github.com/52fisher/XMusicPlayer)
- [一个第三方的主题](https://github.com/DarrenWen/xiaomusicui)
- [Umami 统计](https://github.com/umami-software/umami)
- [Sentry 报错监控](https://github.com/getsentry/sentry)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
### 👉 其他教程
更多功能见 [📝 文档汇总](https://github.com/hanxi/xiaomusic/issues/211)
## 🚨 免责声明
本项目仅供学习和研究目的,不得用于任何商业活动。用户在使用本项目时应遵守所在地区的法律法规,对于违法使用所导致的后果,本项目及作者不承担任何责任。
本项目可能存在未知的缺陷和风险(包括但不限于设备损坏和账号封禁等),使用者应自行承担使用本项目所产生的所有风险及责任。
作者不保证本项目的准确性、完整性、及时性、可靠性,也不承担任何因使用本项目而产生的任何损失或损害责任。
使用本项目即表示您已阅读并同意本免责声明的全部内容。
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=hanxi/xiaomusic&type=Date)](https://star-history.com/#hanxi/xiaomusic&Date)
## 赞赏
- :moneybag: 爱发电 <https://afdian.com/a/imhanxi>
- 点个 Star :star:
- 谢谢 :heart:
- ![喝杯奶茶](https://i.v2ex.co/7Q03axO5l.png)
## License
[MIT](https://github.com/hanxi/xiaomusic/blob/main/LICENSE) License © 2024 涵曦

3680
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

14
docs/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"devDependencies": {
"vitepress": "^1.5.0"
},
"scripts": {
"docs:dev": "vitepress dev --host=0.0.0.0 --port=3030",
"docs:build": "vitepress build",
"docs:preview": "vitepress preview"
},
"dependencies": {
"axios": "^1.7.9",
"vite-plugin-vitepress-auto-sidebar": "^1.7.0"
}
}

64
get_release.py Normal file
View File

@@ -0,0 +1,64 @@
import json
import os
import requests
# 替换为你的 GitHub 仓库信息
GITHUB_OWNER = "hanxi"
GITHUB_REPO = "xiaomusic"
GITHUB_API_URL = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases"
def fetch_releases():
headers = {}
github_token = os.getenv("GITHUB_TOKEN")
if github_token:
headers["Authorization"] = f"token {github_token}"
try:
response = requests.get(GITHUB_API_URL, headers=headers)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"请求 GitHub API 失败: {e}")
return []
def extract_tar_gz_files(releases):
versions = []
for release in releases:
version = release.get("tag_name")
if not version:
continue
files = []
for asset in release.get("assets", []):
if asset.get("name", "").endswith(".tar.gz"):
files.append(asset["name"])
if files:
versions.append({"version": version, "files": files})
return versions
def save_to_json(data, filename="versions.json"):
try:
with open(filename, "w") as f:
json.dump(data, f, indent=4)
print(f"数据已保存到 {filename}")
except OSError as e:
print(f"保存文件失败: {e}")
def main():
releases = fetch_releases()
if not releases:
print("未获取到任何 release 数据")
return
versions = extract_tar_gz_files(releases)
save_to_json(versions, "docs/.vitepress/dist/versions.json")
if __name__ == "__main__":
main()

119
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "dev", "lint"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:37be8846356b5717495cac7252d78b1554985d607e286cf38ab863e307e5e25e"
content_hash = "sha256:be03d4a0e0e150847ab753a6b65d34c908944d20c37203ca0e3edba976f0aa80"
[[metadata.targets]]
requires_python = "==3.10.12"
@@ -39,7 +39,7 @@ files = [
[[package]]
name = "aiohttp"
version = "3.11.8"
version = "3.11.9"
requires_python = ">=3.9"
summary = "Async http client/server framework (asyncio)"
groups = ["default"]
@@ -55,8 +55,8 @@ dependencies = [
"yarl<2.0,>=1.17.0",
]
files = [
{file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df9bf08eb93611b1d4d6245b6fecf88728e90eece00e00d554e1b0c445557d83"},
{file = "aiohttp-3.11.8.tar.gz", hash = "sha256:7bc9d64a2350cbb29a9732334e1a0743cbb6844de1731cbdf5949b235653f3fd"},
{file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"},
{file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"},
]
[[package]]
@@ -165,6 +165,17 @@ files = [
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
]
[[package]]
name = "brotli"
version = "1.1.0"
summary = "Python bindings for the Brotli compression library"
groups = ["default"]
marker = "implementation_name == \"cpython\" and python_full_version == \"3.10.12\""
files = [
{file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"},
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
]
[[package]]
name = "certifi"
version = "2024.8.30"
@@ -658,6 +669,18 @@ files = [
{file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"},
]
[[package]]
name = "pycryptodomex"
version = "3.21.0"
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
summary = "Cryptographic library for Python"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"},
{file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"},
]
[[package]]
name = "pydantic"
version = "2.8.2"
@@ -760,14 +783,14 @@ files = [
[[package]]
name = "python-multipart"
version = "0.0.17"
version = "0.0.19"
requires_python = ">=3.8"
summary = "A streaming multipart parser for Python"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
{file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"},
{file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"},
]
[[package]]
@@ -856,14 +879,47 @@ files = [
[[package]]
name = "ruff"
version = "0.8.0"
version = "0.8.1"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
groups = ["lint"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"},
{file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"},
{file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"},
]
[[package]]
name = "sentry-sdk"
version = "1.45.1"
summary = "Python client for Sentry (https://sentry.io)"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
dependencies = [
"certifi",
"urllib3>=1.25.7; python_version <= \"3.4\"",
"urllib3>=1.26.11; python_version >= \"3.6\"",
"urllib3>=1.26.9; python_version == \"3.5\"",
]
files = [
{file = "sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1"},
{file = "sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26"},
]
[[package]]
name = "sentry-sdk"
version = "1.45.1"
extras = ["fastapi"]
summary = "Python client for Sentry (https://sentry.io)"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
dependencies = [
"fastapi>=0.79.0",
"sentry-sdk==1.45.1",
]
files = [
{file = "sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1"},
{file = "sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26"},
]
[[package]]
@@ -989,6 +1045,19 @@ files = [
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]]
name = "websockets"
version = "14.1"
requires_python = ">=3.9"
summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"},
{file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"},
{file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"},
]
[[package]]
name = "yarl"
version = "1.17.1"
@@ -1009,12 +1078,36 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.11.18"
version = "2024.12.1.232904.dev0"
requires_python = ">=3.9"
summary = "A feature-rich command-line audio/video downloader"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "yt_dlp-2024.11.18-py3-none-any.whl", hash = "sha256:b9741695911dc566498b5f115cdd6b1abbc5be61cb01fd98abe649990a41656c"},
{file = "yt_dlp-2024.11.18.tar.gz", hash = "sha256:b8a4c23d3c9afd7e476bcdb87f38b6c0e8e12e3a239d7988f13acb434200f54d"},
{file = "yt_dlp-2024.12.1.232904.dev0-py3-none-any.whl", hash = "sha256:67c1fa0986662f2f26ba70789c79c6d04fda45eb530944872f4a47ef4f94047e"},
{file = "yt_dlp-2024.12.1.232904.dev0.tar.gz", hash = "sha256:4fcce6637e70bad7c63220e4dd69a3a6b8969cbc17c93bfe569940dd173c0548"},
]
[[package]]
name = "yt-dlp"
version = "2024.12.1.232904.dev0"
extras = ["default"]
requires_python = ">=3.9"
summary = "A feature-rich command-line audio/video downloader"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
dependencies = [
"brotli; implementation_name == \"cpython\"",
"brotlicffi; implementation_name != \"cpython\"",
"certifi",
"mutagen",
"pycryptodomex",
"requests<3,>=2.32.2",
"urllib3<3,>=1.26.17",
"websockets>=13.0",
"yt-dlp==2024.12.1.232904.dev0",
]
files = [
{file = "yt_dlp-2024.12.1.232904.dev0-py3-none-any.whl", hash = "sha256:67c1fa0986662f2f26ba70789c79c6d04fda45eb530944872f4a47ef4f94047e"},
{file = "yt_dlp-2024.12.1.232904.dev0.tar.gz", hash = "sha256:4fcce6637e70bad7c63220e4dd69a3a6b8969cbc17c93bfe569940dd173c0548"},
]

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.3.49"
version = "0.3.67"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
@@ -9,7 +9,7 @@ dependencies = [
"aiohttp>=3.8.6",
"miservice-fork>=2.7.0",
"mutagen>=1.47.0",
"yt-dlp>=2024.11.18",
"yt-dlp[default]>=2024.12.1.232904.dev0",
"uvicorn>=0.30.1",
"fastapi>=0.115.4",
"starlette>=0.37.2",
@@ -20,6 +20,7 @@ dependencies = [
"pillow>=10.4.0",
"python-multipart>=0.0.12",
"requests>=2.32.3",
"sentry-sdk[fastapi]==1.45.1",
]
requires-python = ">=3.10,<=3.12"
readme = "README.md"

27
supervisor.conf Normal file
View File

@@ -0,0 +1,27 @@
[unix_http_server]
file=/var/run/supervisor.sock ; Unix套接字文件路径
chmod=0777 ; 套接字权限
[supervisord]
pidfile=/tmp/supervisord.pid ; PID文件路径
logfile=/app/supervisord.log ; 日志文件路径
logfile_maxbytes=50MB ; 日志文件的最大大小
logfile_backups=3 ; 保留的旧日志文件数量
loglevel=info ; 日志级别
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; 使用Unix套接字与服务通信
[program:xiaomusic]
command=/app/.venv/bin/python3 /app/xiaomusic.py
directory=/app
autostart=true
autorestart=true
startretries=99999
startsecs=60
stderr_logfile=AUTO ; 将错误日志输出到 supervisord 控制台
stdout_logfile=AUTO ; 将标准日志输出到 supervisord 控制台

View File

@@ -1,8 +1,29 @@
from xiaomusic.utils import (
remove_common_prefix,
)
import re
def removepre(filename):
match = re.search(r"^(\d+)\s+\d*(.+?)\.(.*$)", filename.strip())
new_filename = filename
if match:
num = match.group(1)
name = match.group(2).replace(".", " ").strip()
suffix = match.group(3)
# print(name)
# print(num)
# print(suffix)
new_filename = f"{num}.{name}.{suffix}"
print(filename, "=>", new_filename)
if __name__ == "__main__":
remove_common_prefix(
"./tmp/【无损音质】2024年9月酷狗热歌榜TOP100合集只选热歌最高的首首王炸分P合集"
)
removepre(" 17 《白色风车》.mp3")
removepre(" 17 《白色风车》.mp3")
removepre(" 17 17 《白色风车》.mp3")
removepre(" 17 17 《白色风车》.mp3")
removepre(" 18 风车.mp3")
removepre(" 18 色风车.mp3")
removepre(" 18 18 你好.mp3")
removepre(" 18 18 我好.mp3")
removepre("09 009. 梁静茹-亲亲.mp3")

10
test/test_update.py Normal file
View File

@@ -0,0 +1,10 @@
from xiaomusic.utils import (
download_and_extract,
)
if __name__ == "__main__":
import asyncio
url = "https://github.hanxi.cc/proxy/hanxi/xiaomusic/releases/download/main/app-amd64-lite.tar.gz"
target_directory = "./tmp/app"
asyncio.run(download_and_extract(url, target_directory))

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
from xiaomusic.cli import main
if __name__ == "__main__":
from xiaomusic.cli import main
main()

View File

@@ -1 +1 @@
__version__ = "0.3.49"
__version__ = "0.3.67"

View File

@@ -1,16 +1,21 @@
import asyncio
import copy
import platform
import traceback
from datetime import datetime
import aiohttp
from ga4mp import GtagMP
from xiaomusic import __version__
class Analytics:
def __init__(self, log):
def __init__(self, log, config):
self.gtag = None
self.current_date = None
self.log = log
self.config = config
self.init()
def init(self):
@@ -27,34 +32,12 @@ class Analytics:
self.gtag = gtag
self.log.info("analytics init ok")
async def run_with_cancel(self, func, *args, **kwargs):
try:
asyncio.ensure_future(asyncio.to_thread(func, *args, **kwargs))
except Exception as e:
self.log.warning(f"analytics run_with_cancel failed {e}")
return None
async def send_startup_event(self):
try:
await self.run_with_cancel(self._send_startup_event)
except Exception as e:
self.log.warning(f"analytics send_startup_event failed {e}")
self.init()
def _send_startup_event(self):
event = self.gtag.create_new_event(name="startup")
event.set_event_param(name="version", value=__version__)
event_list = [event]
self.gtag.send(events=event_list)
await self._send(event)
async def send_daily_event(self):
try:
await self.run_with_cancel(self._send_daily_event)
except Exception as e:
self.log.warning(f"analytics send_daily_event failed {e}")
self.init()
def _send_daily_event(self):
current_date = datetime.now().strftime("%Y-%m-%d")
if self.current_date == current_date:
return
@@ -62,21 +45,93 @@ class Analytics:
event = self.gtag.create_new_event(name="daily_active_user")
event.set_event_param(name="version", value=__version__)
event.set_event_param(name="date", value=current_date)
event_list = [event]
self.gtag.send(events=event_list)
await self._send(event)
self.current_date = current_date
async def send_play_event(self, name, sec):
try:
await self.run_with_cancel(self._send_play_event, name, sec)
except Exception as e:
self.log.warning(f"analytics send_play_event failed {e}")
self.init()
def _send_play_event(self, name, sec):
async def send_play_event(self, name, sec, hardware):
event = self.gtag.create_new_event(name="play")
event.set_event_param(name="version", value=__version__)
event.set_event_param(name="music", value=name)
event.set_event_param(name="sec", value=sec)
event_list = [event]
self.gtag.send(events=event_list)
event.set_event_param(name="hardware", value=hardware)
await self._send(event)
async def _send(self, event):
await self.post_to_umami(event)
events = [event]
await self.run_with_cancel(self._google_send, events)
async def _google_send(self, events):
try:
self.gtag.send(events)
except Exception as e:
self.log.warning(f"google analytics run_with_cancel failed {e}")
async def run_with_cancel(self, func, *args, **kwargs):
try:
asyncio.ensure_future(asyncio.to_thread(func, *args, **kwargs))
self.log.info("analytics run_with_cancel success")
except Exception as e:
self.log.warning(f"analytics run_with_cancel failed {e}")
return None
async def post_to_umami(self, event):
try:
url = "https://umami.hanxi.cc/api/send"
user_agent = self._get_user_agent()
params = copy.copy(event.get_event_params())
params["useragent"] = user_agent
data = {
"payload": {
"hostname": self.config.hostname,
"language": "zh-CN",
"referrer": "",
"screen": "430x932",
"title": "后端统计",
"url": "/backend",
"website": "7bfb0890-4115-4260-8892-b391513e7e99",
"name": event.get_event_name(),
"data": params,
},
"type": "event",
}
async with aiohttp.ClientSession() as session:
headers = {
"User-Agent": user_agent,
}
# self.log.info(f"headers {headers}, {data}")
async with session.post(url, json=data, headers=headers) as response:
self.log.info(f"umami Status: {response.status}")
await response.text()
except Exception as e:
self.log.exception(f"Execption {e}")
def _get_user_agent(self):
try:
# 获取系统信息
os_name = platform.system() # 操作系统名称,如 'Windows', 'Linux', 'Darwin'
os_version = platform.version() # 操作系统版本号
architecture = "unknow"
try:
architecture = platform.architecture()[0] # '32bit' or '64bit'
except Exception as e:
architecture = f"Error {e}"
pass
machine = platform.machine() # 机器类型,如 'x86_64', 'arm64'
# 获取 Python 版本信息
python_version = platform.python_version() # Python 版本
# 组合 User-Agent 字符串
user_agent = (
f"XiaoMusic/{__version__} "
f"({os_name} {os_version}; {architecture}; {machine}) "
f"Python/{python_version}"
)
except Exception as e:
# 获取报错的堆栈信息
error_info = traceback.format_exc()
user_agent = f"Error: {e} {error_info}"
return user_agent

View File

@@ -1,16 +1,13 @@
#!/usr/bin/env python3
import argparse
import json
import logging
import os
import signal
import uvicorn
from xiaomusic import __version__
from xiaomusic.config import Config
from xiaomusic.httpserver import HttpInit
from xiaomusic.httpserver import app as HttpApp
from xiaomusic.xiaomusic import XiaoMusic
import sentry_sdk
from sentry_sdk.integrations.asyncio import AsyncioIntegration
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
LOGO = r"""
__ __ _ __ __ _
@@ -22,7 +19,30 @@ LOGO = r"""
"""
sentry_sdk.init(
# dsn="https://659690a901a37237df8097a9eb95e60f@github.hanxi.cc/sentry/4508470200434688",
dsn="https://ffe4962642d04b29afe62ebd1a065231@glitchtip.hanxi.cc/1",
integrations=[
AsyncioIntegration(),
LoggingIntegration(
level=logging.WARNING,
event_level=logging.ERROR,
),
],
# debug=True,
)
ignore_logger("miservice")
def main():
import uvicorn
from xiaomusic import __version__
from xiaomusic.config import Config
from xiaomusic.httpserver import HttpInit
from xiaomusic.httpserver import app as HttpApp
from xiaomusic.xiaomusic import XiaoMusic
parser = argparse.ArgumentParser()
parser.add_argument(
"--port",

View File

@@ -75,7 +75,7 @@ class Device:
device_id: str = ""
hardware: str = ""
name: str = ""
play_type: int = ""
play_type: int = PLAY_TYPE_RND
cur_music: str = ""
cur_playlist: str = ""
@@ -103,9 +103,10 @@ class Config:
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
active_cmd: str = os.getenv(
"XIAOMUSIC_ACTIVE_CMD",
"play,set_play_type_rnd,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop",
"play,search_play,set_play_type_rnd,playlocal,search_playlocal,play_music_list,play_music_list_index,stop_after_minute,stop",
)
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir,tmp")
ignore_tag_dirs: str = os.getenv("XIAOMUSIC_IGNORE_TAG_DIRS", "")
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
disable_httpauth: bool = (
os.getenv("XIAOMUSIC_DISABLE_HTTPAUTH", "true").lower() == "true"
@@ -127,7 +128,7 @@ class Config:
"XIAOMUSIC_USE_MUSIC_AUDIO_ID", "1582971365183456177"
)
use_music_id: str = os.getenv("XIAOMUSIC_USE_MUSIC_ID", "355454500")
log_file: str = os.getenv("XIAOMUSIC_LOG_FILE", "/tmp/xiaomusic.txt")
log_file: str = os.getenv("XIAOMUSIC_LOG_FILE", "xiaomusic.log.txt")
# 模糊搜索匹配的最低相似度阈值
fuzzy_match_cutoff: float = float(os.getenv("XIAOMUSIC_FUZZY_MATCH_CUTOFF", "0.6"))
# 开启模糊搜索
@@ -140,7 +141,11 @@ class Config:
keywords_playlocal: str = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
)
keywords_search_playlocal: str = os.getenv(
"XIAOMUSIC_KEYWORDS_SEARCH_PLAYLOCAL", "本地搜索播放"
)
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
keywords_search_play: str = os.getenv("XIAOMUSIC_KEYWORDS_SEARCH_PLAY", "搜索播放")
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
keywords_playlist: str = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLIST", "播放列表,播放歌单"
@@ -168,6 +173,9 @@ class Config:
enable_yt_dlp_cookies: bool = (
os.getenv("XIAOMUSIC_ENABLE_YT_DLP_COOKIES", "false").lower() == "true"
)
enable_save_tag: bool = (
os.getenv("XIAOMUSIC_ENABLE_SAVE_TAG", "false").lower() == "true"
)
get_ask_by_mina: bool = (
os.getenv("XIAOMUSIC_GET_ASK_BY_MINA", "false").lower() == "true"
)
@@ -186,6 +194,9 @@ class Config:
play_type_seq_tts_msg: str = os.getenv(
"XIAOMUSIC_PLAY_TYPE_SEQ_TTS_MSG", "已经设置为顺序播放"
)
recently_added_playlist_len: int = int(
os.getenv("XIAOMUSIC_RECENTLY_ADDED_PLAYLIST_LEN", "50")
)
def append_keyword(self, keys, action):
for key in keys.split(","):
@@ -204,7 +215,9 @@ class Config:
self.key_match_order = default_key_match_order()
self.key_word_dict = default_key_word_dict()
self.append_keyword(self.keywords_playlocal, "playlocal")
self.append_keyword(self.keywords_search_playlocal, "search_playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_search_play, "search_play")
self.append_keyword(self.keywords_stop, "stop")
self.append_keyword(self.keywords_playlist, "play_music_list")
self.append_user_keyword()
@@ -324,3 +337,11 @@ class Config:
if play_type == PLAY_TYPE_SEQ:
return self.play_type_seq_tts_msg
return ""
def get_ignore_tag_dirs(self):
ignore_tag_absolute_dirs = []
for ignore_tag_dir in self.ignore_tag_dirs.split(","):
if ignore_tag_dir:
ignore_tag_absolute_path = os.path.abspath(ignore_tag_dir)
ignore_tag_absolute_dirs.append(ignore_tag_absolute_path)
return ignore_tag_absolute_dirs

View File

@@ -20,3 +20,10 @@ PLAY_TYPE_SEQ = 4 # 顺序播放
GET_ASK_BY_MINA = {
"M01",
}
# 需要使用 play_musci 接口的设备型号
NEED_USE_PLAY_MUSIC_API = {
"X08C",
"X08E",
"X8F",
}

View File

@@ -2,7 +2,6 @@ import asyncio
import hashlib
import json
import os
import re
import secrets
import shutil
import tempfile
@@ -30,10 +29,12 @@ from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from starlette.background import BackgroundTask
from starlette.middleware.gzip import GZipMiddleware
from starlette.responses import FileResponse, Response
from xiaomusic import __version__
from xiaomusic.utils import (
chmoddir,
convert_file_to_mp3,
deepcopy_data_no_sensitive_info,
download_one_music,
@@ -43,7 +44,9 @@ from xiaomusic.utils import (
is_mp3,
remove_common_prefix,
remove_id3_tags,
restart_xiaomusic,
try_add_access_control_param,
update_version,
)
xiaomusic = None
@@ -105,6 +108,8 @@ app.add_middleware(
allow_methods=["*"], # 允许使用的请求方法
allow_headers=["*"], # 允许携带的 Headers
)
# 添加 GZip 中间件
app.add_middleware(GZipMiddleware, minimum_size=500)
def reset_http_server():
@@ -303,6 +308,23 @@ async def musicinfos(
return ret
class MusicInfoObj(BaseModel):
musicname: str
title: str = ""
artist: str = ""
album: str = ""
year: str = ""
genre: str = ""
lyrics: str = ""
picture: str = "" # base64
@app.post("/setmusictag")
async def setmusictag(info: MusicInfoObj, Verifcation=Depends(verification)):
ret = xiaomusic.set_music_tag(info.musicname, info)
return {"ret": ret}
@app.get("/curplaylist")
async def curplaylist(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
@@ -325,6 +347,44 @@ class UrlInfo(BaseModel):
url: str
class DidPlayMusic(BaseModel):
did: str
musicname: str = ""
searchkey: str = ""
@app.post("/playmusic")
async def playmusic(data: DidPlayMusic, Verifcation=Depends(verification)):
did = data.did
musicname = data.musicname
searchkey = data.searchkey
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
log.info(f"playmusic {did} musicname:{musicname} searchkey:{searchkey}")
await xiaomusic.do_play(did, musicname, searchkey)
return {"ret": "OK"}
class DidPlayMusicList(BaseModel):
did: str
listname: str = ""
musicname: str = ""
@app.post("/playmusiclist")
async def playmusiclist(data: DidPlayMusicList, Verifcation=Depends(verification)):
did = data.did
listname = data.listname
musicname = data.musicname
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
log.info(f"playmusiclist {did} listname:{listname} musicname:{musicname}")
await xiaomusic.do_play_music_list(did, listname, musicname)
return {"ret": "OK"}
@app.post("/downloadjson")
async def downloadjson(data: UrlInfo, Verifcation=Depends(verification)):
log.info(data)
@@ -430,6 +490,7 @@ async def downloadplaylist(data: DownloadPlayList, Verifcation=Depends(verificat
log.debug(f"Download dir_path: {dir_path}")
# 可能只是部分失败,都需要整理下载目录
remove_common_prefix(dir_path)
chmoddir(dir_path)
asyncio.create_task(check_download_proc())
return {"ret": "OK"}
@@ -448,7 +509,15 @@ class DownloadOneMusic(BaseModel):
@app.post("/downloadonemusic")
async def downloadonemusic(data: DownloadOneMusic, Verifcation=Depends(verification)):
try:
await download_one_music(config, data.url, data.name)
download_proc = await download_one_music(config, data.url, data.name)
async def check_download_proc():
# 等待子进程完成
exit_code = await download_proc.wait()
log.info(f"Download completed with exit code {exit_code}")
chmoddir(config.download_path)
asyncio.create_task(check_download_proc())
return {"ret": "OK"}
except Exception as e:
log.exception(f"Execption {e}")
@@ -490,6 +559,33 @@ async def playlistdel(data: PlayListObj, Verifcation=Depends(verification)):
return {"ret": "Del failed, may be not exist."}
class PlayListUpdateObj(BaseModel):
oldname: str # 旧歌单名字
newname: str # 新歌单名字
# 修改歌单名字
@app.post("/playlistupdatename")
async def playlistupdatename(
data: PlayListUpdateObj, Verifcation=Depends(verification)
):
ret = xiaomusic.play_list_update_name(data.oldname, data.newname)
if ret:
return {"ret": "OK"}
return {"ret": "Update failed, may be not exist."}
# 获取所有自定义歌单
@app.get("/playlistnames")
async def getplaylistnames(Verifcation=Depends(verification)):
names = xiaomusic.get_play_list_names()
log.info(f"names {names}")
return {
"ret": "OK",
"names": names,
}
class PlayListMusicObj(BaseModel):
name: str = "" # 歌单名
music_list: list[str] # 歌曲名列表
@@ -513,6 +609,40 @@ async def playlistdelmusic(data: PlayListMusicObj, Verifcation=Depends(verificat
return {"ret": "Del failed, may be playlist not exist."}
# 歌单更新歌曲
@app.post("/playlistupdatemusic")
async def playlistupdatemusic(
data: PlayListMusicObj, Verifcation=Depends(verification)
):
ret = xiaomusic.play_list_update_music(data.name, data.music_list)
if ret:
return {"ret": "OK"}
return {"ret": "Del failed, may be playlist not exist."}
# 获取歌单中所有歌曲
@app.get("/playlistmusics")
async def getplaylist(name: str, Verifcation=Depends(verification)):
ret, musics = xiaomusic.play_list_musics(name)
return {
"ret": "OK",
"musics": musics,
}
# 更新版本
@app.post("/updateversion")
async def updateversion(
version: str = "", lite: bool = True, Verifcation=Depends(verification)
):
ret = await update_version(version, lite)
if ret != "OK":
return {"ret": ret}
asyncio.create_task(restart_xiaomusic())
return {"ret": "OK"}
async def file_iterator(file_path, start, end):
async with aiofiles.open(file_path, mode="rb") as file:
await file.seek(start)
@@ -558,9 +688,6 @@ def access_key_verification(file_path, key, code):
return False
range_pattern = re.compile(r"bytes=(\d+)-(\d*)")
def safe_redirect(url):
url = try_add_access_control_param(config, url)
url = url.replace("\\", "")

View File

@@ -6,9 +6,9 @@
<meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
@@ -19,6 +19,9 @@
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<script>
var vConsole = new window.VConsole();
@@ -57,15 +60,19 @@ function sendDebugCmd() {
</head>
<body>
<h1>Debug For XiaoMusic</h1>
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
<button onclick="postJSON()">提交</button><br>
<div class="debug">
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
<button onclick="postJSON()">提交</button><br>
</div>
<hr>
<input id="cmd" type="text"></input>
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
<div class="debug">
<input id="cmd" type="text"></input>
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
</div>
</body>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
</footer>
</html>

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>歌曲下载工具</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
@@ -16,6 +16,9 @@
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
</head>
<body>

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

View File

@@ -1,12 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小爱音箱操控面板</title>
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
<script src="./app.js?version=1732797036"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
<link href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet">
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
<link rel="stylesheet" href="./main.css?version=1735438766">
<link rel="icon" href="./favicon.ico">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
@@ -17,82 +19,209 @@
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
</head>
<body class="index_page">
<div class="player">
<h1>小爱音箱播放器
<a id="version" href="javascript:void(0);">1.0.0</a><span id="versionnew" class="new-badge"></span>
</h1>
<label for="did">选择播放设备:</label>
<select id="did" class="device-selector">
<option value="default">默认设备</option>
</select>
<label for="music_list" style="display: flex;align-items: center;">选择播放列表:
<div class="option-inline" onclick="sendcmd('刷新列表')">
<span class="material-icons">refresh</span>
<span class="tooltip">刷新列表</span>
</div>
</label>
<select id="music_list" class="playlist-selector">
</select>
<label for="music_name" style="display: flex;align-items: center;">选择歌曲:
<div class="option-inline" onclick="toggleDelete()">
<span class="material-icons">delete</span>
<span class="tooltip">删除歌曲</span>
</div>
</label>
<select id="music_name" class="song-selector">
</select>
<div id="device-audio">
<progress class="progress" id="progress" value="0" max="100"></progress>
<div style="display: flex; justify-content: space-between; width: 100%;">
<span class="current-time" id="current-time">0:00</span>
<div class="current-song" id="playering-music">当前播放歌曲:无</div>
<span class="duration" id="duration">00:00</span>
</div>
</div>
<audio id="audio" controls src="" autoplay></audio>
<div class="buttons">
<div class="player-controls button-group">
<div id="modeBtn" onclick="togglePlayMode()" class="control-button device-enable">
<span class="material-icons">shuffle</span>
<span class="tooltip">切换播放模式</span>
</div>
<div onclick="prevTrack()" class="control-button device-enable">
<span class="material-icons">skip_previous</span>
<span class="tooltip">上一首</span>
</div>
<div onclick="play()" class="control-button">
<span class="material-icons-outlined play">play_circle_outline</span>
<span class="tooltip">播放</span>
</div>
<div onclick="nextTrack()" class="control-button device-enable">
<span class="material-icons">skip_next</span>
<span class="tooltip">下一首</span>
</div>
<div onclick="stopPlay()" class="control-button device-enable">
<span class="material-icons">stop</span>
<span class="tooltip">关机</span>
</div>
</div>
<div class="mode-controls button-group">
<div onclick="addToFavorites()" class="favorite icon-item device-enable">
<span class="material-icons">favorite</span>
<p>收藏</p>
</div>
<div onclick="toggleVolume()" class="icon-item device-enable">
<span class="material-icons">volume_up</span>
<p>音量</p>
</div>
<!--
<div onclick="toggleLocalPlay()" id="web_play">
<span class="material-icons">headphones</span>
<span class="tooltip">网页播放</span>
</div>
-->
</head>
<body>
<h2>小爱音箱操控面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">版本未知</a>)
<span id="versionnew" class="blink"></span>
</h2>
<hr>
<div class="rows">
<select id="did">
</select>
<div onclick="toggleSearch()" class="icon-item device-enable">
<span class="material-icons">search</span>
<p>搜索</p>
</div>
<div onclick="togglePlayLink()" class="icon-item device-enable">
<span class="material-icons">link</span>
<p>链接</p>
</div>
<div onclick="toggleTimer()" class="icon-item device-enable">
<span class="material-icons">timer</span>
<p>定时</p>
</div>
<div onclick="openSettings()" class="icon-item">
<span class="material-icons">settings</span>
<p>设置</p>
</div>
</div>
</div>
</div>
<div id="cmds">
<a class="button" href="./setting.html">设置</a>
</div>
<hr>
<div style="margin: 20px;">
<div style="display: flex; align-items: center;">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
<input id="volume" type="range"></input>
</div>
</div>
<hr>
<div class="rows">
<label for="search">搜索歌曲:</label>
<input type="text" id="search" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)">
<label for="music-name" id="music-name-label" style="display: none;">确认选择:</label>
<select id="music-name" style="display: none;">
<!-- 动态生成选项 -->
</select>
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)" style="display: none;"></input>
<div style="display: flex; align-items: center">
<progress id="progress" value="0" max="100" style="width: 270px"></progress>
<div id="play-time" style="margin-left: 10px">00:00/00:00</div>
</div>
<div>
<button id="play">播放</button>
<div id="playering-music" class="text"></div>
</div>
<!-- 搜索组件 -->
<div class="component" id="search-component">
<h2>搜索歌曲</h2>
<input type="text" id="search" class="search-input" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)">
<label for="music-name" id="music-name-label" style="display: none;">确认选择:</label>
<select id="music-name" style="display: none;">
<!-- 动态生成选项 -->
</select>
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)" style="display: none;"></input>
<div class="component-button-group">
<button onclick="confirmSearch()">确定</button>
<button onclick="toggleSearch()">关闭</button>
</div>
</div>
<hr>
<div class="rows">
<label for="music_list">播放列表:</label>
<select id="music_list"></select>
<label for="music_name">歌曲:</label>
<select id="music_name"></select>
<div>
<button id="play_music_list">播放选中歌曲</button>
<button id="del_music">删除选中歌曲</button>
<button id="web_play">网页播放</button>
</div>
<div class="play_pannel">
<audio autoplay controls src=""></audio>
</div>
<!-- 定时关机组件 -->
<div class="component" id="timer-component">
<h2>定时关机</h2>
<button onclick="timedShutDown('10分钟后关机')">10分钟后关机</button>
<button onclick="timedShutDown('30分钟后关机')">30分钟后关机</button>
<button onclick="timedShutDown('60分钟后关机')">60分钟后关机</button>
<span class="tooltip timer-tooltip" style="display: none;">已发送指令</span>
<div class="component-button-one">
<button onclick="toggleTimer()">关闭</button>
</div>
</div>
<hr>
<div class="rows">
<input id="music-url" type="text" value="https://lhttp.qtfm.cn/live/4915/64k.mp3"></input>
<button id="playurl">播放链接</button>
<!-- 播放链接组件 -->
<div class="component" id="playlink-component">
<h2>播放链接</h2>
<input type="text" id="music-url" class="search-input" placeholder="请输入播放链接"
value="https://lhttp.qtfm.cn/live/4915/64k.mp3">
<div class="component-button-group">
<button id="playurl">播放链接</button>
<button onclick="togglePlayLink()">关闭</button>
</div>
</div>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>
</body>
<!-- 音量组件 -->
<div class="component" id="volume-component">
<h2>调节音量</h2>
<input type="range" id="volume" class="volume-slider" />
<div class="component-button-one">
<button onclick="toggleVolume()">关闭</button>
</div>
</div>
<!-- 删除确认组件 -->
<div class="component" id="delete-component">
<h2>警告</h2>
<p>你确定要删除歌曲 <span id="delete-music-name"></span> 吗?</p>
<p style="font-weight: bold;">注意:该操作会永久删除该歌曲且不可撤销</p>
<div class="component-button-group">
<button onclick="confirmDelete()">确定</button>
<button onclick="toggleDelete()">关闭</button>
</div>
</div>
<!-- 警告组件 -->
<div class="component" id="warning-component">
<h2>警告</h2>
<p>当前页面的HOST与设置中的HOST不一致请检查是否设置错误</p>
<p>当前HOST: <span id="local-host"></span></p>
<p>设置中的HOST: <span id="setting-host"></span></p>
<div class="component-button-group">
<a href="./setting.html" target="_blank"><button>立即修改</button></a>
<button onclick="nowarning()">继续并不再显示</button>
<button onclick="toggleWarning()">取消</button>
</div>
</div>
<!-- 更新组件 -->
<div class="component" id="update-component" style="display: none;">
<h2>更新</h2>
<label for="update-version" style="display: flex;align-items: center;">选择版本:
<a class="option-inline changelog-button" href="https://xdocs.hanxi.cc/issues/changelog.html" target="_blank">
<span class="material-icons">list_alt</span>
<span>版本日志</span>
</a>
</label>
<select id="update-version" class="version-selector">
<option value="test" selected>test</option>
</select>
<label for="lite">选择类型:</label>
<select id="lite" class="version-selector">
<option value="true" selected>轻量(不更新ffmpeg)</option>
<option value="false">完整(更新ffmpeg)</option>
</select>
<div class="component-button-group">
<button onclick="doUpdates()">更新</button>
<button onclick="toggleUpdate()">关闭</button>
</div>
</div>
<div class="footer">
Powered by XiaoMusic
</div>
<script src="./md.js?version=1735438766">
</script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<title>M3U to JSON Converter</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
@@ -16,6 +16,9 @@
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
@@ -69,7 +72,7 @@ function convertToJSON() {
<textarea id="json-output" rows="10" cols="50" placeholder="转换后的JSON..."></textarea>
</body>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
</footer>
</html>

View File

@@ -0,0 +1,353 @@
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0;
padding: 0;
}
.index_page {
height: 100vh;
}
.player {
background-color: #ffffff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
text-align: center;
max-width: 360px;
position: relative;
overflow: hidden;
}
h1 {
font-size: 28px;
margin-bottom: 15px;
color: #333;
display: flex;
justify-content: center;
align-items: center;
}
h1 a {
margin-left: 10px;
font-size: 14px;
color: #007bff;
text-decoration: none;
}
h1 a:hover {
text-decoration: underline;
}
.new-badge {
background-color: #ff4757;
color: white;
border-radius: 12px;
padding: 2px 6px;
font-size: 12px;
margin-left: 5px;
display: none;
width: fit-content;
}
label {
font-size: 14px;
color: #555;
margin: 5px 0;
display: block;
text-align: left;
}
select,
input[type="range"],
input[type="text"],
input[type="password"],
input[type="number"] {
padding: 10px;
border: 1px solid #cccccc;
border-radius: 6px;
margin-bottom: 12px;
font-size: 14px;
transition: border 0.2s ease;
box-sizing: border-box
}
select:focus,
input[type="text"]:focus {
border-color: #007bff;
outline: none;
}
select {
overflow: hidden;
}
button {
background-color: #007bff;
color: white;
border: none;
border-radius: 6px;
padding: 10px 15px;
cursor: pointer;
margin: 5px;
font-size: 16px;
transition: background-color 0.3s;
position: relative; /* 为tooltip绝对定位做准备 */
}
button:hover {
background-color: #0056b3;
}
.tooltip {
visibility: hidden;
background-color: #555;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 150%; /* 控制tooltip相对按钮的位置 */
left: 50%;
margin-left: -50px; /* 调整位置以居中显示 Tooltip */
width: 80px; /* 设置固定宽度以保证足够的显示空间 */
white-space: nowrap; /* 防止换行 */
opacity: 0;
transition: opacity 0.3s;
}
button:hover .tooltip,.option-inline:hover .tooltip,.control-button:hover .tooltip {
visibility: visible;
opacity: 1;
}
.option-inline{
display: flex;
margin-left: auto;
position: relative;
align-items: center;
}
.option-inline:hover .tooltip {
/* 位置调整到左边 */
left: -50px; /* 调整位置以居中显示 Tooltip */
bottom: 0;
}
.control-button{
position: relative; /* 为tooltip绝对定位做准备 */
}
.progress {
width: 90%;
height: 6px;
background: #e0e0e0;
border-radius: 6px;
margin: 15px 0;
cursor: pointer;
}
progress::-webkit-progress-bar {
background: #e0e0e0;
}
progress::-moz-progress-bar,
progress::-webkit-progress-value {
background: #007bff;
}
.current-song {
color: #333;
margin: 10px 0;
font-weight: bold;
text-align: center;
}
.component {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #ffffff;
padding: 5px 15px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
z-index: 100;
width: 300px;
}
.component input {
width: 90%;
}
.component-button-group {
display: flex;
justify-content: space-between;
}
.component-button-one {
display: flex;
justify-content: center;
}
#warning-component p span {
color: #007bff;
}
.button-group {
display: flex;
justify-content: center;
margin: 5px 0;
}
.player-controls {
display: flex;
justify-content: space-around;
align-items: center;
margin-top: 20px;
margin-bottom: 20px;
}
.play {
font-size: 48px;
}
.footer {
position: absolute;
bottom: 20px;
font-size: 14px;
color: #555;
user-select: none;
}
.timer-tooltip {
bottom: 50%;
visibility: visible;
opacity: 1;
}
.favorite.favorite-active .material-icons {
color: #ff6347;
}
#audio {
width: 100%;
display: none;
}
.qrcode {
width: 100%;
max-width: 400px;
height: auto;
}
.login-tips {
color: red;
font-size: 12px;
margin-left: 10px;
}
.login-tips a {
color: rgb(9, 105, 218);
text-decoration: underline;
}
/* setting.html */
.rows {
display: flex;
flex-direction: column;
justify-content: center;
}
.rows a {
color: rgb(9, 105, 218);
text-decoration: none;
}
.rows a:hover {
color: rgb(9, 95, 198);
text-decoration: underline;
}
textarea {
margin-left: 5%;
margin-right: 5%;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
max-width: 400px;
height: 200px;
}
.custom-checkbox {
display: inline-block;
margin: 10px;
width: 20px;
height: 20px;
vertical-align: middle; /* 确保与标签垂直居中对齐 */
}
.checkbox-label {
display: inline-block;
width: 180px;
background-color: #fff;
border: 0px solid #ccc;
border-radius: 3px;
position: relative;
cursor: pointer;
vertical-align: middle; /* 确保与复选框垂直居中对齐 */
margin-left: 1px; /* 给复选框和标签之间一些距离,如果需要的话 */
padding: 5px 10px;
}
.debug {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
@media screen and (max-width: 440px) {
.player{
width: 90%;
}
.footer {
position: relative;
}
}
.mini-button {
padding: 0px 0px;
margin: 0px;
font-size: 14px;
margin-left: auto;
}
.playlist-selector,
.device-selector,
.version-selector,
.song-selector {
width: 300px;
}
.mode-controls {
display: flex;
justify-content: space-around;
align-items: center;
padding: 10px 0;
border-top: 1px solid #ddd;
color: #555;
}
.icon-item {
text-align: center;
}
.icon-item p {
font-size: 12px;
margin: 5px 0 0;
}
.disabled {
color: #ccc;
pointer-events: none;
}
span,p {
cursor: pointer;
user-select: none;
}
.changelog-button {
padding: 0px 0px;
margin: 0px;
font-size: 14px;
margin-left: auto;
text-decoration: none;
}

View File

@@ -0,0 +1,713 @@
// $(function () {
// })
let isPlaying = false;
let playModeIndex = 2;
//重新设计playModes
const playModes = {
0: {
icon: "repeat_one",
cmd: "单曲循环",
},
1: {
icon: "repeat",
cmd: "全部循环",
},
2: {
icon: "shuffle",
cmd: "随机播放",
},
3: {
icon: "filter_1",
cmd: "单曲播放",
},
4: {
icon: "playlist_play",
cmd: "顺序播放",
},
};
let favoritelist = []; //收藏列表
function webPlay() {
console.log("webPlay");
const music_name = $("#music_name").val();
$.get(`/musicinfo?name=${music_name}`, function (data, status) {
console.log(data);
if (data.ret == "OK") {
validHost(data.url) && $("audio").attr("src", data.url);
}
});
}
function play() {
var did = $("#did").val();
if (did == "web_device") {
webPlay();
} else {
playOnDevice();
}
}
function playOnDevice() {
console.log("playOnDevice");
var music_list = $("#music_list").val();
var music_name = $("#music_name").val();
if (no_warning) {
do_play_music_list(music_list, music_name);
return;
}
$.get(`/musicinfo?name=${music_name}`, function (data, status) {
console.log(data);
if (data.ret == "OK") {
console.log(
"%cmd.js:42 validHost(data.url) ",
"color: #007acc;",
validHost(data.url)
);
validHost(data.url) && do_play_music_list(music_list, music_name);
}
});
}
function stopPlay() {
sendcmd("关机");
}
function prevTrack() {
sendcmd("上一首");
}
function nextTrack() {
sendcmd("下一首");
}
function togglePlayMode(isSend = true) {
const modeBtnIcon = $("#modeBtn .material-icons");
if (playModeIndex == '') {
playModeIndex = 2;
}
modeBtnIcon.text(playModes[playModeIndex].icon);
$("#modeBtn .tooltip").text(playModes[playModeIndex].cmd);
// return;
isSend && sendcmd(playModes[playModeIndex].cmd);
console.log(`当前播放模式: ${playModeIndex} ${playModes[playModeIndex].cmd}`);
playModeIndex = (playModeIndex + 1) % Object.keys(playModes).length;
}
function addToFavorites() {
const isLiked = $(".favorite").hasClass("favorite-active");
const cmd = isLiked ? "取消收藏" : "加入收藏";
if (isLiked) {
$(".favorite").removeClass("favorite-active");
// 取消收藏
favoritelist = favoritelist.filter((item) => item != $("#music_name").val());
} else {
$(".favorite").addClass("favorite-active");
// 加入收藏
favoritelist.push($("#music_name").val());
}
sendcmd(cmd);
}
function openSettings() {
console.log("打开设置");
//新建标签页打开setting.html页面
window.open("setting.html", "_blank");
}
function toggleVolume() {
$("#volume-component").toggle();
}
function toggleSearch() {
$("#search-component").toggle();
}
function toggleTimer() {
$("#timer-component").toggle();
}
function togglePlayLink() {
$("#playlink-component").toggle(); // 切换播放链接的显示状态
}
function toggleLocalPlay() {
$("#audio").fadeIn();
}
function toggleWarning() {
$("#warning-component").toggle(); // 切换警告框的显示状态
}
function toggleDelete() {
var del_music_name = $("#music_name").val();
$("#delete-music-name").text(del_music_name);
$("#delete-component").toggle(); // 切换删除框的显示状态
}
function confirmDelete() {
var del_music_name = $("#music_name").val();
console.log(`删除歌曲 ${del_music_name}`);
$("#delete-component").hide(); // 隐藏删除框
$.ajax({
type: "POST",
url: "/delmusic",
data: JSON.stringify({ name: del_music_name }),
contentType: "application/json; charset=utf-8",
success: () => {
alert(`删除 ${del_music_name} 成功`);
refresh_music_list();
},
error: () => {
alert(`删除 ${del_music_name} 失败`);
},
});
}
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs < 10 ? "0" : ""}${secs}`; // Format time as mm:ss
}
var offset = 0;
var duration = 0;
let no_warning = localStorage.getItem("no-warning");
// 拉取现有配置
$.get("/getsetting", function (data, status) {
console.log(data, status);
localStorage.setItem("mi_did", data.mi_did);
var did = localStorage.getItem("cur_did");
var dids = [];
if (data.mi_did != null) {
dids = data.mi_did.split(",");
}
console.log("cur_did", did);
console.log("dids", dids);
if (did != "web_device" && dids.length > 0 && (did == null || did == "" || !dids.includes(did))) {
did = dids[0];
localStorage.setItem("cur_did", did);
}
window.did = did;
$.get(`/getvolume?did=${did}`, function (data, status) {
console.log(data, status, data["volume"]);
$("#volume").val(data.volume);
});
refresh_music_list();
$("#did").empty();
var dids = data.mi_did.split(",");
$.each(dids, function (index, value) {
var cur_device = Object.values(data.devices).find(
(device) => device.did === value
);
if (cur_device) {
var option = $("<option></option>")
.val(value)
.text(cur_device.name)
.prop("selected", value === did);
$("#did").append(option);
if (value === did) {
playModeIndex = cur_device.play_type;
console.log(
"%c当前设备播放模式: ",
"color: #007acc;",
cur_device.play_type
);
togglePlayMode(false);
}
}
});
var option = $("<option></option>")
.val("web_device")
.text("本机")
.prop("selected", "web_device" === did);
$("#did").append(option);
console.log("cur_did", did);
$("#did").change(function () {
did = $(this).val();
localStorage.setItem("cur_did", did);
window.did = did;
console.log("cur_did", did);
location.reload();
});
if (did == "web_device") {
$("#audio").fadeIn();
$("#device-audio").fadeOut();
$(".device-enable").addClass('disabled');
} else {
$("#audio").fadeOut();
$("#device-audio").fadeIn();
$(".device-enable").removeClass('disabled');
}
});
function compareVersion(version1, version2) {
const v1 = version1.split(".").map(Number);
const v2 = version2.split(".").map(Number);
const len = Math.max(v1.length, v2.length);
for (let i = 0; i < len; i++) {
const num1 = v1[i] || 0;
const num2 = v2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
// 拉取版本
$.get("/getversion", function (data, status) {
console.log(data, status, data["version"]);
$("#version").text(`${data.version}`);
$.get("/latestversion", function (ret, status) {
console.log(ret, status);
if (ret.ret == "OK") {
const result = compareVersion(ret.version, data.version);
if (result > 0) {
console.log(`${ret.version} is greater than ${data.version}`);
$("#versionnew").text("new").css("display", "inline-block");
}
}
});
});
function _refresh_music_list(callback) {
$("#music_list").empty();
$.get("/musiclist", function (data, status) {
console.log(data, status);
favoritelist = data["收藏"];
$.each(data, function (key, value) {
let cnt = value.length;
$("#music_list").append(
$("<option></option>").val(key).text(`${key} (${cnt})`)
);
});
$("#music_list").change(function () {
const selectedValue = $(this).val();
localStorage.setItem("cur_playlist", selectedValue);
$("#music_name").empty();
const cur_music = localStorage.getItem("cur_music");
console.log("#music_name cur_music", cur_music);
$.each(data[selectedValue], function (index, item) {
$("#music_name").append($("<option></option>").val(item).text(item).prop("selected", item == cur_music));
});
});
$("#music_list").trigger("change");
// 获取当前播放列表
$.get(`/curplaylist?did=${did}`, function (playlist, status) {
if (playlist != "") {
$("#music_list").val(playlist);
$("#music_list").trigger("change");
} else {
// 使用本地记录的
playlist = localStorage.getItem("cur_playlist");
if (data.hasOwnProperty(playlist)) {
$("#music_list").val(playlist);
$("#music_list").trigger("change");
}
}
});
callback();
});
}
// 拉取播放列表
function refresh_music_list() {
// 刷新列表时清空并临时禁用搜索框
const searchInput = document.getElementById("search");
const oriPlaceHolder = searchInput.placeholder;
const oriValue = searchInput.value;
const inputEvent = new Event("input", { bubbles: true });
searchInput.value = "";
// 分发事件,让其他控件改变状态
searchInput.dispatchEvent(inputEvent);
searchInput.disabled = true;
searchInput.placeholder = "请等待...";
_refresh_music_list(() => {
// 刷新完成再启用
searchInput.disabled = false;
searchInput.value = oriValue;
searchInput.dispatchEvent(inputEvent);
searchInput.placeholder = oriPlaceHolder;
// 每3秒获取下正在播放的音乐
get_playing_music();
setInterval(() => {
get_playing_music();
}, 3000);
});
}
function do_play_music_list(listname, musicname) {
$.ajax({
type: "POST",
url: "/playmusiclist",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
did: did,
listname: listname,
musicname: musicname,
}),
success: () => {
console.log("do_play_music_list succ", listname, musicname);
},
error: () => {
console.log("do_play_music_list failed", listname, musicname);
},
});
}
$("#play_music_list").on("click", () => {
var music_list = $("#music_list").val();
var music_name = $("#music_name").val();
if (no_warning) {
do_play_music_list(music_list, music_name);
return;
}
$.get(`/musicinfo?name=${music_name}`, function (data, status) {
console.log(data);
if (data.ret == "OK") {
validHost(data.url) && do_play_music_list(music_list, music_name);
}
});
});
$("#playurl").on("click", () => {
var url = $("#music-url").val();
const encoded_url = encodeURIComponent(url);
$.get(`/playurl?url=${encoded_url}&did=${did}`, function (data, status) {
console.log(data);
});
});
function do_play_music(musicname, searchkey) {
$.ajax({
type: "POST",
url: "/playmusic",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({
did: did,
musicname: musicname,
searchkey: searchkey,
}),
success: () => {
console.log("do_play_music succ", musicname, searchkey);
},
error: () => {
console.log("do_play_music failed", musicname, searchkey);
},
});
}
$("#play").on("click", () => {
var search_key = $("#music-name").val();
if (search_key == null) {
search_key = "";
}
var filename = $("#music-filename").val();
if (filename == null || filename == "") {
filename = search_key;
}
do_play_music(filename, search_key);
});
$("#volume").on("change", function () {
var value = $(this).val();
$.ajax({
type: "POST",
url: "/setvolume",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ did: did, volume: value }),
success: () => { },
error: () => { },
});
});
function check_status_refresh_music_list(retries) {
$.get("/cmdstatus", function (data) {
if (data.status === "finish") {
refresh_music_list();
} else if (retries > 0) {
setTimeout(function () {
check_status_refresh_music_list(retries - 1);
}, 1000); // 等待1秒后重试
}
});
}
function sendcmd(cmd) {
$.ajax({
type: "POST",
url: "/cmd",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ did: did, cmd: cmd }),
success: () => {
if (cmd == "刷新列表") {
check_status_refresh_music_list(3); // 最多重试3次
}
if (
["全部循环", "单曲循环", "随机播放", "单曲播放", "顺序播放"].includes(
cmd
)
) {
location.reload();
}
},
error: () => {
// 请求失败时执行的操作
},
});
}
// 监听输入框的输入事件
function debounce(func, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
function handleSearch() {
const searchInput = document.getElementById("search");
const musicSelect = document.getElementById("music-name");
const musicSelectLabel = document.getElementById("music-name-label");
searchInput.addEventListener(
"input",
debounce(function () {
const query = searchInput.value.trim();
if (query.length === 0) {
musicSelect.innerHTML = "";
musicSelect.style.display = "none";
musicSelectLabel.style.display = "none";
return;
}
musicSelect.style.display = "block";
musicSelectLabel.style.display = "block";
fetch(`/searchmusic?name=${encodeURIComponent(query)}`)
.then((response) => response.json())
.then((data) => {
musicSelect.innerHTML = ""; // 清空现有选项
// 找到的优先显示
if (data.length > 0) {
data.forEach((song) => {
const option = document.createElement("option");
option.value = song;
option.textContent = song;
musicSelect.appendChild(option);
});
}
// 添加用户输入作为一个选项
const userOption = document.createElement("option");
userOption.value = query;
userOption.textContent = `使用关键词播放: ${query}`;
musicSelect.appendChild(userOption);
// 提示没找到
if (data.length === 0) {
const option = document.createElement("option");
option.textContent = "没有匹配的结果";
option.disabled = true;
musicSelect.appendChild(option);
}
})
.catch((error) => {
console.error("Error fetching data:", error);
});
}, 600)
);
// 动态显示保存文件名输入框
const musicNameSelect = document.getElementById("music-name");
const musicFilenameInput = document.getElementById("music-filename");
function updateInputVisibility() {
const selectedOption =
musicNameSelect.options[musicNameSelect.selectedIndex];
var startsWithKeyword;
if (musicNameSelect.options.length === 0) {
startsWithKeyword = false;
} else {
startsWithKeyword = selectedOption.text.startsWith("使用关键词播放:");
}
if (startsWithKeyword) {
musicFilenameInput.style.display = "block";
musicFilenameInput.placeholder =
"请输入保存为的文件名称(默认:" + selectedOption.value + ")";
} else {
musicFilenameInput.style.display = "none";
}
}
// 观察元素修改
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
updateInputVisibility();
}
}
});
observer.observe(musicNameSelect, { childList: true });
// 监听用户输入
musicNameSelect.addEventListener("change", updateInputVisibility);
}
handleSearch();
function get_playing_music() {
$.get(`/playingmusic?did=${did}`, function (data, status) {
console.log(data);
if (data.ret == "OK") {
if (data.is_playing) {
$("#playering-music").text(`【播放中】 ${data.cur_music}`);
} else {
$("#playering-music").text(`【空闲中】 ${data.cur_music}`);
}
offset = data.offset;
duration = data.duration;
//检查歌曲是否在收藏中,如果是,设置收藏按钮为选中状态
console.log(
"%cmd.js:614 object",
"color: #007acc;",
favoritelist.includes(data.cur_music)
);
if (favoritelist.includes(data.cur_music)) {
$(".favorite").addClass("favorite-active");
} else {
$(".favorite").removeClass("favorite-active");
}
localStorage.setItem("cur_music", data.cur_music);
}
});
}
setInterval(() => {
if (duration > 0) {
offset++;
$("#progress").val((offset / duration) * 100);
$("#current-time").text(formatTime(offset));
$("#duration").text(formatTime(duration));
} else {
$("#current-time").text(formatTime(0));
$("#duration").text(formatTime(0));
}
}, 1000);
function formatTime(seconds) {
var minutes = Math.floor(seconds / 60);
var remainingSeconds = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, "0")}:${remainingSeconds
.toString()
.padStart(2, "0")}`;
}
$("audio").on("error", (e) => {
//如果audio标签的src为空则不做任何操作兼容安卓端的低版本webview
if ($("audio").attr("src") === "") {
return;
}
console.log(
"%c网页播放出现错误: ",
"color: #007acc;",
e.currentTarget.error.code,
e.currentTarget.error.message
);
alert(
e.currentTarget.error.code == 4
? "无法打开媒体文件XIAOMUSIC_HOSTNAME或端口地址错误请重新设置"
: "在线播放失败,请截图反馈: " + e.currentTarget.error.message
);
});
function validHost(url) {
//如果 localStorage 中有 no-warning 则直接返回true
if (no_warning) {
return true;
}
const local = location.host;
const host = new URL(url).host;
// 如果当前页面的Host与设置中的XIAOMUSIC_HOSTNAME、PORT一致, 不再提醒
if (local === host) {
return true;
}
$("#local-host").text(local);
$("#setting-host").text(host);
$("#warning-component").show();
console.log("%c 验证返回false", "color: #007acc;");
return false;
}
function nowarning() {
localStorage.setItem("no-warning", "true");
no_warning = true;
$("#warning-component").hide();
}
function timedShutDown(cmd) {
$(".timer-tooltip").toggle();
sendcmd(cmd);
setTimeout(() => {
$(".timer-tooltip").fadeOut();
}, 3000);
}
// 绑定点击事件,显示弹窗
$('#version').on('click', function () {
$.get("https://xdocs.hanxi.cc/versions.json", function (data, status) {
console.log(data);
const versionSelect = document.getElementById("update-version");
versionSelect.innerHTML = "";
data.forEach((item) => {
const option = document.createElement("option");
option.value = item.version;
option.textContent = item.version;
versionSelect.appendChild(option);
});
});
$('#update-component').show();
});
// 关闭更新弹窗
function toggleUpdate() {
$('#update-component').hide();
}
function doUpdates() {
const version = $("#update-version").val();
let lite = $("#lite").val();
$.ajax({
type: "POST",
url: `/updateversion?version=${version}&lite=${lite}`,
contentType: "application/json; charset=utf-8",
success: (data) => {
if (data.ret == "OK") {
alert(`更新成功,请刷新页面`);
location.reload();
} else {
alert(`更新失败: ${data.ret}`);
}
},
error: () => {
alert(`更新失败`);
},
});
}
function confirmSearch() {
var search_key = $("#search").val();
if (search_key == null) {
search_key = "";
}
var filename = $("#music-name").val();
var musicfilename = $("#music-filename").val();
if ((filename == null || filename == "" || filename == search_key)
&& (musicfilename != null && musicfilename != "")) {
filename = musicfilename;
}
console.log("confirmSearch", filename, search_key);
do_play_music(filename, search_key);
}

View File

@@ -4,9 +4,10 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
<script src="./setting.js?version=1732797036"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
<script src="./setting.js?version=1735438766"></script>
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
@@ -17,6 +18,9 @@
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
@@ -27,9 +31,7 @@ var vConsole = new window.VConsole();
</head>
<body>
<h2>小爱音箱设置面板
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
版本未知
</a>)
(<a id="version" href="https://xdocs.hanxi.cc/issues/changelog.html">版本未知</a>)
</h2>
<hr>
@@ -43,54 +45,61 @@ var vConsole = new window.VConsole();
<div id="setting">
<div class="rows">
<label for="account">*小米账号:</label>
<input id="account" type="text" placeholder="填写小米登录账号"></input>
<input id="account" type="text" placeholder="填写小米登录账号" />
<label for="password">*小米密码:</label>
<input id="password" type="password" placeholder="填写小米登录密码"></input>
<input id="password" type="password" placeholder="填写小米登录密码" />
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
<input id="hostname" type="text"></input>
<label for="hostname">*XIAOMUSIC_HOSTNAME(NAS的IP或域名):</label>
<input id="hostname" type="text" />
<label for="public_port">*本地端口(0表示跟容器端口一致):</label>
<input id="public_port" type="number" value="0" />
</div>
<hr>
<div class="rows">
<label for="verbose">是否开启调试日志:</label>
<select id="verbose">
<option value="true" selected>true</option>
<option value="false">false</option>
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="group_list">设备分组配置:<a href="https://github.com/hanxi/xiaomusic/issues/65#issuecomment-2215736529" target="_blank">文档</a></label>
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2"></input>
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2" />
<label for="music_path">音乐目录:</label>
<input id="music_path" type="text" value="music"></input>
<input id="music_path" type="text" value="music" />
<label for="download_path">音乐下载目录(必须是music的子目录):</label>
<input id="download_path" type="text" value='music/download'></input>
<input id="download_path" type="text" value='music/download' />
<label for="conf_path">配置文件目录:</label>
<input id="conf_path" type="text"></input>
<input id="conf_path" type="text" />
<label for="cache_dir">缓存文件目录:</label>
<input id="cache_dir" type="text"></input>
<input id="cache_dir" type="text" />
<label for="temp_path">临时文件目录:</label>
<input id="temp_path" type="text" value="music/tmp"></input>
<input id="temp_path" type="text" value="music/tmp" />
<label for="ffmpeg_location">ffmpeg路径:</label>
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin"></input>
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin" />
<label for="log_file">日志路径:</label>
<input id="log_file" type="text" value="/tmp/xiaomusic.txt"></input>
<input id="log_file" type="text" value="xiaomusic.log.txt" />
<label for="active_cmd">允许唤醒的命令:</label>
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop"></input>
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop" />
<label for="exclude_dirs">忽略目录(逗号分割):</label>
<input id="exclude_dirs" type="text" value="@eaDir,tmp"></input>
<input id="exclude_dirs" type="text" value="@eaDir,tmp" />
<label for="ignore_tag_dirs">不扫描标签信息目录(逗号分割):</label>
<input id="ignore_tag_dirs" type="text" value="" />
<label for="music_path_depth">目录深度:</label>
<input id="music_path_depth" type="number" value="10"></input>
<input id="music_path_depth" type="number" value="10" />
<label for="search_prefix">XIAOMUSIC_SEARCH(歌曲下载方式):</label>
<select id="search_prefix">
@@ -99,7 +108,7 @@ var vConsole = new window.VConsole();
</select>
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080" />
<label for="remove_id3tag">去除MP3 ID3v2和填充:</label>
<select id="remove_id3tag">
@@ -114,7 +123,7 @@ var vConsole = new window.VConsole();
</select>
<label for="miio_tts_command">MiIO tts 指令(解决部分型号没有提示音的问题):</label>
<input id="miio_tts_command" type="text" placeholder="如5 或者 5-3"></input>
<input id="miio_tts_command" type="text" placeholder="如5 或者 5-3" />
<label for="disable_httpauth">关闭控制台密码验证:</label>
<select id="disable_httpauth">
@@ -122,9 +131,9 @@ var vConsole = new window.VConsole();
<option value="false">false</option>
</select>
<label for="httpauth_username">控制台账户:</label>
<input id="httpauth_username" type="text" value=""></input>
<input id="httpauth_username" type="text" value="" />
<label for="httpauth_password">控制台密码:</label>
<input id="httpauth_password" type="password" value=""></input>
<input id="httpauth_password" type="password" value="" />
<label for="disable_download">关闭下载功能:</label>
<select id="disable_download">
@@ -133,12 +142,12 @@ var vConsole = new window.VConsole();
</select>
<label for="use_music_audio_id">触屏版显示歌曲ID:</label>
<input id="use_music_audio_id" type="text" value="1582971365183456177"></input>
<input id="use_music_audio_id" type="text" value="1582971365183456177" />
<label for="use_music_id">触屏版显示歌曲分段ID:</label>
<input id="use_music_id" type="text" value="355454500"></input>
<input id="use_music_id" type="text" value="355454500" />
<label for="fuzzy_match_cutoff">模糊匹配阈值(0.1~0.9):</label>
<input id="fuzzy_match_cutoff" type="number" value="0.6"></input>
<input id="fuzzy_match_cutoff" type="number" value="0.6" />
<label for="enable_fuzzy_match">开启模糊搜索:</label>
<select id="enable_fuzzy_match">
@@ -158,39 +167,37 @@ var vConsole = new window.VConsole();
<option value="false" selected>false</option>
</select>
<label for="port">监听端口(修改后需要重启):</label>
<input id="port" type="number" value="8090"></input>
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
<input id="public_port" type="number" value="0"></input>
<label for="pull_ask_sec">获取对话记录间隔(秒):</label>
<input id="pull_ask_sec" type="number" value="1"></input>
<input id="pull_ask_sec" type="number" value="1" />
<label for="delay_sec">下一首歌延迟播放秒数:</label>
<input id="delay_sec" type="number" value="3"></input>
<input id="delay_sec" type="number" value="3" />
<label for="stop_tts_msg">停止提示音:</label>
<input id="stop_tts_msg" type="text" value="收到,再见"></input>
<input id="stop_tts_msg" type="text" value="收到,再见" />
<label for="play_type_one_tts_msg">单曲循环提示音:</label>
<input id="play_type_one_tts_msg" type="text" value="已经设置为单曲循环"></input>
<input id="play_type_one_tts_msg" type="text" value="已经设置为单曲循环" />
<label for="play_type_all_tts_msg">全部循环提示音:</label>
<input id="play_type_all_tts_msg" type="text" value="已经设置为全部循环"></input>
<input id="play_type_all_tts_msg" type="text" value="已经设置为全部循环" />
<label for="play_type_rnd_tts_msg">随机播放提示音:</label>
<input id="play_type_rnd_tts_msg" type="text" value="已经设置为随机播放"></input>
<input id="play_type_rnd_tts_msg" type="text" value="已经设置为随机播放" />
<label for="play_type_sin_tts_msg">单曲播放提示音:</label>
<input id="play_type_sin_tts_msg" type="text" value="已经设置为单曲播放"></input>
<input id="play_type_sin_tts_msg" type="text" value="已经设置为单曲播放" />
<label for="play_type_seq_tts_msg">顺序播放提示音:</label>
<input id="play_type_seq_tts_msg" type="text" value="已经设置为顺序播放"></input>
<input id="play_type_seq_tts_msg" type="text" value="已经设置为顺序播放" />
<label for="keywords_playlocal">播放本地歌曲口令:</label>
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲"></input>
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲" />
<label for="keywords_play">播放歌曲口令:</label>
<input id="keywords_play" type="text" value="播放歌曲,放歌曲"></input>
<input id="keywords_play" type="text" value="播放歌曲,放歌曲" />
<label for="keywords_playlist">播放列表口令:</label>
<input id="keywords_playlist" type="text" value="播放列表,播放歌单"></input>
<input id="keywords_playlist" type="text" value="播放列表,播放歌单" />
<label for="keywords_stop">停止口令:</label>
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放"></input>
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放" />
<label for="keywords_search_playlocal">本地搜索播放口令(会产生临时播放列表):</label>
<input id="keywords_search_playlocal" type="text" value="本地搜索播放" />
<label for="keywords_search_play">搜索播放口令(会产生临时播放列表):</label>
<input id="keywords_search_play" type="text" value="搜索播放" />
<label for="enable_yt_dlp_cookies">启用yt-dlp-cookies(需要先上传yt-dlp-cookies.txt文件):</label>
<select id="enable_yt_dlp_cookies">
@@ -204,8 +211,16 @@ var vConsole = new window.VConsole();
<option value="false" selected>false</option>
</select>
<label for="music_list_url">歌单地址:</label>
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json"></input>
<label for="recently_added_playlist_len">最近新增的歌曲数量:</label>
<input id="recently_added_playlist_len" type="number" value="50" />
<label for="music_list_url" style="display: flex;align-items: center;">歌单地址:
<button class="option-inline mini-button" id="get_music_list">
<span class="material-icons">sync_alt</span>
<span>获取歌单</span>
</button>
</label>
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json" />
<label for="music_list_json">歌单内容:<a href="https://github.com/hanxi/xiaomusic/issues/78" target="_blank">格式文档</a></label>
<textarea id="music_list_json" type="text"></textarea>
@@ -223,31 +238,36 @@ var vConsole = new window.VConsole();
</div>
<hr>
<button onclick="location.href='/static/default/index.html';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button class="save-button">保存</button>
<hr>
<button id="refresh_music_tag">刷新tag</button>
<button id="clear_cache">清空缓存</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<hr>
<button onclick="location.href='/docs';">查看接口文档</button>
<a class="button" href="./m3u.html" target="_blank">m3u文件转换</a>
<a class="button" href="./downloadtool.html" target="_blank">歌曲下载工具</a>
<hr>
<a class="button" href="./debug.html" target="_blank">调试工具</a>
<a class="button" href="https://afdian.com/a/imhanxi" target="_blank">💰 爱发电</a>
<a class="button" href="https://github.com/hanxi/xiaomusic" target="_blank">点个 Star ⭐</a>
<div class="buttons">
<div class="button-group">
<button onclick="location.href='/static/default/index.html';">返回首页</button>
<button class="save-button">保存</button>
</div>
<div class="button-group">
<button id="refresh_music_tag">刷新tag</button>
<button id="clear_cache">清空缓存</button>
<a href="/downloadlog" download="xiaomusic.txt"><button>下载日志文件</button></a>
</div>
<div class="button-group">
<a href="/docs" target="_blank"><button>接口文档</button></a>
<a href="./m3u.html" target="_blank"><button>m3u转换</button></a>
<a href="./downloadtool.html" target="_blank"><button>歌曲下载工具</button></a>
</div>
<div class="button-group">
<a href="./debug.html" target="_blank"><button>调试工具</button></a>
<a href="https://afdian.com/a/imhanxi" target="_blank"><button>💰 爱发电</button></a>
<a href="https://github.com/hanxi/xiaomusic" target="_blank"><button>点个 Star ⭐</button></a>
</div>
</div>
<div class="rows">
<img class="qrcode" src="./qrcode.png" alt="请涵曦喝奶茶🧋">
</div>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
</footer>
</body>
</html>

View File

@@ -16,13 +16,19 @@ $(function(){
});
};
function updateCheckbox(selector, mi_did, device_list) {
function updateCheckbox(selector, mi_did, device_list,accountPassValid) {
// 清除现有的内容
$(selector).empty();
// 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项
var selected_dids = mi_did.split(',');
//如果device_list为空则可能是未设置小米账号密码或者已设置密码但是没有过小米验证此处需要提示用户
if (device_list.length == 0) {
const loginTips = accountPassValid ? `<div class="login-tips">未发现可用的小爱设备,请检查账号密码是否输错,并关闭加速代理或在<a href="https://www.mi.com">小米官网</a>登陆过人脸或滑块验证。如仍未解决。请根据<a href="https://github.com/hanxi/xiaomusic/issues/99">FAQ</a>的内容解决问题。</div>` : `<div class="login-tips">未发现可用的小爱设备,请先在下面的输入框中设置小米的<b>账号、密码</b></div>`;
$(selector).append(loginTips);
return;
}
$.each(device_list, function(index, device) {
var did = device.miotDID;
var hardware = device.hardware;
@@ -64,7 +70,8 @@ $(function(){
// 拉取现有配置
$.get("/getsetting?need_device_list=true", function(data, status) {
console.log(data, status);
updateCheckbox("#mi_did", data.mi_did, data.device_list);
const accountPassValid = data.account && data.password;
updateCheckbox("#mi_did", data.mi_did, data.device_list, accountPassValid);
// 初始化显示
for (const key in data) {
@@ -184,4 +191,14 @@ $(function(){
$("#clear_cache").on("click", () => {
localStorage.clear();
});
$("#hostname").on("change", function(){
const hostname = $(this).val();
// 检查是否包含端口号1到5位数字
if (hostname.match(/:\d{1,5}$/)) {
alert("hostname禁止带端口号");
// 移除端口号
$(this).val(hostname.replace(/:\d{1,5}$/,""));
}
});
});

View File

@@ -29,7 +29,7 @@ $(function(){
var offset = 0;
var duration = 0;
let no_warning = localStorage.getItem('no-warning');
// 拉取现有配置
$.get("/getsetting", function(data, status) {
console.log(data, status);
@@ -155,7 +155,7 @@ $(function(){
} else {
// 使用本地记录的
playlist = localStorage.getItem('cur_playlist');
if (data.includes(playlist)) {
if (data.hasOwnProperty(playlist)) {
$('#music_list').val(playlist);
$('#music_list').trigger('change');
}
@@ -192,11 +192,34 @@ $(function(){
});
}
function do_play_music_list(listname, musicname) {
$.ajax({
type: "POST",
url: "/playmusiclist",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({did: did, listname: listname, musicname: musicname}),
success: () => {
console.log("do_play_music_list succ", listname, musicname);
},
error: () => {
console.log("do_play_music_list failed", listname, musicname);
}
});
}
$("#play_music_list").on("click", () => {
var music_list = $("#music_list").val();
var music_name = $("#music_name").val();
let cmd = "播放列表" + music_list + "|" + music_name;
sendcmd(cmd);
if (no_warning) {
do_play_music_list(music_list, music_name);
return;
}
$.get(`/musicinfo?name=${music_name}`, function(data, status) {
console.log(data);
if (data.ret == "OK") {
validHost(data.url) && do_play_music_list(music_list, music_name);
}
});
});
$("#web_play").on("click", () => {
@@ -204,7 +227,7 @@ $(function(){
$.get(`/musicinfo?name=${music_name}`, function(data, status) {
console.log(data);
if (data.ret == "OK") {
$('audio').attr('src',data.url);
validHost(data.url) && $('audio').attr('src',data.url);
}
});
});
@@ -259,17 +282,32 @@ $(function(){
$container.append($button);
}
function do_play_music(musicname, searchkey) {
$.ajax({
type: "POST",
url: "/playmusic",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({did: did, musicname: musicname, searchkey: searchkey}),
success: () => {
console.log("do_play_music succ", musicname, searchkey);
},
error: () => {
console.log("do_play_music failed", musicname, searchkey);
}
});
}
$("#play").on("click", () => {
var search_key = $("#music-name").val();
if (search_key == null) {
search_key = "";
}
var filename = $("#music-filename").val();
if (filename == null) {
filename = "";
if (filename == null || filename == "") {
filename = search_key;
}
let cmd = "播放歌曲" + search_key + "|" + filename;
sendcmd(cmd);
do_play_music(filename, search_key);
});
$("#volume").on('change', function () {
@@ -375,7 +413,7 @@ $(function(){
.catch(error => {
console.error('Error fetching data:', error);
});
}, 300));
}, 500));
// 动态显示保存文件名输入框
const musicNameSelect = document.getElementById('music-name');
@@ -386,7 +424,7 @@ $(function(){
if (musicNameSelect.options.length === 0) {
startsWithKeyword = false;
} else {
startsWithKeyword = selectedOption.text.startsWith('使用关键词联网搜索:');
startsWithKeyword = selectedOption.text.startsWith("使用关键词播放:");
}
if (startsWithKeyword) {
@@ -435,5 +473,48 @@ $(function(){
var minutes = Math.floor(seconds / 60);
var remainingSeconds =Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
}
$("audio").on("error", (e) => {
//如果audio标签的src为空则不做任何操作兼容安卓端的低版本webview
if ($("audio").attr("src") === "") {
return;
}
console.log('%c网页播放出现错误: ', 'color: #007acc;', e.currentTarget.error.code,e.currentTarget.error.message);
alert(e.currentTarget.error.code==4 ? "无法打开媒体文件XIAOMUSIC_HOSTNAME或端口地址错误请重新设置" : "在线播放失败,请截图反馈: "+e.currentTarget.error.message);
});
function validHost(url) {
//如果 localStorage 中有 no-warning 则直接返回true
if (no_warning) {
return true;
}
const local = location.host;
const host = new URL(url).host;
// 如果当前页面的Host与设置中的XIAOMUSIC_HOSTNAME、PORT一致, 不再提醒
if (local === host) {
localStorage.setItem('no-warning', 'true');
// 设置全局变量
no_warning = true;
return true;
}
// 如果当前页面的Host与设置中的XIAOMUSIC_HOSTNAME、PORT不一致
const validHost = document.getElementById('valid-host');
let validFlag = false;
$('#local-host').text(local);
$('#setting-host').text(host);
validHost.showModal();
//监听validHost的close事件
function _handleClose() {
console.log('%c提醒HOST不一致弹窗,用户已选择: ', 'color: #007acc;', validHost.returnValue);
if (validHost.returnValue == "no-warning") {
localStorage.setItem('no-warning', 'true');
no_warning = true;
validFlag = true;
}
validHost.removeEventListener('close', _handleClose)
}
validHost.addEventListener('close', _handleClose)
return validFlag;
}
});

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<script>
var vConsole = new window.VConsole();
function postJSON() {
var data = $('#post-input').val();
$.ajax({
type: 'POST',
url: '/debug_play_by_music_url',
data: data,
contentType: "application/json; charset=utf-8",
success: (err) => {
console.log("succ", res);
},
error: (res) => {
console.log("error", res);
}
});
}
function sendDebugCmd() {
var cmd = $("#cmd").val();
var did = localStorage.getItem('cur_did');
$.ajax({
type: "POST",
url: "/cmd",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({did: did, cmd: cmd}),
success: () => {
},
error: () => {
// 请求失败时执行的操作
}
});
}
</script>
</head>
<body>
<h1>Debug For XiaoMusic</h1>
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
<button onclick="postJSON()">提交</button><br>
<hr>
<input id="cmd" type="text"></input>
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
</body>
<footer>
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
</footer>
</html>

View File

@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>歌曲下载工具</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
</head>
<body>
<h1>歌曲下载工具</h1>
<div class="rows">
<!-- 歌单的输入 -->
<label for="playlistUrl">输入歌单 URL:</label>
<input type="text" id="playlistUrl" value="https://m.bilibili.com/video/BV1WUsDezE88">
<label for="dirname">输入歌单名字:</label>
<input type="text" id="dirname" placeholder="流行歌曲">
<button id="downloadPlaylistBtn">下载歌单</button>
</div>
<hr>
<div class="rows">
<!-- 单曲的输入 -->
<label for="songUrl">输入歌曲 URL:</label>
<input type="text" id="songUrl" value="https://m.bilibili.com/video/BV1qD4y1U7fs">
<label for="songName">输入歌曲名字:</label>
<input type="text" id="songName" placeholder="歌曲名">
<button id="downloadSongBtn">下载单曲</button>
</div>
<script>
// 下载歌单
$('#downloadPlaylistBtn').click(function() {
var playlistUrl = $('#playlistUrl').val();
var dirname = $('#dirname').val();
if (!playlistUrl || !dirname) {
alert('请填写完整的歌单 URL 和歌单名字');
return;
}
var data = {
dirname: dirname,
url: playlistUrl
};
$.ajax({
type: "POST",
url: "/downloadplaylist",
contentType: "application/json",
data: JSON.stringify(data),
success: (msg) => {
alert('歌单下载请求已发送!');
console.log(response);
},
error: (msg) => {
alert('歌单下载请求失败,请重试。');
}
});
});
// 下载单曲
$('#downloadSongBtn').click(function() {
var songName = $('#songName').val();
var songUrl = $('#songUrl').val();
if (!songUrl || !songName) {
alert('请填写完整的歌曲 URL 和歌曲名字');
return;
}
var data = {
name: songName,
url: songUrl
};
$.ajax({
type: "POST",
url: "/downloadonemusic",
contentType: "application/json",
data: JSON.stringify(data),
success: (msg) => {
alert('单曲下载请求已发送!');
console.log(response);
},
error: (msg) => {
alert('单曲下载请求失败,请重试。');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,113 @@
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
<script src="./app.js?version=1733563859"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
-->
</head>
<body>
<h2>小爱音箱操控面板
(<a id="version" href="https://xdocs.hanxi.cc/issues/changelog.html">版本未知</a>)
<span id="versionnew" class="blink"></span>
</h2>
<hr>
<div class="rows">
<select id="did">
</select>
</div>
<div id="cmds">
<a class="button" href="./setting.html">设置</a>
</div>
<hr>
<div style="margin: 20px;">
<div style="display: flex; align-items: center;">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
<input id="volume" type="range"></input>
</div>
</div>
<hr>
<div class="rows">
<label for="search">搜索歌曲:</label>
<input type="text" id="search" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)">
<label for="music-name" id="music-name-label" style="display: none;">确认选择:</label>
<select id="music-name" style="display: none;">
<!-- 动态生成选项 -->
</select>
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)" style="display: none;"></input>
<div style="display: flex; align-items: center">
<progress id="progress" value="0" max="100" style="width: 270px"></progress>
<div id="play-time" style="margin-left: 10px">00:00/00:00</div>
</div>
<div>
<button id="play">播放</button>
<div id="playering-music" class="text"></div>
</div>
</div>
<hr>
<div class="rows">
<label for="music_list">播放列表:</label>
<select id="music_list"></select>
<label for="music_name">歌曲:</label>
<select id="music_name"></select>
<div>
<button id="play_music_list">播放选中歌曲</button>
<button id="del_music">删除选中歌曲</button>
<button id="web_play">网页播放</button>
</div>
<div class="play_pannel">
<audio autoplay controls src=""></audio>
</div>
</div>
<hr>
<div class="rows">
<input id="music-url" type="text" value="https://lhttp.qtfm.cn/live/4915/64k.mp3"></input>
<button id="playurl">播放链接</button>
</div>
<footer>
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
</footer>
<dialog id="valid-host">
<form method="dialog">
<p>当前页面的HOST与设置中的HOST不一致请检查是否设置错误</p>
<p>当前HOST: <span id="local-host"></span></p>
<p>设置中的HOST: <span id="setting-host"></span></p>
<div class="btn-list">
<a href="./setting.html" target="_blank">立即修改</a>
<button value="no-warning" type="submit">继续并不再显示</button>
<button value="cancle" type="submit">取消</button>
</div>
</form>
</dialog>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<title>M3U to JSON Converter</title>
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
// VConsole 默认会挂载到 `window.VConsole` 上
var vConsole = new window.VConsole();
</script>
-->
<script>
function handleFileSelect(evt) {
var file = evt.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById('m3u-input').value = e.target.result;
};
reader.readAsText(file);
} else {
alert('无法加载文件');
}
}
function convertToJSON() {
var m3uContent = document.getElementById('m3u-input').value;
var lines = m3uContent.split('\n');
console.log(lines);
var musicsArray = [];
var currentName = '';
lines.forEach(function(line) {
line = line.trim();
if (line.startsWith('#EXTINF:')) {
currentName = line.replace(/.*,/g, '');
} else if (line.startsWith('http') && currentName !== '') {
musicsArray.push({"name": currentName, "type": "radio", "url": line});
currentName = ''; // Reset the name for the next entry
}
});
var output = [{
"name": "m3u电台",
"musics": musicsArray
}];
document.getElementById('json-output').value = JSON.stringify(output, null, 2);
}
</script>
</head>
<body>
<h1>M3U to JSON Converter</h1>
<input type="file" id="file-input" accept=".m3u" onchange="handleFileSelect(event)"/><br>
<textarea id="m3u-input" rows="10" cols="50" placeholder="粘贴m3u内容或上传文件..."></textarea><br>
<button onclick="convertToJSON()">转换</button><br>
<textarea id="json-output" rows="10" cols="50" placeholder="转换后的JSON..."></textarea>
</body>
<footer>
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
</footer>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,270 @@
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
<script src="./setting.js?version=1733563859"></script>
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script>
-->
</head>
<body>
<h2>小爱音箱设置面板
(<a id="version" href="https://xdocs.hanxi.cc/issues/changelog.html">版本未知</a>)
</h2>
<hr>
<div class="rows">
<label for="mi_did">*勾选设备(至少勾选1个):</label>
<div id="mi_did">
</div>
</div>
<br>
<div id="setting">
<div class="rows">
<label for="account">*小米账号:</label>
<input id="account" type="text" placeholder="填写小米登录账号" />
<label for="password">*小米密码:</label>
<input id="password" type="password" placeholder="填写小米登录密码" />
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
<input id="hostname" type="text" />
</div>
<hr>
<div class="rows">
<label for="verbose">是否开启调试日志:</label>
<select id="verbose">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<label for="port">监听端口(修改后需要重启):</label>
<input id="port" type="number" value="8090" />
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
<input id="public_port" type="number" value="0" />
<label for="group_list">设备分组配置:<a href="https://github.com/hanxi/xiaomusic/issues/65#issuecomment-2215736529" target="_blank">文档</a></label>
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2" />
<label for="music_path">音乐目录:</label>
<input id="music_path" type="text" value="music" />
<label for="download_path">音乐下载目录(必须是music的子目录):</label>
<input id="download_path" type="text" value='music/download' />
<label for="conf_path">配置文件目录:</label>
<input id="conf_path" type="text" />
<label for="cache_dir">缓存文件目录:</label>
<input id="cache_dir" type="text" />
<label for="temp_path">临时文件目录:</label>
<input id="temp_path" type="text" value="music/tmp" />
<label for="ffmpeg_location">ffmpeg路径:</label>
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin" />
<label for="log_file">日志路径:</label>
<input id="log_file" type="text" value="xiaomusic.log.txt" />
<label for="active_cmd">允许唤醒的命令:</label>
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop" />
<label for="exclude_dirs">忽略目录(逗号分割):</label>
<input id="exclude_dirs" type="text" value="@eaDir,tmp" />
<label for="ignore_tag_dirs">不扫描标签信息目录(逗号分割):</label>
<input id="ignore_tag_dirs" type="text" value="" />
<label for="music_path_depth">目录深度:</label>
<input id="music_path_depth" type="number" value="10" />
<label for="search_prefix">XIAOMUSIC_SEARCH(歌曲下载方式):</label>
<select id="search_prefix">
<option value="bilisearch:">bilisearch:</option>
<option value="ytsearch:">ytsearch:</option>
</select>
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080" />
<label for="remove_id3tag">去除MP3 ID3v2和填充:</label>
<select id="remove_id3tag">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="convert_to_mp3">转换为MP3:</label>
<select id="convert_to_mp3">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="miio_tts_command">MiIO tts 指令(解决部分型号没有提示音的问题):</label>
<input id="miio_tts_command" type="text" placeholder="如5 或者 5-3" />
<label for="disable_httpauth">关闭控制台密码验证:</label>
<select id="disable_httpauth">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<label for="httpauth_username">控制台账户:</label>
<input id="httpauth_username" type="text" value="" />
<label for="httpauth_password">控制台密码:</label>
<input id="httpauth_password" type="password" value="" />
<label for="disable_download">关闭下载功能:</label>
<select id="disable_download">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="use_music_audio_id">触屏版显示歌曲ID:</label>
<input id="use_music_audio_id" type="text" value="1582971365183456177" />
<label for="use_music_id">触屏版显示歌曲分段ID:</label>
<input id="use_music_id" type="text" value="355454500" />
<label for="fuzzy_match_cutoff">模糊匹配阈值(0.1~0.9):</label>
<input id="fuzzy_match_cutoff" type="number" value="0.6" />
<label for="enable_fuzzy_match">开启模糊搜索:</label>
<select id="enable_fuzzy_match">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<label for="use_music_api">型号兼容模式:</label>
<select id="use_music_api">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="continue_play">启用继续播放(可能导致兼容性问题):</label>
<select id="continue_play">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="pull_ask_sec">获取对话记录间隔(秒):</label>
<input id="pull_ask_sec" type="number" value="1" />
<label for="delay_sec">下一首歌延迟播放秒数:</label>
<input id="delay_sec" type="number" value="3" />
<label for="stop_tts_msg">停止提示音:</label>
<input id="stop_tts_msg" type="text" value="收到,再见" />
<label for="play_type_one_tts_msg">单曲循环提示音:</label>
<input id="play_type_one_tts_msg" type="text" value="已经设置为单曲循环" />
<label for="play_type_all_tts_msg">全部循环提示音:</label>
<input id="play_type_all_tts_msg" type="text" value="已经设置为全部循环" />
<label for="play_type_rnd_tts_msg">随机播放提示音:</label>
<input id="play_type_rnd_tts_msg" type="text" value="已经设置为随机播放" />
<label for="play_type_sin_tts_msg">单曲播放提示音:</label>
<input id="play_type_sin_tts_msg" type="text" value="已经设置为单曲播放" />
<label for="play_type_seq_tts_msg">顺序播放提示音:</label>
<input id="play_type_seq_tts_msg" type="text" value="已经设置为顺序播放" />
<label for="keywords_playlocal">播放本地歌曲口令:</label>
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲" />
<label for="keywords_play">播放歌曲口令:</label>
<input id="keywords_play" type="text" value="播放歌曲,放歌曲" />
<label for="keywords_playlist">播放列表口令:</label>
<input id="keywords_playlist" type="text" value="播放列表,播放歌单" />
<label for="keywords_stop">停止口令:</label>
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放" />
<label for="keywords_search_playlocal">本地搜索播放口令(会产生临时播放列表):</label>
<input id="keywords_search_playlocal" type="text" value="本地搜索播放" />
<label for="keywords_search_play">搜索播放口令(会产生临时播放列表):</label>
<input id="keywords_search_play" type="text" value="搜索播放" />
<label for="enable_yt_dlp_cookies">启用yt-dlp-cookies(需要先上传yt-dlp-cookies.txt文件):</label>
<select id="enable_yt_dlp_cookies">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="enable_save_tag">启用ID3标签写入文件:</label>
<select id="enable_save_tag">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="get_ask_by_mina">特殊型号获取对话记录:</label>
<select id="get_ask_by_mina">
<option value="true">true</option>
<option value="false" selected>false</option>
</select>
<label for="recently_added_playlist_len">最近新增的歌曲数量:</label>
<input id="recently_added_playlist_len" type="number" value="50" />
<label for="music_list_url">歌单地址:</label>
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json" />
<label for="music_list_json">歌单内容:<a href="https://github.com/hanxi/xiaomusic/issues/78" target="_blank">格式文档</a></label>
<textarea id="music_list_json" type="text"></textarea>
<label for="crontab_json">定时任务:<a href="https://github.com/hanxi/xiaomusic/issues/182" target="_blank">格式文档</a></label>
<textarea id="crontab_json" type="text"></textarea>
</div>
</div>
<hr>
<div class="rows">
<label for="yt_dlp_cookies_file">上传yt_dlp_cookies.txt文件:<a href="https://github.com/hanxi/xiaomusic/issues/210" target="_blank">文档</a></label>
<input id="yt_dlp_cookies_file" name="file" type="file">
<button id="upload_yt_dlp_cookie">上传</button>
</div>
<hr>
<button onclick="location.href='/static/default/index.html';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button class="save-button">保存</button>
<hr>
<button id="refresh_music_tag">刷新tag</button>
<button id="clear_cache">清空缓存</button>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<hr>
<button onclick="location.href='/docs';">查看接口文档</button>
<a class="button" href="./m3u.html" target="_blank">m3u文件转换</a>
<a class="button" href="./downloadtool.html" target="_blank">歌曲下载工具</a>
<hr>
<a class="button" href="./debug.html" target="_blank">调试工具</a>
<a class="button" href="https://afdian.com/a/imhanxi" target="_blank">💰 爱发电</a>
<a class="button" href="https://github.com/hanxi/xiaomusic" target="_blank">点个 Star ⭐</a>
<div class="rows">
<img class="qrcode" src="./qrcode.png" alt="请涵曦喝奶茶🧋">
</div>
<footer>
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,203 @@
$(function(){
// 拉取版本
$.get("/getversion", function(data, status) {
console.log(data, status, data["version"]);
$("#version").text(`${data.version}`);
});
// 遍历所有的select元素默认选中只有1个选项的
const autoSelectOne = () => {
$('select').each(function() {
// 如果select元素仅有一个option子元素
if ($(this).children('option').length === 1) {
// 选中这个option
$(this).find('option').prop('selected', true);
}
});
};
function updateCheckbox(selector, mi_did, device_list,accountPassValid) {
// 清除现有的内容
$(selector).empty();
// 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项
var selected_dids = mi_did.split(',');
//如果device_list为空则可能是未设置小米账号密码或者已设置密码但是没有过小米验证此处需要提示用户
if (device_list.length == 0) {
const loginTips = accountPassValid ? `<div class="login-tips">未发现可用的小爱设备,请检查账号密码是否输错,并关闭加速代理或在<a href="https://www.mi.com">小米官网</a>登陆过人脸或滑块验证。如仍未解决。请根据<a href="https://github.com/hanxi/xiaomusic/issues/99">FAQ</a>的内容解决问题。</div>` : `<div class="login-tips">未发现可用的小爱设备,请先在下面的输入框中设置小米的<b>账号、密码</b></div>`;
$(selector).append(loginTips);
return;
}
$.each(device_list, function(index, device) {
var did = device.miotDID;
var hardware = device.hardware;
var name = device.name;
// 创建复选框元素
var checkbox = $('<input>', {
type: 'checkbox',
id: did,
value: `${did}`,
class: 'custom-checkbox', // 添加样式类
// 如果mi_did中包含了该did则默认选中
checked: selected_dids.indexOf(did) !== -1
});
// 创建标签元素
var label = $('<label>', {
for: did,
class: 'checkbox-label', // 添加样式类
text: `${hardware} ${did}${name}` // 设定标签内容
});
// 将复选框和标签添加到目标选择器元素中
$(selector).append(checkbox).append(label);
});
}
function getSelectedDids(containerSelector) {
var selectedDids = [];
// 仅选择给定容器中选中的复选框
$(containerSelector + ' .custom-checkbox:checked').each(function() {
var did = this.value;
selectedDids.push(did);
});
return selectedDids.join(',');
}
// 拉取现有配置
$.get("/getsetting?need_device_list=true", function(data, status) {
console.log(data, status);
const accountPassValid = data.account && data.password;
updateCheckbox("#mi_did", data.mi_did, data.device_list, accountPassValid);
// 初始化显示
for (const key in data) {
const $element = $("#" + key);
if ($element.length) {
if (data[key] === true) {
$element.val('true');
} else if (data[key] === false) {
$element.val('false');
} else {
$element.val(data[key]);
}
}
}
autoSelectOne();
});
$(".save-button").on("click", () => {
var setting = $('#setting');
var inputs = setting.find('input, select, textarea');
var data = {};
inputs.each(function() {
var id = this.id;
if (id) {
data[id] = $(this).val();
}
});
var did_list = getSelectedDids("#mi_did");
data["mi_did"] = did_list;
console.log(data)
$.ajax({
type: "POST",
url: "/savesetting",
contentType: "application/json",
data: JSON.stringify(data),
success: (msg) => {
alert(msg);
location.reload();
},
error: (msg) => {
alert(msg);
}
});
});
$("#get_music_list").on("click", () => {
var music_list_url = $("#music_list_url").val();
console.log("music_list_url", music_list_url);
var data = {
url: music_list_url,
};
$.ajax({
type: "POST",
url: "/downloadjson",
contentType: "application/json",
data: JSON.stringify(data),
success: (res) => {
if (res.ret == "OK") {
$("#music_list_json").val(res.content);
} else {
console.log(res);
alert(res.ret);
}
},
error: (res) => {
console.log(res);
alert(res);
}
});
});
$("#refresh_music_tag").on("click", () => {
$.ajax({
type: "POST",
url: "/refreshmusictag",
contentType: "application/json",
success: (res) => {
console.log(res);
alert(res.ret);
},
error: (res) => {
console.log(res);
alert(res);
}
});
});
$("#upload_yt_dlp_cookie").on("click", () => {
var fileInput = document.getElementById('yt_dlp_cookies_file');
var file = fileInput.files[0]; // 获取文件对象
if (file) {
var formData = new FormData();
formData.append("file", file);
$.ajax({
url: "/uploadytdlpcookie",
type: "POST",
data: formData,
processData: false,
contentType: false,
success: function(res) {
console.log(res);
alert("上传成功");
},
error: function(jqXHR, textStatus, errorThrown) {
console.log(res);
alert("上传失败");
}
});
} else {
alert("请选择一个文件");
}
});
$("#clear_cache").on("click", () => {
localStorage.clear();
});
$("#hostname").on("change", function(){
const hostname = $(this).val();
// 检查是否包含端口号1到5位数字
if (hostname.match(/:\d{1,5}$/)) {
alert("hostname禁止带端口号");
// 移除端口号
$(this).val(hostname.replace(/:\d{1,5}$/,""));
}
});
});

View File

@@ -21,7 +21,6 @@ button:active, .button:active {
}
label {
margin-left: 10px;
width: 300px;
}
input,select {
margin-left: 5%;
@@ -31,6 +30,7 @@ input,select {
width: 90%;
max-width: 400px;
height: 40px;
box-sizing: border-box;
}
.rows {
@@ -100,3 +100,67 @@ footer {
animation: blink 1s infinite;
}
.login-tips {
color: red;
font-size: 12px;
margin-left: 10px;
}
.login-tips a {
color: rgb(9, 105, 218);
text-decoration: underline;
}
#valid-host {
padding: 20px;
border: none;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
#valid-host::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
#valid-host form input {
width: fit-content;
margin: 0;
height: fit-content;
}
#valid-host p {
word-break: break-all;
}
#valid-host p span {
color: red;
}
#valid-host a, #valid-host a:visited {
color: rgb(9, 105, 218);;
text-decoration: underline;
display: flex;
align-items: center;
}
#valid-host a:hover {
color: rgb(9, 95, 198);
}
#valid-host .btn-list {
display: flex;
justify-content: center;
margin-top: 20px;
}
#valid-host .btn-list button {
width: fit-content;
min-width: 60px;
height: 40px;
border: none;
color: white;
text-align: center;
text-decoration: none;
display: inline-block;
border-radius: 10px;
background-color: #008CBA;
}
#valid-host .btn-list button:hover {
font-weight:bold;
background-color: #007CBA;
}

View File

@@ -2,36 +2,41 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="manifest" href="/static/manifest.json">
<link rel="icon" href="/static/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<link rel="manifest" href="/static/manifest.json" />
<link rel="icon" href="/static/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>小爱音箱操控面板</title>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments)};
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-Z09NC1K7ZW");
</script>
<script type="text/javascript">
if (navigator.serviceWorker != null) {
navigator.serviceWorker.register('/static/sw.js')
.then(function(registration) {
console.log('Registered events at scope: ', registration.scope);
});
}
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
<script type="text/javascript">
if (navigator.serviceWorker != null) {
navigator.serviceWorker
.register("/static/sw.js")
.then(function (registration) {
console.log("Registered events at scope: ", registration.scope);
});
}
</script>
</head>
<body>
<div class="container_wrapper">
<div class="logo">
<img src="/static/xiaoai.png" alt="">
<img src="/static/xiaoai.png" alt="" />
</div>
<div class="desc">
<h1>谁家灯火夜通明</h1>
@@ -40,32 +45,229 @@
</div>
<div class="options">
<!-- 选择主题 /static/[theme] -->
<a href="/static/default/index.html" class="href">默认主题</a>
<a href="/static/pure/index.html" class="href">Pure主题</a>
<a href="/static/xplayer/index.html" class="href">XMusicPlayer</a>
<a href="https://afdian.com/a/imhanxi" target="_blank">爱发电</a>
<a href="https://github.com/hanxi/xiaomusic" target="_blank">GitHub</a>
<div class="options_list">
<a href="/static/default/index.html">默认主题</a>
</div>
<div class="options_list">
<a href="/static/default_past/index.html">怀旧主题</a>
</div>
<div class="options_list">
<a href="/static/pure/index.html">Pure主题</a>
</div>
<div class="options_list">
<a href="/static/xplayer/index.html">XMusicPlayer</a>
</div>
<div class="options_list weapp">
<a href="https://github.com/F-loat/xiaoplayer" target="_blank">微信小程序</a>
<iframe width="240px" height="240px" src="/static/weapp/qrcode.html"></iframe>
</div>
<div class="options_list">
<a href="https://afdian.com/a/imhanxi" target="_blank">爱发电</a>
</div>
<div class="options_list">
<a href="https://github.com/hanxi/xiaomusic" target="_blank">GitHub</a>
</div>
<div class="options_list">
<a href="https://github.com/hanxi/xiaomusic/issues/211" target="_blank">帮助</a>
</div>
</div>
</div>
<footer>
power by <a href="https://github.com/hanxi/xiaomusic">XiaoMusic</a>
Powered by <a href="https://xdocs.hanxi.cc">XiaoMusic</a>
</footer>
<style>
@font-face{ font-family: "得意黑 斜体"; font-weight: 400; src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/gJk2ny0v51vn.woff2") format("woff2"), url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/e2C1wSBHH86h.woff") format("woff"); font-display: swap;} @font-face{ font-family: "阿里妈妈数黑体 Bold"; font-weight: 700; src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/4DWYdFK3dz5J.woff2") format("woff2"), url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/V7EBEKlNSdxC.woff") format("woff"); font-display: swap;} body{ background-color: rgb(47, 44, 67); height: 100%; overflow: hidden;}
.container_wrapper{display: flex; justify-content: space-around; align-items: center; flex-wrap: wrap; height: 90vh; cursor: default;}
h1{ font-weight: bold; color: #a2a9af; max-width: 600px; font-family: '得意黑 斜体', 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-size: 2.5em; border-bottom: 1px solid #a2a9af;}
.container_wrapper .logo img{ width: 140px; height: auto; filter: drop-shadow(10px 10px 10px rgba(0, 0, 0, 0.5));}
.desc{ text-align: center; color: #fff; margin: auto 30px; backdrop-filter: blur(5px);}
.desc p{ font-size: 1.2em; margin: 0; padding: 0; font-family: '阿里妈妈数黑体 Bold'; font-weight: 800;}
p.call{ letter-spacing: 0.4em; font-size: 2.2em; line-height: 1.5; font-style: normal;}
p.answer{ letter-spacing: 0.23em; line-height: 1.5; font-style: normal; color: #a2a9af; margin-top: 10px;}
.desc p::before, .desc p::after{ font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-size: 1.5em; color: #4c5870;}
.desc p::before{ content: "“";} .desc p::after{ content: "”";}
.options{ display: flex; flex-direction: column;}
.options a{ color: #a2a9af; text-decoration: none; font-size: 1.1em; position: relative; display: inline; margin: 10px auto;}
.options a::before{ content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #ebedec; transform-origin: bottom right; transform: scaleX(0); transition: transform 0.3s ease;}
.options a:hover::before{ transform-origin: bottom left; transform: scaleX(1);} .options a:hover{ color:#ebedec;}
footer{ display: flex; justify-content: center; color: #4c5870;} footer a{ color:inherit; text-decoration: none; margin: auto 10px;}
@font-face {
font-family: "得意黑 斜体";
font-weight: 400;
src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/gJk2ny0v51vn.woff2") format("woff2"),
url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/e2C1wSBHH86h.woff") format("woff");
font-display: swap;
}
@font-face {
font-family: "阿里妈妈数黑体 Bold";
font-weight: 700;
src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/4DWYdFK3dz5J.woff2") format("woff2"),
url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/V7EBEKlNSdxC.woff") format("woff");
font-display: swap;
}
body {
background-color: rgb(47, 44, 67);
height: 100%;
overflow: hidden;
}
.container_wrapper {
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
height: 90vh;
cursor: default;
}
h1 {
font-weight: bold;
color: #a2a9af;
max-width: 600px;
font-family: "得意黑 斜体", "Lucida Sans", "Lucida Sans Regular",
"Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 2.5em;
border-bottom: 1px solid #a2a9af;
}
.container_wrapper .logo img {
width: 140px;
height: auto;
filter: drop-shadow(10px 10px 10px rgba(0, 0, 0, 0.5));
}
.desc {
text-align: center;
color: #fff;
margin: auto 30px;
backdrop-filter: blur(5px);
}
.desc p {
font-size: 1.2em;
margin: 0;
padding: 0;
font-family: "阿里妈妈数黑体 Bold";
font-weight: 800;
}
p.call {
letter-spacing: 0.4em;
font-size: 2.2em;
line-height: 1.5;
font-style: normal;
}
p.answer {
letter-spacing: 0.23em;
line-height: 1.5;
font-style: normal;
color: #a2a9af;
margin-top: 10px;
}
.desc p::before,
.desc p::after {
font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
"Lucida Sans Unicode", Geneva, Verdana, sans-serif;
font-size: 1.5em;
color: #4c5870;
}
.desc p::before {
content: "“";
}
.desc p::after {
content: "”";
}
.options {
display: flex;
flex-direction: column;
user-select: none;
}
.options .options_list {
font-size: 1.1em;
position: relative;
margin: 5px 2px;
border: 1px solid #a2a9af;
padding: 10px 20px;
border-radius: 10px;
color: #a2a9af;
transition: all 0.3s ease-in-out;
width: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.options a {
color: #a2a9af;
text-decoration: none;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.options a::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background-color: #ebedec;
transform-origin: bottom right;
transform: scaleX(0);
transition: transform 0.3s ease;
}
.options a:hover::before {
transform-origin: bottom left;
transform: scaleX(1);
}
.options a:hover {
color: #ebedec;
}
.weapp:hover iframe {
display: block;
}
.weapp iframe {
display: none;
border: none;
position: absolute;
top: -80px;
right: 180px;
}
@media screen and (max-width: 510px) {
.weapp iframe {
position: fixed;
top: 40%;
left: 50%;
transform: translate(-50%, 0);
}
.options{
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
}
.options .options_list {
width: 100px;
}
h1{
margin: 0;
}
p.call {
font-size: 1.5em;
}
}
footer {
display: flex;
justify-content: center;
color: #4c5870;
}
footer a {
color: inherit;
text-decoration: none;
margin: auto 10px;
}
</style>
</body>

View File

@@ -21,6 +21,9 @@
gtag('js', new Date());
gtag('config', 'G-Z09NC1K7ZW');
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
</body>
</html>
</html>

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weapp QRCode</title>
<style>
html,
body {
height: 100%;
margin: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
}
#qrcode {
flex: 1;
max-width: 180px;
border-radius: 12px;
background-color: white;
padding: 8px;
}
</style>
<script src="https://web-9gikcbug35bad3a8-1304825656.tcloudbaseapp.com/sdk/1.4.0/cloud.js"></script>
</head>
<body>
<img id="qrcode" src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/weappcode.jpg" />
<script>
const c1 = new cloud.Cloud({
identityless: true,
resourceAppid: 'wx5931d820da6e8e50',
resourceEnv: 'cards-ahoy-3g50hglqe5f630e4'
})
c1.init().then(() => {
c1.callFunction({
name: 'qrcode',
data: {
host: location.host,
protocol: location.protocol
},
complete: (res) => {
if (res.errMsg === 'cloud.callFunction:ok' && !res.result.errCode) {
const blob = new Blob([res.result.buffer])
const url = URL.createObjectURL(blob)
document.querySelector('#qrcode').setAttribute('src', url)
}
}
})
})
</script>
</body>
</html>

View File

@@ -7,6 +7,20 @@
<title>XMusicPlayer</title>
<script type="module" crossorigin src="/static/xplayer/assets/index-C1eAAj9j.js"></script>
<link rel="stylesheet" crossorigin href="/static/xplayer/assets/index-BBmHnUeL.css">
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-Z09NC1K7ZW");
</script>
<!-- umami -->
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
</head>
<body>
<div id="app"></div>

View File

@@ -11,6 +11,7 @@ import json
import logging
import mimetypes
import os
import platform
import random
import re
import shutil
@@ -27,7 +28,19 @@ import aiohttp
import mutagen
from mutagen.asf import ASF
from mutagen.flac import FLAC
from mutagen.id3 import APIC, ID3, Encoding, TextFrame, TimeStampTextFrame
from mutagen.id3 import (
APIC,
ID3,
TALB,
TCON,
TDRC,
TIT2,
TPE1,
USLT,
Encoding,
TextFrame,
TimeStampTextFrame,
)
from mutagen.mp3 import MP3
from mutagen.mp4 import MP4
from mutagen.oggvorbis import OggVorbis
@@ -323,7 +336,7 @@ async def get_local_music_duration(filename, ffmpeg_location="./ffmpeg/bin"):
m = await loop.run_in_executor(None, mutagen.File, filename)
duration = m.info.length
except Exception as e:
log.error(f"Error getting local music {filename} duration: {e}")
log.warning(f"Error getting local music {filename} duration: {e}")
return duration
@@ -568,6 +581,16 @@ class Metadata:
picture: str = ""
lyrics: str = ""
def __init__(self, info=None):
if info:
self.title = info.get("title", "")
self.artist = info.get("artist", "")
self.album = info.get("album", "")
self.year = info.get("year", "")
self.genre = info.get("genre", "")
self.picture = info.get("picture", "")
self.lyrics = info.get("lyrics", "")
def _get_alltag_value(tags, k):
v = tags.getall(k)
@@ -596,6 +619,15 @@ def _to_utf8(v):
return str(v)
def save_picture_by_base64(picture_base64_data, save_root, file_path):
try:
picture_data = base64.b64decode(picture_base64_data)
except (TypeError, ValueError) as e:
log.exception(f"Error decoding base64 data: {e}")
return None
return _save_picture(picture_data, save_root, file_path)
def _save_picture(picture_data, save_root, file_path):
# 计算文件名的哈希值
file_hash = hashlib.md5(file_path.encode("utf-8")).hexdigest()
@@ -611,14 +643,19 @@ def _save_picture(picture_data, save_root, file_path):
try:
_resize_save_image(picture_data, picture_path)
except Exception as e:
log.exception(f"Error _resize_save_image: {e}")
log.warning(f"Error _resize_save_image: {e}")
return picture_path
def _resize_save_image(image_bytes, save_path, max_size=300):
# 将 bytes 转换为 PIL Image 对象
image = Image.open(io.BytesIO(image_bytes))
image = image.convert("RGB")
image = None
try:
image = Image.open(io.BytesIO(image_bytes))
image = image.convert("RGB")
except Exception as e:
log.warning(f"Error _resize_save_image: {e}")
return
# 获取原始尺寸
original_width, original_height = image.size
@@ -641,8 +678,16 @@ def _resize_save_image(image_bytes, save_path, max_size=300):
def extract_audio_metadata(file_path, save_root):
audio = mutagen.File(file_path)
metadata = Metadata()
audio = None
try:
audio = mutagen.File(file_path)
except Exception as e:
log.warning(f"Error extract_audio_metadata file: {file_path} {e}")
if audio is None:
return asdict(metadata)
tags = audio.tags
if tags is None:
return asdict(metadata)
@@ -722,6 +767,110 @@ def extract_audio_metadata(file_path, save_root):
return asdict(metadata)
def set_music_tag_to_file(file_path, info):
audio = mutagen.File(file_path, easy=True)
if audio is None:
log.error(f"Unable to open file {file_path}")
return "Unable to open file"
if isinstance(audio, MP3):
_set_mp3_tags(audio, info)
elif isinstance(audio, FLAC):
_set_flac_tags(audio, info)
elif isinstance(audio, MP4):
_set_mp4_tags(audio, info)
elif isinstance(audio, OggVorbis):
_set_ogg_tags(audio, info)
elif isinstance(audio, ASF):
_set_asf_tags(audio, info)
elif isinstance(audio, WAVE):
_set_wave_tags(audio, info)
else:
log.error(f"Unsupported file type for {file_path}")
return "Unsupported file type"
try:
audio.save()
log.info(f"Tags saved successfully to {file_path}")
return "OK"
except Exception as e:
log.exception(f"Error saving tags: {e}")
return "Error saving tags"
def _set_mp3_tags(audio, info):
audio["TIT2"] = TIT2(encoding=3, text=info.title)
audio["TPE1"] = TPE1(encoding=3, text=info.artist)
audio["TALB"] = TALB(encoding=3, text=info.album)
audio["TDRC"] = TDRC(encoding=3, text=info.year)
audio["TCON"] = TCON(encoding=3, text=info.genre)
if info.lyrics:
audio["USLT"] = USLT(encoding=3, lang="eng", text=info.lyrics)
if info.picture:
with open(info.picture, "rb") as img_file:
image_data = img_file.read()
audio["APIC"] = APIC(
encoding=3, mime="image/jpeg", type=3, desc="Cover", data=image_data
)
def _set_flac_tags(audio, info):
audio["TITLE"] = info.title
audio["ARTIST"] = info.artist
audio["ALBUM"] = info.album
audio["DATE"] = info.year
audio["GENRE"] = info.genre
if info.lyrics:
audio["LYRICS"] = info.lyrics
if info.picture:
with open(info.picture, "rb") as img_file:
image_data = img_file.read()
audio.add_picture(image_data)
def _set_mp4_tags(audio, info):
audio["\xa9nam"] = info.title
audio["\xa9ART"] = info.artist
audio["\xa9alb"] = info.album
audio["\xa9day"] = info.year
audio["\xa9gen"] = info.genre
if info.picture:
with open(info.picture, "rb") as img_file:
image_data = img_file.read()
audio["covr"] = [image_data]
def _set_ogg_tags(audio, info):
audio["TITLE"] = info.title
audio["ARTIST"] = info.artist
audio["ALBUM"] = info.album
audio["DATE"] = info.year
audio["GENRE"] = info.genre
if info.lyrics:
audio["LYRICS"] = info.lyrics
if info.picture:
with open(info.picture, "rb") as img_file:
image_data = img_file.read()
audio["metadata_block_picture"] = base64.b64encode(image_data).decode()
def _set_asf_tags(audio, info):
audio["Title"] = info.title
audio["Author"] = info.artist
audio["WM/AlbumTitle"] = info.album
audio["WM/Year"] = info.year
audio["WM/Genre"] = info.genre
if info.picture:
with open(info.picture, "rb") as img_file:
image_data = img_file.read()
audio["WM/Picture"] = image_data
def _set_wave_tags(audio, info):
audio["Title"] = info.title
audio["Artist"] = info.artist
# 下载播放列表
async def download_playlist(config, url, dirname):
title = f"{dirname}/%(title)s.%(ext)s"
@@ -731,6 +880,8 @@ async def download_playlist(config, url, dirname):
"-x",
"--audio-format",
"mp3",
"--audio-quality",
"0",
"--paths",
config.download_path,
"-o",
@@ -764,6 +915,8 @@ async def download_one_music(config, url, name=""):
"-x",
"--audio-format",
"mp3",
"--audio-quality",
"0",
"--paths",
config.download_path,
"-o",
@@ -812,6 +965,7 @@ def remove_common_prefix(directory):
log.info(f'Common prefix identified: "{common_prefix}"')
pattern = re.compile(r"^(\d+)\s+\d*(.+?)\.(.*$)")
for filename in files:
if filename == common_prefix:
continue
@@ -819,6 +973,12 @@ def remove_common_prefix(directory):
if filename.startswith(common_prefix):
# 构造新的文件名
new_filename = filename[len(common_prefix) :]
match = pattern.search(new_filename.strip())
if match:
num = match.group(1)
name = match.group(2).replace(".", " ").strip()
suffix = match.group(3)
new_filename = f"{num}.{name}.{suffix}"
# 生成完整的文件路径
old_file_path = os.path.join(directory, filename)
new_file_path = os.path.join(directory, new_filename)
@@ -855,3 +1015,135 @@ def try_add_access_control_param(config, url):
).geturl()
return new_url
# 判断文件在不在排除目录列表
def not_in_dirs(filename, ignore_absolute_dirs):
file_absolute_path = os.path.abspath(filename)
file_dir = os.path.dirname(file_absolute_path)
for ignore_dir in ignore_absolute_dirs:
if file_dir.startswith(ignore_dir):
log.info(f"{file_dir} in {ignore_dir}")
return False # 文件在排除目录中
return True # 文件不在排除目录中
def is_docker():
return os.path.exists("/app/.dockerenv")
async def restart_xiaomusic():
# 重启 xiaomusic 程序
sbp_args = (
"supervisorctl",
"restart",
"xiaomusic",
)
cmd = " ".join(sbp_args)
log.info(f"restart_xiaomusic: {cmd}")
await asyncio.sleep(2)
proc = await asyncio.create_subprocess_exec(*sbp_args)
exit_code = await proc.wait() # 等待子进程完成
log.info(f"restart_xiaomusic completed with exit code {exit_code}")
return exit_code
async def update_version(version: str, lite: bool = True):
if not is_docker():
ret = "xiaomusic 更新只能在 docker 中进行"
log.info(ret)
return ret
lite_tag = ""
if lite:
lite_tag = "-lite"
arch = get_os_architecture()
if "unknown" in arch:
log.warning(f"update_version failed: {arch}")
return arch
# https://github.com/hanxi/xiaomusic/releases/download/main/app-amd64-lite.tar.gz
url = f"https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/releases/download/{version}/app-{arch}{lite_tag}.tar.gz"
target_directory = "/app"
return await download_and_extract(url, target_directory)
def get_os_architecture():
"""
获取操作系统架构类型amd64、arm64、arm-v7。
Returns:
str: 架构类型
"""
arch = platform.machine().lower()
if arch in ("x86_64", "amd64"):
return "amd64"
elif arch in ("aarch64", "arm64"):
return "arm64"
elif "arm" in arch or "armv7" in arch:
return "arm-v7"
else:
return f"unknown architecture: {arch}"
async def download_and_extract(url: str, target_directory: str):
ret = "OK"
# 创建目标目录
os.makedirs(target_directory, exist_ok=True)
# 使用 aiohttp 异步下载文件
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
file_name = os.path.join(target_directory, url.split("/")[-1])
file_name = os.path.normpath(file_name)
if not file_name.startswith(target_directory):
log.warning(f"Invalid file path: {file_name}")
return
with open(file_name, "wb") as f:
# 以块的方式下载文件,防止内存占用过大
async for chunk in response.content.iter_any():
f.write(chunk)
log.info(f"文件下载完成: {file_name}")
# 解压下载的文件
if file_name.endswith(".tar.gz"):
await extract_tar_gz(file_name, target_directory)
else:
ret = f"下载失败, 包有问题: {file_name}"
log.warning(ret)
else:
ret = f"下载失败, 状态码: {response.status}"
log.warning(ret)
return ret
async def extract_tar_gz(file_name: str, target_directory: str):
# 使用 asyncio.create_subprocess_exec 执行 tar 解压命令
command = ["tar", "-xzvf", file_name, "-C", target_directory]
# 启动子进程执行解压命令
await asyncio.create_subprocess_exec(*command)
# 不等待子进程完成
log.info(f"extract_tar_gz ing {file_name}")
def chmodfile(file_path: str):
try:
os.chmod(file_path, 0o775)
except Exception as e:
log.info(f"chmodfile failed: {e}")
def chmoddir(dir_path: str):
# 获取指定目录下的所有文件和子目录
for item in os.listdir(dir_path):
item_path = os.path.join(dir_path, item)
# 确保是文件,且不是目录
if os.path.isfile(item_path):
try:
os.chmod(item_path, 0o775)
log.info(f"Changed permissions of file: {item_path}")
except Exception as e:
log.info(f"chmoddir failed: {e}")

View File

@@ -28,6 +28,7 @@ from xiaomusic.const import (
COOKIE_TEMPLATE,
GET_ASK_BY_MINA,
LATEST_ASK_API,
NEED_USE_PLAY_MUSIC_API,
PLAY_TYPE_ALL,
PLAY_TYPE_ONE,
PLAY_TYPE_RND,
@@ -40,6 +41,7 @@ from xiaomusic.plugin import PluginManager
from xiaomusic.utils import (
Metadata,
chinese_to_number,
chmodfile,
custom_sort_key,
deepcopy_data_no_sensitive_info,
extract_audio_metadata,
@@ -48,8 +50,11 @@ from xiaomusic.utils import (
get_local_music_duration,
get_web_music_duration,
list2str,
not_in_dirs,
parse_cookie_string,
parse_str_to_dict,
save_picture_by_base64,
set_music_tag_to_file,
traverse_music_directory,
try_add_access_control_param,
)
@@ -71,6 +76,7 @@ class XiaoMusic:
self.all_music = {}
self._all_radio = {} # 电台列表
self.music_list = {} # 播放列表 key 为目录名, value 为 play_list
self.default_music_list_names = [] # 非自定义个歌单
self.devices = {} # key 为 did
self.running_task = []
self.all_music_tags = {} # 歌曲额外信息
@@ -100,7 +106,7 @@ class XiaoMusic:
self.update_devices()
# 启动统计
self.analytics = Analytics(self.log)
self.analytics = Analytics(self.log, self.config)
debug_config = deepcopy_data_no_sensitive_info(self.config)
self.log.info(f"Startup OK. {debug_config}")
@@ -156,7 +162,7 @@ class XiaoMusic:
log_file = self.config.log_file
log_path = os.path.dirname(log_file)
if not os.path.exists(log_path):
if log_path and not os.path.exists(log_path):
os.makedirs(log_path)
if os.path.exists(log_file):
os.remove(log_file)
@@ -186,7 +192,7 @@ class XiaoMusic:
self.last_timestamp[did] = int(time.time() * 1000)
hardware = self.get_hardward(device_id)
if hardware in GET_ASK_BY_MINA or self.config.get_ask_by_mina:
if (hardware in GET_ASK_BY_MINA) or self.config.get_ask_by_mina:
tasks.append(self.get_latest_ask_by_mina(device_id))
else:
tasks.append(
@@ -216,16 +222,19 @@ class XiaoMusic:
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)
try:
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)
except Exception as e:
self.log.warning(f"可能登录失败. {e}")
async def try_update_device_id(self):
try:
@@ -245,11 +254,12 @@ class XiaoMusic:
device.device_id = device_id
device.hardware = hardware
device.name = name
device.play_type = PLAY_TYPE_RND
devices[did] = device
self.config.devices = devices
self.log.info(f"选中的设备: {devices}")
except Exception as e:
self.log.exception(f"Execption {e}")
self.log.warning(f"可能登录失败. {e}")
def get_cookie(self):
if self.config.cookie:
@@ -257,7 +267,7 @@ class XiaoMusic:
return cookie_jar
if not os.path.exists(self.mi_token_home):
self.log.error(f"{self.mi_token_home} file not exist")
self.log.warning(f"{self.mi_token_home} file not exist")
return None
with open(self.mi_token_home, encoding="utf-8") as f:
@@ -314,19 +324,31 @@ class XiaoMusic:
)
# self.log.debug(f"url:{url} device_id:{device_id} hardware:{hardware}")
r = await session.get(url, timeout=timeout, cookies=cookies)
# 检查响应状态码
if r.status != 200:
self.log.warning(f"Request failed with status {r.status}")
continue
except asyncio.CancelledError:
self.log.warning("Task was cancelled.")
return None
except Exception as e:
self.log.exception(f"Execption {e}")
self.log.warning(f"Execption {e}")
continue
try:
data = await r.json()
except Exception as e:
self.log.exception(f"Execption {e}")
self.log.warning(f"Execption {e}")
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(device_id, data)
self.log.warning("get_latest_ask_from_xiaoai. All retries failed.")
async def get_latest_ask_by_mina(self, device_id):
try:
@@ -357,7 +379,7 @@ class XiaoMusic:
}
self._check_last_query(last_record)
except Exception as e:
self.log.exception(f"get_latest_ask_by_mina {e}")
self.log.warning(f"get_latest_ask_by_mina {e}")
return
def _get_last_query(self, device_id, data):
@@ -463,6 +485,29 @@ class XiaoMusic:
)
return tags
# 修改标签信息
def set_music_tag(self, name, info):
if self._tag_generation_task:
self.log.info("tag 更新中,请等待")
return "Tag generation task running"
tags = copy.copy(self.all_music_tags.get(name, asdict(Metadata())))
tags["title"] = info.title
tags["artist"] = info.artist
tags["album"] = info.album
tags["year"] = info.year
tags["genre"] = info.genre
tags["lyrics"] = info.lyrics
file_path = self.all_music[name]
if info.picture:
tags["picture"] = save_picture_by_base64(
info.picture, self.config.picture_cache_path, file_path
)
if self.config.enable_save_tag and (not self.is_web_music(name)):
set_music_tag_to_file(file_path, Metadata(tags))
self.all_music_tags[name] = tags
self.try_save_tag_cache()
return "OK"
def get_music_url(self, name):
if self.is_web_music(name):
url = self.all_music[name]
@@ -551,6 +596,9 @@ class XiaoMusic:
all_music_tags = self.try_load_from_tag_cache()
all_music_tags.update(self.all_music_tags) # 保证最新
ignore_tag_absolute_dirs = self.config.get_ignore_tag_dirs()
self.log.info(f"ignore_tag_absolute_dirs: {ignore_tag_absolute_dirs}")
for name, file_or_url in only_items.items():
start = time.perf_counter()
if name not in all_music_tags:
@@ -558,7 +606,9 @@ class XiaoMusic:
if self.is_web_music(name):
# TODO: 网络歌曲获取歌曲额外信息
pass
elif os.path.exists(file_or_url):
elif os.path.exists(file_or_url) and not_in_dirs(
file_or_url, ignore_tag_absolute_dirs
):
all_music_tags[name] = extract_audio_metadata(
file_or_url, self.config.picture_cache_path
)
@@ -618,13 +668,15 @@ class XiaoMusic:
"全部": [], # 包含所有歌曲和所有电台
"下载": [], # 下载目录下的
"其他": [], # 主目录下的
"最近新增": [], # 按文件时间排序
}
)
# 全部,所有,自定义歌单(收藏)
self.music_list["全部"] = list(self.all_music.keys())
self.music_list["所有歌曲"] = [
name for name in self.all_music.keys() if name not in self._all_radio
]
# 最近新增(不包含网络歌单)
self.music_list["最近新增"] = sorted(
self.all_music.keys(),
key=lambda x: os.path.getmtime(self.all_music[x]),
reverse=True,
)[: self.config.recently_added_playlist_len]
# 网络歌单
try:
@@ -633,6 +685,12 @@ class XiaoMusic:
except Exception as e:
self.log.exception(f"Execption {e}")
# 全部,所有,自定义歌单(收藏)
self.music_list["全部"] = list(self.all_music.keys())
self.music_list["所有歌曲"] = [
name for name in self.all_music.keys() if name not in self._all_radio
]
# 文件夹歌单
for dir_name, musics in all_music_by_dir.items():
self.music_list[dir_name] = list(musics.keys())
@@ -642,6 +700,9 @@ class XiaoMusic:
for _, play_list in self.music_list.items():
play_list.sort(key=custom_sort_key)
# 非自定义个歌单
self.default_music_list_names = list(self.music_list.keys())
# 刷新自定义歌单
self.refresh_custom_play_list()
@@ -660,6 +721,11 @@ class XiaoMusic:
def refresh_custom_play_list(self):
try:
# 删除旧的自定义个歌单
for k in list(self.music_list.keys()):
if k not in self.default_music_list_names:
del self.music_list[k]
# 合并新的自定义个歌单
custom_play_list = self.get_custom_play_list()
for k, v in custom_play_list.items():
self.music_list[k] = list(v)
@@ -950,14 +1016,17 @@ class XiaoMusic:
parts = arg1.split("|")
list_name = parts[0]
music_name = ""
if len(parts) > 1:
music_name = parts[1]
return await self.do_play_music_list(did, list_name, music_name)
async def do_play_music_list(self, did, list_name, music_name=""):
list_name = self._find_real_music_list_name(list_name)
if list_name not in self.music_list:
await self.do_tts(did, f"播放列表{list_name}不存在")
return
music_name = ""
if len(parts) > 1:
music_name = parts[1]
await self.devices[did].play_music_list(list_name, music_name)
# 播放一个播放列表里第几个
@@ -989,14 +1058,42 @@ class XiaoMusic:
parts = arg1.split("|")
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
if name == "":
if not name:
name = search_key
return await self.devices[did].play(name, search_key)
# 语音播放会根据歌曲匹配更新当前播放列表
return await self.do_play(
did, name, search_key, exact=True, update_cur_list=True
)
# 搜索播放:会产生临时播放列表
async def search_play(self, did="", arg1="", **kwargs):
parts = arg1.split("|")
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
if not name:
name = search_key
# 语音搜索播放会更新当前播放列表为临时播放列表
return await self.do_play(
did, name, search_key, exact=False, update_cur_list=False
)
# 后台搜索播放
async def do_play(
self, did, name, search_key="", exact=False, update_cur_list=False
):
return await self.devices[did].play(name, search_key, exact, update_cur_list)
# 本地播放
async def playlocal(self, did="", arg1="", **kwargs):
return await self.devices[did].playlocal(arg1)
return await self.devices[did].playlocal(arg1, update_cur_list=True)
# 本地搜索播放
async def search_playlocal(self, did="", arg1="", **kwargs):
return await self.devices[did].playlocal(
arg1, exact=False, update_cur_list=False
)
async def play_next(self, did="", **kwargs):
return await self.devices[did].play_next()
@@ -1016,18 +1113,22 @@ class XiaoMusic:
# 添加歌曲到收藏列表
async def add_to_favorites(self, did="", arg1="", **kwargs):
name = arg1 if arg1 else self.playingmusic(did)
self.log.info(f"add_to_favorites {name}")
if not name:
self.log.warning("当前没有在播放歌曲,添加歌曲到收藏列表失败")
return
self.play_list_add_music("收藏", name)
self.play_list_add_music("收藏", [name])
# 从收藏列表中移除
async def del_from_favorites(self, did="", arg1="", **kwargs):
name = arg1 if arg1 else self.playingmusic(did)
self.log.info(f"del_from_favorites {name}")
if not name:
self.log.warning("当前没有在播放歌曲,从收藏列表中移除失败")
return
self.play_list_del_music("收藏", name)
self.play_list_del_music("收藏", [name])
# 更新每个设备的歌单
def update_all_playlist(self):
@@ -1067,11 +1168,57 @@ class XiaoMusic:
self.save_custom_play_list()
return True
# 修改歌单名字
def play_list_update_name(self, oldname, newname):
custom_play_list = self.get_custom_play_list()
if oldname not in custom_play_list:
self.log.info(f"旧歌单名字不存在 {oldname}")
return False
if newname in custom_play_list:
self.log.info(f"新歌单名字已存在 {newname}")
return False
play_list = custom_play_list[oldname]
custom_play_list.pop(oldname)
custom_play_list[newname] = play_list
self.save_custom_play_list()
return True
# 获取所有自定义歌单
def get_play_list_names(self):
custom_play_list = self.get_custom_play_list()
return list(custom_play_list.keys())
# 获取歌单中所有歌曲
def play_list_musics(self, name):
custom_play_list = self.get_custom_play_list()
if name not in custom_play_list:
return "歌单不存在", []
play_list = custom_play_list[name]
return "OK", play_list
# 歌单更新歌曲
def play_list_update_music(self, name, music_list):
custom_play_list = self.get_custom_play_list()
if name not in custom_play_list:
# 歌单不存在则新建
if not self.play_list_add(name):
return False
play_list = []
for music_name in music_list:
if (music_name in self.all_music) and (music_name not in play_list):
play_list.append(music_name)
# 直接覆盖
custom_play_list[name] = play_list
self.save_custom_play_list()
return True
# 歌单新增歌曲
def play_list_add_music(self, name, music_list):
custom_play_list = self.get_custom_play_list()
if name not in custom_play_list:
return False
# 歌单不存在则新建
if not self.play_list_add(name):
return False
play_list = custom_play_list[name]
for music_name in music_list:
if (music_name in self.all_music) and (music_name not in play_list):
@@ -1087,7 +1234,7 @@ class XiaoMusic:
play_list = custom_play_list[name]
for music_name in music_list:
if music_name in play_list:
play_list.pop(music_name)
play_list.remove(music_name)
self.save_custom_play_list()
return True
@@ -1203,7 +1350,7 @@ class XiaoMusic:
try:
device_list = await self.mina_service.device_list()
except Exception as e:
self.log.exception(f"Execption {e}")
self.log.warning(f"Execption {e}")
return device_list
async def debug_play_by_music_url(self, arg1=None):
@@ -1250,6 +1397,7 @@ class XiaoMusicDevice:
self._start_time = 0
self._duration = 0
self._paused_time = 0
self._play_failed_cnt = 0
self._play_list = []
@@ -1260,7 +1408,11 @@ class XiaoMusicDevice:
@property
def did(self):
return self.xiaomusic.device_id_did[self.device_id]
return self.device.did
@property
def hardware(self):
return self.device.hardware
def get_cur_music(self):
return self.device.cur_music
@@ -1305,11 +1457,16 @@ class XiaoMusicDevice:
)
# 播放歌曲
async def play(self, name="", search_key=""):
async def play(self, name="", search_key="", exact=True, update_cur_list=False):
self._last_cmd = "play"
return await self._play(name=name, search_key=search_key, update_cur=True)
return await self._play(
name=name,
search_key=search_key,
exact=exact,
update_cur_list=update_cur_list,
)
async def _play(self, name="", search_key="", exact=False, update_cur=False):
async def _play(self, name="", search_key="", exact=True, update_cur_list=False):
if search_key == "" and name == "":
if self.check_play_next():
await self._play_next()
@@ -1324,15 +1481,20 @@ class XiaoMusicDevice:
else:
names = self.xiaomusic.find_real_music_name(name)
if len(names) > 0:
if update_cur and len(names) > 1: # 大于一首歌才更新
self._play_list = names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist()
elif update_cur: # 只有一首歌append
self._play_list = self._play_list + names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist(reorder=False)
if not exact:
if len(names) > 1: # 大于一首歌才更新
self._play_list = names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist()
else: # 只有一首歌append
self._play_list = self._play_list + names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist(reorder=False)
name = names[0]
if update_cur_list:
# 根据当前歌曲匹配歌曲列表
self.device.cur_playlist = self.find_cur_playlist(name)
self.update_playlist()
self.log.debug(
f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}"
)
@@ -1341,8 +1503,6 @@ class XiaoMusicDevice:
await self.do_tts(f"本地不存在歌曲{name}")
return
await self.download(search_key, name)
self.log.info(f"正在下载中 {search_key} {name}")
await self._download_proc.wait()
# 把文件插入到播放列表里
await self.add_download_music(name)
await self._playmusic(name)
@@ -1391,7 +1551,7 @@ class XiaoMusicDevice:
await self._play(name, exact=True)
# 播放本地歌曲
async def playlocal(self, name):
async def playlocal(self, name, exact=True, update_cur_list=False):
self._last_cmd = "playlocal"
if name == "":
if self.check_play_next():
@@ -1403,17 +1563,25 @@ class XiaoMusicDevice:
self.log.info(f"playlocal. name:{name}")
# 本地歌曲不存在时下载
names = self.xiaomusic.find_real_music_name(name)
if exact:
names = self.xiaomusic.find_real_music_name(name, n=1)
else:
names = self.xiaomusic.find_real_music_name(name)
if len(names) > 0:
if len(names) > 1: # 大于一首歌才更新
self._play_list = names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist()
else: # 只有一首歌append
self._play_list = self._play_list + names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist(reorder=False)
if not exact:
if len(names) > 1: # 大于一首歌才更新
self._play_list = names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist()
else: # 只有一首歌append
self._play_list = self._play_list + names
self.device.cur_playlist = "临时搜索列表"
self.update_playlist(reorder=False)
name = names[0]
if update_cur_list:
# 根据当前歌曲匹配歌曲列表
self.device.cur_playlist = self.find_cur_playlist(name)
self.update_playlist()
self.log.debug(
f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}"
)
@@ -1435,14 +1603,21 @@ class XiaoMusicDevice:
self.log.info(f"播放 {url}")
results = await self.group_player_play(url, name)
if all(ele is None for ele in results):
self.log.info(f"播放 {name} 失败")
self.log.info(f"播放 {name} 失败. 失败次数: {self._play_failed_cnt}")
await asyncio.sleep(1)
if self.isplaying() and self._last_cmd != "stop":
if (
self.isplaying()
and self._last_cmd != "stop"
and self._play_failed_cnt < 10
):
self._play_failed_cnt = self._play_failed_cnt + 1
await self._play_next()
return
# 重置播放失败次数
self._play_failed_cnt = 0
self.log.info(f"{name}】已经开始播放了")
await self.xiaomusic.analytics.send_play_event(name, sec)
await self.xiaomusic.analytics.send_play_event(name, sec, self.hardware)
if self.device.play_type == PLAY_TYPE_SIN:
self.log.info(f"{name}】单曲播放时不会设置下一首歌的定时器")
@@ -1482,7 +1657,7 @@ class XiaoMusicDevice:
)
await self.stop_if_xiaoai_is_playing(device_id)
except Exception as e:
self.log.exception(f"Execption {e}")
self.log.warning(f"Execption {e}")
async def get_if_xiaoai_is_playing(self):
playing_info = await self.xiaomusic.mina_service.player_get_status(
@@ -1533,6 +1708,8 @@ class XiaoMusicDevice:
"-x",
"--audio-format",
"mp3",
"--audio-quality",
"0",
"--paths",
self.download_path,
"-o",
@@ -1552,6 +1729,11 @@ class XiaoMusicDevice:
self.log.info(f"download cmd: {cmd}")
self._download_proc = await asyncio.create_subprocess_exec(*sbp_args)
await self.do_tts(f"正在下载歌曲{search_key}")
self.log.info(f"正在下载中 {search_key} {name}")
await self._download_proc.wait()
# 下载完成后,修改文件权限
file_path = os.path.join(self.download_path, f"{name}.mp3")
chmodfile(file_path)
# 继续播放被打断的歌曲
async def check_replay(self):
@@ -1686,7 +1868,9 @@ class XiaoMusicDevice:
self.log.info(
f"play_one_url continue_play device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
)
elif self.config.use_music_api:
elif self.config.use_music_api or (
self.hardware in NEED_USE_PLAY_MUSIC_API
):
ret = await self.xiaomusic.mina_service.play_by_music_url(
device_id, url, audio_id=audio_id
)
@@ -1703,7 +1887,7 @@ class XiaoMusicDevice:
return ret
async def _get_audio_id(self, name):
audio_id = 1582971365183456177
audio_id = self.config.use_music_audio_id or "1582971365183456177"
if not (self.config.use_music_api or self.config.continue_play):
return str(audio_id)
try:
@@ -1770,13 +1954,17 @@ class XiaoMusicDevice:
self.log.exception(f"Execption {e}")
async def get_volume(self):
playing_info = await self.xiaomusic.mina_service.player_get_status(
self.device_id
)
self.log.info(f"get_volume. playing_info:{playing_info}")
volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 0
)
volume = 0
try:
playing_info = await self.xiaomusic.mina_service.player_get_status(
self.device_id
)
self.log.info(f"get_volume. playing_info:{playing_info}")
volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 0
)
except Exception as e:
self.log.warning(f"Execption {e}")
volume = int(volume)
self.log.info("get_volume. volume:%d", volume)
return volume
@@ -1793,7 +1981,7 @@ class XiaoMusicDevice:
self._last_cmd = "play_music_list"
self.device.cur_playlist = list_name
self.update_playlist()
self.log.info(f"开始播放列表{list_name}")
self.log.info(f"开始播放列表{list_name} {music_name}")
await self._play(music_name, exact=True)
async def stop(self, arg1=""):
@@ -1867,3 +2055,27 @@ class XiaoMusicDevice:
for key in list(d):
val = d.pop(key)
val.cancel_all_timer()
# 根据当前歌曲匹配歌曲列表
def find_cur_playlist(self, name):
# 匹配顺序:
# 1. 收藏
# 2. 最近新增
# 3. 排除(全部,所有歌曲,所有电台,临时搜索列表)
# 4. 所有歌曲
# 5. 所有电台
# 6. 全部
if name in self.xiaomusic.music_list.get("收藏", []):
return "收藏"
if name in self.xiaomusic.music_list.get("最近新增", []):
return "最近新增"
for list_name, play_list in self.xiaomusic.music_list.items():
if (list_name not in ["全部", "所有歌曲", "所有电台", "临时搜索列表"]) and (
name in play_list
):
return list_name
if name in self.xiaomusic.music_list.get("所有歌曲", []):
return "所有歌曲"
if name in self.xiaomusic.music_list.get("所有电台", []):
return "所有电台"
return "全部"