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

Compare commits

...

13 Commits

Author SHA1 Message Date
涵曦
eccb52c197 new version v0.1.62 2024-06-25 17:34:47 +00:00
涵曦
603d60d8b8 优化图标大小 2024-06-25 17:30:50 +00:00
涵曦
3ef04f4159 优化获取音乐时长接口 2024-06-25 17:27:32 +00:00
涵曦
7888ee7938 new version v0.1.61 2024-06-25 11:13:57 +00:00
涵曦
b2a3cda7b5 #78 支持配置自定义网络歌单 2024-06-25 11:13:46 +00:00
涵曦
80c6d29079 new version v0.1.60 2024-06-25 06:20:28 +00:00
涵曦
0b020deaef new version v0.1.59 2024-06-25 06:17:41 +00:00
涵曦
74c8bea756 cue不是音乐文件,排除下 2024-06-25 06:17:41 +00:00
涵曦
af10d6261f Update README.md 2024-06-25 10:50:32 +08:00
涵曦
a178278576 new version v0.1.58 2024-06-25 01:03:13 +00:00
涵曦
474fea8434 优化播放被打断的问题 2024-06-25 01:03:01 +00:00
涵曦
d271f7b0f7 代码优化 2024-06-24 16:03:10 +00:00
涵曦
7e2af515ed fix: 登陆失败不阻塞启动 2024-06-24 15:38:41 +00:00
11 changed files with 299 additions and 70 deletions

View File

@@ -1,6 +1,6 @@
# xiaomusic
使用小爱/红米音箱播放音乐,音乐使用 yt-dlp 下载。
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
## 最简配置运行

View File

@@ -1,6 +1,6 @@
[project]
name = "xiaomusic"
version = "0.1.57"
version = "0.1.62"
description = "Play Music with xiaomi AI speaker"
authors = [
{name = "涵曦", email = "im.hanxi@gmail.com"},

View File

@@ -1 +1 @@
__version__ = "0.1.57"
__version__ = "0.1.62"

View File

@@ -53,7 +53,6 @@ SUPPORT_MUSIC_TYPE = [
".flac",
".wav",
".ape",
".cue",
]
@@ -82,6 +81,8 @@ class Config:
)
httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "admin")
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "admin")
music_list_url: str = os.getenv("XIAOMUSIC_MUSIC_LIST_URL", "")
music_list_json: str = os.getenv("XIAOMUSIC_MUSIC_LIST_JSON", "")
def __post_init__(self) -> None:
if self.proxy:

View File

@@ -12,6 +12,9 @@ from xiaomusic import (
from xiaomusic.config import (
KEY_WORD_DICT,
)
from xiaomusic.utils import (
downloadfile,
)
app = Flask(__name__)
auth = HTTPBasicAuth()
@@ -71,6 +74,12 @@ def playingmusic():
return xiaomusic.playingmusic()
@app.route("/isplaying", methods=["GET"])
@auth.login_required
def isplaying():
return xiaomusic.isplaying()
@app.route("/", methods=["GET"])
def index():
return send_from_directory("static", "index.html")
@@ -103,6 +112,8 @@ async def getsetting():
"mi_hardware_list": alldevices["hardware_list"],
"xiaomusic_search": config.search_prefix,
"xiaomusic_proxy": config.proxy,
"xiaomusic_music_list_url": config.music_list_url,
"xiaomusic_music_list_json": config.music_list_json,
}
return data
@@ -137,6 +148,18 @@ def delmusic():
return "success"
@app.route("/downloadjson", methods=["POST"])
@auth.login_required
def downloadjson():
data = request.get_json()
log.info(data)
ret, content = downloadfile(data["url"])
return {
"ret": ret,
"content": content,
}
def static_path_handler(filename):
log.debug(filename)
log.debug(static_path)

View File

