From 043a9303a51642c65911007e35fce4514f83be22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B6=B5=E6=9B=A6?= Date: Wed, 10 Jul 2024 13:26:01 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=9A=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=88=86=E5=BC=80=E6=92=AD=E6=94=BE=20see=20#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 +- config-example.json | 158 ++--- xiaomusic/config.py | 33 +- xiaomusic/const.py | 10 + xiaomusic/httpserver.py | 70 +- xiaomusic/static/app.js | 79 ++- xiaomusic/static/index.html | 8 + xiaomusic/static/setting.html | 4 +- xiaomusic/static/setting.js | 37 +- xiaomusic/utils.py | 16 + xiaomusic/xiaomusic.py | 1164 ++++++++++++++++++--------------- 11 files changed, 904 insertions(+), 678 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d3446bd0..9361b44c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,8 @@ name: ci on: push: - branches: [ main ] + branches: + - "*" workflow_dispatch: jobs: diff --git a/config-example.json b/config-example.json index dc7be86be..144406536 100644 --- a/config-example.json +++ b/config-example.json @@ -1,81 +1,81 @@ { - "hardware": "L07A", - "account": "", - "password": "", - "mi_did": "", - "cookie": "", - "verbose": false, - "music_path": "music", - "conf_path": null, - "hostname": "192.168.2.5", - "port": 8090, - "public_port": 0, - "proxy": null, - "search_prefix": "bilisearch:", - "ffmpeg_location": "./ffmpeg/bin", - "active_cmd": "play,random_play,playlocal,play_music_list,stop", - "exclude_dirs": "@eaDir", - "music_path_depth": 10, - "disable_httpauth": true, - "httpauth_username": "admin", - "httpauth_password": "admin", - "music_list_url": "", - "music_list_json": "", - "disable_download": false, - "key_word_dict": { - "播放歌曲": "play", - "播放本地歌曲": "playlocal", - "关机": "stop", - "下一首": "play_next", - "单曲循环": "set_play_type_one", - "全部循环": "set_play_type_all", - "随机播放": "random_play", - "分钟后关机": "stop_after_minute", - "播放列表": "play_music_list", - "刷新列表": "gen_music_list", - "set_volume#": "set_volume", - "get_volume#": "get_volume", - "本地播放歌曲": "playlocal", - "放歌曲": "play", - "暂停": "stop", - "停止": "stop", - "停止播放": "stop", - "测试自定义口令": "exec#code1(\"hello\")", - "测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")" - }, - "key_match_order": [ - "set_volume#", - "get_volume#", - "分钟后关机", - "播放歌曲", - "下一首", - "单曲循环", - "全部循环", - "随机播放", - "关机", - "刷新列表", - "播放列表", - "播放本地歌曲", - "本地播放歌曲", - "放歌曲", - "暂停", - "停止", - "停止播放", - "测试自定义口令", - "测试链接" - ], - "use_music_api": false, - "use_music_audio_id": "1582971365183456177", - "use_music_id": "355454500", - "log_file": "/tmp/xiaomusic.txt", - "fuzzy_match_cutoff": 0.6, - "enable_fuzzy_match": true, - "stop_tts_msg": "收到,再见", - "keywords_playlocal": "播放本地歌曲,本地播放歌曲", - "keywords_play": "播放歌曲,放歌曲", - "keywords_stop": "关机,暂停,停止,停止播放", - "user_key_word_dict": { - "测试自定义口令": "exec#code1(\"hello\")", - "测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")" - } + "account": "", + "password": "", + "mi_did": "", + "cookie": "", + "verbose": false, + "music_path": "music", + "download_path": "", + "conf_path": null, + "hostname": "192.168.2.5", + "port": 8090, + "public_port": 0, + "proxy": null, + "search_prefix": "bilisearch:", + "ffmpeg_location": "./ffmpeg/bin", + "active_cmd": "play,set_random_play,playlocal,play_music_list,stop", + "exclude_dirs": "@eaDir", + "music_path_depth": 10, + "disable_httpauth": true, + "httpauth_username": "", + "httpauth_password": "", + "music_list_url": "", + "music_list_json": "", + "disable_download": false, + "key_word_dict": { + "播放歌曲": "play", + "播放本地歌曲": "playlocal", + "关机": "stop", + "下一首": "play_next", + "单曲循环": "set_play_type_one", + "全部循环": "set_play_type_all", + "随机播放": "set_random_play", + "分钟后关机": "stop_after_minute", + "播放列表": "play_music_list", + "刷新列表": "gen_music_list", + "本地播放歌曲": "playlocal", + "放歌曲": "play", + "暂停": "stop", + "停止": "stop", + "停止播放": "stop", + "测试自定义口令": "exec#code1(\"hello\")", + "测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")" + }, + "key_match_order": [ + "分钟后关机", + "播放歌曲", + "下一首", + "单曲循环", + "全部循环", + "随机播放", + "关机", + "刷新列表", + "播放列表", + "播放本地歌曲", + "本地播放歌曲", + "放歌曲", + "暂停", + "停止", + "停止播放", + "测试自定义口令", + "测试链接" + ], + "use_music_api": false, + "use_music_audio_id": "1582971365183456177", + "use_music_id": "355454500", + "log_file": "/tmp/xiaomusic.txt", + "fuzzy_match_cutoff": 0.6, + "enable_fuzzy_match": true, + "stop_tts_msg": "收到,再见", + "enable_config_example": true, + "keywords_playlocal": "播放本地歌曲,本地播放歌曲", + "keywords_play": "播放歌曲,放歌曲", + "keywords_stop": "关机,暂停,停止,停止播放", + "user_key_word_dict": { + "测试自定义口令": "exec#code1(\"hello\")", + "测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")" + }, + "enable_force_stop": false, + "devices": {}, + "group_list": "" } \ No newline at end of file diff --git a/xiaomusic/config.py b/xiaomusic/config.py index 6e847f378..f894f0ca0 100644 --- a/xiaomusic/config.py +++ b/xiaomusic/config.py @@ -18,12 +18,10 @@ def default_key_word_dict(): "下一首": "play_next", "单曲循环": "set_play_type_one", "全部循环": "set_play_type_all", - "随机播放": "random_play", + "随机播放": "set_random_play", "分钟后关机": "stop_after_minute", "播放列表": "play_music_list", "刷新列表": "gen_music_list", - "set_volume#": "set_volume", - "get_volume#": "get_volume", } @@ -43,8 +41,6 @@ def default_user_key_word_dict(): # 口令匹配优先级 def default_key_match_order(): return [ - "set_volume#", - "get_volume#", "分钟后关机", "播放歌曲", "下一首", @@ -57,12 +53,22 @@ def default_key_match_order(): ] +@dataclass +class Device: + did: str = "" + device_id: str = "" + hardware: str = "" + name: str = "" + play_type: int = "" + cur_music: str = "" + cur_playlist: str = "" + + @dataclass class Config: account: str = os.getenv("MI_USER", "") password: str = os.getenv("MI_PASS", "") mi_did: str = os.getenv("MI_DID", "") # 逗号分割支持多设备 - hardware: str = os.getenv("MI_HARDWARE", "L07A") # 逗号分割支持多设备 cookie: str = "" verbose: bool = os.getenv("XIAOMUSIC_VERBOSE", "").lower() == "true" music_path: str = os.getenv( @@ -79,7 +85,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,playlocal,play_music_list,stop" + "XIAOMUSIC_ACTIVE_CMD", "play,set_random_play,playlocal,play_music_list,stop" ) exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir") music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10")) @@ -123,7 +129,10 @@ class Config: enable_force_stop: bool = ( os.getenv("XIAOMUSIC_ENABLE_FORCE_STOP", "false").lower() == "true" ) - play_type: int = int(os.getenv("XIAOMUSIC_PLAY_TYPE", "2")) + devices: dict[str, Device] = field(default_factory=dict) + group_list: str = os.getenv( + "XIAOMUSIC_GROUP_LIST", "" + ) # did2:group_name,did2:group_name def append_keyword(self, keys, action): for key in keys.split(","): @@ -150,7 +159,7 @@ def __post_init__(self) -> None: if self.enable_config_example: with open("config-example.json", "w") as f: data = asdict(self) - json.dump(data, f, ensure_ascii=False, indent=4) + json.dump(data, f, ensure_ascii=False, indent=2) @classmethod def from_options(cls, options: argparse.Namespace) -> Config: @@ -171,6 +180,10 @@ def convert_value(cls, k, v, type_hints): converted_value = False if str(v).lower() == "true": converted_value = True + elif expected_type == dict[str, Device]: + converted_value = {} + for kk, vv in v.items(): + converted_value[kk] = Device(**vv) else: converted_value = expected_type(v) return converted_value @@ -192,7 +205,7 @@ def read_from_file(cls, config_path: str) -> dict: return result def update_config(self, data): - type_hints = get_type_hints(self) + type_hints = get_type_hints(self, globals(), locals()) for k, v in data.items(): converted_value = self.convert_value(k, v, type_hints) diff --git a/xiaomusic/const.py b/xiaomusic/const.py index e8d115c4c..570952de8 100644 --- a/xiaomusic/const.py +++ b/xiaomusic/const.py @@ -9,3 +9,13 @@ LATEST_ASK_API = "https://userprofile.mina.mi.com/device_profile/v2/conversation?source=dialogu&hardware={hardware}×tamp={timestamp}&limit=2" COOKIE_TEMPLATE = "deviceId={device_id}; serviceToken={service_token}; userId={user_id}" + +PLAY_TYPE_ONE = 0 # 单曲循环 +PLAY_TYPE_ALL = 1 # 全部循环 +PLAY_TYPE_RND = 2 # 随机播放 + +PLAY_TYPE_TTS = { + PLAY_TYPE_ONE: "已经设置为单曲循环", + PLAY_TYPE_ALL: "已经设置为全部循环", + PLAY_TYPE_RND: "已经设置为随机播放", +} diff --git a/xiaomusic/httpserver.py b/xiaomusic/httpserver.py index 3503f9fa7..21e7dccf2 100644 --- a/xiaomusic/httpserver.py +++ b/xiaomusic/httpserver.py @@ -53,11 +53,29 @@ def getversion(): @app.route("/getvolume", methods=["GET"]) @auth.login_required -def getvolume(): - volume = xiaomusic.get_volume_ret() - return { - "volume": volume, - } +async def getvolume(): + did = request.args.get("did") + if not xiaomusic.did_exist(did): + return {"volume": 0} + + volume = await xiaomusic.call_main_thread_function(xiaomusic.get_volume, did=did) + return {"volume": volume} + + +@app.route("/setvolume", methods=["POST"]) +@auth.login_required +async def setvolume(): + data = request.get_json() + did = data.get("did") + volume = data.get("volume") + if not xiaomusic.did_exist(did): + return {"ret": "Did not exist"} + + log.info(f"set_volume {did} {volume}") + await xiaomusic.call_main_thread_function( + xiaomusic.set_volume, did=did, arg1=volume + ) + return {"ret": "OK", "volume": volume} @app.route("/searchmusic", methods=["GET"]) @@ -70,13 +88,19 @@ def searchmusic(): @app.route("/playingmusic", methods=["GET"]) @auth.login_required def playingmusic(): - return xiaomusic.playingmusic() + did = request.args.get("did") + if not xiaomusic.did_exist(did): + return "" + return xiaomusic.playingmusic(did) @app.route("/isplaying", methods=["GET"]) @auth.login_required def isplaying(): - return xiaomusic.isplaying() + did = request.args.get("did") + if not xiaomusic.did_exist(did): + return False + return xiaomusic.isplaying(did) @app.route("/", methods=["GET"]) @@ -88,10 +112,14 @@ def index(): @auth.login_required async def do_cmd(): data = request.get_json() + did = data.get("did") cmd = data.get("cmd") + if not xiaomusic.did_exist(did): + return {"ret": "Did not exist"} + if len(cmd) > 0: - log.debug("docmd. cmd:%s", cmd) - xiaomusic.set_last_record(cmd) + log.info(f"docmd. did:{did} cmd:{cmd}") + xiaomusic.set_last_record(did, cmd) return {"ret": "OK"} return {"ret": "Unknow cmd"} @@ -101,10 +129,9 @@ async def do_cmd(): async def getsetting(): config = xiaomusic.getconfig() data = asdict(config) - alldevices = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices) - log.info(f"getsetting alldevices: {alldevices}") - data["mi_did_list"] = alldevices["did_list"] - data["mi_hardware_list"] = alldevices["hardware_list"] + device_list = await xiaomusic.call_main_thread_function(xiaomusic.getalldevices) + log.info(f"getsetting device_list: {device_list}") + data["device_list"] = device_list return data @@ -127,7 +154,10 @@ async def musiclist(): @app.route("/curplaylist", methods=["GET"]) @auth.login_required async def curplaylist(): - return xiaomusic.get_cur_play_list() + did = request.args.get("did") + if not xiaomusic.did_exist(did): + return "" + return xiaomusic.get_cur_play_list(did) @app.route("/delmusic", methods=["POST"]) @@ -149,7 +179,7 @@ def downloadjson(): ret = "OK" content = downloadfile(url) except Exception as e: - log.warning(f"downloadjson failed. url:{url} e:{e}") + log.exception(f"Execption {e}") ret = "Download JSON file failed." return { "ret": ret, @@ -166,9 +196,15 @@ def downloadlog(): @app.route("/playurl", methods=["GET"]) @auth.login_required async def playurl(): + did = request.args.get("did") url = request.args.get("url") - log.info(f"play_url:{url}") - return await xiaomusic.call_main_thread_function(xiaomusic.play_url, arg1=url) + if not xiaomusic.did_exist(did): + return {"ret": "Did not exist"} + + log.info(f"playurl did: {did} url: {url}") + return await xiaomusic.call_main_thread_function( + xiaomusic.play_url, did=did, arg1=url + ) @app.route("/debug_play_by_music_url", methods=["POST"]) diff --git a/xiaomusic/static/app.js b/xiaomusic/static/app.js index e3d9fc762..70556d8d6 100644 --- a/xiaomusic/static/app.js +++ b/xiaomusic/static/app.js @@ -13,11 +13,40 @@ $(function(){ append_op_button_name("30分钟后关机"); append_op_button_name("60分钟后关机"); - // 拉取声音 - sendcmd("get_volume#"); - $.get("/getvolume", function(data, status) { - console.log(data, status, data["volume"]); - $("#volume").val(data.volume); + // 拉取现有配置 + $.get("/getsetting", function(data, status) { + console.log(data, status); + localStorage.setItem('mi_did', data.mi_did); + + var did = localStorage.getItem('cur_did'); + if ((did == null || did == "") && data.mi_did != null) { + var dids = data.mi_did.split(','); + did = dids[0]; + localStorage.setItem('cur_did', did); + } + + window.did = did; + $.get(`/getvolume?did=${did}`, function(data, status) { + console.log(data, status, data["volume"]); + $("#volume").val(data.volume); + }); + refresh_music_list(); + + $("#did").empty(); + var dids = data.mi_did.split(','); + $.each(dids, function(index, value) { + var device = data.device_list.find(function(device) { + return device.miotDID == value; + }); + + if (device) { + var option = $('') + .val(value) + .text(device.name) + .prop('selected', value === did); + $("#did").append(option); + } + }); }); // 拉取版本 @@ -47,13 +76,20 @@ $(function(){ $('#music_list').trigger('change'); // 获取当前播放列表 - $.get("curplaylist", function(data, status) { - $('#music_list').val(data); - $('#music_list').trigger('change'); + $.get(`curplaylist?did=${did}`, function(data, status) { + if (data != "") { + $('#music_list').val(data); + $('#music_list').trigger('change'); + } }) }) + + // 每3秒获取下正在播放的音乐 + get_playing_music(); + setInterval(() => { + get_playing_music(); + }, 3000); } - refresh_music_list(); $("#play_music_list").on("click", () => { var music_list = $("#music_list").val(); @@ -84,7 +120,7 @@ $(function(){ $("#playurl").on("click", () => { var url = $("#music-url").val(); - $.get(`/playurl?url=${url}`, function(data, status) { + $.get(`/playurl?url=${url}&did=${did}`, function(data, status) { console.log(data); }); }); @@ -115,9 +151,18 @@ $(function(){ sendcmd(cmd); }); - $("#volume").on('input', function () { + $("#volume").on('change', function () { var value = $(this).val(); - sendcmd("set_volume#"+value); + $.ajax({ + type: "POST", + url: "/setvolume", + contentType: "application/json; charset=utf-8", + data: JSON.stringify({did: did, volume: value}), + success: () => { + }, + error: () => { + } + }); }); function sendcmd(cmd) { @@ -125,7 +170,7 @@ $(function(){ type: "POST", url: "/cmd", contentType: "application/json; charset=utf-8", - data: JSON.stringify({cmd: cmd}), + data: JSON.stringify({did: did, cmd: cmd}), success: () => { if (cmd == "刷新列表") { setTimeout(refresh_music_list, 3000); @@ -160,18 +205,12 @@ $(function(){ }); function get_playing_music() { - $.get("/playingmusic", function(data, status) { + $.get(`/playingmusic?did=${did}`, function(data, status) { console.log(data); $("#playering-music").text(data); }); } - // 每3秒获取下正在播放的音乐 - get_playing_music(); - setInterval(() => { - get_playing_music(); - }, 3000); - function custom_sort_key(a, b) { // 使用正则表达式提取数字前缀 const numericPrefixA = a.match(/^(\d+)/) ? parseInt(a.match(/^(\d+)/)[1], 10) : null; diff --git a/xiaomusic/static/index.html b/xiaomusic/static/index.html index aaf17f606..d5cd80e19 100644 --- a/xiaomusic/static/index.html +++ b/xiaomusic/static/index.html @@ -6,6 +6,12 @@ + + + +

