diff --git a/.gitignore b/.gitignore index 11d2edc..efb1376 100644 --- a/.gitignore +++ b/.gitignore @@ -173,3 +173,4 @@ xiaomusic.log.txt* node_modules reference/ .aone_copilot/ +.idea/ diff --git a/package-lock.json b/package-lock.json index 2b77c17..f9d8e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "axios": "^1.6.0", + "big-integer": "^1.6.52", "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "dayjs": "^1.11.10", @@ -33,6 +34,15 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", diff --git a/package.json b/package.json index c7284d7..26f800f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "xiaomusic/js_plugin_runner.js", "dependencies": { "axios": "^1.6.0", + "big-integer": "^1.6.52", "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.2.0", "dayjs": "^1.11.10", diff --git a/xiaomusic/api/routers/plugin.py b/xiaomusic/api/routers/plugin.py index 859c6e9..0a62075 100644 --- a/xiaomusic/api/routers/plugin.py +++ b/xiaomusic/api/routers/plugin.py @@ -116,11 +116,19 @@ async def upload_js_plugin( plugin_dir = xiaomusic.js_plugin_manager.plugins_dir os.makedirs(plugin_dir, exist_ok=True) + # 校验命名是否是保留字段【ALL/all/OpenAPI】,是的话抛错 + sys_files = ["ALL.js", "all.js", "OpenAPI.js", "OPENAPI.js"] + if file.filename in sys_files: + raise HTTPException( + status_code=409, + detail=f"插件名非法,不能命名为: {sys_files} ,请修改后再上传!", + ) file_path = os.path.join(plugin_dir, file.filename) # 校验是否已存在同名js插件 存在则提示,停止上传 if os.path.exists(file_path): raise HTTPException( - status_code=409, detail=f"插件 {file.filename} 已存在,请重命名后再上传" + status_code=409, + detail=f"插件 {file.filename} 已存在,请重命名后再上传!", ) file_path = os.path.join(plugin_dir, file.filename) @@ -142,6 +150,9 @@ async def upload_js_plugin( return {"success": False, "error": str(e)} +# ----------------------------开放接口相关函数--------------------------------------- + + @router.get("/api/openapi/load") def get_openapi_info(Verifcation=Depends(verification)): """获取开放接口配置信息""" @@ -172,3 +183,38 @@ async def update_openapi_url(request: Request, Verifcation=Depends(verification) return xiaomusic.js_plugin_manager.update_openapi_url(search_url) except Exception as e: return {"success": False, "error": str(e)} + + +# ----------------------------插件源接口--------------------------------------- + + +@router.get("/api/plugin-source/load") +def get_plugin_source_info(Verifcation=Depends(verification)): + """获取插件源配置信息""" + try: + plugin_source = xiaomusic.js_plugin_manager.get_plugin_source() + return {"success": True, "data": plugin_source} + except Exception as e: + return {"success": False, "error": str(e)} + + +@router.post("/api/plugin-source/refresh") +def refresh_plugin_source(Verifcation=Depends(verification)): + """更新订阅源""" + try: + return xiaomusic.js_plugin_manager.refresh_plugin_source() + except Exception as e: + return {"success": False, "error": str(e)} + + +@router.post("/api/plugin-source/updateUrl") +async def update_plugin_source(request: Request, Verifcation=Depends(verification)): + """更新插件源地址""" + try: + request_json = await request.json() + source_url = request_json.get("source_url") + if not request_json or "source_url" not in request_json: + return {"success": False, "error": "Missing 'search_url' in request body"} + return xiaomusic.js_plugin_manager.update_plugin_source_url(source_url) + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/xiaomusic/js_plugin_manager.py b/xiaomusic/js_plugin_manager.py index e639064..c184470 100644 --- a/xiaomusic/js_plugin_manager.py +++ b/xiaomusic/js_plugin_manager.py @@ -286,6 +286,182 @@ class JSPluginManager: self.log.error(f"Failed to update OpenAPI config: {e}") return {"success": False, "error": str(e)} + def get_plugin_source(self) -> dict[str, Any]: + """获取插件源配置信息 + Returns: + Dict[str, Any]: 包含 OpenAPI 配置信息的字典,包括启用状态和搜索 URL + """ + try: + # 读取配置文件中的 OpenAPI 配置信息 + config_data = self._get_config_data() + if config_data: + # 返回 openapi_info 配置项 + return config_data.get("plugin_source", {}) + else: + return {"enabled": False} + except Exception as e: + self.log.error(f"Failed to read plugin source info from config: {e}") + return {} + + def refresh_plugin_source(self) -> dict[str, Any]: + """更新订阅源""" + try: + if os.path.exists(self.plugins_config_path): + with open(self.plugins_config_path, encoding="utf-8") as f: + config_data = json.load(f) + plugin_source = config_data.get("plugin_source", {}) + source_url = plugin_source.get("source_url", "") + if source_url: + import requests + + # 请求源地址 + response = requests.get(source_url, timeout=30) + response.raise_for_status() # 抛出HTTP错误 + # 解析响应JSON + json_data = response.json() + # 校验响应格式 - 检查是否包含 plugins 数组 + if not isinstance(json_data, dict) or "plugins" not in json_data: + return {"success": False, "error": "无效订阅源!"} + plugins_array = json_data["plugins"] + if not isinstance(plugins_array, list): + return {"success": False, "error": "无效订阅源!"} + # 写入插件文本 + self.download_and_save_plugin(plugins_array) + # 使缓存失效 + self._invalidate_config_cache() + self.reload_plugins() + return {"success": True} + else: + return {"success": False, "error": "未找到配置订阅源!"} + else: + return {"success": False} + except Exception as e: + self.log.error(f"Failed to toggle OpenAPI config: {e}") + return {"success": False, "error": str(e)} + + def download_and_save_plugin(self, plugins_array: list) -> bool: + """下载并保存插件数组中的所有插件 + + Args: + plugins_array: 插件信息列表,格式如 [{"name": "plugin_name", "url": "plugin_url", "version": "version"}, ...] + + Returns: + bool: 所有插件下载保存是否全部成功 + """ + if not plugins_array or not isinstance(plugins_array, list): + self.log.warning("Empty or invalid plugins array provided") + return False + + all_success = True + + for plugin_info in plugins_array: + if ( + not isinstance(plugin_info, dict) + or "name" not in plugin_info + or "url" not in plugin_info + ): + self.log.warning(f"Invalid plugin entry: {plugin_info}") + all_success = False + continue + + plugin_name = plugin_info["name"] + plugin_url = plugin_info["url"] + + if not plugin_name or not plugin_url: + self.log.warning(f"Invalid plugin entry: {plugin_name} -> {plugin_url}") + all_success = False + continue + + # 调用单个插件下载方法 + success = self.download_single_plugin(plugin_name, plugin_url) + if not success: + all_success = False + self.log.error(f"Failed to download plugin: {plugin_name}") + + return all_success + + def download_single_plugin(self, plugin_name: str, plugin_url: str) -> bool: + """下载并保存单个插件 + + Args: + plugin_name: 插件名称 + plugin_url: 插件下载URL + + Returns: + bool: 下载保存是否成功 + """ + import requests + + # 检查插件名称是否合法 + sys_files = ["ALL", "all", "OpenAPI", "OPENAPI"] + if plugin_name in sys_files: + self.log.error(f"Plugin name {plugin_name} is reserved and cannot be used") + return False + + # 创建插件目录 + os.makedirs(self.plugins_dir, exist_ok=True) + + # 生成文件路径 + plugin_filename = f"{plugin_name}.js" + file_path = os.path.join(self.plugins_dir, plugin_filename) + + # 检查是否已存在同名插件 + if os.path.exists(file_path): + self.log.warning(f"Plugin {plugin_name} already exists, will overwrite") + + try: + # 下载插件内容 + response = requests.get(plugin_url, timeout=30) + response.raise_for_status() + + # 验证下载的内容是否为有效的JS代码(简单检查是否以有意义的JS字符开头) + content = response.text.strip() + if not content: + self.log.error(f"Downloaded plugin {plugin_name} has empty content") + return False + + # 保存插件文件 + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + + self.log.info(f"Successfully downloaded and saved plugin: {plugin_name}") + + # 更新插件配置 + self.update_plugin_config(plugin_name, plugin_filename) + return True + + except requests.exceptions.RequestException as e: + self.log.error( + f"Failed to download plugin {plugin_name} from {plugin_url}: {e}" + ) + return False + except Exception as e: + self.log.error(f"Failed to save plugin {plugin_name}: {e}") + return False + + def update_plugin_source_url(self, source_url: str) -> dict[str, Any]: + """更新开放接口地址""" + try: + if os.path.exists(self.plugins_config_path): + with open(self.plugins_config_path, encoding="utf-8") as f: + config_data = json.load(f) + + plugin_source = config_data.get("plugin_source", {}) + plugin_source["source_url"] = source_url + config_data["plugin_source"] = plugin_source + + with open(self.plugins_config_path, "w", encoding="utf-8") as f: + json.dump(config_data, f, ensure_ascii=False, indent=2) + + # 使缓存失效 + self._invalidate_config_cache() + return {"success": True} + else: + return {"success": False} + except Exception as e: + self.log.error(f"Failed to update plugin source config: {e}") + return {"success": False, "error": str(e)} + """----------------------------------------------------------------------""" def _get_config_data(self): @@ -332,9 +508,12 @@ class JSPluginManager: base_config = { "account": "", "password": "", + "auto_add_song": True, + "aiapi_info": {"enabled": False, "api_key": ""}, "enabled_plugins": [], - "plugins_info": [], "openapi_info": {"enabled": False, "search_url": ""}, + "plugin_source": {"source_url": ""}, + "plugins_info": [], } with open(self.plugins_config_path, "w", encoding="utf-8") as f: json.dump(base_config, f, ensure_ascii=False, indent=2) @@ -423,6 +602,7 @@ class JSPluginManager: """刷新插件列表,强制重新加载配置数据""" # 强制使缓存失效,重新加载配置 self._invalidate_config_cache() + self.reload_plugins() # 返回最新的插件列表 return self.get_plugin_list() @@ -464,7 +644,13 @@ class JSPluginManager: # 读取配置文件中的启用插件列表 config_data = self._get_config_data() if config_data: - return config_data.get("enabled_plugins", []) + enabled_plugins = config_data.get("enabled_plugins", []) + # 追加开放接口名称 + openapi_info = config_data.get("openapi_info", {}) + enabled_openapi = openapi_info.get("enabled", False) + if enabled_openapi and "OpenAPI" not in enabled_plugins: + enabled_plugins.insert(0, "OpenAPI") + return enabled_plugins else: return [] except Exception as e: @@ -554,7 +740,8 @@ class JSPluginManager: # 构造请求参数 params = {"type": "aggregateSearch", "keyword": keyword, "limit": limit} # 使用aiohttp发起异步HTTP GET请求 - async with aiohttp.ClientSession() as session: + connector = aiohttp.TCPConnector(ssl=False) # 跳过 SSL 验证 + async with aiohttp.ClientSession(connector=connector) as session: async with session.get( url, params=params, timeout=aiohttp.ClientTimeout(total=10) ) as response: @@ -667,7 +854,6 @@ class JSPluginManager: # 获取待处理的数据列表 data_list = result_data["data"] - self.log.info(f"列表信息::{data_list}") # 预计算平台权重,启用插件列表中的前9个插件有权重,排名越靠前权重越高 enabled_plugins = self.get_enabled_plugins() plugin_weights = {p: 9 - i for i, p in enumerate(enabled_plugins[:9])} @@ -708,8 +894,11 @@ class JSPluginManager: artist_score = 800 elif ar in artist: artist_score = 600 - - platform_bonus = plugin_weights.get(platform, 0) + # 开放接口的平台权重最高 20 + if platform.startswith("OpenAPI-"): + platform_bonus = 20 + else: + platform_bonus = plugin_weights.get(platform, 0) return title_score + artist_score + platform_bonus sorted_data = sorted(data_list, key=calculate_match_score, reverse=True) @@ -1120,7 +1309,7 @@ class JSPluginManager: self.plugins.clear() # 重新加载插件 self._load_plugins() - self.log.info("Plugins reloaded successfully") + self.log.info(f"最新插件信息:{self.plugins}") def update_plugin_config(self, plugin_name: str, plugin_file: str): """更新插件配置文件""" diff --git a/xiaomusic/js_plugin_runner.js b/xiaomusic/js_plugin_runner.js index 53e9f8b..3b266f3 100644 --- a/xiaomusic/js_plugin_runner.js +++ b/xiaomusic/js_plugin_runner.js @@ -195,7 +195,8 @@ class PluginRunner { 'he': require('he'), 'dayjs': require('dayjs'), 'cheerio': require('cheerio'), - 'qs': require('qs') + 'qs': require('qs'), + 'big-integer': require('big-integer') }; const safeRequire = (moduleName) => { diff --git a/xiaomusic/online_music.py b/xiaomusic/online_music.py index 4ccfdf5..b1a09f5 100644 --- a/xiaomusic/online_music.py +++ b/xiaomusic/online_music.py @@ -78,14 +78,85 @@ class OnlineMusicService: if not self.js_plugin_manager: return {"success": False, "error": "JS Plugin Manager not available"} - # 初始化 artist 变量 - artist = "" - # 解析关键词,可能通过AI或直接分割 - parsed_keyword, parsed_artist = await self._parse_keyword_with_ai(keyword) - keyword = parsed_keyword or keyword - artist = parsed_artist or artist + # 解析关键词和艺术家 + keyword, artist = await self._parse_keyword_and_artist(keyword) + # 获取API配置信息 openapi_info = self.js_plugin_manager.get_openapi_info() + + if plugin == "all": + # 并发执行插件搜索和OpenAPI搜索 + return await self._execute_concurrent_searches( + keyword, artist, page, limit, openapi_info + ) + elif plugin == "OpenAPI": + # OpenAPI搜索 + return await self._execute_openapi_search(openapi_info, keyword, artist) + else: + # 插件在线搜索 + return await self._execute_plugin_search( + plugin, keyword, artist, page, limit + ) + + async def _parse_keyword_and_artist(self, keyword): + """解析关键词和艺术家""" + parsed_keyword, parsed_artist = await self._parse_keyword_with_ai(keyword) + keyword = parsed_keyword or keyword + artist = parsed_artist or "" + return keyword, artist + + async def _execute_concurrent_searches( + self, keyword, artist, page, limit, openapi_info + ): + """执行并发搜索 - 插件和OpenAPI""" + tasks = [] + + # 插件在线搜索任务 + plugin_task = asyncio.create_task( + self.get_music_list_mf( + "all", keyword=keyword, artist=artist, page=page, limit=limit + ) + ) + tasks.append(plugin_task) + + # OpenAPI搜索任务(只有在配置正确时才创建) + if ( + openapi_info.get("enabled", False) + and openapi_info.get("search_url", "") != "" + ): + openapi_task = asyncio.create_task( + self.js_plugin_manager.openapi_search( + url=openapi_info.get("search_url"), keyword=keyword, artist=artist + ) + ) + tasks.append(openapi_task) + + # 并发执行任务 + results = await asyncio.gather(*tasks, return_exceptions=True) + + plugin_result = results[0] + openapi_result = results[1] if len(results) > 1 else None + + # 处理异常情况 + plugin_result = self._handle_search_exception(plugin_result, "插件") + openapi_result = self._handle_search_exception(openapi_result, "OpenAPI") + + # 合并结果 + combined_result = self._merge_search_results( + plugin_result, openapi_result, keyword, artist, limit + ) + combined_result["artist"] = artist or "佚名" + return combined_result + + def _handle_search_exception(self, result, source_name): + """处理搜索异常""" + if result and isinstance(result, Exception): + self.log.error(f"{source_name}搜索发生异常: {result}") + return {"success": False, "error": str(result)} + return result + + async def _execute_openapi_search(self, openapi_info, keyword, artist): + """执行OpenAPI搜索""" if ( openapi_info.get("enabled", False) and openapi_info.get("search_url", "") != "" @@ -94,17 +165,71 @@ class OnlineMusicService: result_data = await self.js_plugin_manager.openapi_search( url=openapi_info.get("search_url"), keyword=keyword, artist=artist ) - result_data["isOpenAPI"] = True else: - # 插件在线搜索 - result_data = await self.get_music_list_mf( - plugin, keyword=keyword, artist=artist, page=page, limit=limit - ) - result_data["isOpenAPI"] = False - # 将歌手名当作附加值,用于歌手搜索 + return {"success": False, "error": "OpenAPI未启用或配置错误"} + result_data["artist"] = artist or "佚名" return result_data + async def _execute_plugin_search(self, plugin, keyword, artist, page, limit): + """执行插件搜索""" + result_data = await self.get_music_list_mf( + plugin, keyword=keyword, artist=artist, page=page, limit=limit + ) + result_data["artist"] = artist or "佚名" + return result_data + + def _merge_search_results( + self, plugin_result, openapi_result, keyword, artist, limit + ): + merged_data = [] + sources = {} + + # 先处理 OpenAPI 结果 + if openapi_result and openapi_result.get("success"): + openapi_data = openapi_result.get("data", []) + if openapi_data: + for item in openapi_data: + item["source"] = "openapi" + merged_data.extend(openapi_data) + if "sources" in openapi_result: + sources.update(openapi_result["sources"]) + + # 再处理插件结果 + if plugin_result and plugin_result.get("success"): + plugin_data = plugin_result.get("data", []) + if plugin_data: + for item in plugin_data: + item["source"] = "plugin" + merged_data.extend(plugin_data) + sources.update(plugin_result.get("sources", {})) + + # 如果都没有成功结果,返回错误 + if not plugin_result.get("success") and not ( + openapi_result and openapi_result.get("success") + ): + # 优先返回第一个错误 + error_result = ( + plugin_result if not plugin_result.get("success") else openapi_result + ) + return error_result + + # 优化合并后的结果 + optimized_result = self.js_plugin_manager.optimize_search_results( + {"data": merged_data}, + search_keyword=keyword, + limit=limit, + search_artist=artist, + ) + + return { + "success": True, + "data": optimized_result.get("data", []), + "total": len(optimized_result.get("data", [])), + "sources": sources, + "merged": True, # 标识这是合并结果 + } + async def get_music_list_mf( self, plugin="all", keyword="", artist="", page=1, limit=20, **kwargs ): @@ -216,7 +341,8 @@ class OnlineMusicService: song_name = result.get("name", "") artist = result.get("artist", "") # 构建新的关键词 - keyword = _build_keyword(song_name, artist) + # keyword = _build_keyword(song_name, artist) + keyword = song_name self.log.info(f"AI提取到的信息: {result}") return keyword, artist @@ -725,9 +851,12 @@ class OnlineMusicService: return {"success": False, "error": str(e)} @staticmethod - async def get_real_url_of_openapi(url: str, timeout: int = 10) -> str: + async def _make_request_with_validation( + url: str, timeout: int, convert_m4s: bool = False + ) -> str: """ - 通过服务端代理获取开放接口真实的音乐播放URL,避免CORS问题 + 通用的URL请求和验证方法 + Args: url (str): 原始音乐URL timeout (int): 请求超时时间(秒) diff --git a/xiaomusic/static/onlineSearch/config.js b/xiaomusic/static/onlineSearch/config.js index 43f4328..a7f88be 100644 --- a/xiaomusic/static/onlineSearch/config.js +++ b/xiaomusic/static/onlineSearch/config.js @@ -1,6 +1,6 @@ // config.js window.appConfig = { // TODO 版本号 - version: "1.0.4", + version: "1.0.5", // 其他配置项可继续添加 }; diff --git a/xiaomusic/static/onlineSearch/favicon.ico b/xiaomusic/static/onlineSearch/favicon.ico new file mode 100644 index 0000000..b6346d0 Binary files /dev/null and b/xiaomusic/static/onlineSearch/favicon.ico differ diff --git a/xiaomusic/static/onlineSearch/index.html b/xiaomusic/static/onlineSearch/index.html index d834361..7a9ea10 100644 --- a/xiaomusic/static/onlineSearch/index.html +++ b/xiaomusic/static/onlineSearch/index.html @@ -2,6 +2,7 @@
+