@@ -18,7 +18,7 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
<input id="volume" type="range"></input>
<a href="/static/setting.html">
<svg fill="#8e43e7" height="64px" width="64px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-11.88 -11.88 77.76 77.76" xml:space="preserve" stroke="#8e43e7" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)" stroke-width="0.00054"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"><rect x="-11.88" y="-11.88" width="77.76" height="77.76" rx="18.6624" fill="#addcff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.512"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M51.22,21h-5.052c-0.812,0-1.481-0.447-1.792-1.197s-0.153-1.54,0.42-2.114l3.572-3.571 c0.525-0.525,0.814-1.224,0.814-1.966c0-0.743-0.289-1.441-0.814-1.967l-4.553-4.553c-1.05-1.05-2.881-1.052-3.933,0l-3.571,3.571 c-0.574,0.573-1.366,0.733-2.114,0.421C33.447,9.313,33,8.644,33,7.832V2.78C33,1.247,31.753,0,30.22,0H23.78 C22.247,0,21,1.247,21,2.78v5.052c0,0.812-0.447,1.481-1.197,1.792c-0.748,0.313-1.54,0.152-2.114-0.421l-3.571-3.571 c-1.052-1.052-2.883-1.05-3.933,0l-4.553,4.553c-0.525,0.525-0.814,1.224-0.814,1.967c0,0.742,0.289,1.44,0.814,1.966l3.572,3.571 c0.573,0.574,0.73,1.364,0.42,2.114S8.644,21,7.832,21H2.78C1.247,21,0,22.247,0,23.78v6.439C0,31.753,1.247,33,2.78,33h5.052 c0.812,0,1.481,0.447,1.792,1.197s0.153,1.54-0.42,2.114l-3.572,3.571c-0.525,0.525-0.814,1.224-0.814,1.966 c0,0.743,0.289,1.441,0.814,1.967l4.553,4.553c1.051,1.051,2.881,1.053,3.933,0l3.571-3.572c0.574-0.573,1.363-0.731,2.114-0.42 c0.75,0.311,1.197,0.98,1.197,1.792v5.052c0,1.533,1.247,2.78,2.78,2.78h6.439c1.533,0,2.78-1.247,2.78-2.78v-5.052 c0-0.812,0.447-1.481,1.197-1.792c0.751-0.312,1.54-0.153,2.114,0.42l3.571,3.572c1.052,1.052,2.883,1.05,3.933,0l4.553-4.553 c0.525-0.525,0.814-1.224,0.814-1.967c0-0.742-0.289-1.44-0.814-1.966l-3.572-3.571c-0.573-0.574-0.73-1.364-0.42-2.114 S45.356,33,46.168,33h5.052c1.533,0,2.78-1.247,2.78-2.78V23.78C54,22.247,52.753,21,51.22,21z M52,30.22 C52,30.65,51.65,31,51.22,31h-5.052c-1.624,0-3.019,0.932-3.64,2.432c-0.622,1.5-0.295,3.146,0.854,4.294l3.572,3.571 c0.305,0.305,0.305,0.8,0,1.104l-4.553,4.553c-0.304,0.304-0.799,0.306-1.104,0l-3.571-3.572c-1.149-1.149-2.794-1.474-4.294-0.854 c-1.5,0.621-2.432,2.016-2.432,3.64v5.052C31,51.65,30.65,52,30.22,52H23.78C23.35,52,23,51.65,23,51.22v-5.052 c0-1.624-0.932-3.019-2.432-3.64c-0.503-0.209-1.021-0.311-1.533-0.311c-1.014,0-1.997,0.4-2.761,1.164l-3.571,3.572 c-0.306,0.306-0.801,0.304-1.104,0l-4.553-4.553c-0.305-0.305-0.305-0.8,0-1.104l3.572-3.571c1.148-1.148,1.476-2.794,0.854-4.294 C10.851,31.932,9.456,31,7.832,31H2.78C2.35,31,2,30.65,2,30.22V23.78C2,23.35,2.35,23,2.78,23h5.052 c1.624,0,3.019-0.932,3.64-2.432c0.622-1.5,0.295-3.146-0.854-4.294l-3.572-3.571c-0.305-0.305-0.305-0.8,0-1.104l4.553-4.553 c0.304-0.305,0.799-0.305,1.104,0l3.571,3.571c1.147,1.147,2.792,1.476,4.294,0.854C22.068,10.851,23,9.456,23,7.832V2.78 C23,2.35,23.35,2,23.78,2h6.439C30.65,2,31,2.35,31,2.78v5.052c0,1.624,0.932,3.019,2.432,3.64 c1.502,0.622,3.146,0.294,4.294-0.854l3.571-3.571c0.306-0.305,0.801-0.305,1.104,0l4.553,4.553c0.305,0.305,0.305,0.8,0,1.104 l-3.572,3.571c-1.148,1.148-1.476,2.794-0.854,4.294c0.621,1.5,2.016,2.432,3.64,2.432h5.052C51.65,23,52,23.35,52,23.78V30.22z"></path> <path d="M27,18c-4.963,0-9,4.037-9,9s4.037,9,9,9s9-4.037,9-9S31.963,18,27,18z M27,34c-3.859,0-7-3.141-7-7s3.141-7,7-7 s7,3.141,7,7S30.859,34,27,34z"></path> </g> </g></svg>
<svg fill="#8e43e7" height="48px" width="48px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-11.88 -11.88 77.76 77.76" xml:space="preserve" stroke="#8e43e7" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)" stroke-width="0.00054"><g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"><rect x="-11.88" y="-11.88" width="77.76" height="77.76" rx="18.6624" fill="#addcff" strokewidth="0"></rect></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="1.512"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M51.22,21h-5.052c-0.812,0-1.481-0.447-1.792-1.197s-0.153-1.54,0.42-2.114l3.572-3.571 c0.525-0.525,0.814-1.224,0.814-1.966c0-0.743-0.289-1.441-0.814-1.967l-4.553-4.553c-1.05-1.05-2.881-1.052-3.933,0l-3.571,3.571 c-0.574,0.573-1.366,0.733-2.114,0.421C33.447,9.313,33,8.644,33,7.832V2.78C33,1.247,31.753,0,30.22,0H23.78 C22.247,0,21,1.247,21,2.78v5.052c0,0.812-0.447,1.481-1.197,1.792c-0.748,0.313-1.54,0.152-2.114-0.421l-3.571-3.571 c-1.052-1.052-2.883-1.05-3.933,0l-4.553,4.553c-0.525,0.525-0.814,1.224-0.814,1.967c0,0.742,0.289,1.44,0.814,1.966l3.572,3.571 c0.573,0.574,0.73,1.364,0.42,2.114S8.644,21,7.832,21H2.78C1.247,21,0,22.247,0,23.78v6.439C0,31.753,1.247,33,2.78,33h5.052 c0.812,0,1.481,0.447,1.792,1.197s0.153,1.54-0.42,2.114l-3.572,3.571c-0.525,0.525-0.814,1.224-0.814,1.966 c0,0.743,0.289,1.441,0.814,1.967l4.553,4.553c1.051,1.051,2.881,1.053,3.933,0l3.571-3.572c0.574-0.573,1.363-0.731,2.114-0.42 c0.75,0.311,1.197,0.98,1.197,1.792v5.052c0,1.533,1.247,2.78,2.78,2.78h6.439c1.533,0,2.78-1.247,2.78-2.78v-5.052 c0-0.812,0.447-1.481,1.197-1.792c0.751-0.312,1.54-0.153,2.114,0.42l3.571,3.572c1.052,1.052,2.883,1.05,3.933,0l4.553-4.553 c0.525-0.525,0.814-1.224,0.814-1.967c0-0.742-0.289-1.44-0.814-1.966l-3.572-3.571c-0.573-0.574-0.73-1.364-0.42-2.114 S45.356,33,46.168,33h5.052c1.533,0,2.78-1.247,2.78-2.78V23.78C54,22.247,52.753,21,51.22,21z M52,30.22 C52,30.65,51.65,31,51.22,31h-5.052c-1.624,0-3.019,0.932-3.64,2.432c-0.622,1.5-0.295,3.146,0.854,4.294l3.572,3.571 c0.305,0.305,0.305,0.8,0,1.104l-4.553,4.553c-0.304,0.304-0.799,0.306-1.104,0l-3.571-3.572c-1.149-1.149-2.794-1.474-4.294-0.854 c-1.5,0.621-2.432,2.016-2.432,3.64v5.052C31,51.65,30.65,52,30.22,52H23.78C23.35,52,23,51.65,23,51.22v-5.052 c0-1.624-0.932-3.019-2.432-3.64c-0.503-0.209-1.021-0.311-1.533-0.311c-1.014,0-1.997,0.4-2.761,1.164l-3.571,3.572 c-0.306,0.306-0.801,0.304-1.104,0l-4.553-4.553c-0.305-0.305-0.305-0.8,0-1.104l3.572-3.571c1.148-1.148,1.476-2.794,0.854-4.294 C10.851,31.932,9.456,31,7.832,31H2.78C2.35,31,2,30.65,2,30.22V23.78C2,23.35,2.35,23,2.78,23h5.052 c1.624,0,3.019-0.932,3.64-2.432c0.622-1.5,0.295-3.146-0.854-4.294l-3.572-3.571c-0.305-0.305-0.305-0.8,0-1.104l4.553-4.553 c0.304-0.305,0.799-0.305,1.104,0l3.571,3.571c1.147,1.147,2.792,1.476,4.294,0.854C22.068,10.851,23,9.456,23,7.832V2.78 C23,2.35,23.35,2,23.78,2h6.439C30.65,2,31,2.35,31,2.78v5.052c0,1.624,0.932,3.019,2.432,3.64 c1.502,0.622,3.146,0.294,4.294-0.854l3.571-3.571c0.306-0.305,0.801-0.305,1.104,0l4.553,4.553c0.305,0.305,0.305,0.8,0,1.104 l-3.572,3.571c-1.148,1.148-1.476,2.794-0.854,4.294c0.621,1.5,2.016,2.432,3.64,2.432h5.052C51.65,23,52,23.35,52,23.78V30.22z"></path> <path d="M27,18c-4.963,0-9,4.037-9,9s4.037,9,9,9s9-4.037,9-9S31.963,18,27,18z M27,34c-3.859,0-7-3.141-7-7s3.141-7,7-7 s7,3.141,7,7S30.859,34,27,34z"></path> </g> </g></svg>
</a>
</div>
</div>
@@ -29,7 +29,7 @@
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)"></input>
</div>
<button id="play">播放</button>
<div class="container">
<div class="cawait get_web_music_duration(url)ontainer">
<div id="playering-music" class="text"></div>
</div>

