#78 支持配置自定义网络歌单

This commit is contained in:
涵曦 2024-06-25 11:13:46 +00:00
parent 80c6d29079
commit b2a3cda7b5
7 changed files with 220 additions and 36 deletions

View File

@ -81,6 +81,8 @@ class Config:
) )
httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "admin") httpauth_username: str = os.getenv("XIAOMUSIC_HTTPAUTH_USERNAME", "admin")
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "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: def __post_init__(self) -> None:
if self.proxy: if self.proxy:

View File

@ -12,6 +12,9 @@ from xiaomusic import (
from xiaomusic.config import ( from xiaomusic.config import (
KEY_WORD_DICT, KEY_WORD_DICT,
) )
from xiaomusic.utils import (
downloadfile,
)
app = Flask(__name__) app = Flask(__name__)
auth = HTTPBasicAuth() auth = HTTPBasicAuth()
@ -109,6 +112,8 @@ async def getsetting():
"mi_hardware_list": alldevices["hardware_list"], "mi_hardware_list": alldevices["hardware_list"],
"xiaomusic_search": config.search_prefix, "xiaomusic_search": config.search_prefix,
"xiaomusic_proxy": config.proxy, "xiaomusic_proxy": config.proxy,
"xiaomusic_music_list_url": config.music_list_url,
"xiaomusic_music_list_json": config.music_list_json,
} }
return data return data
@ -143,6 +148,18 @@ def delmusic():
return "success" 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): def static_path_handler(filename):
log.debug(filename) log.debug(filename)
log.debug(static_path) log.debug(static_path)

View File

@ -22,9 +22,14 @@
</select> </select>
<label for="xiaomusic_proxy">XIAOMUSIC_PROXY(ytsearch需要):</label> <label for="xiaomusic_proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
<input id="xiaomusic_proxy" type="text" placeholder="http://192.168.2.5:8080"></input> <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> </div>
<hr> <hr>
<button onclick="location.href='/';">返回首页</button> <button onclick="location.href='/';">返回首页</button>
<button id="get_music_list">获取歌单</button>
<button id="save">保存</button> <button id="save">保存</button>
<footer> <footer>

View File

