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

Compare commits

..

29 Commits

Author SHA1 Message Date
涵曦
f01665c998 new version v0.1.45 2024-06-15 15:04:07 +00:00
涵曦
ac23080f6a 播放列表歌曲前打乱顺序 2024-06-15 15:03:56 +00:00
涵曦
15ee6c4dd1 new version v0.1.44 2024-06-14 15:47:02 +00:00
涵曦
aeaa8f8925 fmt 2024-06-14 15:46:47 +00:00
涵曦
59d7e056c4 new version v0.1.43 2024-06-14 15:14:56 +00:00
涵曦
e79afa46b3 新增删除歌曲按钮 2024-06-14 15:14:34 +00:00
涵曦
9714f3d064 new version v0.1.41 2024-06-14 14:11:00 +00:00
涵曦
2c35c6cfd6 add XIAOMUSIC_VERBOSE env 2024-06-14 14:10:56 +00:00
涵曦
88f0ce7e51 use ruff lint and fmt code 2024-06-14 01:58:10 +00:00
涵曦
e484164fad 修复刷新列表问题 2024-06-13 14:49:36 +00:00
涵曦
aa6bce75cd update readme 2024-06-12 17:26:00 +00:00
涵曦
512efb595a new version v0.1.40 2024-06-12 17:21:13 +00:00
涵曦
e5dea8e693 新增刷新列表指令 2024-06-12 17:21:09 +00:00
涵曦
a704f8003c new version v0.1.39 2024-06-12 17:13:00 +00:00
涵曦
349a25ad58 新增播放列表功能 #51 2024-06-12 17:12:07 +00:00
涵曦
746f46edb3 new version v0.1.38 2024-06-12 15:39:23 +00:00
涵曦
4a29c7a124 fix: #70 下一首歌曲不存在时从播放列表中删除并继续找下一首 2024-06-12 01:18:08 +00:00
涵曦
0e1e412ee9 new version v0.1.37 2024-06-04 12:17:50 +00:00
涵曦
2e84f7c830 Update ci.yml 2024-06-04 18:46:17 +08:00
涵曦
61a0d68b6a Update release.yml 2024-06-04 18:45:53 +08:00
涵曦
ae90029d8e Update README.md 2024-06-04 15:14:14 +08:00
涵曦
ccc83a518c new version v0.1.36 2024-05-30 14:42:32 +00:00
涵曦
7884a5769f 继续修复启动失败问题 2024-05-30 14:42:12 +00:00
涵曦
a663bb330e new version v0.1.35 2024-05-30 13:49:52 +00:00
涵曦
346f0af543 fix: #67 没配置did时也允许启动 http 服务 2024-05-27 14:47:40 +00:00
涵曦
49ec1bb7c0 Update README.md 2024-05-20 00:03:27 +08:00
涵曦
fc0cc75dea new version v0.1.34 2024-05-19 15:53:50 +00:00
涵曦
6fc2be5d31 消除flask启动告警 2024-05-19 15:53:31 +00:00
涵曦
db680bf1ba update readme 2024-05-19 15:22:52 +00:00
15 changed files with 365 additions and 86 deletions

View File

@@ -25,6 +25,6 @@ jobs:
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
push: true push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }} tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}

View File

@@ -61,6 +61,6 @@ jobs:
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
push: true push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable

View File

