mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28797edc7c | ||
|
|
be1a643071 | ||
|
|
ee6b9778ac | ||
|
|
881c34bcb5 | ||
|
|
c22fc99235 | ||
|
|
0874efe58b | ||
|
|
f01665c998 | ||
|
|
ac23080f6a | ||
|
|
15ee6c4dd1 | ||
|
|
aeaa8f8925 | ||
|
|
59d7e056c4 | ||
|
|
e79afa46b3 | ||
|
|
9714f3d064 | ||
|
|
2c35c6cfd6 | ||
|
|
88f0ce7e51 | ||
|
|
e484164fad | ||
|
|
aa6bce75cd | ||
|
|
512efb595a | ||
|
|
e5dea8e693 | ||
|
|
a704f8003c | ||
|
|
349a25ad58 | ||
|
|
746f46edb3 | ||
|
|
4a29c7a124 | ||
|
|
0e1e412ee9 | ||
|
|
2e84f7c830 | ||
|
|
61a0d68b6a | ||
|
|
ae90029d8e | ||
|
|
ccc83a518c | ||
|
|
7884a5769f |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -25,6 +25,6 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -61,6 +61,6 @@ jobs:
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable
|
||||
|
||||
11
README.md
11
README.md
@@ -49,6 +49,11 @@ pdm run xiaomusic.py
|
||||
- 下一首
|
||||
- 单曲循环
|
||||
- 全部循环
|
||||
- 随机播放
|
||||
- 关机
|
||||
- 停止播放
|
||||
- 刷新列表
|
||||
- 播放列表+列表名 比如:播放列表其他
|
||||
|
||||
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。
|
||||
|
||||
@@ -72,10 +77,10 @@ pdm run xiaomusic.py
|
||||
|
||||
```shell
|
||||
docker run -e MI_USER=<your-xiaomi-account> \
|
||||
-e MI_PASS=<your-xiaomi-password> \
|
||||
-e MI_DID=<your-xiaomi-speaker-mid> \
|
||||
-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_PROXY='proxy-for-yt-dlp' \
|
||||
-e XIAOMUSIC_HOSTNAME=192.168.2.5 \
|
||||
-e XIAOMUSIC_SEARCH='bilisearch:' \
|
||||
-p 8090:8090 \
|
||||
|
||||
29
pdm.lock
generated
29
pdm.lock
generated
@@ -2,10 +2,10 @@
|
||||
# It is not intended for manual editing.
|
||||
|
||||
[metadata]
|
||||
groups = ["default"]
|
||||
groups = ["default", "lint"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:d771311a452ca58665efe3b74af341cb202d75d83a250896c293ea9c696e5696"
|
||||
content_hash = "sha256:fa83f8134ccb4a432304dead5fe1303899418558634abaa0824e8d9fdfb1f490"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@@ -648,6 +648,31 @@ files = [
|
||||
{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]]
|
||||
name = "typing-extensions"
|
||||
version = "4.9.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.1.35"
|
||||
version = "0.1.48"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
@@ -24,3 +24,29 @@ requires = ["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 ."
|
||||
|
||||
@@ -1 +1 @@
|
||||
pdm export -o requirements.txt
|
||||
pdm export --prod -o requirements.txt
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.1.35"
|
||||
__version__ = "0.1.48"
|
||||
|
||||
@@ -3,8 +3,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from xiaomusic.utils import validate_proxy
|
||||
|
||||
@@ -13,7 +12,7 @@ COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={u
|
||||
HARDWARE_COMMAND_DICT = {
|
||||
# hardware: (tts_command, wakeup_command, volume_command)
|
||||
"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
|
||||
"S12A": ("5-1", "5-5", "2-1"),
|
||||
"LX01": ("5-1", "5-5", "2-1"),
|
||||
@@ -43,6 +42,8 @@ KEY_WORD_DICT = {
|
||||
"关机": "stop",
|
||||
"停止播放": "stop",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"set_volume#": "set_volume",
|
||||
"get_volume#": "get_volume",
|
||||
}
|
||||
@@ -65,6 +66,8 @@ KEY_MATCH_ORDER = [
|
||||
"随机播放",
|
||||
"关机",
|
||||
"停止播放",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
]
|
||||
|
||||
SUPPORT_MUSIC_TYPE = [
|
||||
@@ -82,7 +85,7 @@ class Config:
|
||||
mute_xiaoai: bool = True
|
||||
cookie: str = ""
|
||||
use_command: bool = False
|
||||
verbose: bool = False
|
||||
verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true"
|
||||
music_path: str = os.getenv("XIAOMUSIC_MUSIC_PATH", "music")
|
||||
hostname: str = os.getenv("XIAOMUSIC_HOSTNAME", "192.168.2.5")
|
||||
port: int = int(os.getenv("XIAOMUSIC_PORT", "8090"))
|
||||
@@ -92,6 +95,7 @@ class Config:
|
||||
) # "bilisearch:" or "ytsearch:"
|
||||
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
|
||||
active_cmd: str = os.getenv("XIAOMUSIC_ACTIVE_CMD", "play,random_play")
|
||||
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.proxy:
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import asyncio
|
||||
from threading import Thread
|
||||
|
||||
from flask import Flask, request, send_from_directory
|
||||
from waitress import serve
|
||||
from threading import Thread
|
||||
|
||||
from xiaomusic.config import (
|
||||
KEY_WORD_DICT,
|
||||
)
|
||||
|
||||
from xiaomusic import (
|
||||
__version__,
|
||||
)
|
||||
from xiaomusic.config import (
|
||||
KEY_WORD_DICT,
|
||||
)
|
||||
|
||||
# 隐藏 flask 启动告警
|
||||
# https://gist.github.com/jerblack/735b9953ba1ab6234abb43174210d356
|
||||
#from flask import cli
|
||||
#cli.show_server_banner = lambda *_: None
|
||||
# from flask import cli
|
||||
# cli.show_server_banner = lambda *_: None
|
||||
|
||||
app = Flask(__name__)
|
||||
host = "0.0.0.0"
|
||||
@@ -33,6 +29,7 @@ log = None
|
||||
def allcmds():
|
||||
return KEY_WORD_DICT
|
||||
|
||||
|
||||
@app.route("/getversion", methods=["GET"])
|
||||
def getversion():
|
||||
log.debug("getversion %s", __version__)
|
||||
@@ -40,6 +37,7 @@ def getversion():
|
||||
"version": __version__,
|
||||
}
|
||||
|
||||
|
||||
@app.route("/getvolume", methods=["GET"])
|
||||
def getvolume():
|
||||
volume = xiaomusic.get_volume_ret()
|
||||
@@ -47,15 +45,18 @@ def getvolume():
|
||||
"volume": volume,
|
||||
}
|
||||
|
||||
|
||||
@app.route("/searchmusic", methods=["GET"])
|
||||
def searchmusic():
|
||||
name = request.args.get('name')
|
||||
name = request.args.get("name")
|
||||
return xiaomusic.searchmusic(name)
|
||||
|
||||
|
||||
@app.route("/playingmusic", methods=["GET"])
|
||||
def playingmusic():
|
||||
return xiaomusic.playingmusic()
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
def redirect_to_index():
|
||||
return send_from_directory("static", "index.html")
|
||||
@@ -71,6 +72,7 @@ async def do_cmd():
|
||||
return {"ret": "OK"}
|
||||
return {"ret": "Unknow cmd"}
|
||||
|
||||
|
||||
@app.route("/getsetting", methods=["GET"])
|
||||
async def getsetting():
|
||||
config = xiaomusic.getconfig()
|
||||
@@ -88,6 +90,7 @@ async def getsetting():
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
@app.route("/savesetting", methods=["POST"])
|
||||
async def savesetting():
|
||||
data = request.get_json()
|
||||
@@ -95,6 +98,25 @@ async def savesetting():
|
||||
await xiaomusic.saveconfig(data)
|
||||
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):
|
||||
log.debug(filename)
|
||||
log.debug(static_path)
|
||||
@@ -102,9 +124,11 @@ def static_path_handler(filename):
|
||||
log.debug(absolute_path)
|
||||
return send_from_directory(absolute_path, filename)
|
||||
|
||||
|
||||
def run_app():
|
||||
serve(app, host=host, port=port)
|
||||
|
||||
|
||||
def StartHTTPServer(_port, _static_path, _xiaomusic):
|
||||
global port, static_path, xiaomusic, log
|
||||
port = _port
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
$(function(){
|
||||
$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("关机");
|
||||
|
||||
$container.append($("<hr>"));
|
||||
|
||||
@@ -26,6 +26,60 @@ $(function(){
|
||||
$("#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) {
|
||||
append_op_button(name, name);
|
||||
@@ -48,8 +102,8 @@ $(function(){
|
||||
|
||||
$("#play").on("click", () => {
|
||||
var search_key = $("#music-name").val();
|
||||
var filename=$("#music-filename").val();
|
||||
let cmd = "播放歌曲"+search_key+"|"+filename;
|
||||
var filename = $("#music-filename").val();
|
||||
let cmd = "播放歌曲" + search_key + "|" + filename;
|
||||
sendcmd(cmd);
|
||||
});
|
||||
|
||||
@@ -62,10 +116,12 @@ $(function(){
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({cmd: cmd}),
|
||||
success: () => {
|
||||
// 请求成功时执行的操作
|
||||
if (cmd == "刷新列表") {
|
||||
setTimeout(refresh_music_list, 3000);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
|
||||
@@ -33,6 +33,16 @@
|
||||
<div id="playering-music" class="text"></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>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
</footer>
|
||||
|
||||
@@ -62,7 +62,6 @@ input,select {
|
||||
}
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
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 re
|
||||
from collections.abc import AsyncIterator
|
||||
from http.cookies import SimpleCookie
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from requests.utils import cookiejar_from_dict
|
||||
|
||||
@@ -62,6 +60,7 @@ def validate_proxy(proxy_str: str) -> bool:
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# 模糊搜索
|
||||
def fuzzyfinder(user_input, collection):
|
||||
return difflib.get_close_matches(user_input, collection, 10, cutoff=0.1)
|
||||
|
||||
@@ -3,45 +3,43 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import queue
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import urllib.parse
|
||||
import traceback
|
||||
import mutagen
|
||||
import queue
|
||||
from xiaomusic.httpserver import StartHTTPServer
|
||||
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
import mutagen
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from miservice import MiAccount, MiIOService, MiNAService, miio_command
|
||||
from rich import print
|
||||
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 (
|
||||
__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()
|
||||
|
||||
PLAY_TYPE_ONE = 0 # 单曲循环
|
||||
PLAY_TYPE_ALL = 1 # 全部循环
|
||||
|
||||
|
||||
class XiaoMusic:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
@@ -64,6 +62,7 @@ class XiaoMusic:
|
||||
self.search_prefix = config.search_prefix
|
||||
self.ffmpeg_location = config.ffmpeg_location
|
||||
self.active_cmd = config.active_cmd.split(",")
|
||||
self.exclude_dirs = set(config.exclude_dirs.split(","))
|
||||
|
||||
# 下载对象
|
||||
self.download_proc = None
|
||||
@@ -75,6 +74,8 @@ class XiaoMusic:
|
||||
self._volume = 0
|
||||
self._all_music = {}
|
||||
self._play_list = []
|
||||
self._cur_play_list = ""
|
||||
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
|
||||
self._playing = False
|
||||
|
||||
# 关机定时器
|
||||
@@ -84,7 +85,7 @@ class XiaoMusic:
|
||||
logging.basicConfig(
|
||||
format=f"[{__version__}]\t%(message)s",
|
||||
datefmt="[%X]",
|
||||
handlers=[RichHandler(rich_tracebacks=True)]
|
||||
handlers=[RichHandler(rich_tracebacks=True)],
|
||||
)
|
||||
self.log = logging.getLogger("xiaomusic")
|
||||
self.log.setLevel(logging.DEBUG if config.verbose else logging.INFO)
|
||||
@@ -94,7 +95,7 @@ class XiaoMusic:
|
||||
self.try_init_setting()
|
||||
|
||||
# 启动时重新生成一次播放列表
|
||||
self.gen_all_music_list()
|
||||
self._gen_all_music_list()
|
||||
|
||||
# 启动时初始化获取声音
|
||||
self.set_last_record("get_volume#")
|
||||
@@ -154,7 +155,7 @@ class XiaoMusic:
|
||||
self.device_id = h.get("deviceID")
|
||||
break
|
||||
else:
|
||||
raise Exception(
|
||||
self.log.error(
|
||||
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
|
||||
)
|
||||
|
||||
@@ -288,7 +289,10 @@ class XiaoMusic:
|
||||
def is_downloading(self):
|
||||
if not self.download_proc:
|
||||
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 True
|
||||
|
||||
@@ -340,9 +344,17 @@ class XiaoMusic:
|
||||
return f"http://{self.hostname}:{self.port}/{encoded_name}"
|
||||
|
||||
# 递归获取目录下所有歌曲,生成随机播放列表
|
||||
def gen_all_music_list(self):
|
||||
def _gen_all_music_list(self):
|
||||
self._all_music = {}
|
||||
all_music_by_dir = {}
|
||||
for root, dirs, filenames in os.walk(self.music_path):
|
||||
dirs[:] = [d for d in dirs if d not in self.exclude_dirs]
|
||||
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:
|
||||
self.log.debug("gen_all_music_list. filename:%s", filename)
|
||||
# 过滤隐藏文件
|
||||
@@ -361,10 +373,20 @@ class XiaoMusic:
|
||||
|
||||
# 歌曲名字相同会覆盖
|
||||
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._cur_play_list = "全部"
|
||||
random.shuffle(self._play_list)
|
||||
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):
|
||||
self._all_music[name] = os.path.join(self.music_path, f"{name}.mp3")
|
||||
@@ -377,7 +399,7 @@ class XiaoMusic:
|
||||
def get_next_music(self):
|
||||
play_list_len = len(self._play_list)
|
||||
if play_list_len == 0:
|
||||
self.log.warning(f"没有随机到歌曲")
|
||||
self.log.warning("没有随机到歌曲")
|
||||
return ""
|
||||
# 随机选择一个文件
|
||||
index = 0
|
||||
@@ -388,8 +410,13 @@ class XiaoMusic:
|
||||
next_index = index + 1
|
||||
if next_index >= play_list_len:
|
||||
next_index = 0
|
||||
filename = self._play_list[next_index]
|
||||
return filename
|
||||
name = self._play_list[next_index]
|
||||
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):
|
||||
@@ -406,7 +433,7 @@ class XiaoMusic:
|
||||
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec} 秒")
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info(f"定时器已取消")
|
||||
self.log.info("定时器已取消")
|
||||
self._timeout = sec
|
||||
|
||||
async def _do_next():
|
||||
@@ -426,11 +453,11 @@ class XiaoMusic:
|
||||
await self.init_all_data(session)
|
||||
task = asyncio.create_task(self.poll_latest_ask())
|
||||
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)
|
||||
self.log.info(
|
||||
f"Running xiaomusic now, 用`{joined_keywords}`开头来控制"
|
||||
)
|
||||
self.log.info(f"Running xiaomusic now, 用`{joined_keywords}`开头来控制")
|
||||
|
||||
while True:
|
||||
self.polling_event.set()
|
||||
@@ -498,17 +525,35 @@ class XiaoMusic:
|
||||
return ("stop", {})
|
||||
return (None, None)
|
||||
|
||||
# 判断是否播放一下私募歌曲
|
||||
def check_play_next(self):
|
||||
# 当前没我在播放的歌曲
|
||||
if self.cur_music == "":
|
||||
return True
|
||||
else:
|
||||
filename = self.get_filename(self.cur_music)
|
||||
# 当前播放的歌曲不存在了
|
||||
if len(filename) <= 0:
|
||||
return True
|
||||
pass
|
||||
return False
|
||||
|
||||
# 播放歌曲
|
||||
async def play(self, **kwargs):
|
||||
self._playing = True
|
||||
parts = kwargs["arg1"].split("|")
|
||||
search_key = parts[0]
|
||||
name = parts[1] if len(parts) > 1 else search_key
|
||||
if search_key == "" and name == "":
|
||||
await self.play_next()
|
||||
return
|
||||
if name == "":
|
||||
name = search_key
|
||||
|
||||
if search_key == "" and name == "":
|
||||
if self.check_play_next():
|
||||
await self.play_next()
|
||||
return
|
||||
else:
|
||||
name = self.cur_music
|
||||
|
||||
self.log.debug("play. search_key:%s name:%s", search_key, name)
|
||||
filename = self.get_filename(name)
|
||||
|
||||
@@ -537,39 +582,75 @@ class XiaoMusic:
|
||||
if self.play_type == PLAY_TYPE_ALL or name == "":
|
||||
name = self.get_next_music()
|
||||
if name == "":
|
||||
await self.do_tts(f"本地没有歌曲")
|
||||
await self.do_tts("本地没有歌曲")
|
||||
return
|
||||
await self.play(arg1=name)
|
||||
|
||||
# 单曲循环
|
||||
async def set_play_type_one(self, **kwargs):
|
||||
self.play_type = PLAY_TYPE_ONE
|
||||
await self.do_tts(f"已经设置为单曲循环")
|
||||
await self.do_tts("已经设置为单曲循环")
|
||||
|
||||
# 全部循环
|
||||
async def set_play_type_all(self, **kwargs):
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
await self.do_tts(f"已经设置为全部循环")
|
||||
await self.do_tts("已经设置为全部循环")
|
||||
|
||||
# 随机播放
|
||||
async def random_play(self, **kwargs):
|
||||
self.play_type = PLAY_TYPE_ALL
|
||||
await self.do_tts(f"已经设置为全部循环并随机播放")
|
||||
# 重新生成随机播放列表
|
||||
self.gen_all_music_list()
|
||||
await self.play_next()
|
||||
random.shuffle(self._play_list)
|
||||
await self.do_tts("已经设置为随机播放")
|
||||
|
||||
# 刷新列表
|
||||
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):
|
||||
self._playing = False
|
||||
if self._next_timer:
|
||||
self._next_timer.cancel()
|
||||
self.log.info(f"定时器已取消")
|
||||
self.log.info("定时器已取消")
|
||||
await self.force_stop_xiaoai()
|
||||
|
||||
async def stop_after_minute(self, **kwargs):
|
||||
if self._stop_timer:
|
||||
self._stop_timer.cancel()
|
||||
self.log.info(f"关机定时器已取消")
|
||||
self.log.info("关机定时器已取消")
|
||||
minute = int(kwargs["arg1"])
|
||||
|
||||
async def _do_stop():
|
||||
@@ -589,7 +670,9 @@ class XiaoMusic:
|
||||
async def get_volume(self, **kwargs):
|
||||
playing_info = await self.mina_service.player_get_status(self.device_id)
|
||||
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)
|
||||
|
||||
def get_volume_ret(self):
|
||||
@@ -597,10 +680,19 @@ class XiaoMusic:
|
||||
|
||||
# 搜索音乐
|
||||
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)
|
||||
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):
|
||||
self.log.debug("playingmusic. cur_music:%s", self.cur_music)
|
||||
@@ -625,7 +717,7 @@ class XiaoMusic:
|
||||
async def saveconfig(self, data):
|
||||
# 默认暂时配置保存到 music 目录下
|
||||
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)
|
||||
self.update_config_from_setting(data)
|
||||
await self.call_main_thread_function(self.reinit)
|
||||
@@ -670,12 +762,13 @@ class XiaoMusic:
|
||||
async def call_main_thread_function(self, func, arg1=None):
|
||||
loop = asyncio.get_event_loop()
|
||||
future = loop.create_future()
|
||||
|
||||
def callback(ret):
|
||||
nonlocal future
|
||||
loop.call_soon_threadsafe(future.set_result, ret)
|
||||
|
||||
self.queue.put((func, callback, arg1))
|
||||
self.last_record = None
|
||||
self.new_record_event.set()
|
||||
result = await future
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user