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

Compare commits

...

9 Commits

Author SHA1 Message Date
涵曦
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
11 changed files with 200 additions and 86 deletions

View File

@@ -49,6 +49,11 @@ pdm run xiaomusic.py
- 下一首
- 单曲循环
- 全部循环
- 随机播放
- 关机
- 停止播放
- 刷新列表
- 播放列表+列表名 比如:播放列表其他
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会播放小猪佩奇的故事。

29
pdm.lock generated
View File

@@ -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"

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.1.40"
version = "0.1.44"
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 ."

View File

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

View File

@@ -1 +1 @@
__version__ = "0.1.40"
__version__ = "0.1.44"

View File

@@ -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"),
@@ -86,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"))

View File

@@ -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,14 +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)
@@ -110,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

View File

@@ -27,35 +27,59 @@ $(function(){
});
// 拉取播放列表
$.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));
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').trigger('change');
$('#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));
});
});
// 获取当前播放列表
$.get("curplaylist", function(data, status) {
$('#music_list').val(data);
$('#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);
@@ -92,11 +116,11 @@ $(function(){
$.ajax({
type: "POST",
url: "/cmd",
contentType: "application/json",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({cmd: cmd}),
success: () => {
if (cmd == "刷新列表") {
location.reload();
setTimeout(refresh_music_list, 3000);
}
},
error: () => {

View File

@@ -41,6 +41,7 @@
<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>

View File

@@ -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)

View File

@@ -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
@@ -76,7 +74,7 @@ class XiaoMusic:
self._all_music = {}
self._play_list = []
self._cur_play_list = ""
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
self._playing = False
# 关机定时器
@@ -86,7 +84,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)
@@ -290,7 +288,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
@@ -379,7 +380,7 @@ class XiaoMusic:
self._music_list = {}
self._music_list["全部"] = self._play_list
for dir_name,musics in all_music_by_dir.items():
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
@@ -396,7 +397,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
@@ -411,6 +412,7 @@ class XiaoMusic:
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
@@ -429,7 +431,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():
@@ -449,11 +451,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()
@@ -560,30 +562,44 @@ 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
random.shuffle(self._play_list)
await self.do_tts(f"已经设置为随机播放")
await self.do_tts("已经设置为随机播放")
# 刷新列表
async def gen_music_list(self, **kwargs):
self._gen_all_music_list()
await self.do_tts(f"生成播放列表完毕")
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):
@@ -607,14 +623,14 @@ class XiaoMusic:
self._playing = False
if self._next_timer:
self._next_timer.cancel()
self.log.info(f"定时器已取消")
self.log.info("定时器已取消")
self.cur_music = ""
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():
@@ -634,7 +650,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):
@@ -679,7 +697,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)
@@ -724,12 +742,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