@@ -2,7 +2,28 @@
使用小爱/红米音箱播放音乐,音乐使用 yt-dlp 下载。 使用小爱/红米音箱播放音乐,音乐使用 yt-dlp 下载。
## 运行 ## 最简配置运行
已经支持在 web 页面配置其他参数docker compose 配置如下:
```yaml
services:
xiaomusic:
image: hanxi/xiaomusic
container_name: xiaomusic
restart: unless-stopped
ports:
- 8090:8090
volumes:
- ./music:/app/music
environment:
MI_USER: '小米账号'
MI_PASS: '小米密码'
XIAOMUSIC_HOSTNAME: 'docker 主机 ip'
```
启动成功后,在 web 页面可以配置 MI_DID, MI_HARDWARE, XIAOMUSIC_SEARCH, XIAOMUSIC_PROXY 参数。
## 开发环境运行
- 使用 install_dependencies.sh 下载依赖 - 使用 install_dependencies.sh 下载依赖
- 使用 pdm 安装环境 - 使用 pdm 安装环境
@@ -28,6 +49,11 @@ pdm run xiaomusic.py
- 下一首 - 下一首
- 单曲循环 - 单曲循环
- 全部循环 - 全部循环
- 随机播放
- 关机
- 停止播放
- 刷新列表
- 播放列表+列表名 比如:播放列表其他
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。 > 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
@@ -50,8 +76,17 @@ pdm run xiaomusic.py
## 在 Docker 里使用 ## 在 Docker 里使用
```shell ```shell
docker run -e MI_USER=<your-xiaomi-account> -e MI_PASS=<your-xiaomi-password> -e MI_DID=<your-xiaomi-speaker-mid> -e MI_HARDWARE='L07A' -e XIAOMUSIC_PROXY=<proxy-for-yt-dlp> -e XIAOMUSIC_HOSTNAME=192.168.2.5 -e XIAOMUSIC_SEARCH='bilisearch:' -p 8090:8090 -v ./music:/app/music hanxi/xiaomusic docker run -e MI_USER=<your-xiaomi-account> \
-e MI_PASS='your-xiaomi-password' \
-e MI_DID='your-xiaomi-speaker-mid' \
-e MI_HARDWARE='L07A' \
-e XIAOMUSIC_PROXY='proxy-for-yt-dlp' \
-e XIAOMUSIC_HOSTNAME=192.168.2.5 \
-e XIAOMUSIC_SEARCH='bilisearch:' \
-p 8090:8090 \
-v ./music:/app/music hanxi/xiaomusic
``` ```
- XIAOMUSIC_SEARCH 可以配置为 'bilisearch:' 表示歌曲从哔哩哔哩下载; - XIAOMUSIC_SEARCH 可以配置为 'bilisearch:' 表示歌曲从哔哩哔哩下载;
- 配置为 'ytsearch:' 表示歌曲从 youtube 下载。 - 配置为 'ytsearch:' 表示歌曲从 youtube 下载。
- XIAOMUSIC_PROXY 用于配置代理,默认为空; - XIAOMUSIC_PROXY 用于配置代理,默认为空;
@@ -137,6 +172,19 @@ services:
- 新功能 - 新功能
- 显示正在播放的歌曲 - 显示正在播放的歌曲
- 模糊搜索本地歌曲 - 模糊搜索本地歌曲
- 设置页面
采用新的设置页面之后,必须在启动前配置的环境变量只剩下:
- MI_USER
- MI_PASS
- XIAOMUSIC_HOSTNAME
其他的这些可以在网页里配置:
- MI_DID
- MI_HARDWARE
- XIAOMUSIC_SEARCH
- XIAOMUSIC_PROXY
## 讨论区 ## 讨论区

39
pdm.lock generated
View File