@ -40,6 +40,14 @@ $(function(){
if (data.xiaomusic_proxy != "") { if (data.xiaomusic_proxy != "") {
$("#xiaomusic_proxy").val(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", () => { $("#save").on("click", () => {
@ -47,15 +55,21 @@ $(function(){
var mi_hardware = $("#mi_hardware").val(); var mi_hardware = $("#mi_hardware").val();
var xiaomusic_search = $("#xiaomusic_search").val(); var xiaomusic_search = $("#xiaomusic_search").val();
var xiaomusic_proxy = $("#xiaomusic_proxy").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_did", mi_did);
console.log("mi_hardware", mi_hardware); console.log("mi_hardware", mi_hardware);
console.log("xiaomusic_search", xiaomusic_search); console.log("xiaomusic_search", xiaomusic_search);
console.log("xiaomusic_proxy", xiaomusic_proxy); 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 = { var data = {
mi_did: mi_did, mi_did: mi_did,
mi_hardware: mi_hardware, mi_hardware: mi_hardware,
xiaomusic_search: xiaomusic_search, xiaomusic_search: xiaomusic_search,
xiaomusic_proxy: xiaomusic_proxy, xiaomusic_proxy: xiaomusic_proxy,
xiaomusic_music_list_url: xiaomusic_music_list_url,
xiaomusic_music_list_json: xiaomusic_music_list_json,
}; };
$.ajax({ $.ajax({
type: "POST", 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; text-align: center;
padding: 10px 0; padding: 10px 0;
} }
textarea{
margin: 10px;
width: 300px;
height: 200px;
}

View File

@ -4,10 +4,14 @@ from __future__ import annotations
import difflib import difflib
import os import os
import re import re
import tempfile
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from urllib.parse import urlparse from urllib.parse import urlparse
import aiohttp
import mutagen
import requests
from requests.utils import cookiejar_from_dict from requests.utils import cookiejar_from_dict
@ -144,3 +148,47 @@ def walk_to_depth(root, depth=None, *args, **kwargs):
except KeyError: except KeyError:
yield from main_func(root, depth, *args, **kwargs) yield from main_func(root, depth, *args, **kwargs)
return 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(url, start=0, end=500):
headers = {"Range": f"bytes={start}-{end}"}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
array_buffer = await response.read()
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(array_buffer)
try:
m = mutagen.File(tmp)
except Exception:
headers = {"Range": f"bytes={0}-{1000}"}
async with session.get(url, headers=headers) as response:
array_buffer = await response.read()
with tempfile.NamedTemporaryFile() as tmp2:
tmp2.write(array_buffer)
m = mutagen.File(tmp2)
return m.info.length
# 获取文件播放时长
def get_local_music_duration(filename):
# 获取音频文件对象
audio = mutagen.File(filename)
# 获取播放时长
duration = audio.info.length
return duration

View File

@ -11,7 +11,6 @@ import traceback
import urllib.parse import urllib.parse
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 from miservice import MiAccount, MiIOService, MiNAService
@ -31,6 +30,8 @@ from xiaomusic.httpserver import StartHTTPServer
from xiaomusic.utils import ( from xiaomusic.utils import (
custom_sort_key, custom_sort_key,
fuzzyfinder, fuzzyfinder,
get_local_music_duration,
get_web_music_duration,
parse_cookie_string, parse_cookie_string,
walk_to_depth, walk_to_depth,
) )
@ -80,6 +81,7 @@ class XiaoMusic:
self._timeout = 0 self._timeout = 0
self._volume = 0 self._volume = 0
self._all_music = {} self._all_music = {}
self._all_radio = {} # 电台列表
self._play_list = [] self._play_list = []
self._cur_play_list = "" self._cur_play_list = ""
self._music_list = {} # 播放列表 key 为目录名, value 为 play_list self._music_list = {} # 播放列表 key 为目录名, value 为 play_list
@ -169,7 +171,6 @@ class XiaoMusic:
) )
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
pass
async def _init_data_hardware(self): async def _init_data_hardware(self):
if self.config.cookie: if self.config.cookie:
@ -268,7 +269,6 @@ class XiaoMusic:
await self.mina_service.text_to_speech(self.device_id, value) await self.mina_service.text_to_speech(self.device_id, value)
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
pass
if self._playing: if self._playing:
# 继续播放歌曲 # 继续播放歌曲
await self.play() await self.play()
@ -281,7 +281,6 @@ class XiaoMusic:
await self.mina_service.player_set_volume(self.device_id, value) await self.mina_service.player_set_volume(self.device_id, value)
except Exception as e: except Exception as e:
self.log.error(f"Execption {e}") self.log.error(f"Execption {e}")
pass
async def force_stop_xiaoai(self): async def force_stop_xiaoai(self):
await self.mina_service.player_stop(self.device_id) await self.mina_service.player_stop(self.device_id)
@ -327,7 +326,6 @@ class XiaoMusic:
self.download_proc = await asyncio.create_subprocess_exec(*sbp_args) self.download_proc = await asyncio.create_subprocess_exec(*sbp_args)
await self.do_tts(f"正在下载歌曲{search_key}") await self.do_tts(f"正在下载歌曲{search_key}")
# 本地是否存在歌曲
def get_filename(self, name): def get_filename(self, name):
if name not in self._all_music: if name not in self._all_music:
self.log.debug("get_filename not in. name:%s", name) self.log.debug("get_filename not in. name:%s", name)
@ -338,10 +336,39 @@ class XiaoMusic:
return filename return filename
return "" 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) 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) encoded_name = urllib.parse.quote(filename)
return f"http://{self.hostname}:{self.port}/{encoded_name}" return f"http://{self.hostname}:{self.port}/{encoded_name}"
@ -378,7 +405,6 @@ 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 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 = "全部" self._cur_play_list = "全部"
self._gen_play_list() self._gen_play_list()
@ -389,7 +415,46 @@ class XiaoMusic:
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._music_list[dir_name] = list(musics.keys())
self.log.debug("dir_name:%s, list:%s", dir_name, self._music_list[dir_name]) 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): def _gen_play_list(self):
@ -423,26 +488,27 @@ class XiaoMusic:
if next_index >= play_list_len: if next_index >= play_list_len:
next_index = 0 next_index = 0
name = self._play_list[next_index] name = self._play_list[next_index]
filename = self.get_filename(name) if not self.is_music_exist(name):
if len(filename) <= 0:
self._play_list.pop(next_index) self._play_list.pop(next_index)
self.log.info(f"pop not exist music:{name}") self.log.info(f"pop not exist music:{name}")
return self.get_next_music() return self.get_next_music()
return name return name
# 获取文件播放时长
def get_file_duration(self, filename):
# 获取音频文件对象
audio = mutagen.File(filename)
# 获取播放时长
duration = audio.info.length
return duration
# 设置下一首歌曲的播放定时器 # 设置下一首歌曲的播放定时器
def set_next_music_timeout(self): async def set_next_music_timeout(self):
filename = self.get_filename(self.cur_music) name = self.cur_music
sec = int(self.get_file_duration(filename)) if self.is_web_radio_music(name):
self.log.info(f"歌曲 {self.cur_music} : {filename} 的时长 {sec}") self.log.info("歌曲电台不会有下一首的定时器")
return
if self.is_web_music(name):
url = self._all_music[name]
sec = int(await get_web_music_duration(url))
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: if self._next_timer:
self._next_timer.cancel() self._next_timer.cancel()
self.log.info("定时器已取消") self.log.info("定时器已取消")
@ -543,11 +609,9 @@ class XiaoMusic:
if self.cur_music == "": if self.cur_music == "":
return True return True
else: else:
filename = self.get_filename(self.cur_music)
# 当前播放的歌曲不存在了 # 当前播放的歌曲不存在了
if len(filename) <= 0: if self.is_music_exist(self.cur_music):
return True return True
pass
return False return False
# 播放歌曲 # 播放歌曲
@ -567,9 +631,9 @@ class XiaoMusic:
name = self.cur_music name = self.cur_music
self.log.debug("play. search_key:%s name:%s", search_key, name) 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) await self.download(search_key, name)
self.log.info("正在下载中 %s", search_key + ":" + name) self.log.info("正在下载中 %s", search_key + ":" + name)
await self.download_proc.wait() await self.download_proc.wait()
@ -578,13 +642,13 @@ class XiaoMusic:
self.cur_music = name self.cur_music = name
self.log.info("cur_music %s", self.cur_music) 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) self.log.info("播放 %s", url)
await self.force_stop_xiaoai() await self.force_stop_xiaoai()
await self.mina_service.play_by_url(self.device_id, url) await self.mina_service.play_by_url(self.device_id, url)
self.log.info("已经开始播放了") self.log.info("已经开始播放了")
# 设置下一首歌曲的播放定时器 # 设置下一首歌曲的播放定时器
self.set_next_music_timeout() await self.set_next_music_timeout()
# 下一首 # 下一首
async def play_next(self, **kwargs): async def play_next(self, **kwargs):
@ -634,7 +698,6 @@ class XiaoMusic:
self.log.info(f"del ${filename} success") self.log.info(f"del ${filename} success")
except OSError: except OSError:
self.log.error(f"del ${filename} failed") self.log.error(f"del ${filename} failed")
pass
self._gen_all_music_list() self._gen_all_music_list()
# 播放一个播放列表 # 播放一个播放列表
@ -752,10 +815,12 @@ class XiaoMusic:
await self.call_main_thread_function(self.reinit) await self.call_main_thread_function(self.reinit)
def update_config_from_setting(self, data): def update_config_from_setting(self, data):
self.config.mi_did = data["mi_did"] self.config.mi_did = data.get("mi_did")
self.config.hardware = data["mi_hardware"] self.config.hardware = data.get("mi_hardware")
self.config.search_prefix = data["xiaomusic_search"] self.config.search_prefix = data.get("xiaomusic_search")
self.config.proxy = data["xiaomusic_proxy"] 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.search_prefix = self.config.search_prefix
self.proxy = self.config.proxy self.proxy = self.config.proxy
@ -764,6 +829,7 @@ class XiaoMusic:
# 重新初始化 # 重新初始化
async def reinit(self, **kwargs): async def reinit(self, **kwargs):
await self.try_update_device_id() await self.try_update_device_id()
self._gen_all_music_list()
self.log.info("reinit success") self.log.info("reinit success")
# 获取所有设备 # 获取所有设备