View File

@@ -22,9 +22,14 @@
</select>
<label for="xiaomusic_proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
<input id="xiaomusic_proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
<label for="xiaomusic_music_list_url">歌单地址:</label>
<input id="xiaomusic_music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json"></input>
<label for="xiaomusic_music_list_json">歌单内容:</label>
<textarea id="xiaomusic_music_list_json" type="text"></textarea>
</div>
<hr>
<button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button id="save">保存</button>
<footer>

View File

@@ -40,6 +40,14 @@ $(function(){
if (data.xiaomusic_proxy != "") {
$("#xiaomusic_proxy").val(data.xiaomusic_proxy);
}
if (data.xiaomusic_music_list_url != "") {
$("#xiaomusic_music_list_url").val(data.xiaomusic_music_list_url);
}
if (data.xiaomusic_music_list_json != "") {
$("#xiaomusic_music_list_json").val(data.xiaomusic_music_list_json);
}
});
$("#save").on("click", () => {
@@ -47,15 +55,21 @@ $(function(){
var mi_hardware = $("#mi_hardware").val();
var xiaomusic_search = $("#xiaomusic_search").val();
var xiaomusic_proxy = $("#xiaomusic_proxy").val();
var xiaomusic_music_list_url = $("#xiaomusic_music_list_url").val();
var xiaomusic_music_list_json = $("#xiaomusic_music_list_json").val();
console.log("mi_did", mi_did);
console.log("mi_hardware", mi_hardware);
console.log("xiaomusic_search", xiaomusic_search);
console.log("xiaomusic_proxy", xiaomusic_proxy);
console.log("xiaomusic_music_list_url", xiaomusic_music_list_url);
console.log("xiaomusic_music_list_json", xiaomusic_music_list_json);
var data = {
mi_did: mi_did,
mi_hardware: mi_hardware,
xiaomusic_search: xiaomusic_search,
xiaomusic_proxy: xiaomusic_proxy,
xiaomusic_music_list_url: xiaomusic_music_list_url,
xiaomusic_music_list_json: xiaomusic_music_list_json,
};
$.ajax({
type: "POST",
@@ -70,4 +84,30 @@ $(function(){
}
});
});
$("#get_music_list").on("click", () => {
var xiaomusic_music_list_url = $("#xiaomusic_music_list_url").val();
console.log("xiaomusic_music_list_url", xiaomusic_music_list_url);
var data = {
url: xiaomusic_music_list_url,
};
$.ajax({
type: "POST",
url: "/downloadjson",
contentType: "application/json",
data: JSON.stringify(data),
success: (res) => {
if (res.ret == "OK") {
$("#xiaomusic_music_list_json").val(res.content);
} else {
console.log(res);
alert(res.ret);
}
},
error: (res) => {
console.log(res);
alert(res);
}
});
});
});

View File

@@ -67,3 +67,9 @@ footer {
text-align: center;
padding: 10px 0;
}
textarea{
margin: 10px;
width: 300px;
height: 200px;
}

View File

@@ -4,10 +4,14 @@ from __future__ import annotations
import difflib
import os
import re
import tempfile
from collections.abc import AsyncIterator
from http.cookies import SimpleCookie
from urllib.parse import urlparse
import aiohttp
import mutagen
import requests
from requests.utils import cookiejar_from_dict
@@ -144,3 +148,62 @@ def walk_to_depth(root, depth=None, *args, **kwargs):
except KeyError:
yield from main_func(root, depth, *args, **kwargs)
return
def downloadfile(url):
try:
response = requests.get(url, timeout=5) # 增加超时以避免长时间挂起
response.raise_for_status() # 如果响应不是200引发HTTPError异常
return ("OK", response.text)
except requests.exceptions.HTTPError as errh:
return (f"HTTP Error: {errh}", "")
except requests.exceptions.ConnectionError as errc:
return (f"Error Connecting: {errc}", "")
except requests.exceptions.Timeout as errt:
return (f"Timeout Error: {errt}", "")
except requests.exceptions.RequestException as err:
return (f"Oops: Something Else, {err}", "")
return ("Unknow Error", "")
async def _get_web_music_duration(session, url, start=0, end=500):
duration = 0
headers = {"Range": f"bytes={start}-{end}"}
async with session.get(url, headers=headers) as response:
array_buffer = await response.read()
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write(array_buffer)
name = tmp.name
try:
m = mutagen.File(name)
duration = m.info.length
except Exception:
pass
os.remove(name)
return duration
async def get_web_music_duration(url, start=0, end=500):
duration = 0
try:
# 设置总超时时间为3秒
timeout = aiohttp.ClientTimeout(total=3)
async with aiohttp.ClientSession(timeout=timeout) as session:
duration = await _get_web_music_duration(session, url, start=0, end=500)
if duration <= 0:
duration = await _get_web_music_duration(session, url, start=0, end=1000)
except Exception:
pass
return duration
# 获取文件播放时长
def get_local_music_duration(filename):
duration = 0
try:
m = mutagen.File(filename)
duration = m.info.length
except Exception:
pass
return duration

View File

@@ -11,7 +11,6 @@ import traceback
import urllib.parse
from pathlib import Path
import mutagen
from aiohttp import ClientSession, ClientTimeout
from miservice import MiAccount, MiIOService, MiNAService
@@ -31,6 +30,8 @@ from xiaomusic.httpserver import StartHTTPServer
from xiaomusic.utils import (
custom_sort_key,
fuzzyfinder,
get_local_music_duration,
get_web_music_duration,
parse_cookie_string,
walk_to_depth,
)
@@ -80,6 +81,7 @@ class XiaoMusic:
self._timeout = 0
self._volume = 0
self._all_music = {}
self._all_radio = {} # 电台列表
self._play_list = []
self._cur_play_list = ""
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
@@ -127,7 +129,9 @@ class XiaoMusic:
async def init_all_data(self, session):
await self.login_miboy(session)
await self._init_data_hardware()
session.cookie_jar.update_cookies(self.get_cookie())
cookie_jar = self.get_cookie()
if cookie_jar:
session.cookie_jar.update_cookies(cookie_jar)
self.cookie_jar = session.cookie_jar
async def login_miboy(self, session):
@@ -143,27 +147,30 @@ class XiaoMusic:
self.miio_service = MiIOService(account)
async def try_update_device_id(self):
hardware_data = await self.mina_service.device_list()
# fix multi xiaoai problems we check did first
# why we use this way to fix?
# some videos and articles already in the Internet
# we do not want to change old way, so we check if miotDID in `env` first
# to set device id
for h in hardware_data:
if did := self.config.mi_did:
if h.get("miotDID", "") == str(did):
try:
hardware_data = await self.mina_service.device_list()
for h in hardware_data:
if did := self.config.mi_did:
if h.get("miotDID", "") == str(did):
self.device_id = h.get("deviceID")
break
else:
continue
if h.get("hardware", "") == self.config.hardware:
self.device_id = h.get("deviceID")
break
else:
continue
if h.get("hardware", "") == self.config.hardware:
self.device_id = h.get("deviceID")
break
else:
self.log.error(
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
)
else:
self.log.error(
f"we have no hardware: {self.config.hardware} please use `micli mina` to check"
)
except Exception as e:
self.log.error(f"Execption {e}")
async def _init_data_hardware(self):
if self.config.cookie:
@@ -193,15 +200,19 @@ class XiaoMusic:
cookie_dict = cookie_jar.get_dict()
self.device_id = cookie_dict["deviceId"]
return cookie_jar
else:
with open(self.mi_token_home) as f:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
cookie_string = COOKIE_TEMPLATE.format(
device_id=self.device_id, service_token=service_token, user_id=user_id
)
return parse_cookie_string(cookie_string)
if not os.path.exists(self.mi_token_home):
self.log.error(f"{self.mi_token_home} file not exist")
return None
with open(self.mi_token_home) as f:
user_data = json.loads(f.read())
user_id = user_data.get("userId")
service_token = user_data.get("micoapi")[1]
cookie_string = COOKIE_TEMPLATE.format(
device_id=self.device_id, service_token=service_token, user_id=user_id
)
return parse_cookie_string(cookie_string)
async def get_latest_ask_from_xiaoai(self, session):
retries = 3
@@ -221,8 +232,8 @@ class XiaoMusic:
continue
try:
data = await r.json()
except Exception:
self.log.warning("get latest ask from xiaoai error, retry")
except Exception as e:
self.log.warning(f"get latest ask from xiaoai error {e}, retry")
if i == 2:
# tricky way to fix #282 #272 # if it is the third time we re init all data
self.log.info("Maybe outof date trying to re init it")
@@ -256,8 +267,11 @@ class XiaoMusic:
await self.force_stop_xiaoai()
try:
await self.mina_service.text_to_speech(self.device_id, value)
except Exception:
pass
except Exception as e:
self.log.error(f"Execption {e}")
if self._playing:
# 继续播放歌曲
await self.play()
async def do_set_volume(self, value):
value = int(value)
@@ -265,8 +279,8 @@ class XiaoMusic:
self.log.info(f"声音设置为{value}")
try:
await self.mina_service.player_set_volume(self.device_id, value)
except Exception:
pass
except Exception as e:
self.log.error(f"Execption {e}")
async def force_stop_xiaoai(self):
await self.mina_service.player_stop(self.device_id)
@@ -312,7 +326,6 @@ class XiaoMusic:
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
await self.do_tts(f"正在下载歌曲{search_key}")
# 本地是否存在歌曲
def get_filename(self, name):
if name not in self._all_music:
self.log.debug("get_filename not in. name:%s", name)
@@ -323,10 +336,39 @@ class XiaoMusic:
return filename
return ""
# 获取歌曲播放地址
def get_file_url(self, name):
# 判断本地音乐是否存在,网络歌曲不判断
def is_music_exist(self, name):
if name not in self._all_music:
return False
if self.is_web_music(name):
return True
filename = self.get_filename(name)
self.log.debug("get_file_url. name:%s, filename:%s", name, filename)
if filename:
return True
return False
# 是否是网络电台
def is_web_radio_music(self, name):
return name in self._all_radio
# 是否是网络歌曲
def is_web_music(self, name):
if name not in self._all_music:
return False
url = self._all_music[name]
return url.startswith(("http://", "https://"))
# 获取歌曲播放地址
def get_music_url(self, name):
if self.is_web_music(name):
url = self._all_music[name]
self.log.debug("get_music_url web music. name:%s, url:%s", name, url)
return url
filename = self.get_filename(name)
self.log.debug(
"get_music_url local music. name:%s, filename:%s", name, filename
)
encoded_name = urllib.parse.quote(filename)
return f"http://{self.hostname}:{self.port}/{encoded_name}"
@@ -363,7 +405,6 @@ 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 = "全部"
self._gen_play_list()
@@ -374,7 +415,46 @@ class XiaoMusic:
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
try:
self._append_music_list()
except Exception as e:
self.log.error(f"Execption _append_music_list {e}")
# 给歌单里补充网络歌单
def _append_music_list(self):
if not self.config.music_list_json:
return
music_list = json.loads(self.config.music_list_json)
try:
for item in music_list:
list_name = item.get("name")
musics = item.get("musics")
if (not list_name) or (not musics):
continue
one_music_list = []
for music in musics:
name = music.get("name")
url = music.get("url")
music_type = music.get("type")
if (not name) or (not url):
continue
self._all_music[name] = url
one_music_list.append(name)
# 处理电台列表
if music_type == "radio":
self._all_radio[name] = url
self.log.info(one_music_list)
# 歌曲名字相同会覆盖
self._music_list[list_name] = one_music_list
if self._all_radio:
self._music_list["所有电台"] = list(self._all_radio.keys())
self.log.debug(self._all_music)
self.log.debug(self._music_list)
except Exception as e:
self.log.error(f"Execption music_list:{music_list} {e}")
# 歌曲排序或者打乱顺序
def _gen_play_list(self):
@@ -408,29 +488,37 @@ class XiaoMusic:
if next_index >= play_list_len:
next_index = 0
name = self._play_list[next_index]
filename = self.get_filename(name)
if len(filename) <= 0:
if not self.is_music_exist(name):
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):
# 获取音频文件对象
audio = mutagen.File(filename)
# 获取播放时长
duration = audio.info.length
return duration
# 设置下一首歌曲的播放定时器
def set_next_music_timeout(self):
filename = self.get_filename(self.cur_music)
sec = int(self.get_file_duration(filename))
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec}")
async def set_next_music_timeout(self):
name = self.cur_music
if self.is_web_radio_music(name):
self.log.info("电台不会有下一首的定时器")
return
if self.is_web_music(name):
url = self._all_music[name]
duration = await get_web_music_duration(url)
sec = int(duration)
self.log.info(f"网络歌曲 {name} : {url} 的时长 {sec}")
else:
filename = self.get_filename(name)
sec = int(get_local_music_duration(filename))
self.log.info(f"本地歌曲 {name} : {filename} 的时长 {sec}")
if self._next_timer:
self._next_timer.cancel()
self.log.info("定时器已取消")
if sec <= 0:
self.log.warning("获取歌曲时长失败,不会开启下一首歌曲的定时器")
return
self._timeout = sec
async def _do_next():
@@ -522,17 +610,15 @@ 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:
if self.is_music_exist(self.cur_music):
return True
pass
return False
# 播放歌曲
@@ -552,9 +638,9 @@ class XiaoMusic:
name = self.cur_music
self.log.debug("play. search_key:%s name:%s", search_key, name)
filename = self.get_filename(name)
if len(filename) <= 0:
# 本地歌曲不存在时下载
if not self.is_music_exist(name):
await self.download(search_key, name)
self.log.info("正在下载中 %s", search_key + ":" + name)
await self.download_proc.wait()
@@ -563,13 +649,13 @@ class XiaoMusic:
self.cur_music = name
self.log.info("cur_music %s", self.cur_music)
url = self.get_file_url(name)
url = self.get_music_url(name)
self.log.info("播放 %s", url)
await self.force_stop_xiaoai()
await self.mina_service.play_by_url(self.device_id, url)
self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器
self.set_next_music_timeout()
await self.set_next_music_timeout()
# 下一首
async def play_next(self, **kwargs):
@@ -607,7 +693,6 @@ class XiaoMusic:
# 刷新列表
async def gen_music_list(self, **kwargs):
self._gen_all_music_list()
await self.do_tts("生成播放列表完毕")
# 删除歌曲
def del_music(self, name):
@@ -620,7 +705,6 @@ class XiaoMusic:
self.log.info(f"del ${filename} success")
except OSError:
self.log.error(f"del ${filename} failed")
pass
self._gen_all_music_list()
# 播放一个播放列表
@@ -700,6 +784,10 @@ class XiaoMusic:
self.log.debug("playingmusic. cur_music:%s", self.cur_music)
return self.cur_music
# 当前是否正在播放歌曲
def isplaying(self):
return self._playing
# 获取当前配置
def getconfig(self):
return self.config
@@ -734,10 +822,12 @@ class XiaoMusic:
await self.call_main_thread_function(self.reinit)
def update_config_from_setting(self, data):
self.config.mi_did = data["mi_did"]
self.config.hardware = data["mi_hardware"]
self.config.search_prefix = data["xiaomusic_search"]
self.config.proxy = data["xiaomusic_proxy"]
self.config.mi_did = data.get("mi_did")
self.config.hardware = data.get("mi_hardware")
self.config.search_prefix = data.get("xiaomusic_search")
self.config.proxy = data.get("xiaomusic_proxy")
self.config.music_list_url = data.get("xiaomusic_music_list_url")
self.config.music_list_json = data.get("xiaomusic_music_list_json")
self.search_prefix = self.config.search_prefix
self.proxy = self.config.proxy
@@ -746,6 +836,7 @@ class XiaoMusic:
# 重新初始化
async def reinit(self, **kwargs):
await self.try_update_device_id()
self._gen_all_music_list()
self.log.info("reinit success")
# 获取所有设备