@@ -2,10 +2,10 @@
# It is not intended for manual editing. # It is not intended for manual editing.
[metadata] [metadata]
groups = ["default"] groups = ["default", "lint"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:3137b159cf24210912a6a3d16e6ae42511177b211996625fe6bdabebdaec230e" content_hash = "sha256:fa83f8134ccb4a432304dead5fe1303899418558634abaa0824e8d9fdfb1f490"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@@ -648,6 +648,31 @@ files = [
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
] ]
[[package]]
name = "ruff"
version = "0.4.8"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [
{file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
{file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
{file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
{file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
{file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
{file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.9.0" version = "4.9.0"
@@ -668,6 +693,16 @@ files = [
{file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"}, {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"},
] ]
[[package]]
name = "waitress"
version = "3.0.0"
requires_python = ">=3.8.0"
summary = "Waitress WSGI server"
files = [
{file = "waitress-3.0.0-py3-none-any.whl", hash = "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669"},
{file = "waitress-3.0.0.tar.gz", hash = "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1"},
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "12.0" version = "12.0"

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "xiaomusic" name = "xiaomusic"
version = "0.1.33" version = "0.1.45"
description = "Play Music with xiaomi AI speaker" description = "Play Music with xiaomi AI speaker"
authors = [ authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"}, {name = "涵曦", email = "im.hanxi@gmail.com"},
@@ -13,6 +13,7 @@ dependencies = [
"mutagen>=1.47.0", "mutagen>=1.47.0",
"yt-dlp>=2024.04.09", "yt-dlp>=2024.04.09",
"flask[async]>=3.0.1", "flask[async]>=3.0.1",
"waitress>=3.0.0",
] ]
requires-python = ">=3.10" requires-python = ">=3.10"
readme = "README.md" readme = "README.md"
@@ -21,3 +22,31 @@ license = {text = "MIT"}
[build-system] [build-system]
requires = ["pdm-backend"] requires = ["pdm-backend"]
build-backend = "pdm.backend" build-backend = "pdm.backend"
[tool.pdm]
[tool.pdm.dev-dependencies]
lint = [
"ruff>=0.4.8",
]
[tool.ruff]
select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"E", # pycodestyle - Error
"F", # Pyflakes
"I", # isort
"W", # pycodestyle - Warning
"UP", # pyupgrade
]
ignore = [
"E501", # line-too-long
"W191", # tab-indentation
]
include = ["**/*.py", "**/*.pyi", "**/pyproject.toml"]
[tool.ruff.pydocstyle]
convention = "google"
[tool.pdm.scripts]
lint = "ruff check ."
fmt = "ruff format ."

View File

@@ -380,6 +380,9 @@ typing-extensions==4.9.0; python_version < "3.11" \
urllib3==2.0.6 \ urllib3==2.0.6 \
--hash=sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2 \ --hash=sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2 \
--hash=sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564 --hash=sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564
waitress==3.0.0 \
--hash=sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1 \
--hash=sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669
websockets==12.0 \ websockets==12.0 \
--hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \ --hash=sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b \
--hash=sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6 \ --hash=sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6 \

View File

@@ -1 +1 @@
pdm export -o requirements.txt pdm export --prod -o requirements.txt

View File

@@ -1 +1 @@
__version__ = "0.1.33" __version__ = "0.1.45"

View File

@@ -3,8 +3,7 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os import os
from dataclasses import dataclass, field from dataclasses import dataclass
from typing import Any, Iterable
from xiaomusic.utils import validate_proxy from xiaomusic.utils import validate_proxy
@@ -13,7 +12,7 @@ COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={u
HARDWARE_COMMAND_DICT = { HARDWARE_COMMAND_DICT = {
# hardware: (tts_command, wakeup_command, volume_command) # hardware: (tts_command, wakeup_command, volume_command)
"LX06": ("5-1", "5-5", "2-1"), "LX06": ("5-1", "5-5", "2-1"),
"L05B": ("5-3", "5-4", "2-1"), "L05B": ("5-3", "5-4", "2-1"),
"S12": ("5-1", "5-5", "2-1"), # 第一代小爱型号MDZ-25-DA "S12": ("5-1", "5-5", "2-1"), # 第一代小爱型号MDZ-25-DA
"S12A": ("5-1", "5-5", "2-1"), "S12A": ("5-1", "5-5", "2-1"),
"LX01": ("5-1", "5-5", "2-1"), "LX01": ("5-1", "5-5", "2-1"),
@@ -43,6 +42,8 @@ KEY_WORD_DICT = {
"关机": "stop", "关机": "stop",
"停止播放": "stop", "停止播放": "stop",
"分钟后关机": "stop_after_minute", "分钟后关机": "stop_after_minute",
"播放列表": "play_music_list",
"刷新列表": "gen_music_list",
"set_volume#": "set_volume", "set_volume#": "set_volume",
"get_volume#": "get_volume", "get_volume#": "get_volume",
} }
@@ -65,6 +66,8 @@ KEY_MATCH_ORDER = [
"随机播放", "随机播放",
"关机", "关机",
"停止播放", "停止播放",
"刷新列表",
"播放列表",
] ]
SUPPORT_MUSIC_TYPE = [ SUPPORT_MUSIC_TYPE = [
@@ -82,7 +85,7 @@ class Config:
mute_xiaoai: bool = True mute_xiaoai: bool = True
cookie: str = "" cookie: str = ""
use_command: bool = False use_command: bool = False
verbose: bool = False verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music") music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5") hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090")) port: int = int(os.getenv("XIAOMUSIC_PORT", "8090"))

View File

@@ -1,24 +1,21 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import sys
import traceback
import asyncio
from flask import Flask, request, send_from_directory
from threading import Thread from threading import Thread
from xiaomusic.config import ( from flask import Flask, request, send_from_directory
KEY_WORD_DICT, from waitress import serve
)
from xiaomusic import ( from xiaomusic import (
__version__, __version__,
) )
from xiaomusic.config import (
KEY_WORD_DICT,
)
# 隐藏 flask 启动告警 # 隐藏 flask 启动告警
# https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356 # https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356
cli = sys.modules['flask.cli'] # from flask import cli
cli.show_server_banner = lambda *x: None # cli.show_server_banner = lambda *_: None
app = Flask(__name__) app = Flask(__name__)
host = "0.0.0.0" host = "0.0.0.0"
@@ -32,6 +29,7 @@ log = None
def allcmds(): def allcmds():
return KEY_WORD_DICT return KEY_WORD_DICT
@app.route("/getversion", methods=["GET"]) @app.route("/getversion", methods=["GET"])
def getversion(): def getversion():
log.debug("getversion %s", __version__) log.debug("getversion %s", __version__)
@@ -39,6 +37,7 @@ def getversion():
"version": __version__, "version": __version__,
} }
@app.route("/getvolume", methods=["GET"]) @app.route("/getvolume", methods=["GET"])
def getvolume(): def getvolume():
volume = xiaomusic.get_volume_ret() volume = xiaomusic.get_volume_ret()
@@ -46,15 +45,18 @@ def getvolume():
"volume": volume, "volume": volume,
} }
@app.route("/searchmusic", methods=["GET"]) @app.route("/searchmusic", methods=["GET"])
def searchmusic(): def searchmusic():
name = request.args.get('name') name = request.args.get("name")
return xiaomusic.searchmusic(name) return xiaomusic.searchmusic(name)
@app.route("/playingmusic", methods=["GET"]) @app.route("/playingmusic", methods=["GET"])
def playingmusic(): def playingmusic():
return xiaomusic.playingmusic() return xiaomusic.playingmusic()
@app.route("/", methods=["GET"]) @app.route("/", methods=["GET"])
def redirect_to_index(): def redirect_to_index():
return send_from_directory("static", "index.html") return send_from_directory("static", "index.html")
@@ -70,6 +72,7 @@ async def do_cmd():
return {"ret": "OK"} return {"ret": "OK"}
return {"ret": "Unknow cmd"} return {"ret": "Unknow cmd"}
@app.route("/getsetting", methods=["GET"]) @app.route("/getsetting", methods=["GET"])
async def getsetting(): async def getsetting():
config = xiaomusic.getconfig() config = xiaomusic.getconfig()
@@ -87,6 +90,7 @@ async def getsetting():
} }
return data return data
@app.route("/savesetting", methods=["POST"]) @app.route("/savesetting", methods=["POST"])
async def savesetting(): async def savesetting():
data = request.get_json() data = request.get_json()
@@ -94,6 +98,25 @@ async def savesetting():
await xiaomusic.saveconfig(data) await xiaomusic.saveconfig(data)
return "save success" return "save success"
@app.route("/musiclist", methods=["GET"])
async def musiclist():
return xiaomusic.get_music_list()
@app.route("/curplaylist", methods=["GET"])
async def curplaylist():
return xiaomusic.get_cur_play_list()
@app.route("/delmusic", methods=["POST"])
def delmusic():
data = request.get_json()
log.info(data)
xiaomusic.del_music(data["name"])
return "success"
def static_path_handler(filename): def static_path_handler(filename):
log.debug(filename) log.debug(filename)
log.debug(static_path) log.debug(static_path)
@@ -103,7 +126,7 @@ def static_path_handler(filename):
def run_app(): def run_app():
app.run(host=host, port=port) serve(app, host=host, port=port)
def StartHTTPServer(_port, _static_path, _xiaomusic): def StartHTTPServer(_port, _static_path, _xiaomusic):

View File

@@ -1,11 +1,11 @@
$(function(){ $(function(){
$container=$("#cmds"); $container=$("#cmds");
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("随机播放"); append_op_button_name("随机播放");
append_op_button_name("刷新列表");
append_op_button_name("下一首");
append_op_button_name("关机");
$container.append($("<hr>")); $container.append($("<hr>"));
@@ -26,6 +26,60 @@ $(function(){
$("#version").text(`(${data.version})`); $("#version").text(`(${data.version})`);
}); });
// 拉取播放列表
function refresh_music_list() {
$('#music_list').empty();
$.get("/musiclist", function(data, status) {
console.log(data, status);
$.each(data, function(key, value) {
$('#music_list').append($('<option></option>').val(key).text(key));
});
$('#music_list').change(function() {
const selectedValue = $(this).val();
$('#music_name').empty();
$.each(data[selectedValue], function(index, item) {
$('#music_name').append($('<option></option>').val(item).text(item));
});
});
$('#music_list').trigger('change');
// 获取当前播放列表
$.get("curplaylist", function(data, status) {
$('#music_list').val(data);
$('#music_list').trigger('change');
})
})
}
refresh_music_list();
$("#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);
});
$("#del_music").on("click", () => {
var del_music_name = $("#music_name").val();
if (confirm(`确定删除歌曲 ${del_music_name} 吗?`)) {
console.log(`删除歌曲 ${del_music_name}`);
$.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 append_op_button_name(name) { function append_op_button_name(name) {
append_op_button(name, name); append_op_button(name, name);
@@ -48,8 +102,8 @@ $(function(){
$("#play").on("click", () => { $("#play").on("click", () => {
var search_key = $("#music-name").val(); var search_key = $("#music-name").val();
var filename=$("#music-filename").val(); var filename = $("#music-filename").val();
let cmd = "播放歌曲"+search_key+"|"+filename; let cmd = "播放歌曲" + search_key + "|" + filename;
sendcmd(cmd); sendcmd(cmd);
}); });
@@ -62,10 +116,12 @@ $(function(){
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: "/cmd", url: "/cmd",
contentType: "application/json", contentType: "application/json; charset=utf-8",
data: JSON.stringify({cmd: cmd}), data: JSON.stringify({cmd: cmd}),
success: () => { success: () => {
// 请求成功时执行的操作 if (cmd == "刷新列表") {
setTimeout(refresh_music_list, 3000);
}
}, },
error: () => { error: () => {
// 请求失败时执行的操作 // 请求失败时执行的操作

View File

@@ -33,6 +33,16 @@
<div id="playering-music" class="text"></div> <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>
<footer> <footer>
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p> <p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
</footer> </footer>

View File

@@ -62,7 +62,6 @@ input,select {
} }
footer { footer {
position: fixed;
bottom: 0; bottom: 0;
width: 100%; width: 100%;
text-align: center; text-align: center;

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import os
import re
import socket
from http.cookies import SimpleCookie
from typing import AsyncIterator
from urllib.parse import urlparse
import difflib import difflib
import re
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
from urllib.parse import urlparse
from requests.utils import cookiejar_from_dict from requests.utils import cookiejar_from_dict
@@ -62,6 +60,7 @@ def validate_proxy(proxy_str: str) -> bool:
return True return True
# 模糊搜索 # 模糊搜索
def fuzzyfinder(user_input, collection): def fuzzyfinder(user_input, collection):
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1) return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1)

View File

@@ -3,45 +3,43 @@ import asyncio
import json import json
import logging import logging
import os import os
import queue
import random import random
import re import re
import time import time
import urllib.parse
import traceback import traceback
import mutagen import urllib.parse
import queue
from xiaomusic.httpserver import StartHTTPServer
from pathlib import Path from pathlib import Path
import mutagen
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiIOService, MiNAService, miio_command from miservice import MiAccount, MiIOService, MiNAService, miio_command
from rich import print
from rich.logging import RichHandler from rich.logging import RichHandler
from xiaomusic.config import (
COOKIE_TEMPLATE,
LATEST_ASK_API,
KEY_WORD_DICT,
KEY_WORD_ARG_BEFORE_DICT,
KEY_MATCH_ORDER,
SUPPORT_MUSIC_TYPE,
Config,
)
from xiaomusic.utils import (
parse_cookie_string,
fuzzyfinder,
)
from xiaomusic import ( from xiaomusic import (
__version__, __version__,
) )
from xiaomusic.config import (
COOKIE_TEMPLATE,
KEY_MATCH_ORDER,
KEY_WORD_ARG_BEFORE_DICT,
KEY_WORD_DICT,
LATEST_ASK_API,
SUPPORT_MUSIC_TYPE,
Config,
)
from xiaomusic.httpserver import StartHTTPServer
from xiaomusic.utils import (
fuzzyfinder,
parse_cookie_string,
)
EOF = object() EOF = object()
PLAY_TYPE_ONE = 0 # 单曲循环 PLAY_TYPE_ONE = 0 # 单曲循环
PLAY_TYPE_ALL = 1 # 全部循环 PLAY_TYPE_ALL = 1 # 全部循环
class XiaoMusic: class XiaoMusic:
def __init__(self, config: Config): def __init__(self, config: Config):
self.config = config self.config = config
@@ -75,6 +73,8 @@ class XiaoMusic:
self._volume = 0 self._volume = 0
self._all_music = {} self._all_music = {}
self._play_list = [] self._play_list = []
self._cur_play_list = ""
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
self._playing = False self._playing = False
# 关机定时器 # 关机定时器
@@ -84,7 +84,7 @@ class XiaoMusic:
logging.basicConfig( logging.basicConfig(
format=f"[{__version__}]\t%(message)s", format=f"[{__version__}]\t%(message)s",
datefmt="[%X]", datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)] handlers=[RichHandler(rich_tracebacks=True)],
) )
self.log = logging.getLogger("xiaomusic") self.log = logging.getLogger("xiaomusic")
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO) self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
@@ -94,7 +94,7 @@ class XiaoMusic:
self.try_init_setting() self.try_init_setting()
# 启动时重新生成一次播放列表 # 启动时重新生成一次播放列表
self.gen_all_music_list() self._gen_all_music_list()
# 启动时初始化获取声音 # 启动时初始化获取声音
self.set_last_record("get_volume#") self.set_last_record("get_volume#")
@@ -154,7 +154,7 @@ class XiaoMusic:
self.device_id = h.get("deviceID") self.device_id = h.get("deviceID")
break break
else: else:
raise Exception( self.log.error(
f"we have no hardware: {self.config.hardware} please use `micli mina` to check" f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
) )
@@ -172,7 +172,7 @@ class XiaoMusic:
if d["model"].endswith(self.config.hardware.lower()) if d["model"].endswith(self.config.hardware.lower())
) )
except StopIteration: except StopIteration:
raise Exception( self.log.error(
f"cannot find did for hardware: {self.config.hardware} " f"cannot find did for hardware: {self.config.hardware} "
"please set it via MI_DID env" "please set it via MI_DID env"
) )
@@ -288,7 +288,10 @@ class XiaoMusic:
def is_downloading(self): def is_downloading(self):
if not self.download_proc: if not self.download_proc:
return False return False
if self.download_proc.returncode != None and self.download_proc.returncode < 0: if (
self.download_proc.returncode is not None
and self.download_proc.returncode < 0
):
return False return False
return True return True
@@ -340,9 +343,16 @@ class XiaoMusic:
return f"http://{self.hostname}:{self.port}/{encoded_name}" return f"http://{self.hostname}:{self.port}/{encoded_name}"
# 递归获取目录下所有歌曲,生成随机播放列表 # 递归获取目录下所有歌曲,生成随机播放列表
def gen_all_music_list(self): def _gen_all_music_list(self):
self._all_music = {} self._all_music = {}
all_music_by_dir = {}
for root, dirs, filenames in os.walk(self.music_path): for root, dirs, filenames in os.walk(self.music_path):
self.log.debug("root:%s dirs:%s music_path:%s", root, dirs, self.music_path)
dir_name = os.path.basename(root)
if self.music_path == root:
dir_name = "其他"
if dir_name not in all_music_by_dir:
all_music_by_dir[dir_name] = {}
for filename in filenames: for filename in filenames:
self.log.debug("gen_all_music_list. filename:%s", filename) self.log.debug("gen_all_music_list. filename:%s", filename)
# 过滤隐藏文件 # 过滤隐藏文件
@@ -361,10 +371,20 @@ class XiaoMusic:
# 歌曲名字相同会覆盖 # 歌曲名字相同会覆盖
self._all_music[name] = os.path.join(root, filename) self._all_music[name] = os.path.join(root, filename)
all_music_by_dir[dir_name][name] = True
pass
self._play_list = list(self._all_music.keys()) self._play_list = list(self._all_music.keys())
self._cur_play_list = "全部"
random.shuffle(self._play_list) random.shuffle(self._play_list)
self.log.debug(self._all_music) self.log.debug(self._all_music)
self._music_list = {}
self._music_list["全部"] = self._play_list
for dir_name, musics in all_music_by_dir.items():
self._music_list[dir_name] = list(musics.keys())
self.log.debug("dir_name:%s, list:%s", dir_name, self._music_list[dir_name])
pass
# 把下载的音乐加入播放列表 # 把下载的音乐加入播放列表
def add_download_music(self, name): def add_download_music(self, name):
self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3") self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3")
@@ -377,7 +397,7 @@ class XiaoMusic:
def get_next_music(self): def get_next_music(self):
play_list_len = len(self._play_list) play_list_len = len(self._play_list)
if play_list_len == 0: if play_list_len == 0:
self.log.warning(f"没有随机到歌曲") self.log.warning("没有随机到歌曲")
return "" return ""
# 随机选择一个文件 # 随机选择一个文件
index = 0 index = 0
@@ -388,8 +408,13 @@ class XiaoMusic:
next_index = index + 1 next_index = index + 1
if next_index >= play_list_len: if next_index >= play_list_len:
next_index = 0 next_index = 0
filename = self._play_list[next_index] name = self._play_list[next_index]
return filename filename = self.get_filename(name)
if len(filename) <= 0:
self._play_list.pop(next_index)
self.log.info(f"pop not exist music:{name}")
return self.get_next_music()
return name
# 获取文件播放时长 # 获取文件播放时长
def get_file_duration(self, filename): def get_file_duration(self, filename):
@@ -406,7 +431,7 @@ class XiaoMusic:
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec}") self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec}")
if self._next_timer: if self._next_timer:
self._next_timer.cancel() self._next_timer.cancel()
self.log.info(f"定时器已取消") self.log.info("定时器已取消")
self._timeout = sec self._timeout = sec
async def _do_next(): async def _do_next():
@@ -426,11 +451,11 @@ class XiaoMusic:
await self.init_all_data(session) await self.init_all_data(session)
task = asyncio.create_task(self.poll_latest_ask()) task = asyncio.create_task(self.poll_latest_ask())
assert task is not None # to keep the reference to task, do not remove this assert task is not None # to keep the reference to task, do not remove this
filtered_keywords = [keyword for keyword in KEY_MATCH_ORDER if "#" not in keyword] filtered_keywords = [
keyword for keyword in KEY_MATCH_ORDER if "#" not in keyword
]
joined_keywords = "/".join(filtered_keywords) joined_keywords = "/".join(filtered_keywords)
self.log.info( self.log.info(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制")
f"Running xiaomusic now, 用`{joined_keywords}`开头来控制"
)
while True: while True:
self.polling_event.set() self.polling_event.set()
@@ -537,39 +562,76 @@ class XiaoMusic:
if self.play_type == PLAY_TYPE_ALL or name == "": if self.play_type == PLAY_TYPE_ALL or name == "":
name = self.get_next_music() name = self.get_next_music()
if name == "": if name == "":
await self.do_tts(f"本地没有歌曲") await self.do_tts("本地没有歌曲")
return return
await self.play(arg1=name) await self.play(arg1=name)
# 单曲循环 # 单曲循环
async def set_play_type_one(self, **kwargs): async def set_play_type_one(self, **kwargs):
self.play_type = PLAY_TYPE_ONE self.play_type = PLAY_TYPE_ONE
await self.do_tts(f"已经设置为单曲循环") await self.do_tts("已经设置为单曲循环")
# 全部循环 # 全部循环
async def set_play_type_all(self, **kwargs): async def set_play_type_all(self, **kwargs):
self.play_type = PLAY_TYPE_ALL self.play_type = PLAY_TYPE_ALL
await self.do_tts(f"已经设置为全部循环") await self.do_tts("已经设置为全部循环")
# 随机播放 # 随机播放
async def random_play(self, **kwargs): async def random_play(self, **kwargs):
self.play_type = PLAY_TYPE_ALL self.play_type = PLAY_TYPE_ALL
await self.do_tts(f"已经设置为全部循环并随机播放") random.shuffle(self._play_list)
# 重新生成随机播放列表 await self.do_tts("已经设置为随机播放")
self.gen_all_music_list()
await self.play_next() # 刷新列表
async def gen_music_list(self, **kwargs):
self._gen_all_music_list()
await self.do_tts("生成播放列表完毕")
# 删除歌曲
def del_music(self, name):
filename = self.get_filename(name)
if filename == "":
self.log.info(f"${name} not exist")
return
try:
os.remove(filename)
self.log.info(f"del ${filename} success")
except OSError:
self.log.error(f"del ${filename} failed")
pass
self._gen_all_music_list()
# 播放一个播放列表
async def play_music_list(self, **kwargs):
parts = kwargs["arg1"].split("|")
list_name = parts[0]
if list_name not in self._music_list:
await self.do_tts(f"播放列表{list_name}不存在")
return
self._play_list = self._music_list[list_name]
self._cur_play_list = list_name
random.shuffle(self._play_list)
self.log.info(f"开始播放列表{list_name}")
music_name = ""
if len(parts) > 1:
music_name = parts[1]
else:
music_name = self.get_next_music()
await self.play(arg1=music_name)
async def stop(self, **kwargs): async def stop(self, **kwargs):
self._playing = False self._playing = False
if self._next_timer: if self._next_timer:
self._next_timer.cancel() self._next_timer.cancel()
self.log.info(f"定时器已取消") self.log.info("定时器已取消")
self.cur_music = ""
await self.force_stop_xiaoai() await self.force_stop_xiaoai()
async def stop_after_minute(self, **kwargs): async def stop_after_minute(self, **kwargs):
if self._stop_timer: if self._stop_timer:
self._stop_timer.cancel() self._stop_timer.cancel()
self.log.info(f"关机定时器已取消") self.log.info("关机定时器已取消")
minute = int(kwargs["arg1"]) minute = int(kwargs["arg1"])
async def _do_stop(): async def _do_stop():
@@ -589,7 +651,9 @@ class XiaoMusic:
async def get_volume(self, **kwargs): async def get_volume(self, **kwargs):
playing_info = await self.mina_service.player_get_status(self.device_id) playing_info = await self.mina_service.player_get_status(self.device_id)
self.log.debug("get_volume. playing_info:%s", playing_info) self.log.debug("get_volume. playing_info:%s", playing_info)
self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get("volume", 5) self._volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
"volume", 5
)
self.log.info("get_volume. volume:%s", self._volume) self.log.info("get_volume. volume:%s", self._volume)
def get_volume_ret(self): def get_volume_ret(self):
@@ -597,10 +661,19 @@ class XiaoMusic:
# 搜索音乐 # 搜索音乐
def searchmusic(self, name): def searchmusic(self, name):
search_list = fuzzyfinder(name, self._play_list) all_music_list = list(self._all_music.keys())
search_list = fuzzyfinder(name, all_music_list)
self.log.debug("searchmusic. name:%s search_list:%s", name, search_list) self.log.debug("searchmusic. name:%s search_list:%s", name, search_list)
return search_list return search_list
# 获取播放列表
def get_music_list(self):
return self._music_list
# 获取当前的播放列表
def get_cur_play_list(self):
return self._cur_play_list
# 正在播放中的音乐 # 正在播放中的音乐
def playingmusic(self): def playingmusic(self):
self.log.debug("playingmusic. cur_music:%s", self.cur_music) self.log.debug("playingmusic. cur_music:%s", self.cur_music)
@@ -625,7 +698,7 @@ class XiaoMusic:
async def saveconfig(self, data): async def saveconfig(self, data):
# 默认暂时配置保存到 music 目录下 # 默认暂时配置保存到 music 目录下
filename = os.path.join(self.music_path, "setting.json") filename = os.path.join(self.music_path, "setting.json")
with open(filename, 'w', encoding='utf-8') as f: with open(filename, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4) json.dump(data, f, ensure_ascii=False, indent=4)
self.update_config_from_setting(data) self.update_config_from_setting(data)
await self.call_main_thread_function(self.reinit) await self.call_main_thread_function(self.reinit)
@@ -670,12 +743,13 @@ class XiaoMusic:
async def call_main_thread_function(self, func, arg1=None): async def call_main_thread_function(self, func, arg1=None):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
future = loop.create_future() future = loop.create_future()
def callback(ret): def callback(ret):
nonlocal future nonlocal future
loop.call_soon_threadsafe(future.set_result, ret) loop.call_soon_threadsafe(future.set_result, ret)
self.queue.put((func, callback, arg1)) self.queue.put((func, callback, arg1))
self.last_record = None self.last_record = None
self.new_record_event.set() self.new_record_event.set()
result = await future result = await future
return result return result