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

Compare commits

..

48 Commits

Author SHA1 Message Date
涵曦
9852feec81 bump: version 0.3.32 → 0.3.33 2024-09-15 23:55:45 +08:00
涵曦
ce79c0f0f7 build: update static version 2024-09-15 23:55:44 +08:00
涵曦
51037fc714 Update README.md 2024-09-15 23:47:15 +08:00
涵曦
f6b9178688 Update README.md 2024-09-15 23:40:12 +08:00
涵曦
7ccbd6ce79 refactor: 优化谷歌统计 2024-09-15 23:10:00 +08:00
涵曦
30926c6b79 feat: 调整页面布局 2024-09-15 22:53:59 +08:00
涵曦
270076b9a7 fix: #168 安全优化: 设置数据接口密码隐藏处理 2024-09-15 15:51:44 +08:00
hui
ba58d45d8b feat: 支持继续播放 (#171) 2024-09-15 14:44:47 +08:00
涵曦
0543c92f37 Update README.md 2024-09-15 10:03:29 +08:00
涵曦
f40a4c5c7b Update README.md 2024-09-15 09:13:07 +08:00
涵曦
cc05933992 Update README.md 2024-09-14 19:59:05 +08:00
涵曦
896eae92ff fix: 修复谷歌统计报错问题 2024-09-14 15:08:38 +08:00
涵曦
07676e8c5d Update README.md 2024-09-14 12:13:12 +08:00
涵曦
4ec70a210b bump: version 0.3.31 → 0.3.32 2024-09-14 08:37:30 +08:00
涵曦
0b395f26ed build: update static version 2024-09-14 08:37:30 +08:00
涵曦
965a8be5bb Update README.md 2024-09-13 20:42:30 +08:00
hui
36ddfc8885 fix: 优化audio_id查询方式 (#165) 2024-09-13 20:14:40 +08:00
涵曦
f82957c73f Update README.md 2024-09-13 18:49:17 +08:00
涵曦
f1625e7d92 fix: 播放链接接口支持复杂的链接 2024-09-13 08:55:33 +08:00
涵曦
48797ddf8f feat: 新增谷歌统计 2024-09-12 20:06:32 +08:00
hui
6f67f515b2 feat: 增加播放进度 (#160)
* fix: windows下保存配置失败

* feat: 增加播放进度
2024-09-12 17:46:21 +08:00
涵曦
9a0146b04e Update README.md 2024-09-12 08:45:20 +08:00
涵曦
7fdb28c352 Update README.md 2024-09-11 17:36:50 +08:00
涵曦
5619584481 bump: version 0.3.30 → 0.3.31 2024-09-10 16:42:15 +00:00
涵曦
3f0a1cb8f5 build: update static version 2024-09-10 16:42:14 +00:00
涵曦
2f6105843b fix: 修复插件示例报错 #105 2024-09-10 16:24:11 +00:00
涵曦
d71f99de53 feat: 新增播放上一首歌曲功能 #90 2024-09-10 16:13:44 +00:00
涵曦
d7385405d9 fix: 修复当前播放歌曲没保存的问题 #90 2024-09-10 14:38:14 +00:00
涵曦
781e5ebb2f feat: 新增所有歌曲列表 2024-09-10 06:42:16 +00:00
Yan Zenghui
980772bf9c feat: 触屏版显示歌曲名称 (#156)
* feat:触屏版显示歌曲名称

* fix:修改日志级别

* fix:修改歌曲名称获取方式
2024-09-09 17:32:02 +08:00
涵曦
632e411c6e Update README.md 2024-09-08 16:43:51 +08:00
涵曦
789d442029 Update README.md 2024-09-08 16:41:02 +08:00
涵曦
0452d49930 Update README.md 2024-09-08 16:39:28 +08:00
涵曦
19d781fa1f Update README.md 2024-09-08 14:30:40 +08:00
涵曦
ad82d13a7e Update README.md 2024-09-08 14:28:28 +08:00
涵曦
9c4d757dc0 bump: version 0.3.29 → 0.3.30 2024-09-07 14:34:42 +00:00
涵曦
e369d80875 build: update static version 2024-09-07 14:34:41 +00:00
涵曦
9b0c8510a3 feat: 修改设置按钮位置 2024-09-07 13:58:20 +00:00
涵曦
94fb158d7d Update README.md 2024-09-07 10:02:47 +08:00
涵曦
11df6e9f0c Update README.md 2024-09-07 09:51:21 +08:00
涵曦
4e8550a56c Update README.md 2024-09-07 09:39:56 +08:00
涵曦
7f349410a0 feat: 新增网页播放接口 #138 2024-09-07 00:16:55 +00:00
涵曦
6610c29fe4 Update README.md 2024-09-07 07:42:10 +08:00
涵曦
3da1b8eac1 Update README.md 2024-09-07 07:38:07 +08:00
涵曦
003068e62c Update README.md 2024-09-07 07:34:50 +08:00
涵曦
1d12f0d508 Update README.md 2024-09-07 07:29:56 +08:00
涵曦
1ee4667a79 Update README.md 2024-09-07 02:36:16 +08:00
涵曦
521605e9c8 Update README.md 2024-09-07 02:31:35 +08:00
17 changed files with 552 additions and 146 deletions

View File

@@ -1,3 +1,51 @@
## v0.3.33 (2024-09-15)
### Feat
- 调整页面布局
- 支持继续播放 (#171)
### Fix
- #168 安全优化: 设置数据接口密码隐藏处理
- 修复谷歌统计报错问题
### Refactor
- 优化谷歌统计
## v0.3.32 (2024-09-14)
### Feat
- 新增谷歌统计
- 增加播放进度 (#160)
### Fix
- 优化audio_id查询方式 (#165)
- 播放链接接口支持复杂的链接
## v0.3.31 (2024-09-10)
### Feat
- 新增播放上一首歌曲功能 #90
- 新增所有歌曲列表
- 触屏版显示歌曲名称 (#156)
### Fix
- 修复插件示例报错 #105
- 修复当前播放歌曲没保存的问题 #90
## v0.3.30 (2024-09-07)
### Feat
- 修改设置按钮位置
- 新增网页播放接口 #138
## v0.3.29 (2024-09-06)
### Feat

View File

@@ -17,7 +17,13 @@
## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下
已经支持在 web 页面配置其他参数docker 启动命令如下:
```bash
docker run -p 8090:8090 -v /xiaomusic/music:/app/music -v /xiaomusic/conf:/app/conf hanxi/xiaomusic
```
对应的 docker compose 配置如下:
```yaml
services:
@@ -28,22 +34,17 @@ services:
ports:
- 8090:8090
volumes:
- ./music:/app/music
- ./conf:/app/conf
```
对应的 docker 启动命令如下:
```yaml
docker run -p 8090:8090 \
-v ./music:/app/music \
-v ./conf:/app/conf \
hanxi/xiaomusic
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
```
其中 conf 目录为配置文件存放目录music 目录为音乐存放目录,建议分开配置为不同的目录。
启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表
> 上面配置的 /xiaomusic/music 和 /xiaomusic/conf 是 docker 主机里的 /xiaomusic 目录下的,可以修改为其他目录。如果报错找不到 /xiaomusic/music 目录,可以先执行 `mkdir -p /xiaomusic/{music,conf}` 命令新建目录
docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
> 目前安装步骤已经是最简化了,如果还是嫌安装麻烦,可以微信或者 QQ 约我远程安装,我一般周末和晚上才有时间,收个辛苦费 :moneybag: 50 元一次,安装失败不收费。
### 修改默认8090端口映射
@@ -58,13 +59,13 @@ services:
ports:
- 5678:5678
volumes:
- ./music:/app/music
- ./conf:/app/conf
- /xiaomusic/music:/app/music
- /xiaomusic/conf:/app/conf
environment:
XIAOMUSIC_PORT: 5678
```
如果不是首次修改端口,还需要修改 setting.json 文件里的端口。
如果不是首次修改端口,还需要修改 /xiaomusic/conf/setting.json 文件里的端口。
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
@@ -80,7 +81,7 @@ services:
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
XiaoMusic v0.3.28 by: github.com/hanxi
XiaoMusic v0.3.29 by: github.com/hanxi
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
[--password PASSWORD] [--cookie COOKIE] [--verbose]
@@ -117,6 +118,17 @@ pdm run xiaomusic.py
如果是开发前端界面,可以通过 <http://localhost:8090/docs>
查看有什么接口。目前的 web 控制台非常简陋,欢迎有兴趣的朋友帮忙实现一个漂亮的前端,需要什么接口可以随时提需求。
### 代码提交规范
提交前请执行
```
pdm fmt
pdm lint --fix
```
用于检查代码和格式化代码。
### 本地编译 Docker Image
```shell
@@ -135,6 +147,10 @@ docker build -t xiaomusic .
- 停止播放
- 刷新列表
- 播放列表+列表名 比如:播放列表其他
- 加入收藏
- 取消收藏
- 播放列表收藏
- 播放本地歌曲+歌名
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
@@ -147,19 +163,21 @@ docker build -t xiaomusic .
| 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 | 已经支持的触屏版 |
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
> 其他触屏版不能播放可以设置 XIAOMUSIC_USE_MUSIC_API 为 true 试试。
> 其他触屏版不能播放可以设置【触屏版兼容模式】选项为 true 试试。见 <https://github.com/hanxi/xiaomusic/issues/30>
## 支持音乐格式
@@ -192,16 +210,7 @@ docker build -t xiaomusic .
采用新的设置页面之后,没有必须在启动前配置的环境变量了,除非是改默认的 8090 端口才需要配置环境变量。
后台的 XIAOMUSIC_PROXY 参数格式参考 yt-dlp 文档说明:
```
Use the specified HTTP/HTTPS/SOCKS proxy. To
enable SOCKS proxy, specify a proper scheme,
e.g. socks5://user:pass@127.0.0.1:1080/.
Pass in an empty string (--proxy "") for
direct connection
```
见 <https://github.com/hanxi/xiaomusic/issues/2> 和 <https://github.com/hanxi/xiaomusic/issues/11>
后台的
## 网络歌单功能
@@ -227,6 +236,12 @@ direct connection
- 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>
### :warning: 安全提醒
- 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
## 高级篇
@@ -247,11 +262,23 @@ direct connection
- [xiaogpt](https://github.com/yihong0618/xiaogpt)
- [MiService](https://github.com/yihong0618/MiService)
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- [awesome-xiaoai](https://github.com/zzz6519003/awesome-xiaoai)
- [微信小程序: XIAO晓音](https://github.com/F-loat/xiaoplayer)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
### 其他教程
> 下面教程可能比较旧,只供参考
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
- [QNAS部署教程](https://post.smzdm.com/p/a5xz5x63/)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友
- [视频教程](https://www.bilibili.com/video/BV1ZZpweHEtT/)
- [TechHive](https://mp.weixin.qq.com/s/4a41muFtPaFKtHeZYu795w)
- [弹个AI](https://mp.weixin.qq.com/s/sIsKxB7Y8b83AhnvaWiMog)
- [简单免费教你用绿联NAS联动小爱音箱私人音乐库也能语音点播](https://post.m.smzdm.com/p/a8pldgg7/)
## Star History
@@ -259,6 +286,7 @@ direct connection
## 赞赏
- 爱发电 <https://afdian.com/a/imhanxi>
- 点个 Star
- 谢谢 ❤️
- :moneybag: 爱发电 <https://afdian.com/a/imhanxi>
- 点个 Star :star:
- 谢谢 :heart:
- ![喝杯奶茶](https://i.v2ex.co/7Q03axO5l.png)

14
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "dev", "lint"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:0a0b1f63fdd9dd2c4ca2a777f12d294126a880631c1b3d48108d1df283ba14a8"
content_hash = "sha256:d78c6aed8ee11387663e36ade149f06fd493f984e253a1936163f85542ab5a52"
[[metadata.targets]]
requires_python = "==3.10.12"
@@ -464,6 +464,18 @@ files = [
{file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
]
[[package]]
name = "ga4mp"
version = "2.0.4"
requires_python = ">=3.6,<4.0"
summary = "Google Analytics 4 Measurement Protocol Python Module"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "ga4mp-2.0.4-py3-none-any.whl", hash = "sha256:11e5072b33a93917bbfcf5b44ee48d21e45124ef18e2a0f1275e1529df340de8"},
{file = "ga4mp-2.0.4.tar.gz", hash = "sha256:2fdcf275e643c5c3ab2c3a03e82edc3551109f7e9175b3604aea8c8e015c15ad"},
]
[[package]]
name = "h11"
version = "0.14.0"

View File

@@ -1,4 +1,5 @@
async def code1(arg1):
global log, xiaomusic
log.info(f"code1:{arg1}")
await xiaomusic.do_tts("你好,我是自定义的测试口令")
did = xiaomusic._cur_did
await xiaomusic.do_tts(did, "你好,我是自定义的测试口令")

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.3.29"
version = "0.3.33"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
@@ -14,6 +14,7 @@ dependencies = [
"fastapi>=0.111.0",
"starlette>=0.37.2",
"aiofiles>=24.1.0",
"ga4mp>=2.0.4",
]
requires-python = ">=3.10,<3.12"
readme = "README.md"

View File

@@ -1 +1 @@
__version__ = "0.3.29"
__version__ = "0.3.33"

82
xiaomusic/analytics.py Normal file
View File

@@ -0,0 +1,82 @@
import asyncio
from datetime import datetime
from ga4mp import GtagMP
from xiaomusic import __version__
class Analytics:
def __init__(self, log):
self.gtag = None
self.current_date = None
self.log = log
self.init()
def init(self):
if self.gtag is not None:
return
gtag = GtagMP(
api_secret="sVRsf3T9StuWc-ZiWZxDVA",
measurement_id="G-Z09NC1K7ZW",
client_id="",
)
gtag.client_id = gtag.random_client_id()
gtag.store.set_user_property(name="version", value=__version__)
self.gtag = gtag
self.log.info("analytics init ok")
async def run_with_timeout(self, func, *args, **kwargs):
try:
return await asyncio.wait_for(func(*args, **kwargs), 3)
except asyncio.TimeoutError as e:
self.log.warning(f"analytics run_with_timeout failed {e}")
return None
async def send_startup_event(self):
try:
await self.run_with_timeout(self._send_startup_event)
except Exception as e:
self.log.warning(f"analytics send_startup_event failed {e}")
self.init()
async 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)
async def send_daily_event(self):
try:
await self.run_with_timeout(self._send_daily_event)
except Exception as e:
self.log.warning(f"analytics send_daily_event failed {e}")
self.init()
async def _send_daily_event(self):
current_date = datetime.now().strftime("%Y-%m-%d")
if self.current_date == current_date:
return
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)
self.current_date = current_date
async def send_play_event(self, name, sec):
try:
await self.run_with_timeout(self._send_play_event, name, sec)
except Exception as e:
self.log.warning(f"analytics send_play_event failed {e}")
self.init()
async def _send_play_event(self, name, sec):
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)

View File

@@ -16,6 +16,7 @@ def default_key_word_dict():
"播放本地歌曲": "playlocal",
"关机": "stop",
"下一首": "play_next",
"上一首": "play_prev",
"单曲循环": "set_play_type_one",
"全部循环": "set_play_type_all",
"随机播放": "set_random_play",
@@ -46,6 +47,7 @@ def default_key_match_order():
"分钟后关机",
"播放歌曲",
"下一首",
"上一首",
"单曲循环",
"全部循环",
"随机播放",
@@ -143,6 +145,9 @@ class Config:
)
convert_to_mp3: bool = os.getenv("CONVERT_TO_MP3", "false").lower() == "true"
delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数
continue_play: bool = (
os.getenv("XIAOMUSIC_CONTINUE_PLAY", "false").lower() == "true"
)
def append_keyword(self, keys, action):
for key in keys.split(","):

View File

@@ -6,6 +6,7 @@ import re
import secrets
import shutil
import tempfile
import urllib.parse
from contextlib import asynccontextmanager
from dataclasses import asdict
from typing import Annotated
@@ -144,10 +145,14 @@ def playingmusic(did: str = "", Verifcation=Depends(verification)):
is_playing = xiaomusic.isplaying(did)
cur_music = xiaomusic.playingmusic(did)
# 播放进度
offset, duration = xiaomusic.get_offset_duration(did)
return {
"ret": "OK",
"is_playing": is_playing,
"cur_music": cur_music,
"offset": offset,
"duration": duration,
}
@@ -179,6 +184,8 @@ async def do_cmd(data: DidCmd, Verifcation=Depends(verification)):
async def getsetting(need_device_list: bool = False, Verifcation=Depends(verification)):
config = xiaomusic.getconfig()
data = asdict(config)
data["password"] = "******"
data["httpauth_password"] = "******"
if need_device_list:
device_list = await xiaomusic.getalldevices()
log.info(f"getsetting device_list: {device_list}")
@@ -193,6 +200,11 @@ async def savesetting(request: Request, Verifcation=Depends(verification)):
data = json.loads(data_json.decode("utf-8"))
debug_data = deepcopy_data_no_sensitive_info(data)
log.info(f"saveconfig: {debug_data}")
config = xiaomusic.getconfig()
if data["password"] == "******" or data["password"] == "":
data["password"] = config.password
if data["httpauth_password"] == "******" or data["httpauth_password"] == "":
data["httpauth_password"] = config.httpauth_password
await xiaomusic.saveconfig(data)
reset_http_server()
return "save success"
@@ -205,6 +217,16 @@ async def musiclist(Verifcation=Depends(verification)):
return xiaomusic.get_music_list()
@app.get("/musicinfo")
async def musicinfo(name: str, Verifcation=Depends(verification)):
url = xiaomusic.get_music_url(name)
return {
"ret": "OK",
"name": name,
"url": url,
}
@app.get("/curplaylist")
async def curplaylist(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
@@ -279,9 +301,9 @@ def downloadlog(Verifcation=Depends(verification)):
async def playurl(did: str, url: str, Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
return {"ret": "Did not exist"}
log.info(f"playurl did: {did} url: {url}")
return await xiaomusic.play_url(did=did, arg1=url)
decoded_url = urllib.parse.unquote(url)
log.info(f"playurl did: {did} url: {decoded_url}")
return await xiaomusic.play_url(did=did, arg1=decoded_url)
@app.post("/debug_play_by_music_url")

View File

@@ -1,6 +1,9 @@
$(function(){
$container=$("#cmds");
append_op_button_name("加入收藏");
append_op_button_name("取消收藏");
const PLAY_TYPE_ONE = 0; // 单曲循环
const PLAY_TYPE_ALL = 1; // 全部循环
const PLAY_TYPE_RND = 2; // 随机播放
@@ -8,12 +11,11 @@ $(function(){
append_op_button("play_type_one", "单曲循环", "单曲循环");
append_op_button("play_type_rnd", "随机播放", "随机播放");
append_op_button_name("刷新列表");
append_op_button_name("下一首");
append_op_button_name("上一首");
append_op_button_name("关机");
append_op_button_name("下一首");
append_op_button_name("加入收藏");
append_op_button_name("取消收藏");
append_op_button_name("刷新列表");
$container.append($("<hr>"));
@@ -21,6 +23,9 @@ $(function(){
append_op_button_name("30分钟后关机");
append_op_button_name("60分钟后关机");
var offset = 0;
var duration = 0;
// 拉取现有配置
$.get("/getsetting", function(data, status) {
console.log(data, status);
@@ -98,9 +103,9 @@ $(function(){
$('#music_list').change(function() {
const selectedValue = $(this).val();
localStorage.setItem('cur_playlist', selectedValue);
$('#music_name').empty();
const sorted_musics = data[selectedValue].sort(custom_sort_key);
$.each(sorted_musics, function(index, item) {
$.each(data[selectedValue], function(index, item) {
$('#music_name').append($('<option></option>').val(item).text(item));
});
});
@@ -108,10 +113,17 @@ $(function(){
$('#music_list').trigger('change');
// 获取当前播放列表
$.get(`curplaylist?did=${did}`, function(data, status) {
if (data != "") {
$('#music_list').val(data);
$.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.includes(playlist)) {
$('#music_list').val(playlist);
$('#music_list').trigger('change');
}
}
})
})
@@ -130,6 +142,17 @@ $(function(){
sendcmd(cmd);
});
$("#web_play").on("click", () => {
const music_name = $("#music_name").val();
$.get(`/musicinfo?name=${music_name}`, function(data, status) {
console.log(data);
if (data.ret == "OK") {
const music = new Audio(data.url);
music.play();
}
});
});
$("#del_music").on("click", () => {
var del_music_name = $("#music_name").val();
if (confirm(`确定删除歌曲 ${del_music_name} 吗?`)) {
@@ -152,7 +175,8 @@ $(function(){
$("#playurl").on("click", () => {
var url = $("#music-url").val();
$.get(`/playurl?url=${url}&did=${did}`, function(data, status) {
const encoded_url = encodeURIComponent(url);
$.get(`/playurl?url=${encoded_url}&did=${did}`, function(data, status) {
console.log(data);
});
});
@@ -251,26 +275,26 @@ $(function(){
} else {
$("#playering-music").text(`【空闲中】 ${data.cur_music}`);
}
offset = data.offset;
duration = data.duration;
}
});
}
function custom_sort_key(a, b) {
// 使用正则表达式提取数字前缀
const numericPrefixA = a.match(/^(\d+)/) ? parseInt(a.match(/^(\d+)/)[1], 10) : null;
const numericPrefixB = b.match(/^(\d+)/) ? parseInt(b.match(/^(\d+)/)[1], 10) : null;
// 如果两个键都有数字前缀,则按数字大小排序
if (numericPrefixA !== null && numericPrefixB !== null) {
return numericPrefixA - numericPrefixB;
}
// 如果一个键有数字前缀而另一个没有,则有数字前缀的键排在前面
if (numericPrefixA !== null) return -1;
if (numericPrefixB !== null) return 1;
// 如果两个键都没有数字前缀,则按照常规字符串排序
return a.localeCompare(b);
}
setInterval(()=>{
if (duration > 0) {
offset++;
$("#progress").val(offset / duration * 100);
$("#play-time").text(`${formatTime(offset)}/${formatTime(duration)}`)
}else{
$("#play-time").text(`${formatTime(0)}/${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')}`;
}
});

View File

@@ -5,9 +5,18 @@
<meta name="viewport" content="width=device-width">
<title>Debug For XiaoMusic</title>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725646067">
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1726415744">
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script src="/static/jquery-3.7.1.min.js?version=1725646067"></script>
<script src="/static/jquery-3.7.1.min.js?version=1726415744"></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>
<script>
var vConsole = new window.VConsole();

View File

@@ -3,9 +3,18 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1725646067"></script>
<script src="/static/app.js?version=1725646067"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725646067">
<script src="/static/jquery-3.7.1.min.js?version=1726415744"></script>
<script src="/static/app.js?version=1726415744"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1726415744">
<!-- 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>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
@@ -29,15 +38,14 @@ var vConsole = new window.VConsole();
</div>
<div id="cmds">
<a class="button" href="/static/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>
<a href="/static/setting.html">
<svg fill="#8e43e7" height="48px" width="48px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-11.88 -11.88 77.76 77.76" xml:space="preserve" stroke="#8e43e7" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)" stroke-width="0.00054"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"><rect x="-11.88" y="-11.88" width="77.76" height="77.76" rx="18.6624" fill="#addcff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.512"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M51.22,21h-5.052c-0.812,0-1.481-0.447-1.792-1.197s-0.153-1.54,0.42-2.114l3.572-3.571 c0.525-0.525,0.814-1.224,0.814-1.966c0-0.743-0.289-1.441-0.814-1.967l-4.553-4.553c-1.05-1.05-2.881-1.052-3.933,0l-3.571,3.571 c-0.574,0.573-1.366,0.733-2.114,0.421C33.447,9.313,33,8.644,33,7.832V2.78C33,1.247,31.753,0,30.22,0H23.78 C22.247,0,21,1.247,21,2.78v5.052c0,0.812-0.447,1.481-1.197,1.792c-0.748,0.313-1.54,0.152-2.114-0.421l-3.571-3.571 c-1.052-1.052-2.883-1.05-3.933,0l-4.553,4.553c-0.525,0.525-0.814,1.224-0.814,1.967c0,0.742,0.289,1.44,0.814,1.966l3.572,3.571 c0.573,0.574,0.73,1.364,0.42,2.114S8.644,21,7.832,21H2.78C1.247,21,0,22.247,0,23.78v6.439C0,31.753,1.247,33,2.78,33h5.052 c0.812,0,1.481,0.447,1.792,1.197s0.153,1.54-0.42,2.114l-3.572,3.571c-0.525,0.525-0.814,1.224-0.814,1.966 c0,0.743,0.289,1.441,0.814,1.967l4.553,4.553c1.051,1.051,2.881,1.053,3.933,0l3.571-3.572c0.574-0.573,1.363-0.731,2.114-0.42 c0.75,0.311,1.197,0.98,1.197,1.792v5.052c0,1.533,1.247,2.78,2.78,2.78h6.439c1.533,0,2.78-1.247,2.78-2.78v-5.052 c0-0.812,0.447-1.481,1.197-1.792c0.751-0.312,1.54-0.153,2.114,0.42l3.571,3.572c1.052,1.052,2.883,1.05,3.933,0l4.553-4.553 c0.525-0.525,0.814-1.224,0.814-1.967c0-0.742-0.289-1.44-0.814-1.966l-3.572-3.571c-0.573-0.574-0.73-1.364-0.42-2.114 S45.356,33,46.168,33h5.052c1.533,0,2.78-1.247,2.78-2.78V23.78C54,22.247,52.753,21,51.22,21z M52,30.22 C52,30.65,51.65,31,51.22,31h-5.052c-1.624,0-3.019,0.932-3.64,2.432c-0.622,1.5-0.295,3.146,0.854,4.294l3.572,3.571 c0.305,0.305,0.305,0.8,0,1.104l-4.553,4.553c-0.304,0.304-0.799,0.306-1.104,0l-3.571-3.572c-1.149-1.149-2.794-1.474-4.294-0.854 c-1.5,0.621-2.432,2.016-2.432,3.64v5.052C31,51.65,30.65,52,30.22,52H23.78C23.35,52,23,51.65,23,51.22v-5.052 c0-1.624-0.932-3.019-2.432-3.64c-0.503-0.209-1.021-0.311-1.533-0.311c-1.014,0-1.997,0.4-2.761,1.164l-3.571,3.572 c-0.306,0.306-0.801,0.304-1.104,0l-4.553-4.553c-0.305-0.305-0.305-0.8,0-1.104l3.572-3.571c1.148-1.148,1.476-2.794,0.854-4.294 C10.851,31.932,9.456,31,7.832,31H2.78C2.35,31,2,30.65,2,30.22V23.78C2,23.35,2.35,23,2.78,23h5.052 c1.624,0,3.019-0.932,3.64-2.432c0.622-1.5,0.295-3.146-0.854-4.294l-3.572-3.571c-0.305-0.305-0.305-0.8,0-1.104l4.553-4.553 c0.304-0.305,0.799-0.305,1.104,0l3.571,3.571c1.147,1.147,2.792,1.476,4.294,0.854C22.068,10.851,23,9.456,23,7.832V2.78 C23,2.35,23.35,2,23.78,2h6.439C30.65,2,31,2.35,31,2.78v5.052c0,1.624,0.932,3.019,2.432,3.64 c1.502,0.622,3.146,0.294,4.294-0.854l3.571-3.571c0.306-0.305,0.801-0.305,1.104,0l4.553,4.553c0.305,0.305,0.305,0.8,0,1.104 l-3.572,3.571c-1.148,1.148-1.476,2.794-0.854,4.294c0.621,1.5,2.016,2.432,3.64,2.432h5.052C51.65,23,52,23.35,52,23.78V30.22z"></path> <path d="M27,18c-4.963,0-9,4.037-9,9s4.037,9,9,9s9-4.037,9-9S31.963,18,27,18z M27,34c-3.859,0-7-3.141-7-7s3.141-7,7-7 s7,3.141,7,7S30.859,34,27,34z"></path> </g> </g></svg>
</a>
</div>
</div>
<hr>
@@ -45,6 +53,10 @@ var vConsole = new window.VConsole();
<datalist id="autocomplete-list"></datalist>
<input id="music-name" type="text" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)" list="autocomplete-list"></input>
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input>
<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>
@@ -60,6 +72,7 @@ var vConsole = new window.VConsole();
<div>
<button id="play_music_list">播放列表歌曲</button>
<button id="del_music">删除选中歌曲</button>
<button id="web_play">网页播放</button>
</div>
</div>

View File

@@ -4,7 +4,17 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>M3U to JSON Converter</title>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725646067">
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1726415744">
<!-- 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>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>

BIN
xiaomusic/static/qrcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -3,9 +3,18 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1725646067"></script>
<script src="/static/setting.js?version=1725646067"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725646067">
<script src="/static/jquery-3.7.1.min.js?version=1726415744"></script>
<script src="/static/setting.js?version=1726415744"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1726415744">
<!-- 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>
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
@@ -40,20 +49,15 @@ var vConsole = new window.VConsole();
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
<input id="hostname" type="text"></input>
</div>
<hr>
<div class="rows">
<label for="verbose">是否开启调试日志:</label>
<select id="verbose">
<option value="true" selected>true</option>
<option value="false">false</option>
</select>
<div>
<button class="save-button">保存</button>
<button onclick="location.href='/';">返回首页</button>
</div>
</div>
<hr>
<div class="rows">
<label for="group_list">设备分组配置:</label>
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2"></input>
@@ -138,6 +142,12 @@ var vConsole = new window.VConsole();
<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="public_port">外网访问端口(0表示跟监听端口一致):</label>
<input id="public_port" type="number" value="0"></input>
@@ -162,17 +172,24 @@ var vConsole = new window.VConsole();
</div>
</div>
<hr>
<button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button class="save-button">保存</button>
<hr>
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
<button onclick="location.href='/docs';">查看接口文档</button>
<a class="button" href="/static/m3u.html" target="_blank">m3u文件转换</a>
<hr>
<a href="/static/m3u.html" target="_blank">m3u文件转换工具</a>
<hr>
<a href="/static/debug.html" target="_blank">调试工具</a>
<a class="button" href="/static/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="/static/qrcode.png" alt="请涵曦喝奶茶🧋">
</div>
<footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer>

View File

@@ -24,16 +24,19 @@ label {
width: 300px;
}
input,select {
margin: 10px;
width: 300px;
margin-left: 5%;
margin-right: 5%;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
max-width: 400px;
height: 40px;
}
.rows {
display: flex;
flex-direction: column;
margin-left: 20px;
margin-right: 20px;
justify-content: center;
}
footer {
@@ -44,8 +47,12 @@ footer {
}
textarea{
margin: 10px;
width: 300px;
margin-left: 5%;
margin-right: 5%;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
max-width: 400px;
height: 200px;
}
@@ -77,3 +84,9 @@ footer {
text-decoration: none;
display: inline-block;
}
.qrcode {
width: 100%;
max-width: 480px;
height: auto;
}

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import asyncio
import copy
import json
import logging
import math
@@ -16,6 +17,7 @@ from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiNAService
from xiaomusic import __version__
from xiaomusic.analytics import Analytics
from xiaomusic.config import (
KEY_WORD_ARG_BEFORE_DICT,
Config,
@@ -83,6 +85,9 @@ class XiaoMusic:
# 更新设备列表
self.update_devices()
# 启动统计
self.analytics = Analytics(self.log)
debug_config = deepcopy_data_no_sensitive_info(self.config)
self.log.info(f"Startup OK. {debug_config}")
@@ -109,6 +114,7 @@ class XiaoMusic:
self.music_path_depth = self.config.music_path_depth
self.remove_id3tag = self.config.remove_id3tag
self.convert_to_mp3 = self.config.convert_to_mp3
self.continue_play = self.config.continue_play
def update_devices(self):
self.device_id_did = {} # key 为 device_id
@@ -171,6 +177,7 @@ class XiaoMusic:
# sleep to avoid too many request
# self.log.debug(f"Sleep {d}, timestamp: {self.last_timestamp}")
await asyncio.sleep(1 - d)
await self.analytics.send_daily_event()
async def init_all_data(self, session):
await self.login_miboy(session)
@@ -453,6 +460,9 @@ class XiaoMusic:
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
]
self._append_custom_play_list()
@@ -511,6 +521,7 @@ class XiaoMusic:
self.log.exception(f"Execption {e}")
async def run_forever(self):
await self.analytics.send_startup_event()
async with ClientSession() as session:
self.session = session
await self.init_all_data(session)
@@ -718,6 +729,9 @@ class XiaoMusic:
async def play_next(self, did="", **kwargs):
return await self.devices[did].play_next()
async def play_prev(self, did="", **kwargs):
return await self.devices[did].play_prev()
# 停止
async def stop(self, did="", arg1="", **kwargs):
return await self.devices[did].stop(arg1=arg1)
@@ -794,10 +808,13 @@ class XiaoMusic:
# 正在播放中的音乐
def playingmusic(self, did):
cur_music = self.devices[did].cur_music
cur_music = self.devices[did].get_cur_music()
self.log.debug(f"playingmusic. cur_music:{cur_music}")
return cur_music
def get_offset_duration(self, did):
return self.devices[did].get_offset_duration()
# 当前是否正在播放歌曲
def isplaying(self, did):
return self.devices[did].isplaying()
@@ -836,6 +853,10 @@ class XiaoMusic:
# 把当前配置落地
def save_cur_config(self):
for did in self.config.devices.keys():
deviceobj = self.devices.get(did)
if deviceobj is not None:
self.config.devices[did] = deviceobj.device
data = asdict(self.config)
self.do_saveconfig(data)
self.log.info("save_cur_config ok")
@@ -854,6 +875,8 @@ class XiaoMusic:
# 重新初始化
async def reinit(self, **kwargs):
for handler in self.log.handlers:
handler.close()
self.setup_logger()
await self.init_all_data(self.session)
self._gen_all_music_list()
@@ -909,21 +932,40 @@ class XiaoMusicDevice:
self.ffmpeg_location = self.config.ffmpeg_location
self._download_proc = None # 下载对象
self.cur_music = self.device.cur_music
self._next_timer = None
self._timeout = 0
self._playing = False
# 播放进度
self._start_time = 0
self._duration = 0
# 关机定时器
self._stop_timer = None
self._last_cmd = None
self.update_playlist()
def get_cur_music(self):
return self.device.cur_music
def get_offset_duration(self):
if not self._playing:
return -1, -1
offset = time.time() - self._start_time
duration = self._duration
return offset, duration
# 初始化播放列表
def update_playlist(self):
self._cur_play_list = self.device.cur_playlist
if self._cur_play_list not in self.xiaomusic.music_list:
self._cur_play_list = "全部"
self._play_list = self.xiaomusic.music_list.get(self._cur_play_list)
if self.device.cur_playlist not in self.xiaomusic.music_list:
self.device.cur_playlist = "全部"
list_name = self.device.cur_playlist
self._play_list = copy.copy(self.xiaomusic.music_list[list_name])
if self.device.play_type == PLAY_TYPE_RND:
random.shuffle(self._play_list)
self.log.info(f"随机打乱 {list_name} {self._play_list}")
else:
self.log.info(f"没打乱 {list_name} {self._play_list}")
# 播放歌曲
async def play(self, name="", search_key=""):
@@ -936,7 +978,7 @@ class XiaoMusicDevice:
await self._play_next()
return
else:
name = self.cur_music
name = self.get_cur_music()
self.log.info(f"play. search_key:{search_key} name:{name}")
# 本地歌曲不存在时下载
@@ -958,7 +1000,7 @@ class XiaoMusicDevice:
async def _play_next(self):
self.log.info("开始播放下一首")
name = self.cur_music
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
@@ -966,7 +1008,27 @@ class XiaoMusicDevice:
or (name not in self._play_list)
):
name = self.get_next_music()
self.log.info(f"_play_next. name:{name}, cur_music:{self.cur_music}")
self.log.info(f"_play_next. name:{name}, cur_music:{self.get_cur_music()}")
if name == "":
await self.do_tts("本地没有歌曲")
return
await self._play(name)
# 上一首
async def play_prev(self):
return await self._play_prev()
async def _play_prev(self):
self.log.info("开始播放上一首")
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or name == ""
or (name not in self._play_list)
):
name = self.get_prev_music()
self.log.info(f"_play_prev. name:{name}, cur_music:{self.get_cur_music()}")
if name == "":
await self.do_tts("本地没有歌曲")
return
@@ -980,7 +1042,7 @@ class XiaoMusicDevice:
await self._play_next()
return
else:
name = self.cur_music
name = self.get_cur_music()
self.log.info(f"playlocal. name:{name}")
@@ -996,12 +1058,13 @@ class XiaoMusicDevice:
self.cancel_group_next_timer()
self._playing = True
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
self.device.cur_music = name
self.log.info(f"cur_music {self.get_cur_music()}")
sec, url = await self.xiaomusic.get_music_sec_url(name)
await self.group_force_stop_xiaoai()
self.log.info(f"播放 {url}")
results = await self.group_player_play(url)
results = await self.group_player_play(url, name)
if all(ele is None for ele in results):
self.log.info(f"播放 {name} 失败")
await asyncio.sleep(1)
@@ -1010,13 +1073,17 @@ class XiaoMusicDevice:
return
self.log.info(f"{name}】已经开始播放了")
await self.xiaomusic.analytics.send_play_event(name, sec)
# 设置下一首歌曲的播放定时器
if sec <= 1:
self.log.info(f"{name}】不会设置下一首歌的定时器")
return
sec = sec + self.config.delay_sec
self._start_time = time.time()
self._duration = sec
await self.set_next_music_timeout(sec)
self.xiaomusic.save_cur_config()
async def do_tts(self, value):
self.log.info(f"try do_tts value:{value}")
@@ -1030,7 +1097,7 @@ class XiaoMusicDevice:
# 最大等8秒
sec = min(8, int(len(value) / 3))
await asyncio.sleep(sec)
self.log.info(f"do_tts ok. cur_music:{self.cur_music}")
self.log.info(f"do_tts ok. cur_music:{self.get_cur_music()}")
await self.check_replay()
async def force_stop_xiaoai(self, device_id):
@@ -1112,9 +1179,14 @@ class XiaoMusicDevice:
# 继续播放被打断的歌曲
async def check_replay(self):
if self.isplaying() and not self.isdownloading():
# 继续播放歌曲
self.log.info("现在继续播放歌曲")
await self._play()
if not self.config.continue_play:
# 重新播放歌曲
self.log.info("现在重新播放歌曲")
await self._play()
else:
self.log.info(
f"继续播放歌曲. self.config.continue_play:{self.config.continue_play}"
)
else:
self.log.info(
f"不会继续播放歌曲. isplaying:{self.isplaying()} isdownloading:{self.isdownloading()}"
@@ -1132,51 +1204,62 @@ class XiaoMusicDevice:
self.log.info(f"add_download_music add_music {name}")
self.log.debug(self._play_list)
# 获取下一首
def get_next_music(self):
def get_music(self, direction="next"):
play_list_len = len(self._play_list)
if play_list_len == 0:
self.log.warning("当前播放列表没有歌曲")
return ""
index = 0
try:
index = self._play_list.index(self.cur_music)
index = self._play_list.index(self.get_cur_music())
except ValueError:
pass
if play_list_len == 1:
next_index = index # 当只有一首歌曲时保持当前索引不变
new_index = index # 当只有一首歌曲时保持当前索引不变
else:
# 顺序往后找1个
next_index = index + 1
if next_index >= play_list_len:
next_index = 0
# 排除当前歌曲随机找1个
if self.device.play_type == PLAY_TYPE_RND:
indices = list(range(play_list_len))
indices.remove(index)
next_index = random.choice(indices)
name = self._play_list[next_index]
if direction == "next":
new_index = index + 1
if new_index >= play_list_len:
new_index = 0
elif direction == "prev":
new_index = index - 1
if new_index < 0:
new_index = play_list_len - 1
else:
self.log.error("无效的方向参数")
return ""
name = self._play_list[new_index]
if not self.xiaomusic.is_music_exist(name):
self._play_list.pop(next_index)
self.log.info(f"pop not exist music:{name}")
return self.get_next_music()
self._play_list.pop(new_index)
self.log.info(f"pop not exist music: {name}")
return self.get_music(direction)
return name
# 获取下一首
def get_next_music(self):
return self.get_music(direction="next")
# 获取上一首
def get_prev_music(self):
return self.get_music(direction="prev")
# 判断是否播放下一首歌曲
def check_play_next(self):
# 当前歌曲不在当前播放列表
if self.cur_music not in self._play_list:
self.log.info(f"当前歌曲 {self.cur_music} 不在当前播放列表")
if self.get_cur_music() not in self._play_list:
self.log.info(f"当前歌曲 {self.get_cur_music()} 不在当前播放列表")
return True
# 当前没我在播放的歌曲
if self.cur_music == "":
if self.get_cur_music() == "":
self.log.info("当前没我在播放的歌曲")
return True
else:
# 当前播放的歌曲不存在了
if not self.xiaomusic.is_music_exist(self.cur_music):
self.log.info(f"当前播放的歌曲 {self.cur_music} 不存在了")
if not self.xiaomusic.is_music_exist(self.get_cur_music()):
self.log.info(f"当前播放的歌曲 {self.get_cur_music()} 不存在了")
return True
return False
@@ -1187,22 +1270,32 @@ class XiaoMusicDevice:
self.log.exception(f"Execption {e}")
# 同一组设备播放
async def group_player_play(self, url):
async def group_player_play(self, url, name=""):
device_id_list = self.xiaomusic.get_group_device_id_list(self.group_name)
tasks = [self.play_one_url(device_id, url) for device_id in device_id_list]
tasks = [
self.play_one_url(device_id, url, name) for device_id in device_id_list
]
results = await asyncio.gather(*tasks)
self.log.info(f"group_player_play {url} {device_id_list} {results}")
return results
async def play_one_url(self, device_id, url):
async def play_one_url(self, device_id, url, name):
ret = None
try:
if self.config.use_music_api:
audio_id = await self._get_audio_id(name)
if self.config.continue_play:
ret = await self.xiaomusic.mina_service.play_by_music_url(
device_id, url
device_id, url, _type=1, audio_id=audio_id
)
self.log.info(
f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url}"
f"play_one_url continue_play device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
)
elif self.config.use_music_api:
ret = await self.xiaomusic.mina_service.play_by_music_url(
device_id, url, audio_id=audio_id
)
self.log.info(
f"play_one_url play_by_music_url device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
)
else:
ret = await self.xiaomusic.mina_service.play_by_url(device_id, url)
@@ -1213,6 +1306,33 @@ class XiaoMusicDevice:
self.log.exception(f"Execption {e}")
return ret
async def _get_audio_id(self, name):
audio_id = 1582971365183456177
if not (self.config.use_music_api or self.config.continue_play):
return str(audio_id)
try:
params = {
"query": name,
"queryType": 1,
"offset": 0,
"count": 6,
"timestamp": int(time.time_ns() / 1000),
}
response = await self.xiaomusic.mina_service.mina_request(
"/music/search", params
)
for song in response["data"]["songList"]:
if song["originName"] == "QQ音乐":
audio_id = song["audioID"]
break
# 没找到QQ音乐的歌曲取第一个
if audio_id == 1582971365183456177:
audio_id = response["data"]["songList"][0]["audioID"]
self.log.debug(f"_get_audio_id. name: {name} songId:{audio_id}")
except Exception as e:
self.log.error(f"_get_audio_id {e}")
return str(audio_id)
# 设置下一首歌曲的播放定时器
async def set_next_music_timeout(self, sec):
self.cancel_next_timer()
@@ -1254,11 +1374,12 @@ class XiaoMusicDevice:
self.xiaomusic.save_cur_config()
tts = PLAY_TYPE_TTS[play_type]
await self.do_tts(tts)
self.update_playlist()
async def play_music_list(self, list_name, music_name):
self._last_cmd = "play_music_list"
self._cur_play_list = list_name
self._play_list = self.xiaomusic.music_list[list_name]
self.device.cur_playlist = list_name
self.update_playlist()
self.log.info(f"开始播放列表{list_name}")
await self._play(music_name)
@@ -1308,7 +1429,7 @@ class XiaoMusicDevice:
device.cancel_next_timer()
def get_cur_play_list(self):
return self._cur_play_list
return self.device.cur_playlist
# 清空所有定时器
def cancel_all_timer(self):