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

Compare commits

...

29 Commits

Author SHA1 Message Date
涵曦
3c232505f8 bump: version 0.3.26 → 0.3.27 2024-09-02 00:37:48 +00:00
涵曦
44177db9b6 build: update static version 2024-09-02 00:37:47 +00:00
涵曦
e72ae973bc refactor: 处理 code review 问题' 2024-09-01 16:09:33 +00:00
Hi-Jiajun
4ab3c5cbee feat: Add feature as requested in issue #143 2024-09-01 16:09:20 +00:00
涵曦
4e532d298d fix: 默认下载目录修改 2024-08-18 02:39:22 +00:00
涵曦
3372440f4e bump: version 0.3.25 → 0.3.26 2024-08-17 15:00:11 +00:00
涵曦
1255239912 build: update static version 2024-08-17 15:00:10 +00:00
涵曦
e401a73595 feat: 删除网关模式 2024-08-17 11:22:23 +00:00
涵曦
cca6e47da5 bump: version 0.3.24 → 0.3.25 2024-08-16 11:18:21 +00:00
涵曦
415e75d4b4 build: update static version 2024-08-16 11:18:20 +00:00
涵曦
3c5573a2fc feat: 设置页面支持配置 use_music_api 选项 2024-08-16 11:18:11 +00:00
涵曦
7275b59d40 Update README.md 2024-08-08 09:35:17 +08:00
涵曦
a8d0631c33 bump: version 0.3.23 → 0.3.24 2024-08-01 23:47:58 +00:00
涵曦
3cfc96b779 build: update static version 2024-08-01 23:47:57 +00:00
涵曦
489f3f1d6f fix: #131 修复多设备切换时播放模式显示错误问题 2024-08-01 23:21:31 +00:00
涵曦
a5f2fc195e bump: version 0.3.22 → 0.3.23 2024-08-01 16:00:06 +00:00
涵曦
393dbabf4b build: update static version 2024-08-01 16:00:05 +00:00
涵曦
444e697f9d fix: 修复部分文件获取不到播放时长问题 2024-08-01 15:52:20 +00:00
涵曦
cf01039b53 fix: 处理安全问题 2024-08-01 00:46:18 +00:00
涵曦
a8fefc6f82 bump: version 0.3.21 → 0.3.22 2024-08-01 00:23:17 +00:00
涵曦
ae0b6066d9 build: update static version 2024-08-01 00:23:17 +00:00
涵曦
53f5e7db8c feat: 网关模式支持配置,默认关闭 2024-08-01 00:22:48 +00:00
涵曦
2a1fa9f8cf fix: 继续优化延迟问题 2024-08-01 00:05:19 +00:00
涵曦
6e2d674758 bump: version 0.3.20 → 0.3.21 2024-07-30 22:55:35 +00:00
涵曦
3bb6573ec0 build: update static version 2024-07-30 22:55:34 +00:00
涵曦
4ae3774a0e fix: 使用前置网关处理静态文件来加速,尝试解决延迟的问题 2024-07-30 11:49:29 +00:00
涵曦
b34215560c feat: 尝试加个网关在前面处理静态文件来加速文件获取 2024-07-30 11:36:51 +00:00
涵曦
4426017ba8 fix: 播放前先立即暂停之前的音乐 2024-07-30 11:36:35 +00:00
涵曦
f1635f8e32 build: 限定python版本必须是3.10和3.11 2024-07-30 06:41:52 +00:00
17 changed files with 326 additions and 71 deletions

View File