小爱音箱操控面板 @@ -14,6 +20,8 @@

小爱音箱操控面板 )


+

diff --git a/xiaomusic/static/setting.html b/xiaomusic/static/setting.html index 767084b46..010e8a87b 100644 --- a/xiaomusic/static/setting.html +++ b/xiaomusic/static/setting.html @@ -24,8 +24,8 @@

小爱音箱设置面板
- -
+ +

diff --git a/xiaomusic/static/setting.js b/xiaomusic/static/setting.js index bd7d02246..2982d488c 100644 --- a/xiaomusic/static/setting.js +++ b/xiaomusic/static/setting.js @@ -16,23 +16,22 @@ $(function(){ }); }; - function updateCheckbox(selector, mi_did_list, mi_did, mi_hardware_list) { + function updateCheckbox(selector, mi_did, device_list) { // 清除现有的内容 $(selector).empty(); // 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项 var selected_dids = mi_did.split(','); - // 遍历传入的 mi_did_list 和 mi_hardware_list - $.each(mi_did_list, function(index, did) { - // 获取硬件标识,假定列表是一一对应的 - var hardware = mi_hardware_list[index]; - + $.each(device_list, function(index, device) { + var did = device.miotDID; + var hardware = device.hardware; + var name = device.name; // 创建复选框元素 var checkbox = $('', { type: 'checkbox', id: did, - value: `${did}|${hardware}`, + value: `${did}`, class: 'custom-checkbox', // 添加样式类 // 如果mi_did中包含了该did,则默认选中 checked: selected_dids.indexOf(did) !== -1 @@ -42,7 +41,7 @@ $(function(){ var label = $('