@@ -1,3 +1,63 @@
## v0.3.27 (2024-09-02)
### Feat
- Add feature as requested in issue #143
### Fix
- 默认下载目录修改
### Refactor
- 处理 code review 问题'
## v0.3.26 (2024-08-17)
### Feat
- 删除网关模式
## v0.3.25 (2024-08-16)
### Feat
- 设置页面支持配置 use_music_api 选项
## v0.3.24 (2024-08-01)
### Fix
- #131 修复多设备切换时播放模式显示错误问题
## v0.3.23 (2024-08-01)
### Fix
- 修复部分文件获取不到播放时长问题
- 处理安全问题
## v0.3.22 (2024-08-01)
### Feat
- 网关模式支持配置,默认关闭
### Fix
- 继续优化延迟问题
## v0.3.21 (2024-07-30)
### Feat
- 尝试加个网关在前面处理静态文件来加速文件获取
### Fix
- 使用前置网关处理静态文件来加速,尝试解决延迟的问题
- 播放前先立即暂停之前的音乐
## v0.3.20 (2024-07-30)
### Fix

View File

@@ -63,6 +63,8 @@ services:
XIAOMUSIC_PORT: 5678
```
如果不是首次修改端口,还需要修改 setting.json 文件里的端口。
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
> 目前除了 XIAOMUSIC_PORT 只能在启动前配置,其他参数都可以在 web 网页里配置。

View File

@@ -77,5 +77,6 @@
},
"enable_force_stop": false,
"devices": {},
"group_list": ""
}
"group_list": "",
"convert_to_mp3": false
}

47
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "dev", "lint"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:662037dc1982f7f3592bd691dffc2ffd7b7d7f7b464fa74f716403a03e3a7725"
content_hash = "sha256:0a0b1f63fdd9dd2c4ca2a777f12d294126a880631c1b3d48108d1df283ba14a8"
[[metadata.targets]]
requires_python = "==3.10.12"
@@ -25,14 +25,27 @@ files = [
{file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"},
]
[[package]]
name = "aiohappyeyeballs"
version = "2.3.4"
requires_python = "<4.0,>=3.8"
summary = "Happy Eyeballs for asyncio"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
files = [
{file = "aiohappyeyeballs-2.3.4-py3-none-any.whl", hash = "sha256:40a16ceffcf1fc9e142fd488123b2e218abc4188cf12ac20c67200e1579baa42"},
{file = "aiohappyeyeballs-2.3.4.tar.gz", hash = "sha256:7e1ae8399c320a8adec76f6c919ed5ceae6edd4c3672f4d9eae2b27e37c80ff6"},
]
[[package]]
name = "aiohttp"
version = "3.9.5"
version = "3.10.0"
requires_python = ">=3.8"
summary = "Async http client/server framework (asyncio)"
groups = ["default"]
marker = "python_full_version == \"3.10.12\""
dependencies = [
"aiohappyeyeballs>=2.3.0",
"aiosignal>=1.1.2",
"async-timeout<5.0,>=4.0; python_version < \"3.11\"",
"attrs>=17.3.0",
@@ -41,8 +54,8 @@ dependencies = [
"yarl<2.0,>=1.0",
]
files = [
{file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
{file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
{file = "aiohttp-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06ef0135d7ab7fb0284342fbbf8e8ddf73b7fee8ecc55f5c3a3d0a6b765e6d8b"},
{file = "aiohttp-3.10.0.tar.gz", hash = "sha256:e8dd7da2609303e3574c95b0ec9f1fd49647ef29b94701a2862cceae76382e1d"},
]
[[package]]
@@ -976,14 +989,14 @@ files = [
[[package]]
name = "ruff"
version = "0.5.4"
version = "0.5.5"
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.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93789f14ca2244fb91ed481456f6d0bb8af1f75a330e133b67d08f06ad85b516"},
{file = "ruff-0.5.4.tar.gz", hash = "sha256:2795726d5f71c4f4e70653273d1c23a8182f07dd8e48c12de5d867bfb7557eed"},
{file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"},
{file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"},
]
[[package]]
@@ -1094,7 +1107,7 @@ files = [
[[package]]
name = "uvicorn"
version = "0.30.3"
version = "0.30.4"
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
groups = ["default"]
@@ -1105,13 +1118,13 @@ dependencies = [
"typing-extensions>=4.0; python_version < \"3.11\"",
]
files = [
{file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
{file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
{file = "uvicorn-0.30.4-py3-none-any.whl", hash = "sha256:06b00e3087e58c6865c284143c0c42f810b32ff4f265ab19d08c566f74a08728"},
{file = "uvicorn-0.30.4.tar.gz", hash = "sha256:00db9a9e3711a5fa59866e2b02fac69d8dc70ce0814aaec9a66d1d9e5c832a30"},
]
[[package]]
name = "uvicorn"
version = "0.30.3"
version = "0.30.4"
extras = ["standard"]
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
@@ -1122,14 +1135,14 @@ dependencies = [
"httptools>=0.5.0",
"python-dotenv>=0.13",
"pyyaml>=5.1",
"uvicorn==0.30.3",
"uvicorn==0.30.4",
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
"watchfiles>=0.13",
"websockets>=10.4",
]
files = [
{file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
{file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
{file = "uvicorn-0.30.4-py3-none-any.whl", hash = "sha256:06b00e3087e58c6865c284143c0c42f810b32ff4f265ab19d08c566f74a08728"},
{file = "uvicorn-0.30.4.tar.gz", hash = "sha256:00db9a9e3711a5fa59866e2b02fac69d8dc70ce0814aaec9a66d1d9e5c832a30"},
]
[[package]]
@@ -1345,7 +1358,7 @@ files = [
[[package]]
name = "yt-dlp"
version = "2024.7.25"
version = "2024.8.1"
requires_python = ">=3.8"
summary = "A feature-rich command-line audio/video downloader"
groups = ["default"]
@@ -1361,6 +1374,6 @@ dependencies = [
"websockets>=12.0",
]
files = [
{file = "yt_dlp-2024.7.25-py3-none-any.whl", hash = "sha256:f44b5f33776b4f718900c670fe6e4698fb6fcd426455cd837cf25a1d6d4d9560"},
{file = "yt_dlp-2024.7.25.tar.gz", hash = "sha256:7587aa25e236cf7b14bdb9378bbffff51202d901b04202be0cf62cbb56d3b52c"},
{file = "yt_dlp-2024.8.1-py3-none-any.whl", hash = "sha256:d0d927038e30a05f6eab26ff6189628456ea21bb159a3d9dc2e855eef2810eac"},
{file = "yt_dlp-2024.8.1.tar.gz", hash = "sha256:4318aa523694611562f01419c8d526b662a72df34ef8ba454016b34c8366c158"},
]

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.3.20"
version = "0.3.27"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},
@@ -15,7 +15,7 @@ dependencies = [
"starlette>=0.37.2",
"aiofiles>=24.1.0",
]
requires-python = ">=3.10"
requires-python = ">=3.10,<3.12"
readme = "README.md"
license = {text = "MIT"}

View File

@@ -0,0 +1,32 @@
import math
from xiaomusic.const import (
SUPPORT_MUSIC_TYPE,
)
from xiaomusic.utils import (
get_local_music_duration,
traverse_music_directory,
)
async def test_one_music(filename):
# 获取播放时长
duration = await get_local_music_duration(filename)
sec = math.ceil(duration)
print(f"本地歌曲 : {filename} 的时长 {duration} {sec}")
async def main(directory):
# 获取所有歌曲文件
local_musics = traverse_music_directory(directory, 10, [], SUPPORT_MUSIC_TYPE)
print(local_musics)
for _, files in local_musics.items():
for file in files:
await test_one_music(file)
if __name__ == "__main__":
import asyncio
directory = "./music" # 替换为你的音乐目录路径
asyncio.run(main(directory))

View File

@@ -1 +1 @@
__version__ = "0.3.20"
__version__ = "0.3.27"

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env python3
import argparse
import json
import os
import signal
import uvicorn
@@ -75,9 +78,6 @@ def main():
options = parser.parse_args()
config = Config.from_options(options)
xiaomusic = XiaoMusic(config)
HttpInit(xiaomusic)
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
@@ -133,12 +133,34 @@ def main():
},
},
}
uvicorn.run(
HttpApp,
host=["::", "0.0.0.0"],
port=config.port,
log_config=LOGGING_CONFIG,
)
try:
filename = config.getsettingfile()
with open(filename) as f:
data = json.loads(f.read())
config.update_config(data)
except Exception as e:
print(f"Execption {e}")
def run_server(port):
xiaomusic = XiaoMusic(config)
HttpInit(xiaomusic)
uvicorn.run(
HttpApp,
host=["0.0.0.0", "::"],
port=port,
log_config=LOGGING_CONFIG,
)
def signal_handler(sig, frame):
print("主进程收到退出信号,准备退出...")
os._exit(0) # 退出主进程
# 捕获主进程的退出信号
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
port = int(config.port)
run_server(port)
if __name__ == "__main__":

View File

@@ -74,7 +74,7 @@ class Config:
music_path: str = os.getenv(
"XIAOMUSIC_MUSIC_PATH", "music"
) # 只能是music目录下的子目录
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "")
download_path: str = os.getenv("XIAOMUSIC_DOWNLOAD_PATH", "music/download")
conf_path: str = os.getenv("XIAOMUSIC_CONF_PATH", "conf")
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) # 监听端口
@@ -136,6 +136,7 @@ class Config:
remove_id3tag: bool = (
os.getenv("XIAOMUSIC_REMOVE_ID3TAG", "false").lower() == "true"
)
convert_to_mp3: bool = os.getenv("CONVERT_TO_MP3", "false").lower() == "true"
delay_sec: int = int(os.getenv("XIAOMUSIC_DELAY_SEC", 3)) # 下一首歌延迟播放秒数
def append_keyword(self, keys, action):

View File

@@ -1,6 +1,8 @@
import asyncio
import json
import mimetypes
import os
import re
import secrets
import shutil
import tempfile
@@ -8,12 +10,14 @@ from contextlib import asynccontextmanager
from dataclasses import asdict
from typing import Annotated
import aiofiles
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.responses import StreamingResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from starlette.background import BackgroundTask
from starlette.responses import FileResponse
from starlette.responses import FileResponse, Response
from xiaomusic import __version__
from xiaomusic.utils import (
@@ -79,14 +83,6 @@ def reset_http_server():
else:
app.dependency_overrides = {}
# 更新 music 链接
app.router.routes = [route for route in app.router.routes if route.path != "/music"]
app.mount(
"/music",
StaticFiles(directory=config.music_path, follow_symlink=True),
name="music",
)
def HttpInit(_xiaomusic):
global xiaomusic, config, log
@@ -170,9 +166,12 @@ async def do_cmd(data: DidCmd):
return {"ret": "Did not exist"}
if len(cmd) > 0:
await xiaomusic.cancel_all_tasks()
task = asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd))
xiaomusic.append_running_task(task)
try:
await xiaomusic.cancel_all_tasks()
task = asyncio.create_task(xiaomusic.do_check_cmd(did=did, query=cmd))
xiaomusic.append_running_task(task)
except Exception as e:
log.warning(f"Execption {e}")
return {"ret": "OK"}
return {"ret": "Unknow cmd"}
@@ -295,3 +294,66 @@ async def debug_play_by_music_url(request: Request):
return await xiaomusic.debug_play_by_music_url(arg1=data_dict)
except json.JSONDecodeError as err:
raise HTTPException(status_code=400, detail="Invalid JSON") from err
async def file_iterator(file_path, start, end):
async with aiofiles.open(file_path, mode="rb") as file:
await file.seek(start)
chunk_size = 1024
while start <= end:
read_size = min(chunk_size, end - start + 1)
data = await file.read(read_size)
if not data:
break
start += len(data)
yield data
range_pattern = re.compile(r"bytes=(\d+)-(\d*)")
@app.get("/music/{file_path:path}")
async def music_file(request: Request, file_path: str):
absolute_path = os.path.abspath(config.music_path)
absolute_file_path = os.path.normpath(os.path.join(absolute_path, file_path))
if not absolute_file_path.startswith(absolute_path):
raise HTTPException(status_code=404, detail="File not found")
if not os.path.exists(absolute_file_path):
raise HTTPException(status_code=404, detail="File not found")
file_size = os.path.getsize(absolute_file_path)
range_start, range_end = 0, file_size - 1
range_header = request.headers.get("Range")
log.info(f"music_file range_header {range_header}")
if range_header:
range_match = range_pattern.match(range_header)
if range_match:
range_start = int(range_match.group(1))
if range_match.group(2):
range_end = int(range_match.group(2))
log.info(f"music_file in range {absolute_file_path}")
log.info(f"music_file {range_start} {range_end} {absolute_file_path}")
headers = {
"Content-Range": f"bytes {range_start}-{range_end}/{file_size}",
"Accept-Ranges": "bytes",
}
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = "application/octet-stream"
return StreamingResponse(
file_iterator(absolute_file_path, range_start, range_end),
headers=headers,
status_code=206 if range_header else 200,
media_type=mime_type,
)
@app.options("/music/{file_path:path}")
async def music_options():
headers = {
"Accept-Ranges": "bytes",
}
return Response(headers=headers)

View File

@@ -53,15 +53,17 @@ $(function(){
.prop('selected', value === did);
$("#did").append(option);
if (cur_device.play_type == PLAY_TYPE_ALL) {
$("#play_type_all").css('background-color', '#b1a8f3');
$("#play_type_all").text('✔️ 全部循环');
} else if (cur_device.play_type == PLAY_TYPE_ONE) {
$("#play_type_one").css('background-color', '#b1a8f3');
$("#play_type_one").text('✔️ 单曲循环');
} else if (cur_device.play_type == PLAY_TYPE_RND) {
$("#play_type_rnd").css('background-color', '#b1a8f3');
$("#play_type_rnd").text('✔️ 随机播放');
if (value === did) {
if (cur_device.play_type == PLAY_TYPE_ALL) {
$("#play_type_all").css('background-color', '#b1a8f3');
$("#play_type_all").text('✔️ 全部循环');
} else if (cur_device.play_type == PLAY_TYPE_ONE) {
$("#play_type_one").css('background-color', '#b1a8f3');
$("#play_type_one").text('✔️ 单曲循环');
} else if (cur_device.play_type == PLAY_TYPE_RND) {
$("#play_type_rnd").css('background-color', '#b1a8f3');
$("#play_type_rnd").text('✔️ 随机播放');
}
}
}
});
@@ -72,6 +74,7 @@ $(function(){
localStorage.setItem('cur_did', did);
window.did = did;
console.log('cur_did', did);
location.reload();
})
});

View File

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

View File

@@ -3,9 +3,9 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1722319766"></script>
<script src="/static/app.js?version=1722319766"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722319766">
<script src="/static/jquery-3.7.1.min.js?version=1725237467"></script>
<script src="/static/app.js?version=1725237467"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725237467">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>

View File

@@ -4,7 +4,7 @@
<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=1722319766">
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725237467">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>

View File

@@ -3,9 +3,9 @@
<head>
<meta name="viewport" content="width=device-width">
<title>小爱音箱操控面板</title>
<script src="/static/jquery-3.7.1.min.js?version=1722319766"></script>
<script src="/static/setting.js?version=1722319766"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1722319766">
<script src="/static/jquery-3.7.1.min.js?version=1725237467"></script>
<script src="/static/setting.js?version=1725237467"></script>
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725237467">
<!--
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
@@ -98,7 +98,13 @@ var vConsole = new window.VConsole();
<label for="remove_id3tag">去除MP3 ID3v2和填充减少播放前延迟:</label>
<select id="remove_id3tag">
<option value="true" >true</option>
<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>
@@ -127,6 +133,12 @@ var vConsole = new window.VConsole();
<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="public_port">外网访问端口(0表示跟监听端口一致):</label>
<input id="public_port" type="number" value="0"></input>

View File

@@ -11,6 +11,7 @@ import random
import re
import shutil
import string
import subprocess
import tempfile
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
@@ -141,9 +142,7 @@ def _append_files_result(result, root, joinpath, files, support_extension):
result[dir_name].append(os.path.join(joinpath, file))
def traverse_music_directory(
directory, depth=10, exclude_dirs=None, support_extension=None
):
def traverse_music_directory(directory, depth, exclude_dirs, support_extension):
result = {}
for root, dirs, files in os.walk(directory, followlinks=True):
# 忽略排除的目录
@@ -247,10 +246,9 @@ async def get_local_music_duration(filename):
m = await loop.run_in_executor(None, mutagen.mp3.MP3, filename)
else:
m = await loop.run_in_executor(None, mutagen.File, filename)
if m and m.info:
duration = m.info.length
duration = m.info.length
except Exception as e:
logging.error(f"Error getting local music duration: {e}")
logging.error(f"Error getting local music {filename} duration: {e}")
return duration
@@ -333,3 +331,35 @@ def remove_id3_tags(file_path):
change = True
return change
def convert_file_to_mp3(input_file: str, ffmpeg_location: str, music_path: str) -> str:
"""
Convert the music file to MP3 format and return the path of the temporary MP3 file.
"""
# 指定临时文件的目录为 music_path 目录下的 tmp 文件夹
temp_dir = os.path.join(music_path, "tmp")
if not os.path.exists(temp_dir):
os.makedirs(temp_dir) # 确保目录存在
out_file_name = os.path.splitext(os.path.basename(input_file))[0]
out_file_path = os.path.join(temp_dir, f"{out_file_name}.mp3")
command = [
os.path.join(ffmpeg_location, "ffmpeg"),
"-i",
input_file,
"-f",
"mp3",
"-y",
out_file_path,
]
try:
subprocess.run(command, check=True)
except subprocess.CalledProcessError as e:
print(f"Error during conversion: {e}")
return None
relative_path = os.path.relpath(out_file_path, music_path)
return relative_path

View File

@@ -32,6 +32,7 @@ from xiaomusic.const import (
)
from xiaomusic.plugin import PluginManager
from xiaomusic.utils import (
convert_file_to_mp3,
custom_sort_key,
deepcopy_data_no_sensitive_info,
find_best_match,
@@ -107,6 +108,7 @@ class XiaoMusic:
self.exclude_dirs = set(self.config.exclude_dirs.split(","))
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
def update_devices(self):
self.device_id_did = {} # key 为 device_id
@@ -381,12 +383,27 @@ class XiaoMusic:
else:
self.log.info("No ID3 tag remove needed")
# 如果开启了MP3转换功能且文件不是MP3格式则进行转换
if self.convert_to_mp3 and not is_mp3(filename):
self.log.info(f"convert_to_mp3 is enabled. Checking file: {filename}")
temp_mp3_file = convert_file_to_mp3(
filename, self.config.ffmpeg_location, self.config.music_path
)
if temp_mp3_file:
self.log.info(f"Converted file: {filename} to {temp_mp3_file}")
filename = temp_mp3_file
else:
self.log.warning(f"Failed to convert file to MP3 format: {filename}")
# 构造音乐文件的URL
filename = filename.replace("\\", "/")
if filename.startswith(self.config.music_path):
filename = filename[len(self.config.music_path) :]
if filename.startswith("/"):
filename = filename[1:]
self.log.info(f"get_music_url local music. name:{name}, filename:{filename}")
encoded_name = urllib.parse.quote(filename)
return f"http://{self.hostname}:{self.public_port}/music/{encoded_name}"
@@ -925,14 +942,14 @@ class XiaoMusicDevice:
self.cur_music = name
self.log.info(f"cur_music {self.cur_music}")
sec, url = await self.xiaomusic.get_music_sec_url(name)
# await self.group_force_stop_xiaoai()
await self.group_force_stop_xiaoai()
self.log.info(f"播放 {url}")
results = await self.group_player_play(url)
if all(ele is None for ele in results):
self.log.info(f"播放 {name} 失败")
await asyncio.sleep(1)
if self.isplaying() and self._last_cmd != "stop":
await self._play_next()
await self._play_next()
return
self.log.info(f"{name}】已经开始播放了")