1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2026-05-24 11:35:46 +08:00

feat: 增加musicfree插件集成功能

This commit is contained in:
Boluofan
2025-12-09 16:21:53 +08:00
committed by 涵曦
parent 881d0bca5c
commit 67d93feec5
25 changed files with 5189 additions and 185 deletions

2
.gitignore vendored
View File

@@ -171,3 +171,5 @@ cache
tmp/
xiaomusic.log.txt*
node_modules
js_plugins/
reference/

View File

@@ -1,22 +1,35 @@
FROM hanxi/xiaomusic:builder AS builder
RUN pip install -U pdm
ENV PDM_CHECK_UPDATE=false
WORKDIR /app
COPY pyproject.toml README.md .
COPY pyproject.toml README.md package.json .
RUN pdm install --prod --no-editable -v
RUN npm install
COPY xiaomusic/ ./xiaomusic/
COPY plugins/ ./plugins/
COPY holiday/ ./holiday/
COPY js_plugins/ ./js_plugins/
COPY xiaomusic.py .
RUN pdm install --prod --no-editable -v
FROM hanxi/xiaomusic:runtime
WORKDIR /app
RUN mkdir -p /app/ffmpeg/bin \
&& ln -s /usr/bin/ffmpeg /app/ffmpeg/bin/ffmpeg \
&& ln -s /usr/bin/ffprobe /app/ffmpeg/bin/ffprobe
COPY --from=builder /app/.venv ./.venv
COPY --from=builder /app/node_modules ./node_modules/
COPY --from=builder /app/xiaomusic/ ./xiaomusic/
COPY --from=builder /app/plugins/ ./plugins/
COPY --from=builder /app/holiday/ ./holiday/
COPY --from=builder /app/js_plugins/ ./js_plugins/
COPY --from=builder /app/xiaomusic.py .
COPY --from=builder /app/xiaomusic/__init__.py /base_version.py
COPY --from=builder /app/package.json .
RUN touch /app/.dockerenv
COPY supervisord.conf /etc/supervisor/supervisord.conf
@@ -26,6 +39,6 @@ VOLUME /app/conf
VOLUME /app/music
EXPOSE 8090
ENV TZ=Asia/Shanghai
ENV PATH=/app/.venv/bin:$PATH
ENV PATH=/app/.venv/bin:/usr/local/bin:$PATH
ENTRYPOINT ["/bin/sh", "-c", "/usr/bin/supervisord -c /etc/supervisor/supervisord.conf && tail -F /app/supervisord.log /app/xiaomusic.log.txt"]

View File

@@ -1,14 +1,18 @@
FROM python:3.12-alpine3.22
RUN apk add --no-cache --virtual .build-deps build-base python3-dev libffi-dev openssl-dev zlib-dev jpeg-dev libc6-compat gcc musl-dev
RUN apk add --no-cache --virtual .build-deps build-base python3-dev libffi-dev openssl-dev zlib-dev jpeg-dev libc6-compat gcc musl-dev \
&& apk add --no-cache nodejs npm
RUN pip install -U pdm
ENV PDM_CHECK_UPDATE=false
WORKDIR /app
COPY pyproject.toml README.md ./
COPY pyproject.toml README.md package.json ./
RUN pdm install --prod --no-editable -v
RUN npm install
COPY xiaomusic/ ./xiaomusic/
COPY plugins/ ./plugins/
COPY holiday/ ./holiday/
COPY xiaomusic.py .
COPY js_plugins/ ./js_plugins/

View File

@@ -10,6 +10,8 @@ RUN apk add --no-cache bash\
vim \
libc6-compat \
ffmpeg \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app

View File

@@ -322,6 +322,7 @@ docker build -t xiaomusic .
- [一个第三方的主题](https://github.com/DarrenWen/xiaomusicui)
- [Umami 统计](https://github.com/umami-software/umami)
- [Sentry 报错监控](https://github.com/getsentry/sentry)
- [JS在线播放插件](https://github.com/boluofan/xiaomusic-online)
- 所有帮忙调试和测试的朋友
- 所有反馈问题和建议的朋友

60
check_plugins.py Normal file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
检查所有插件的加载状态
"""
import sys
sys.path.append('.')
from xiaomusic.config import Config
from xiaomusic.js_plugin_manager import JSPluginManager
def check_all_plugins():
print("=== 检查所有插件加载状态 ===\n")
config = Config()
config.verbose = True
class SimpleLogger:
def info(self, msg): print(f"[INFO] {msg}")
def error(self, msg): print(f"[ERROR] {msg}")
def debug(self, msg): print(f"[DEBUG] {msg}")
print("1. 创建插件管理器...")
manager = JSPluginManager(None)
manager.config = config
manager.log = SimpleLogger()
import time
time.sleep(3) # 等待插件加载
print("\n2. 获取所有插件状态...")
plugins = manager.get_plugin_list()
print(f" 总共找到 {len(plugins)} 个插件")
# 分类插件状态
working_plugins = []
failed_plugins = []
for plugin in plugins:
if plugin.get('loaded', False) and plugin.get('enabled', False):
working_plugins.append(plugin)
else:
failed_plugins.append(plugin)
print(f"\n 正常工作的插件 ({len(working_plugins)} 个):")
for plugin in working_plugins:
print(f"{plugin['name']}")
print(f"\n 失败的插件 ({len(failed_plugins)} 个):")
for plugin in failed_plugins:
print(f"{plugin['name']}: {plugin.get('error', 'Unknown error')}")
# 清理
if hasattr(manager, 'node_process') and manager.node_process:
manager.node_process.terminate()
if __name__ == "__main__":
check_all_plugins()

731
package-lock.json generated Normal file
View File

@@ -0,0 +1,731 @@
{
"name": "xiaomusic-js-plugins",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "xiaomusic-js-plugins",
"version": "1.0.0",
"dependencies": {
"axios": "^1.6.0",
"cheerio": "^1.0.0-rc.12",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"he": "^1.2.0",
"qs": "^6.14.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/cheerio": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.1.2.tgz",
"integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==",
"license": "MIT",
"dependencies": {
"cheerio-select": "^2.1.0",
"dom-serializer": "^2.0.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.2",
"encoding-sniffer": "^0.2.1",
"htmlparser2": "^10.0.0",
"parse5": "^7.3.0",
"parse5-htmlparser2-tree-adapter": "^7.1.0",
"parse5-parser-stream": "^7.1.2",
"undici": "^7.12.0",
"whatwg-mimetype": "^4.0.0"
},
"engines": {
"node": ">=20.18.1"
},
"funding": {
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
}
},
"node_modules/cheerio-select": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-2.1.0.tgz",
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-select": "^5.1.0",
"css-what": "^6.1.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/encoding-sniffer": {
"version": "0.2.1",
"resolved": "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz",
"integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==",
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.3",
"whatwg-encoding": "^3.1.1"
},
"funding": {
"url": "https://github.com/fb55/encoding-sniffer?sponsor=1"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/htmlparser2": {
"version": "10.0.0",
"resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.0.0.tgz",
"integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
"domutils": "^3.2.1",
"entities": "^6.0.0"
}
},
"node_modules/htmlparser2/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-htmlparser2-tree-adapter": {
"version": "7.1.0",
"resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
"license": "MIT",
"dependencies": {
"domhandler": "^5.0.3",
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmmirror.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
"integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==",
"license": "MIT",
"dependencies": {
"parse5": "^7.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parse5/node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/undici": {
"version": "7.16.0",
"resolved": "https://registry.npmmirror.com/undici/-/undici-7.16.0.tgz",
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
}
}
}

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "xiaomusic-js-plugins",
"version": "1.0.0",
"description": "JS plugins for xiaomusic",
"main": "xiaomusic/js_plugin_runner.js",
"dependencies": {
"axios": "^1.6.0",
"cheerio": "^1.0.0-rc.12",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"he": "^1.2.0",
"qs": "^6.14.0"
}
}

View File

@@ -26,6 +26,7 @@ dependencies = [
"fake-useragent>=2.2.0",
"miservice-fork",
"edge-tts>=7.2.3",
"psutil>=5.9.0",
]
requires-python = ">=3.10"
readme = "README.md"

View File

@@ -152,6 +152,7 @@ class Config:
)
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
keywords_search_play: str = os.getenv("XIAOMUSIC_KEYWORDS_SEARCH_PLAY", "搜索播放")
keywords_online_play: str = os.getenv("XIAOMUSIC_KEYWORDS_ONLINE_PLAY", "在线播放")
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
keywords_playlist: str = os.getenv(
"XIAOMUSIC_KEYWORDS_PLAYLIST", "播放列表,播放歌单"
@@ -247,6 +248,7 @@ class Config:
self.append_keyword(self.keywords_search_playlocal, "search_playlocal")
self.append_keyword(self.keywords_play, "play")
self.append_keyword(self.keywords_search_play, "search_play")
self.append_keyword(self.keywords_online_play, "online_play")
self.append_keyword(self.keywords_stop, "stop")
self.append_keyword(self.keywords_playlist, "play_music_list")
self.append_user_keyword()

View File

@@ -95,7 +95,7 @@ security = HTTPBasic()
def verification(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
current_username_bytes = credentials.username.encode("utf8")
correct_username_bytes = config.httpauth_username.encode("utf8")
@@ -253,6 +253,239 @@ def searchmusic(name: str = "", Verifcation=Depends(verification)):
return xiaomusic.searchmusic(name)
@app.get("/api/search/online")
async def search_online_music(
keyword: str = Query(..., description="搜索关键词"),
plugin: str = Query("all", description="指定插件名称all表示搜索所有插件"),
page: int = Query(1, description="页码"),
limit: int = Query(20, description="每页数量"),
Verifcation=Depends(verification)
):
"""在线音乐搜索API"""
try:
if not keyword:
return {"success": False, "error": "Keyword required"}
return await xiaomusic.get_music_list_online(keyword=keyword, plugin=plugin, page=page, limit=limit)
except Exception as e:
return {"success": False, "error": str(e)}
@app.get("/api/proxy/real-music-url")
async def get_real_music_url(url: str = Query(..., description="音乐下载URL"),
Verifcation=Depends(verification)):
"""通过服务端代理获取真实的音乐播放URL避免CORS问题"""
try:
# 获取真实的音乐播放URL
return await xiaomusic.get_real_url_of_openapi(url)
except Exception as e:
log.error(f"获取真实音乐URL失败: {e}")
# 如果代理获取失败仍然返回原始URL
return {
"success": False,
"realUrl": url,
"error": str(e)
}
@app.post("/api/play/getMediaSource")
async def get_media_source(
request: Request, Verifcation=Depends(verification)
):
"""获取音乐真实播放URL"""
try:
# 获取请求数据
data = await request.json()
# 调用公共函数处理
return await xiaomusic.get_media_source_url(data)
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/play/getLyric")
async def get_media_lyric(
request: Request, Verifcation=Depends(verification)
):
"""获取音乐真实播放URL"""
try:
# 获取请求数据
data = await request.json()
# 调用公共函数处理
return await xiaomusic.get_media_lyric(data)
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/play/online")
async def play_online_music(
request: Request, Verifcation=Depends(verification)
):
"""设备端在线播放插件音乐"""
try:
# 获取请求数据
data = await request.json()
did = data.get('did')
openapi_info = xiaomusic.js_plugin_manager.get_openapi_info()
if openapi_info.get("enabled", False):
media_source = await xiaomusic.get_real_url_of_openapi(data.get('url'))
else:
# 调用公共函数处理,获取音乐真实播放URL
media_source = await xiaomusic.get_media_source_url(data)
if not media_source or not media_source.get('url'):
return {"success": False, "error": "Failed to get media source URL"}
url = media_source.get('url')
decoded_url = urllib.parse.unquote(url)
return await xiaomusic.play_url(did=did, arg1=decoded_url)
except Exception as e:
return {"success": False, "error": str(e)}
# =====================================插件入口函数===============
@app.get("/api/js-plugins")
def get_js_plugins(
enabled_only: bool = Query(False, description="是否只返回启用的插件"),
Verifcation=Depends(verification)
):
"""获取插件列表"""
try:
if not hasattr(xiaomusic, 'js_plugin_manager') or not xiaomusic.js_plugin_manager:
return {"success": False, "error": "JS Plugin Manager not available"}
# 重新加载插件
# xiaomusic.js_plugin_manager.reload_plugins()
if enabled_only:
plugins = xiaomusic.js_plugin_manager.get_enabled_plugins()
else:
plugins = xiaomusic.js_plugin_manager.get_plugin_list()
return {"success": True, "data": plugins}
except Exception as e:
return {"success": False, "error": str(e)}
@app.put("/api/js-plugins/{plugin_name}/enable")
def enable_js_plugin(plugin_name: str, Verifcation=Depends(verification)):
"""启用插件"""
try:
if not hasattr(xiaomusic, 'js_plugin_manager') or not xiaomusic.js_plugin_manager:
return {"success": False, "error": "JS Plugin Manager not available"}
success = xiaomusic.js_plugin_manager.enable_plugin(plugin_name)
return {"success": success}
except Exception as e:
return {"success": False, "error": str(e)}
@app.put("/api/js-plugins/{plugin_name}/disable")
def disable_js_plugin(plugin_name: str, Verifcation=Depends(verification)):
"""禁用插件"""
try:
if not hasattr(xiaomusic, 'js_plugin_manager') or not xiaomusic.js_plugin_manager:
return {"success": False, "error": "JS Plugin Manager not available"}
success = xiaomusic.js_plugin_manager.disable_plugin(plugin_name)
return {"success": success}
except Exception as e:
return {"success": False, "error": str(e)}
@app.delete("/api/js-plugins/{plugin_name}/uninstall")
def uninstall_js_plugin(plugin_name: str, Verifcation=Depends(verification)):
"""卸载插件"""
try:
if not hasattr(xiaomusic, 'js_plugin_manager') or not xiaomusic.js_plugin_manager:
return {"success": False, "error": "JS Plugin Manager not available"}
success = xiaomusic.js_plugin_manager.uninstall_plugin(plugin_name)
return {"success": success}
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/js-plugins/upload")
async def upload_js_plugin(
file: UploadFile = File(...),
verification=Depends(verification)
):
"""上传 JS 插件"""
try:
# 验证文件扩展名
if not file.filename.endswith('.js'):
raise HTTPException(status_code=400, detail="只允许上传 .js 文件")
# 使用 JSPluginManager 中定义的插件目录
if not hasattr(xiaomusic, 'js_plugin_manager') or not xiaomusic.js_plugin_manager:
raise HTTPException(status_code=500, detail="JS Plugin Manager not available")
plugin_dir = xiaomusic.js_plugin_manager.plugins_dir
os.makedirs(plugin_dir, exist_ok=True)
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} 已存在,请重命名后再上传")
file_path = os.path.join(plugin_dir, file.filename)
# 写入文件内容
async with aiofiles.open(file_path, 'wb') as f:
content = await file.read()
await f.write(content)
# 更新插件配置文件
plugin_name = os.path.splitext(file.filename)[0]
xiaomusic.js_plugin_manager.update_plugin_config(plugin_name, file.filename)
# 重新加载插件
xiaomusic.js_plugin_manager.reload_plugins()
return {"success": True, "message": "插件上传成功"}
except Exception as e:
return {"success": False, "error": str(e)}
# =====================================开放接口配置函数===============
@app.get("/api/openapi/load")
def get_openapi_info(
Verifcation=Depends(verification)
):
"""获取开放接口配置信息"""
try:
openapi_info = xiaomusic.js_plugin_manager.get_openapi_info()
return {"success": True, "data": openapi_info}
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/openapi/toggle")
def toggle_openapi(Verifcation=Depends(verification)):
"""开放接口状态切换"""
try:
return xiaomusic.js_plugin_manager.toggle_openapi()
except Exception as e:
return {"success": False, "error": str(e)}
@app.post("/api/openapi/updateUrl")
async def update_openapi_url(request: Request, Verifcation=Depends(verification)):
"""更新开放接口地址"""
try:
request_json = await request.json()
search_url = request_json.get('search_url')
if not request_json or 'search_url' not in request_json:
return {"success": False, "error": "Missing 'search_url' in request body"}
return xiaomusic.js_plugin_manager.update_openapi_url(search_url)
except Exception as e:
return {"success": False, "error": str(e)}
# =====================================开放接口函数END===============
@app.get("/playingmusic")
def playingmusic(did: str = "", Verifcation=Depends(verification)):
if not xiaomusic.did_exist(did):
@@ -344,7 +577,7 @@ async def musiclist(Verifcation=Depends(verification)):
@app.get("/musicinfo")
async def musicinfo(
name: str, musictag: bool = False, Verifcation=Depends(verification)
name: str, musictag: bool = False, Verifcation=Depends(verification)
):
url, _ = await xiaomusic.get_music_url(name)
info = {
@@ -359,9 +592,9 @@ async def musicinfo(
@app.get("/musicinfos")
async def musicinfos(
name: list[str] = Query(None),
musictag: bool = False,
Verifcation=Depends(verification),
name: list[str] = Query(None),
musictag: bool = False,
Verifcation=Depends(verification),
):
ret = []
for music_name in name:
@@ -724,7 +957,7 @@ class PlayListUpdateObj(BaseModel):
# 修改歌单名字
@app.post("/playlistupdatename")
async def playlistupdatename(
data: PlayListUpdateObj, Verifcation=Depends(verification)
data: PlayListUpdateObj, Verifcation=Depends(verification)
):
ret = xiaomusic.play_list_update_name(data.oldname, data.newname)
if ret:
@@ -769,7 +1002,7 @@ async def playlistdelmusic(data: PlayListMusicObj, Verifcation=Depends(verificat
# 歌单更新歌曲
@app.post("/playlistupdatemusic")
async def playlistupdatemusic(
data: PlayListMusicObj, Verifcation=Depends(verification)
data: PlayListMusicObj, Verifcation=Depends(verification)
):
ret = xiaomusic.play_list_update_music(data.name, data.music_list)
if ret:
@@ -790,7 +1023,7 @@ async def getplaylist(name: str, Verifcation=Depends(verification)):
# 更新版本
@app.post("/updateversion")
async def updateversion(
version: str = "", lite: bool = True, Verifcation=Depends(verification)
version: str = "", lite: bool = True, Verifcation=Depends(verification)
):
ret = await update_version(version, lite)
if ret != "OK":
@@ -821,7 +1054,7 @@ def access_key_verification(file_path, key, code):
if key is not None:
current_key_bytes = key.encode("utf8")
correct_key_bytes = (
config.httpauth_username + config.httpauth_password
config.httpauth_username + config.httpauth_password
).encode("utf8")
is_correct_key = secrets.compare_digest(correct_key_bytes, current_key_bytes)
if is_correct_key:
@@ -832,7 +1065,7 @@ def access_key_verification(file_path, key, code):
correct_code_bytes = (
hashlib.sha256(
(
file_path + config.httpauth_username + config.httpauth_password
file_path + config.httpauth_username + config.httpauth_password
).encode("utf-8")
)
.hexdigest()
@@ -1017,8 +1250,8 @@ JWT_EXPIRE_SECONDS = 60 * 5 # 5 分钟有效期(足够前端连接和重连
@app.get("/generate_ws_token")
def generate_ws_token(
did: str,
_: bool = Depends(verification), # 复用 HTTP Basic 验证
did: str,
_: bool = Depends(verification), # 复用 HTTP Basic 验证
):
if not xiaomusic.did_exist(did):
raise HTTPException(status_code=400, detail="Invalid did")

215
xiaomusic/js_adapter.py Normal file
View File

@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
JS 插件适配器
将 MusicFree JS 插件的数据格式转换为 xiaomusic 接口规范
"""
import logging
class JSAdapter:
"""JS 插件数据适配器"""
def __init__(self, xiaomusic):
self.xiaomusic = xiaomusic
self.log = logging.getLogger(__name__)
def format_search_results(self, plugin_results: list[dict], plugin_name: str) -> list[str]:
"""格式化搜索结果为 xiaomusic 格式,返回 ID 列表"""
formatted_ids = []
for item in plugin_results:
if not isinstance(item, dict):
self.log.warning(f"Invalid item format in plugin {plugin_name}: {item}")
continue
# 构造符合 xiaomusic 格式的音乐项
music_id = self._generate_music_id(plugin_name, item.get('id', ''), item.get('songmid', ''))
music_item = {
'id': music_id,
'title': item.get('title', item.get('name', '')),
'artist': self._extract_artists(item),
'album': item.get('album', item.get('albumName', '')),
'source': 'online',
'plugin_name': plugin_name,
'original_data': item,
'duration': item.get('duration', 0),
'cover': item.get('artwork', item.get('cover', item.get('albumPic', ''))),
'url': item.get('url', ''),
'lyric': item.get('lyric', item.get('lrc', '')),
'quality': item.get('quality', ''),
}
# 添加到 all_music 字典中
self.xiaomusic.all_music[music_id] = music_item
formatted_ids.append(music_id)
return formatted_ids
def format_media_source_result(self, media_source_result: dict, music_item: dict) -> dict:
"""格式化媒体源结果"""
if not media_source_result:
return {}
formatted = {
'url': media_source_result.get('url', ''),
'headers': media_source_result.get('headers', {}),
'userAgent': media_source_result.get('userAgent', media_source_result.get('user_agent', ''))
}
return formatted
def format_lyric_result(self, lyric_result: dict) -> str:
"""格式化歌词结果为 lrc 格式字符串"""
if not lyric_result:
return ''
# 获取原始歌词和翻译
raw_lrc = lyric_result.get('rawLrc', lyric_result.get('raw_lrc', ''))
translation = lyric_result.get('translation', '')
# 如果有翻译,合并歌词和翻译
if translation and raw_lrc:
# 这里可以实现歌词和翻译的合并逻辑
return f"{raw_lrc}\n{translation}"
return raw_lrc or translation or ''
def format_album_info_result(self, album_info_result: dict) -> dict:
"""格式化专辑信息结果"""
if not album_info_result:
return {}
formatted = {
'isEnd': album_info_result.get('isEnd', True),
'musicList': self.format_search_results(album_info_result.get('musicList', []), 'album'),
'albumItem': {
'title': album_info_result.get('albumItem', {}).get('title', ''),
'artist': album_info_result.get('albumItem', {}).get('artist', ''),
'cover': album_info_result.get('albumItem', {}).get('cover', ''),
'description': album_info_result.get('albumItem', {}).get('description', ''),
}
}
return formatted
def format_music_sheet_info_result(self, music_sheet_result: dict) -> dict:
"""格式化音乐单信息结果"""
if not music_sheet_result:
return {}
formatted = {
'isEnd': music_sheet_result.get('isEnd', True),
'musicList': self.format_search_results(music_sheet_result.get('musicList', []), 'playlist'),
'sheetItem': {
'title': music_sheet_result.get('sheetItem', {}).get('title', ''),
'cover': music_sheet_result.get('sheetItem', {}).get('cover', ''),
'description': music_sheet_result.get('sheetItem', {}).get('description', ''),
}
}
return formatted
def format_artist_works_result(self, artist_works_result: dict) -> dict:
"""格式化艺术家作品结果"""
if not artist_works_result:
return {}
formatted = {
'isEnd': artist_works_result.get('isEnd', True),
'data': self.format_search_results(artist_works_result.get('data', []), 'artist'),
}
return formatted
def format_top_lists_result(self, top_lists_result: list[dict]) -> list[dict]:
"""格式化榜单列表结果"""
if not top_lists_result:
return []
formatted = []
for group in top_lists_result:
formatted_group = {
'title': group.get('title', ''),
'data': []
}
for item in group.get('data', []):
formatted_item = {
'id': item.get('id', ''),
'title': item.get('title', ''),
'description': item.get('description', ''),
'coverImg': item.get('coverImg', item.get('cover', '')),
}
formatted_group['data'].append(formatted_item)
formatted.append(formatted_group)
return formatted
def format_top_list_detail_result(self, top_list_detail_result: dict) -> dict:
"""格式化榜单详情结果"""
if not top_list_detail_result:
return {}
formatted = {
'isEnd': top_list_detail_result.get('isEnd', True),
'musicList': self.format_search_results(top_list_detail_result.get('musicList', []), 'toplist'),
'topListItem': top_list_detail_result.get('topListItem', {}),
}
return formatted
def _generate_music_id(self, plugin_name: str, item_id: str, fallback_id: str = '') -> str:
"""生成唯一音乐ID"""
if item_id:
return f"online_{plugin_name}_{item_id}"
else:
# 如果没有 id尝试使用其他标识符
return f"online_{plugin_name}_{fallback_id}"
def _extract_artists(self, item: dict) -> str:
"""提取艺术家信息"""
# 尝试多种可能的艺术家字段
artist_fields = ['artist', 'artists', 'singer', 'author', 'creator', 'singers']
for field in artist_fields:
if field in item:
value = item[field]
if isinstance(value, list):
# 如果是艺术家列表,连接为字符串
artists = []
for artist in value:
if isinstance(artist, dict):
artists.append(artist.get('name', str(artist)))
else:
artists.append(str(artist))
return ', '.join(artists)
elif isinstance(value, dict):
# 如果是艺术家对象
return value.get('name', str(value))
elif value:
return str(value)
# 如果没有找到艺术家信息,返回默认值
return '未知艺术家'
def convert_music_item_for_plugin(self, music_item: dict) -> dict:
"""将 xiaomusic 音乐项转换为插件兼容格式"""
# 如果原始数据存在,优先使用原始数据
if isinstance(music_item, dict) and 'original_data' in music_item:
return music_item['original_data']
# 否则构造一个基本的音乐项
converted = {
'id': music_item.get('id', ''),
'title': music_item.get('title', ''),
'artist': music_item.get('artist', ''),
'album': music_item.get('album', ''),
'url': music_item.get('url', ''),
'duration': music_item.get('duration', 0),
'artwork': music_item.get('cover', ''),
'lyric': music_item.get('lyric', ''),
'quality': music_item.get('quality', ''),
}
return converted

File diff suppressed because it is too large Load Diff

657
xiaomusic/js_plugin_runner.js vendored Normal file
View File

@@ -0,0 +1,657 @@
#!/usr/bin/env node
/**
* JS 插件运行器
* 在安全的沙箱环境中运行 MusicFree JS 插件
*/
const vm = require('vm');
const fs = require('fs');
// 设置编码
process.stdin.setEncoding('utf8');
process.stdout.setDefaultEncoding('utf8');
class PluginRunner {
constructor() {
this.plugins = new Map();
this.requestId = 0;
this.pendingRequests = new Map();
this.setupMessageHandler();
}
setupMessageHandler() {
let buffer = '';
process.stdin.on('data', (data) => {
buffer += data.toString();
// 按行分割并处理完整的消息
let lines = buffer.split('\n');
buffer = lines.pop(); // 保留最后一行(可能不完整)
for (const line of lines) {
if (line.trim() === '') continue;
try {
const message = JSON.parse(line.trim());
console.log(`[JS_PLUGIN_RUNNER] Raw message received: ${line.trim()}`);
this.handleMessage(message);
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] Failed to parse message: ${line.trim()}`);
console.error(`[JS_PLUGIN_RUNNER] Error: ${error.message}`);
this.sendResponse('unknown', {
success: false,
error: `JSON parse error: ${error.message}`
});
}
}
});
}
async handleMessage(message) {
const { id, action } = message;
// 只在必要时输出日志以避免干扰通信
// console.debug(`[JS_PLUGIN_RUNNER] Received message: ${action} with id: ${id}`);
// if (message.pluginName) console.debug(`[JS_PLUGIN_RUNNER] Plugin: ${message.pluginName}`);
// if (message.params) console.debug(`[JS_PLUGIN_RUNNER] Params:`, message.params);
// if (message.musicItem) console.debug(`[JS_PLUGIN_RUNNER] Music Item:`, message.musicItem);
try {
let result;
switch (action) {
case 'load':
console.log(`[JS_PLUGIN_RUNNER] Loading plugin: ${message.name}`);
result = this.loadPlugin(message.name, message.code);
break;
case 'search':
result = await this.search(message.pluginName, message.params);
break;
case 'getMediaSource':
result = await this.getMediaSource(message.pluginName, message.musicItem, message.quality);
break;
case 'getLyric':
result = await this.getLyric(message.pluginName, message.musicItem);
break;
case 'getMusicInfo':
result = await this.getMusicInfo(message.pluginName, message.musicItem);
break;
case 'getAlbumInfo':
result = await this.getAlbum(message.pluginName, message.albumInfo);
break;
case 'getMusicSheetInfo':
result = await this.getPlaylist(message.pluginName, message.playlistInfo);
break;
case 'getArtistWorks':
result = await this.getArtistWorks(message.pluginName, message.artistItem, message.page, message.type);
break;
case 'importMusicItem':
result = await this.importMusicItem(message.pluginName, message.urlLike);
break;
case 'importMusicSheet':
result = await this.importMusicSheet(message.pluginName, message.urlLike);
break;
case 'getTopLists':
result = await this.getTopLists(message.pluginName);
break;
case 'getTopListDetail':
result = await this.getTopListDetail(message.pluginName, message.topListItem, message.page);
break;
case 'unload':
console.log(`[JS_PLUGIN_RUNNER] Unloading plugin: ${message.name}`);
result = this.unloadPlugin(message.name);
break;
default:
throw new Error(`Unknown action: ${action}`);
}
this.sendResponse(id, { success: true, result });
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] Action ${action} failed:`, error.message);
this.sendResponse(id, { success: false, error: error.message });
}
}
sendResponse(id, response) {
response.id = id;
process.stdout.write(JSON.stringify(response) + '\n');
}
loadPlugin(name, code) {
try {
// 创建安全的沙箱环境
const sandbox = this.createSandbox();
// 创建上下文
const context = vm.createContext(sandbox);
// 包装代码以支持 ES6 模块语法
const wrappedCode = `
(function() {
${code}
// 如果是 TypeScript 编译的代码,需要处理 exports
if (typeof module !== 'undefined' && module.exports) {
return module.exports;
}
// 如果是 ES6 模块,需要处理 exports
if (typeof exports !== 'undefined' && exports.__esModule) {
return exports.default || exports;
}
return module.exports;
})();
`;
// 执行插件代码
const options = {
timeout: 10000,
displayErrors: true,
breakOnSigint: false
};
const plugin = vm.runInContext(wrappedCode, context, options);
// 验证插件接口
if (!plugin || typeof plugin !== 'object') {
throw new Error('Plugin must export an object');
}
this.plugins.set(name, plugin);
// 记录插件信息
this.plugins.set(name + '_meta', {
loadTime: Date.now(),
capabilities: this.detectCapabilities(plugin)
});
return true;
} catch (error) {
console.error(`Failed to load plugin ${name}:`, error.message);
throw error;
}
}
createSandbox() {
const safeConsole = {
log: (...args) => {}, // 禁用插件的 console.log 避免干扰主进程通信
warn: (...args) => console.warn(`[PLUGIN]`, ...args), // 保留警告,但添加标识
error: (...args) => console.error(`[PLUGIN]`, ...args), // 保留错误,但添加标识
debug: (...args) => {} // 禁用调试输出
};
const safeFetch = async (url, options = {}) => {
// 代理网络请求到主进程
return await this.proxyFetch(url, options);
};
const safeTimer = (callback, delay) => {
if (delay > 10000) { // 最大10秒
throw new Error('Timer delay too long');
}
return setTimeout(callback, delay);
};
// 支持的模块列表
const allowedModules = {
'axios': require('axios'),
'crypto-js': require('crypto-js'),
'he': require('he'),
'dayjs': require('dayjs'),
'cheerio': require('cheerio'),
'qs': require('qs')
};
const safeRequire = (moduleName) => {
if (allowedModules[moduleName]) {
return allowedModules[moduleName];
}
throw new Error(`Module '${moduleName}' is not allowed or not installed`);
};
const module = { exports: {} };
const exports = module.exports;
// 模拟 env 对象
const env = {
getUserVariables: () => ({
music_u: '',
ikun_key: '',
source: ''
})
};
return {
// 基础对象
console: safeConsole,
Buffer: Buffer,
Math: Math,
Date: Date,
JSON: JSON,
// 受限的全局对象
global: undefined,
process: undefined,
// 受限的网络访问
fetch: safeFetch,
XMLHttpRequest: undefined,
// 受限的定时器
setTimeout: safeTimer,
setInterval: undefined,
clearTimeout: clearTimeout,
clearInterval: clearInterval,
// 模块系统
module: module,
exports: exports,
require: safeRequire,
// MusicFree 环境对象
env: env
};
}
detectCapabilities(plugin) {
const capabilities = [];
if (typeof plugin.search === 'function') capabilities.push('search');
if (typeof plugin.getMediaSource === 'function') capabilities.push('getMediaSource');
if (typeof plugin.getLyric === 'function') capabilities.push('getLyric');
if (typeof plugin.getAlbum === 'function') capabilities.push('getAlbum');
if (typeof plugin.getPlaylist === 'function') capabilities.push('getPlaylist');
return capabilities;
}
async search(pluginName, params) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 search 方法 - 参考 MusicFreeDesktop 实现
if (!plugin.search || typeof plugin.search !== 'function') {
// 只在详细模式下输出调试信息
console.debug(`[JS_PLUGIN_RUNNER] Plugin ${pluginName} does not have a search function`);
return {
isEnd: true,
data: []
};
}
try {
let query, page, type;
if (typeof params === 'string') {
// 兼容旧的字符串格式
query = params;
page = 1;
type = 'music';
} else if (typeof params === 'object') {
// 新的对象格式,参考 MusicFreeDesktop
query = params.keywords || params.query || '';
page = params.page || 1;
type = params.type || 'music';
} else {
throw new Error('Invalid search parameters');
}
// 移除调试输出,避免干扰 JSON 通信
// console.debug(`[JS_PLUGIN_RUNNER] Calling search with query: ${query}, page: ${page}, type: ${type}`);
const result = await plugin.search(query, page, type);
// 将调试信息写入日志文件而不是控制台
fs.appendFileSync('00-plugin_debug.log', `===========================${pluginName}插件原始返回结果:===================================\n`);
fs.appendFileSync('00-plugin_debug.log', `${JSON.stringify(result, null, 2)}\n`);
// 严格验证返回结果 - 参考 MusicFreeDesktop 实现
if (!result || typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid search result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid search result`);
}
// 确保返回正确的数据结构 - 参考 MusicFreeDesktop 实现
const validatedResult = {
isEnd: result.isEnd !== false, // 默认为 true除非明确设置为 false
data: Array.isArray(result.data) ? result.data : [] // 确保 data 是数组
};
//为 validatedResult中data的 每个元素添加一个 platform 属性
validatedResult.data.forEach(item => {
item.platform = pluginName;
});
// 不输出调试信息以避免干扰通信
return validatedResult;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] Search error in plugin ${pluginName}:`, error.message);
// console.error(`[JS_PLUGIN_RUNNER] Full error:`, error); // 避免输出可能包含非JSON的对象
throw new Error(`Search failed in plugin ${pluginName}: ${error.message}`);
}
}
async getMediaSource(pluginName, musicItem, quality) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getMediaSource 方法 - 参考 MusicFreeDesktop 实现
if (!plugin.getMediaSource || typeof plugin.getMediaSource !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.getMediaSource(musicItem,quality);
// 参考 MusicFreeDesktop 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid media source result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid media source result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getMediaSource error in plugin ${pluginName}:`, error.message);
throw new Error(`getMediaSource failed in plugin ${pluginName}: ${error.message}`);
}
}
async getLyric(pluginName, songId) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getLyric 方法 - 参考 MusicFreeDesktop 实现
if (!plugin.getLyric || typeof plugin.getLyric !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.getLyric(songId);
// 参考 MusicFreeDesktop 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid lyric result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid lyric result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getLyric error in plugin ${pluginName}:`, error.message);
throw new Error(`getLyric failed in plugin ${pluginName}: ${error.message}`);
}
}
async getAlbum(pluginName, albumInfo) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getAlbumInfo 方法 (按照官方文档标准)
if (!plugin.getAlbumInfo || typeof plugin.getAlbumInfo !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
// 使用默认页码 1从MusicFree官方文档得知默认为1
const result = await plugin.getAlbumInfo(albumInfo, 1);
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid album result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid album result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getAlbumInfo error in plugin ${pluginName}:`, error.message);
throw new Error(`getAlbumInfo failed in plugin ${pluginName}: ${error.message}`);
}
}
async getPlaylist(pluginName, playlistInfo) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getMusicSheetInfo 方法 (按照官方文档标准)
if (!plugin.getMusicSheetInfo || typeof plugin.getMusicSheetInfo !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
// 使用默认页码 1从MusicFree官方文档得知默认为1
const result = await plugin.getMusicSheetInfo(playlistInfo, 1);
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid playlist result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid playlist result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getMusicSheetInfo error in plugin ${pluginName}:`, error.message);
throw new Error(`getMusicSheetInfo failed in plugin ${pluginName}: ${error.message}`);
}
}
async getMusicInfo(pluginName, musicItem) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getMusicInfo 方法 (按照官方文档标准)
if (!plugin.getMusicInfo || typeof plugin.getMusicInfo !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.getMusicInfo(musicItem);
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid music info result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid music info result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getMusicInfo error in plugin ${pluginName}:`, error.message);
throw new Error(`getMusicInfo failed in plugin ${pluginName}: ${error.message}`);
}
}
async getArtistWorks(pluginName, artistItem, page = 1, type = 'music') {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getArtistWorks 方法 (按照官方文档标准)
if (!plugin.getArtistWorks || typeof plugin.getArtistWorks !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.getArtistWorks(artistItem, page, type);
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid artist works result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid artist works result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getArtistWorks error in plugin ${pluginName}:`, error.message);
throw new Error(`getArtistWorks failed in plugin ${pluginName}: ${error.message}`);
}
}
async importMusicItem(pluginName, urlLike) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 importMusicItem 方法 (按照官方文档标准)
if (!plugin.importMusicItem || typeof plugin.importMusicItem !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.importMusicItem(urlLike);
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid import music item result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid import music item result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] importMusicItem error in plugin ${pluginName}:`, error.message);
throw new Error(`importMusicItem failed in plugin ${pluginName}: ${error.message}`);
}
}
async importMusicSheet(pluginName, urlLike) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 importMusicSheet 方法 (按照官方文档标准)
if (!plugin.importMusicSheet || typeof plugin.importMusicSheet !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.importMusicSheet(urlLike);
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (!Array.isArray(result)) {
console.error(`[JS_PLUGIN_RUNNER] Invalid import music sheet result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid import music sheet result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] importMusicSheet error in plugin ${pluginName}:`, error.message);
throw new Error(`importMusicSheet failed in plugin ${pluginName}: ${error.message}`);
}
}
async getTopLists(pluginName) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getTopLists 方法 (按照官方文档标准)
if (!plugin.getTopLists || typeof plugin.getTopLists !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.getTopLists();
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (!Array.isArray(result)) {
console.error(`[JS_PLUGIN_RUNNER] Invalid top lists result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid top lists result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getTopLists error in plugin ${pluginName}:`, error.message);
throw new Error(`getTopLists failed in plugin ${pluginName}: ${error.message}`);
}
}
async getTopListDetail(pluginName, topListItem, page = 1) {
const plugin = this.plugins.get(pluginName);
if (!plugin) {
throw new Error(`Plugin ${pluginName} not found`);
}
// 检查插件是否有 getTopListDetail 方法 (按照官方文档标准)
if (!plugin.getTopListDetail || typeof plugin.getTopListDetail !== 'function') {
// 不输出调试信息以避免干扰通信
return null;
}
try {
const result = await plugin.getTopListDetail(topListItem, page);
// 参考 MusicFree 实现,验证结果
if (result === null || result === undefined) {
return null;
}
if (typeof result !== 'object') {
console.error(`[JS_PLUGIN_RUNNER] Invalid top list detail result from plugin ${pluginName}:`, typeof result);
throw new Error(`Plugin ${pluginName} returned invalid top list detail result`);
}
return result;
} catch (error) {
console.error(`[JS_PLUGIN_RUNNER] getTopListDetail error in plugin ${pluginName}:`, error.message);
throw new Error(`getTopListDetail failed in plugin ${pluginName}: ${error.message}`);
}
}
unloadPlugin(name) {
const deleted = this.plugins.delete(name);
this.plugins.delete(name + '_meta');
return deleted;
}
async proxyFetch(url, options) {
// 代理网络请求到主进程
const requestId = ++this.requestId;
return new Promise((resolve, reject) => {
// 发送请求到主进程
this.sendResponse('fetch_' + requestId, {
action: 'fetch',
requestId: requestId,
url: url,
options: options
});
// 等待响应(简化实现)
setTimeout(() => {
reject(new Error('Fetch proxy not implemented'));
}, 1000);
});
}
}
// 启动插件运行器
const runner = new PluginRunner();
// 处理进程退出
process.on('SIGINT', () => {
console.log('Received SIGINT, shutting down gracefully');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('Received SIGTERM, shutting down gracefully');
process.exit(0);
});

View File

@@ -0,0 +1,10 @@
{
"account": "",
"password": "",
"openapi_info": {
"search_url": "https://music-dl.sayqz.com/api/",
"enabled": true
},
"enabled_plugins": [],
"plugins_info": []
}

View File

@@ -108,7 +108,7 @@ var vConsole = new window.VConsole();
<input id="log_file" type="text" value="xiaomusic.log.txt" />
<label for="active_cmd">允许唤醒的命令:</label>
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop" />
<input id="active_cmd" type="text" value="play,online_play,random_play,playlocal,play_music_list,stop" />
<label for="exclude_dirs">忽略目录(逗号分割):</label>
<input id="exclude_dirs" type="text" value="@eaDir,tmp" />
@@ -231,6 +231,8 @@ var vConsole = new window.VConsole();
<input id="keywords_search_playlocal" type="text" value="本地搜索播放" />
<label for="keywords_search_play">搜索播放口令(会产生临时播放列表):</label>
<input id="keywords_search_play" type="text" value="搜索播放" />
<label for="keywords_online_play">在线播放口令(在线搜索接口或插件):</label>
<input id="keywords_online_play" type="text" value="在线播放" />
<label for="enable_yt_dlp_cookies">启用yt-dlp-cookies(需要先上传yt-dlp-cookies.txt文件):</label>
<select id="enable_yt_dlp_cookies">
@@ -300,20 +302,20 @@ var vConsole = new window.VConsole();
<button onclick="location.href='/static/default/index.html';">返回首页</button>
<button class="save-button">保存</button>
</div>
<div class="button-group">
<button id="refresh_music_tag">刷新tag</button>
<button id="clear_cache">清空缓存</button>
<a href="/downloadlog" download="xiaomusic.txt"><button>下载日志文件</button></a>
</div>
<div class="button-group">
<a href="/docs" target="_blank"><button>接口文档</button></a>
<a href="./m3u.html" target="_blank"><button>m3u转换</button></a>
<a href="./downloadtool.html" target="_blank"><button>歌曲下载工具</button></a>
<a href="./merge/index.html" target="_blank"><button>歌单合并工具</button></a>
</div>
<div class="button-group">
<a href="./debug.html" target="_blank"><button>调试工具</button></a>
<a href="https://afdian.com/a/imhanxi" target="_blank"><button>💰 爱发电</button></a>

View File

@@ -63,6 +63,9 @@
<div class="options_list">
<a href="/static/soundSpace/index.html">SoundSpace</a>
</div>
<div class="options_list">
<a href="/static/onlineSearch/index.html">OnlineSearch</a>
</div>
<div class="options_list weapp">
<a href="https://github.com/F-loat/xiaoplayer" target="_blank">微信小程序</a>
<iframe width="240px" height="240px" src="/static/weapp/qrcode.html"></iframe>

939
xiaomusic/static/onlineSearch/index.html vendored Normal file
View File

@@ -0,0 +1,939 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>在线音乐搜索</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: #31c27c;
color: white;
padding: 20px;
text-align: center;
}
.search-section {
padding: 20px;
border-bottom: 1px solid #eee;
}
.search-box {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-btn {
padding: 10px 20px;
background: #31c27c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.search-btn:hover {
background: #28a869;
}
.results-section {
padding: 20px;
}
.music-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
}
.music-item:last-child {
border-bottom: none;
}
.music-cover {
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 15px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #999;
}
.music-info {
flex: 1;
}
.music-title {
font-weight: bold;
margin-bottom: 5px;
}
.music-artist {
color: #666;
font-size: 14px;
margin-bottom: 3px;
}
.music-meta {
color: #999;
font-size: 12px;
}
.music-actions {
display: flex;
gap: 10px;
align-items: center;
}
.play-btn {
padding: 8px 15px;
background: #1976d2;/*31c27c*/
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.play-btn:hover {
background: #1565c0;/*28a869*/
}
.web-play-btn {
padding: 8px 15px;
background: #31c27c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.web-play-btn:hover {
background: #28a869;
}
.source-tag {
padding: 2px 8px;
background: #e3f2fd;
color: #1976d2;
border-radius: 12px;
font-size: 11px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
text-align: center;
padding: 40px;
color: #d32f2f;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
}
/* 三个按钮的样式 */
.header-buttons {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 15px;
}
.header-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #fff;
color: #31c27c;
border: 1px solid #31c27c;/*31c27c*/
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
transition: all 0.2s ease;
}
.header-btn:hover {
background: #f0f0f0;
}
.material-icons {
font-size: 20px;
}
/* 音乐播放器样式 */
.music-player {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid #eee;
padding: 10px 20px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
z-index: 1000;
display: none;
border-bottom: 1px solid #f0f0f0;
}
.player-content {
display: flex;
align-items: center;
gap: 15px;
}
audio {
width: 100%;
outline: none;
margin-right: 10px;
}
audio::-webkit-media-controls-panel {
background: white;
}
/* 优化播放/暂停按钮样式 */
.player-play-pause {
background: #31c27c;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.2s ease;
box-sizing: border-box; /* 添加此行确保padding/border不影响尺寸 */
flex-shrink: 0; /* 防止在flex容器中被压缩 */
}
.player-play-pause:hover {
background: #28a869;/*1565c0*/
transform: scale(1.05);
}
/* 调整播放按钮内 SVG 图标大小 */
.player-play-pause svg {
width: 24px;
height: 24px;
fill: white; /* 添加此行将图标颜色设为白色 */
}
/* 优化播放器整体样式 */
.music-player {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid #eee;
padding: 12px 20px;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
z-index: 1000;
display: none;
border-bottom: 1px solid #f0f0f0;
}
/* 优化播放器封面图样式 */
.player-cover {
width: 50px;
height: 50px;
border-radius: 4px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #999;
margin-right: 15px;
border: 1px solid #eee;
}
.player-cover img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
/* 优化播放器信息显示 */
.player-info {
min-width: 0;
/* 限制最大宽度 */
max-width: 20%;
overflow: hidden;
}
.player-info .title {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
color: #333;
}
.player-info .artist {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 优化播放器控制区域 */
.player-controls {
display: flex;
align-items: center;
gap: 15px;
}
/* 优化关闭按钮 */
.player-close {
background: #f5f5f5;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.player-close:hover {
background: #e0e0e0;
transform: scale(1.05);
}
/* 歌词展示区域样式 */
.lyric-container {
flex: 1;
height: 46px; /* 改为只显示两行的高度 */
overflow: hidden;
margin-top: 10px;
padding: 0 20px;
position: relative;
}
.lyric-content {
text-align: center;
color: #666;
font-size: 14px;
line-height: 20px;
transition: transform 0.3s ease-out;
}
.lyric-line {
padding: 2px 0;
transition: all 0.3s ease;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lyric-line.active {
color: #31c27c;
font-weight: bold;
font-size: 16px;
}
/* 返回顶部按钮样式 */
.back-to-top {
position: fixed;
bottom: 120px;
right: 20px;
width: 40px;
height: 40px;
background: #31c27c;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.back-to-top.show {
opacity: 1;
visibility: visible;
}
.back-to-top:hover {
background: #28a869;
transform: translateY(-2px);
}
.back-to-top svg {
width: 20px;
height: 20px;
fill: white;
}
</style>
</head>
<!-- 返回顶部按钮 -->
<button class="back-to-top" id="backToTop" onclick="scrollToTop()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>
</svg>
</button>
<body>
<div class="container">
<div class="header">
<h1>在线音乐搜索</h1>
<p>通过 MusicFree音源插件、开放音乐接口 搜索在线音乐</p>
<!-- 三个居中按钮 -->
<div class="header-buttons">
<a href="/" class="header-btn">
返回首页
</a>
<a href="./setting.html" target="_blank" class="header-btn">
后台配置
</a>
<a href="https://api.tunefree.fun/" target="_blank" class="header-btn">
OpenAPI
</a>
<a href="https://github.com/maotoumao/MusicFreePlugins" target="_blank" class="header-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
</div>
</div>
<div class="search-section">
<div class="search-box">
<input type="text" class="search-input" id="searchInput" placeholder="输入歌曲名或艺术家名...">
<select class="search-select" id="pluginSelect">
<option value="all">所有插件</option>
</select>
<button class="search-btn" onclick="searchMusic()">搜索</button>
</div>
</div>
<div class="results-section" id="results">
<div class="empty">
请输入关键词开始搜索
</div>
</div>
</div>
<!-- 音乐播放器 -->
<div class="music-player" id="musicPlayer">
<div class="player-content">
<!-- 增加封面图展示 -->
<div class="player-cover" id="playerCover">
<img src="" alt="专辑封面" referrerpolicy="no-referrer" id="coverImage" style="width: 50px; height: 50px; border-radius: 4px; object-fit: cover; display: none;">
<div id="coverPlaceholder" style="font-size: 12px; color: #999;"></div>
</div>
<div class="player-info">
<div class="title" id="playerTitle">未知歌曲</div>
<div class="artist" id="playerArtist">未知艺术家</div>
</div>
<!-- 歌词展示区域 -->
<div class="lyric-container" id="lyricContainer">
<div class="lyric-content" id="lyricContent"></div>
</div>
<div class="player-controls">
<!-- 增加播放/暂停按钮 -->
<button class="player-play-pause" id="playPauseBtn" onclick="togglePlayPause()">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" id="playIcon">
<path d="M8 5v14l11-7z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" id="pauseIcon" style="display: none;">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</button>
<audio id="audioPlayer" controls></audio>
<button class="player-close" onclick="closePlayer()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
</div>
</div>
</div>
<script>
// 全局变量
let currentLyrics = []; // 存储解析后的歌词
let lyricLines = []; // 存储歌词DOM元素
// 页面加载时获取插件列表
window.onload = function() {
loadPlugins();
};
// 显示/隐藏返回顶部按钮
window.addEventListener('scroll', function() {
const backToTopButton = document.getElementById('backToTop');
if (window.pageYOffset > 300) {
backToTopButton.classList.add('show');
} else {
backToTopButton.classList.remove('show');
}
});
// 平滑滚动到顶部
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
// 页面加载完成后初始化按钮状态
window.addEventListener('load', function() {
const backToTopButton = document.getElementById('backToTop');
if (window.pageYOffset > 300) {
backToTopButton.classList.add('show');
}
});
// 切换播放/暂停状态
function togglePlayPause() {
const audio = document.getElementById('audioPlayer');
const playIcon = document.getElementById('playIcon');
const pauseIcon = document.getElementById('pauseIcon');
if (audio.paused) {
audio.play();
playIcon.style.display = 'none';
pauseIcon.style.display = 'block';
} else {
audio.pause();
playIcon.style.display = 'block';
pauseIcon.style.display = 'none';
}
}
// 监听音频播放状态变化,同步按钮图标
document.getElementById('audioPlayer').addEventListener('play', function() {
document.getElementById('playIcon').style.display = 'none';
document.getElementById('pauseIcon').style.display = 'block';
});
document.getElementById('audioPlayer').addEventListener('pause', function() {
document.getElementById('playIcon').style.display = 'block';
document.getElementById('pauseIcon').style.display = 'none';
});
// 加载插件列表
async function loadPlugins() {
try {
const response = await fetch('/api/js-plugins?enabled_only=true');
const data = await response.json();
if (data.success) {
const select = document.getElementById('pluginSelect');
data.data.forEach(plugin => {
const option = document.createElement('option');
option.value = plugin;
option.textContent = plugin;
select.appendChild(option);
});
}
} catch (error) {
console.error('Failed to load plugins:', error);
}
}
// 搜索音乐
async function searchMusic() {
const keyword = document.getElementById('searchInput').value.trim();
const plugin = document.getElementById('pluginSelect').value;
const resultsDiv = document.getElementById('results');
if (!keyword) {
resultsDiv.innerHTML = '<div class="error">请输入搜索关键词</div>';
return;
}
// 显示加载状态
resultsDiv.innerHTML = '<div class="loading">搜索中...</div>';
try {
const response = await fetch(`/api/search/online?keyword=${encodeURIComponent(keyword)}&plugin=${plugin}`);
const data = await response.json();
if (data.success) {
displayResults(data.data);
} else {
resultsDiv.innerHTML = `<div class="error">搜索失败: ${data.error}</div>`;
}
} catch (error) {
resultsDiv.innerHTML = `<div class="error">搜索出错: ${error.message}</div>`;
}
}
// 显示搜索结果
function displayResults(results) {
const resultsDiv = document.getElementById('results');
if (!results || results.length === 0) {
resultsDiv.innerHTML = '<div class="empty">没有找到相关音乐</div>';
return;
}
const html = results.map(item => `
<div class="music-item">
<div class="music-cover">
${item.artwork ?
`<img src="${item.artwork}" referrerpolicy="no-referrer" style="width:100%;height:100%;object-fit:cover;border-radius:4px;">` :
'暂无封面'
}
</div>
<div class="music-info">
<div class="music-title">${item.title}</div>
<div class="music-artist">${item.artist}</div>
${(() => {
const metaItems = [];
if (item.album) {
metaItems.push(`专辑: ${item.album}`);
}
if (item.duration) {
metaItems.push(`时长: ${formatDuration(item.duration)}`);
}
if (item.quality) {
metaItems.push(`音质: ${item.quality}`);
}
return metaItems.length > 0
? `<div class="music-meta">${metaItems.join(' | ')}</div>`
: '';
})()}
</div>
<div class="music-actions">
<span class="source-tag">${item.platform || 'online'}</span>
<button class="play-btn" onclick="playMusic(${JSON.stringify(item).replace(/"/g, '&quot;')})">播放</button>
<button class="web-play-btn" onclick="webPlayMusic(${JSON.stringify(item).replace(/"/g, '&quot;')})">网页播放</button>
</div>
</div>
`).join('');
resultsDiv.innerHTML = html;
}
// 获取当前选中的设备ID
function getCurrentDeviceId() {
// 从 localStorage 获取当前设备ID
return localStorage.getItem('cur_did') || '';
}
// 播放音乐(设备播放)
async function playMusic(mediaItem) {
try {
const deviceId = getCurrentDeviceId();
if (!deviceId || deviceId === '') {
alert('请先设置设备!');
return;
}
// 构造正确的请求数据
const requestData = {...mediaItem, did: deviceId};
const response = await fetch('/api/play/online', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
const data = await response.json();
// 根据新返回结构调整处理逻辑
if (data && data[0]) {
let itemResult = data[0];
if (itemResult.code === 0) {
// 显示服务端返回的具体消息而不是通用提示
alert('推送成功!');
} else {
alert('播放失败: '+itemResult.msg);
}
} else {
alert('播放失败: ' + (data.error || '未知错误'));
}
} catch (error) {
alert('播放出错: ' + error.message);
}
}
// 网页播放音乐
async function webPlayMusic(mediaItem) {
try {
let playUrl;
if (mediaItem && mediaItem.isOpenAPI) {//在线接口
//playUrl = mediaItem.url
// 嗅探真实的音乐URL
playUrl = await sniffRealMusicUrl(mediaItem.url);
} else {//插件方式
const response = await fetch('/api/play/getMediaSource', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(mediaItem)
});
const data = await response.json();
playUrl = data.url
}
if (playUrl) {
// 显示播放器
const player = document.getElementById('musicPlayer');
player.style.display = 'block';
// 指定底部高度,解决播放器显示问题
const container = document.querySelector('.container');
container.style.paddingBottom = '80px';
// 设置播放信息
document.getElementById('playerTitle').textContent = mediaItem.title;
document.getElementById('playerArtist').textContent = mediaItem.artist;
// 设置封面图
const coverImage = document.getElementById('coverImage');
const coverPlaceholder = document.getElementById('coverPlaceholder');
if (mediaItem.artwork) {
coverPlaceholder.textContent = '';
coverImage.src = mediaItem.artwork;
coverImage.style.display = 'block'; // 显示图片
} else {
coverPlaceholder.textContent = '暂无封面';
coverImage.style.display = 'none'; // 隐藏图片,显示默认文本
}
// 设置音频源
const audio = document.getElementById('audioPlayer');
audio.src = playUrl;
// 重置播放按钮图标
document.getElementById('playIcon').style.display = 'none';
document.getElementById('pauseIcon').style.display = 'block';
// 获取并显示歌词
await fetchAndDisplayLyrics(mediaItem);
// 开始播放
audio.play();
} else {
alert('插件获取播放链接失败: ');
}
} catch (error) {
alert('获取播放链接出错: ' + error.message);
}
}
// 关闭播放器
function closePlayer() {
const player = document.getElementById('musicPlayer');
const audio = document.getElementById('audioPlayer');
const container = document.querySelector('.container');
container.style.paddingBottom = '0'; // 移除底部间距
audio.pause();
player.style.display = 'none';
// 清空歌词
document.getElementById('lyricContent').innerHTML = '';
currentLyrics = [];
lyricLines = [];
// 移除音频时间更新监听器
audio.removeEventListener('timeupdate', updateLyricHighlight);
}
// 格式化时长 - 兼容秒和毫秒单位
function formatDuration(duration) {
// 如果时长超过10000认为是毫秒单位需要转换为秒
let seconds = duration;
if (duration > 10000) {
seconds = Math.floor(duration / 1000);
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// 回车键搜索
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
searchMusic();
}
});
// 获取并显示歌词
async function fetchAndDisplayLyrics(mediaItem) {
try {
let lrcText
if (mediaItem && mediaItem.isOpenAPI) {//在线接口
// 调用OpenApi接口 GET获取歌词
const response = await fetch(mediaItem.lrc, {
method: 'GET',
headers: {
'Content-Type': 'text/lrc'
}
})
// 使用 await 获取实际的歌词文本内容
lrcText = await response.text();
} else {
// 调用插件后台接口获取歌词
const response = await fetch('/api/play/getLyric', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(mediaItem)
});
const lyricData = await response.json();
if (lyricData.success) {
lrcText = lyricData.rawLrc
}
}
if (lrcText) {
parseAndDisplayLyrics(lrcText);
} else {
document.getElementById('lyricContent').innerHTML = '<div class="lyric-line">暂无歌词</div>';
}
} catch (error) {
console.error('获取歌词失败:', error);
document.getElementById('lyricContent').innerHTML = '<div class="lyric-line">歌词加载失败</div>';
}
}
// 获取真实音乐URL的函数
async function sniffRealMusicUrl(downloadUrl) {
try {
// 通过服务端代理获取真实URL
const response = await fetch(`/api/proxy/real-music-url?url=${encodeURIComponent(downloadUrl)}`);
const data = await response.json();
if (data.success) {
return data.url;
} else {
return downloadUrl; // 返回原始URL
}
} catch (error) {
console.error("服务端代理获取URL失败:", error);
return downloadUrl; // 失败时返回原始URL
}
}
// 解析并显示歌词
function parseAndDisplayLyrics(lrcText) {
const lyricContainer = document.getElementById('lyricContent');
lyricContainer.innerHTML = ''; // 清空现有歌词
// 解析LRC歌词
const lines = lrcText.split('\n');
currentLyrics = [];
lines.forEach(line => {
const timeMatch = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\]/);
if (timeMatch) {
const minute = parseInt(timeMatch[1]);
const second = parseInt(timeMatch[2]);
const millisecond = timeMatch[3].length === 2 ? parseInt(timeMatch[3]) * 10 : parseInt(timeMatch[3]);
const time = minute * 60 + second + millisecond / 1000;
const text = line.replace(/\[\d{2}:\d{2}\.\d{2,3}\]/g, '').trim();
if (text) {
currentLyrics.push({
time: time,
text: text
});
}
}
});
// 按时间排序
currentLyrics.sort((a, b) => a.time - b.time);
// 创建歌词DOM元素
lyricLines = [];
currentLyrics.forEach((lyric, index) => {
const lineElement = document.createElement('div');
lineElement.className = 'lyric-line';
lineElement.textContent = lyric.text;
lineElement.dataset.time = lyric.time;
lyricContainer.appendChild(lineElement);
lyricLines.push(lineElement);
});
// 如果没有歌词,显示提示
if (currentLyrics.length === 0) {
lyricContainer.innerHTML = '<div class="lyric-line">暂无歌词</div>';
}
// 监听音频时间更新,高亮当前歌词
const audio = document.getElementById('audioPlayer');
audio.removeEventListener('timeupdate', updateLyricHighlight); // 移除之前的监听器
audio.addEventListener('timeupdate', updateLyricHighlight);
}
// 更新歌词高亮
function updateLyricHighlight() {
const audio = document.getElementById('audioPlayer');
const currentTime = audio.currentTime;
// 移除所有高亮
lyricLines.forEach(line => {
line.classList.remove('active');
});
// 找到当前应该高亮的歌词
let activeIndex = -1;
for (let i = 0; i < currentLyrics.length; i++) {
if (currentLyrics[i].time <= currentTime) {
activeIndex = i;
} else {
break;
}
}
// 高亮当前歌词并滚动到相应位置
if (activeIndex >= 0 && activeIndex < lyricLines.length) {
const activeLine = lyricLines[activeIndex];
activeLine.classList.add('active');
// 滚动歌词到可视区域中央
const container = document.getElementById('lyricContainer');
const containerHeight = container.offsetHeight;
const lineOffsetTop = activeLine.offsetTop;
const scrollTop = lineOffsetTop - containerHeight / 2;
// 平滑滚动效果
container.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,605 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MusicFree 插件设置</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: #31c27c;
color: white;
padding: 20px;
text-align: center;
}
.search-section {
padding: 20px;
border-bottom: 1px solid #eee;
}
.search-box {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.search-btn {
padding: 10px 20px;
background: #31c27c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.search-btn:hover {
background: #28a869;
}
.results-section {
padding: 20px;
}
.music-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #eee;
}
.music-item:last-child {
border-bottom: none;
}
.music-cover {
width: 60px;
height: 60px;
border-radius: 4px;
margin-right: 15px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #999;
}
.music-info {
flex: 1;
}
.music-title {
font-weight: bold;
margin-bottom: 5px;
}
.music-artist {
color: #666;
font-size: 14px;
margin-bottom: 3px;
}
.music-meta {
color: #999;
font-size: 12px;
}
.music-actions {
display: flex;
gap: 10px;
}
.play-btn {
padding: 8px 15px;
background: #31c27c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.play-btn:hover {
background: #28a869;
}
.source-tag {
padding: 2px 8px;
background: #e3f2fd;
color: #1976d2;
border-radius: 12px;
font-size: 11px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
text-align: center;
padding: 40px;
color: #d32f2f;
}
.empty {
text-align: center;
padding: 40px;
color: #999;
}
/* 插件设置专用样式 */
.plugins-section {
padding: 20px;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-top: 15px;
margin-bottom: 15px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.plugin-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.plugin-item {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.plugin-info {
flex: 1;
}
.plugin-name {
font-weight: bold;
font-size: 16px;
margin-bottom: 5px;
}
.plugin-status {
display: inline-block;
padding: 3px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
margin-top: 5px; /* 添加上边距 */
}
.status-enabled {
background: #e8f5e9;
color: #4caf50;
}
.status-disabled {
background: #ffebee;
color: #f44336;
}
.plugin-details {
font-size: 13px;
color: #666;
}
.plugin-actions {
display: flex;
gap: 10px;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background 0.2s;
}
.enable-btn {
background: #4caf50;
color: white;
}
.enable-btn:hover {
background: #45a049;
}
.disable-btn {
background: #f44336;
color: white;
}
.disable-btn:hover {
background: #da190b;
}
.uninstall-btn {
background: gray;
color: white;
}
.uninstall-btn:hover {
background: dimgray;
}
.refresh-btn {
display: block;
margin: 20px auto;
padding: 10px 20px;
background: #31c27c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.refresh-btn:hover {
background: #28a869;
}
/* 三个按钮的样式 */
.header-buttons {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 15px;
}
.header-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #fff;
color: #31c27c;
border: 1px solid #31c27c;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
text-decoration: none;
transition: all 0.2s ease;
}
.header-btn:hover {
background: #f0f0f0;
}
.material-icons {
font-size: 20px;
}
.edit-btn {
background: #2196f3;
color: white;
}
.edit-btn:hover {
background: #1976d2;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>插件&接口设置</h1>
<p>管理您的插件和在线接口</p>
<!-- 三个居中按钮 -->
<div class="header-buttons">
<a href="./index.html" class="header-btn">
返回搜索
</a>
<!-- 插件导入 -->
<button class="header-btn" onclick="uploadPlugins()">
导入插件
</button>
<!-- 修改为刷新插件按钮 -->
<button class="header-btn" onclick="loadPlugins()">
刷新插件
</button>
<a href="https://api.tunefree.fun/" target="_blank" class="header-btn">
OpenAPI
</a>
<a href="https://github.com/maotoumao/MusicFreePlugins" target="_blank" class="header-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
</div>
</div>
<div class="plugins-section">
<div class="section-title">接口配置</div>
<!-- 接口配置 内容 -->
<div id="openapi-info">
<div class="plugin-item">
<div class="plugin-info">
<div class="plugin-details">
<div><strong>接口地址: </strong> <span id="openapi-url"></span></div>
<div><strong>接口状态: </strong> <span id="openapi-status" class="plugin-status"></span></div>
</div>
</div>
<div class="plugin-actions">
<button class="action-btn" id="toggle-openapi-btn" onclick="toggleOpenApi()"></button>
<!-- 新增编辑按钮 -->
<button class="action-btn edit-btn" id="edit-openapi-btn" onclick="editOpenApiUrl()" style="display: none;">编辑</button>
</div>
</div>
</div>
<!-- 插件配置 内容 -->
<div class="section-title">插件配置</div>
<div id="plugins-container">
<div class="loading">加载中...</div>
</div>
</div>
</div>
<script>
// 页面加载时获取插件列表
window.onload = function() {
loadPlugins();
loadOpenApiConfig();
};
/*============================开放接口函数=================================*/
// 加载 OpenAPI 配置
async function loadOpenApiConfig() {
const container = document.getElementById('openapi-info');
try {
const response = await fetch('/api/openapi/load');
const data = await response.json();
if (data.success) {
displayOpenApiConfig(data.data);
} else {
container.innerHTML = `<div class="error">加载失败: ${data.error}</div>`;
}
} catch (error) {
container.innerHTML = `<div class="error">加载出错: ${error.message}</div>`;
}
}
// 显示 OpenAPI 配置
function displayOpenApiConfig(config) {
document.getElementById('openapi-url').textContent = config.search_url || '';
const statusElement = document.getElementById('openapi-status');
const buttonElement = document.getElementById('toggle-openapi-btn');
const editButtonElement = document.getElementById('edit-openapi-btn'); // 新增编辑按钮引用
if (config.enabled) {
statusElement.className = 'plugin-status status-enabled';
statusElement.textContent = '已启用';
buttonElement.className = 'action-btn disable-btn';
buttonElement.textContent = '禁用';
// 启用时隐藏编辑按钮
editButtonElement.style.display = 'none';
} else {
statusElement.className = 'plugin-status status-disabled';
statusElement.textContent = '已禁用';
buttonElement.className = 'action-btn enable-btn';
buttonElement.textContent = '启用';
// 禁用时显示编辑按钮
editButtonElement.style.display = 'inline-block';
}
}
// 切换 OpenAPI 状态
async function toggleOpenApi() {
try {
const urlElement = document.getElementById('openapi-url');
const currentUrl = urlElement.textContent;
if (!currentUrl) {
alert('请先设置接口地址!');
return;
}
const response = await fetch('/api/openapi/toggle', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
// 操作成功,重新加载配置
await loadOpenApiConfig();
} else {
alert(`切换失败: ${data.error}`);
}
} catch (error) {
alert(`操作出错: ${error.message}`);
}
}
// 编辑OpenAPI地址功能
function editOpenApiUrl() {
const urlElement = document.getElementById('openapi-url');
const currentUrl = urlElement.textContent;
const newUrl = prompt('请输入新的接口地址:', currentUrl);
// 校验newUrl格式
const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
if (newUrl && urlPattern.test(newUrl)) {
// 更新接口地址
updateOpenApiUrl(newUrl);
}else {
alert('请输入有效的接口地址!');
}
}
// 更新OpenAPI地址
async function updateOpenApiUrl(newUrl) {
try {
const response = await fetch('/api/openapi/updateUrl', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ search_url: newUrl })
});
const data = await response.json();
if (data.success) {
// 更新成功,重新加载配置
await loadOpenApiConfig();
} else {
alert(`更新失败: ${data.error}`);
}
} catch (error) {
alert(`更新出错: ${error.message}`);
}
}
/*============================插件函数=================================*/
// 加载插件列表
async function loadPlugins() {
const container = document.getElementById('plugins-container');
container.innerHTML = '<div class="loading">加载中...</div>';
try {
const response = await fetch('/api/js-plugins');
const data = await response.json();
if (data.success) {
displayPlugins(data.data);
} else {
container.innerHTML = `<div class="error">加载失败: ${data.error}</div>`;
}
} catch (error) {
container.innerHTML = `<div class="error">加载出错: ${error.message}</div>`;
}
}
/// 在现有的 script 标签中添加以下函数
// 插件导入功能
function uploadPlugins() {
// 创建文件输入元素
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.js'; // 只允许上传 js 文件
fileInput.style.display = 'none';
// 监听文件选择事件
fileInput.onchange = async function(event) {
const file = event.target.files[0];
if (!file) return;
// 验证文件类型
if (!file.name.endsWith('.js')) {
alert('只允许上传 .js 格式的文件');
return;
}
// 创建 FormData 对象
const formData = new FormData();
formData.append('file', file);
try {
// 上传文件
const response = await fetch('/api/js-plugins/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
alert('插件导入成功');
// 自动刷新插件列表
await loadPlugins();
} else {
alert(`插件导入失败: ${data.error}`);
}
} catch (error) {
alert(`插件导入出错: ${error.message}`);
}
};
// 触发文件选择对话框
document.body.appendChild(fileInput);
fileInput.click();
document.body.removeChild(fileInput);
}
// 显示插件列表
function displayPlugins(plugins) {
const container = document.getElementById('plugins-container');
if (!plugins || plugins.length === 0) {
container.innerHTML = '<div class="empty">没有找到插件</div>';
return;
}
const html = plugins.map(plugin => `
<div class="plugin-item">
<div class="plugin-info">
<div class="plugin-name">${plugin.name}</div>
<div class="plugin-details">
<span class="plugin-status ${plugin.enabled ? 'status-enabled' : 'status-disabled'}">
${plugin.enabled ? '已启用' : '已禁用'}
</span>
${plugin.error ? ` | 错误: ${plugin.error}` : ''}
</div>
</div>
<div class="plugin-actions">
${plugin.enabled ?
`<button class="action-btn disable-btn" onclick="togglePlugin('${plugin.name}', false)">禁用</button>` :
`<button class="action-btn enable-btn" onclick="togglePlugin('${plugin.name}', true)">启用</button>
<button class="action-btn uninstall-btn" onclick="uninstallPlugin('${plugin.name}')">卸载</button>`
}
</div>
</div>
`).join('');
container.innerHTML = `<div class="plugin-list">${html}</div>`;
}
// 启用/禁用插件
async function togglePlugin(pluginName, enable) {
try {
const url = enable ?
`/api/js-plugins/${pluginName}/enable` :
`/api/js-plugins/${pluginName}/disable`;
const response = await fetch(url, {
method: 'PUT'
});
const data = await response.json();
if (data.success) {
// 操作成功,重新加载插件列表
await loadPlugins();
} else {
alert(`${enable ? '启用' : '禁用'}插件失败: ${data.error}`);
}
} catch (error) {
alert(`${enable ? '启用' : '禁用'}插件出错: ${error.message}`);
}
}
// 卸载插件功能
async function uninstallPlugin(pluginName) {
if (!confirm(`确定要卸载插件 "${pluginName}" 吗?此操作不可恢复。`)) {
return;
}
try {
const response = await fetch(`/api/js-plugins/${pluginName}/uninstall`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
alert('插件卸载成功');
// 重新加载插件列表
await loadPlugins();
} else {
alert(`插件卸载失败: ${data.error}`);
}
} catch (error) {
alert(`插件卸载出错: ${error.message}`);
}
}
</script>
</body>
</html>

BIN
xiaomusic/static/search.mp3 Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -254,7 +254,8 @@
<div class="avatar cursor-pointer">
<!-- <a href="./now_playing.html" target="_blank"> -->
<div class="w-12 rounded-lg">
<img :src="currentSong?.cover" :alt="currentSong?.title" />
<!-- <img :src="currentSong?.cover" :alt="currentSong?.title" />-->
<img :src="currentSong?.cover || '/static/xiaoai.png'" :alt="currentSong?.title" />
</div>
<!-- </a> -->
</div>
@@ -484,7 +485,7 @@
if (window.did === 'web_device') return;
const data = await API.getPlayingStatus(window.did);
if (data.ret === 'OK') {
isPlaying.value = data.is_playing;
currentTime.value = data.offset || 0;
@@ -785,11 +786,11 @@
// 创建新的音频播放器
const audio = document.createElement('audio');
audio.id = 'audio-player';
// 设置音频属性
audio.preload = 'auto'; // 预加载
audio.crossOrigin = 'anonymous'; // 允许跨域
// 添加到文档
document.body.appendChild(audio);
audioPlayer.value = audio;
@@ -799,7 +800,7 @@
console.error('Audio playback error:', e);
const error = e.target.error;
let errorMessage = '播放出错';
if (error) {
switch (error.code) {
case error.MEDIA_ERR_ABORTED:
@@ -816,7 +817,7 @@
break;
}
}
showMessage(errorMessage, 'alert-error');
isPlaying.value = false;
});

View File

@@ -38,7 +38,7 @@ function startProgressUpdate() {
if (progressInterval) {
clearInterval(progressInterval);
}
// 每秒更新一次进度条
progressInterval = setInterval(() => {
if (duration > 0) {
@@ -66,18 +66,18 @@ function stopProgressUpdate() {
window.playMusic = function(songName) {
const currentPlaylist = localStorage.getItem("cur_playlist");
console.log(`播放音乐: ${songName}, 播放列表: ${currentPlaylist}`);
// 检查是否是当前播放的歌曲
const currentPlayingSong = localStorage.getItem("cur_music");
const isCurrentSong = currentPlayingSong === songName;
if (window.did === 'web_device') {
// Web播放模式
$.get(`/musicinfo?name=${songName}`, function (data, status) {
if (data.ret == "OK") {
if (validHost(data.url)) {
const audio = $("#audio")[0];
// 如果是同一首歌,切换播放/暂停状态
if (audio.src && audio.src === data.url) {
if (audio.paused) {
@@ -148,25 +148,25 @@ function do_play_music_list(listname, musicname) {
// 更新播放信息
function updatePlayingInfo(songName, isPlaying) {
if (!songName) return;
// 更新播放栏信息
const displayText = isPlaying ? `【播放中】 ${songName}` : `【暂停中】 ${songName}`;
$("#playering-music").text(displayText);
$("#playering-music-mobile").text(displayText);
// 更新播放按钮图标
$(".play").text(isPlaying ? "pause_circle_outline" : "play_circle_outline");
// 更新收藏状态
updateFavoriteStatus(songName);
// 高亮当前播放的歌曲
highlightPlayingSong(songName, isPlaying);
// 保存当前播放的歌曲
localStorage.setItem("cur_music", songName);
localStorage.setItem("is_playing", isPlaying);
// 根据播放状态控制进度条更新
if (isPlaying) {
startProgressUpdate();
@@ -179,10 +179,10 @@ function updatePlayingInfo(songName, isPlaying) {
function highlightPlayingSong(songName, isPlaying) {
// 移除所有歌曲的高亮状态
$(".song-item").removeClass("bg-blue-50 dark:bg-blue-900/20");
// 重置所有播放按钮为播放图标(只选择播放按钮中的图标)
$(".play-icon").text("play_arrow");
// 高亮当前歌曲,无论是播放还是暂停状态
$(".song-item").each(function() {
const itemSongName = $(this).find("h3").text();
@@ -249,33 +249,33 @@ function nextTrack() {
function togglePlayMode(isSend = true) {
console.log('切换播放模式...');
// 从本地存储获取当前播放模式如果没有则使用默认值2随机播放
if (playModeIndex === undefined || playModeIndex === null) {
playModeIndex = parseInt(localStorage.getItem("playModeIndex")) || 2;
}
// 计算下一个播放模式索引2 -> 3 -> 4 -> 2
const nextModeIndex = playModeIndex >= 4 ? 2 : playModeIndex + 1;
// 获取下一个播放模式
const nextMode = playModes[nextModeIndex];
console.log('切换到播放模式:', nextModeIndex, nextMode.cmd);
// 更新按钮图标和提示文本
const modeBtn = $("#modeBtn");
const modeBtnIcon = modeBtn.find(".material-icons");
const tooltip = modeBtn.find(".tooltip");
modeBtnIcon.text(nextMode.icon);
tooltip.text(nextMode.cmd);
// 如果需要发送命令,则发送到设备
if (isSend && window.did !== 'web_device') {
console.log('发送播放模式命令:', nextMode.cmd);
sendcmd(nextMode.cmd);
}
// 保存新的播放模式到本地存储和全局变量
localStorage.setItem("playModeIndex", nextModeIndex);
playModeIndex = nextModeIndex;
@@ -287,7 +287,7 @@ function addToFavorites() {
const isLiked = favoritelist.includes(currentSong);
const cmd = isLiked ? "取消收藏" : "加入收藏";
// 发送收藏命令
$.ajax({
type: "POST",
@@ -299,7 +299,7 @@ function addToFavorites() {
}),
success: () => {
console.log(`${cmd}成功: ${currentSong}`);
// 更新本地收藏列表
if (isLiked) {
// 取消收藏
@@ -312,7 +312,7 @@ function addToFavorites() {
}
$(".favorite").addClass("favorite-active");
}
// 如果当前在收藏列表页面,刷新列表
if (localStorage.getItem("cur_playlist") === "收藏") {
refresh_music_list();
@@ -390,23 +390,23 @@ $.get("/getsetting", function (data, status) {
if (data.mi_did != null) {
dids = data.mi_did.split(",");
}
if (did != "web_device" && dids.length > 0 && (did == null || did == "" || !dids.includes(did))) {
did = dids[0];
localStorage.setItem("cur_did", did);
}
window.did = did;
// 渲染设备按钮
renderDeviceButtons(data.devices, did);
// 获取音量
$.get(`/getvolume?did=${did}`, function (data, status) {
console.log(data, status, data["volume"]);
$("#volume").val(data.volume);
});
// 刷新音乐列表
refresh_music_list();
@@ -459,7 +459,7 @@ function _refresh_music_list(callback) {
console.error("未获取到音乐列表数据");
return;
}
favoritelist = data["收藏"] || [];
// 设置默认播放列表
@@ -521,7 +521,7 @@ function renderSystemPlaylists(data) {
const songs = data[playlist.name] || [];
const count = songs.length;
const isActive = playlist.name === currentPlaylist;
const button = $(`
<button
onclick="showPlaylist('${playlist.name}')"
@@ -543,7 +543,7 @@ function renderSystemPlaylists(data) {
${isActive ? '<span class="material-icons ml-2 text-blue-500 text-sm hidden md:inline">check</span>' : ''}
</button>
`);
container.append(button);
});
}
@@ -551,35 +551,35 @@ function renderSystemPlaylists(data) {
// 渲染专辑列表
function renderAlbumList(data) {
const container = $("#album-list");
if (!data || typeof data !== 'object') {
return;
}
container.empty();
// 系统预设的播放列表,这些不在专辑列表中显示
const systemPlaylists = [
'收藏', '最近新增', '所有歌曲', '临时搜索列表',
'所有电台', '全部', '下载', '其他'
];
const currentPlaylist = localStorage.getItem("cur_playlist");
// 遍历所有播放列表
for (const [listName, songs] of Object.entries(data)) {
// 跳过系统预设列表
if (systemPlaylists.includes(listName)) {
continue;
}
// 跳过空列表
if (songs.length === 0) {
continue;
}
const isActive = listName === currentPlaylist;
const button = $(`
<button
onclick="showPlaylist('${listName}')"
@@ -605,7 +605,7 @@ function renderAlbumList(data) {
${isActive ? '<span class="material-icons flex-shrink-0 text-blue-500 text-sm">check</span>' : ''}
</button>
`);
container.append(button);
}
}
@@ -625,10 +625,10 @@ window.showPlaylist = function(listName) {
// 渲染歌曲列表
const songs = data[listName] || [];
renderSongList(songs);
// 保存当前播放列表
localStorage.setItem("cur_playlist", listName);
// 重新渲染系统播放列表和专辑列表以更新高亮状态
renderSystemPlaylists(data);
renderAlbumList(data);
@@ -641,10 +641,10 @@ function refresh_music_list() {
// 刷新列表时清空并临时禁用搜索框
const searchInput = document.getElementById("search-input");
if (!searchInput) {
console.error("未找到搜索输入框");
// console.error("未找到搜索输入框");
return;
}
const oriPlaceHolder = searchInput.placeholder;
const oriValue = searchInput.value;
const inputEvent = new Event("input", { bubbles: true });
@@ -802,7 +802,7 @@ function handleSearch() {
console.log("搜索输入框不存在");
return;
}
console.log("触发搜索::!")
searchInput.addEventListener(
"input",
debounce(function () {
@@ -831,18 +831,18 @@ function get_playing_music() {
console.log(data);
if (data.ret == "OK" && data.cur_music) { // 确保cur_music存在
updatePlayingInfo(data.cur_music, data.is_playing);
// 更新进度条和时间显示
offset = data.offset || 0;
duration = data.duration || 0;
if (duration > 0) {
// 更新进度条
$("#progress").val((offset / duration) * 100);
// 更新时间显示
$("#current-time").text(formatTime(offset));
$("#duration").text(formatTime(duration));
// 如果正在播放,启动进度条更新
if (data.is_playing) {
startProgressUpdate();
@@ -856,7 +856,7 @@ function get_playing_music() {
$("#duration").text("00:00");
stopProgressUpdate();
}
// 更新收藏状态
if (favoritelist.includes(data.cur_music)) {
$(".favorite").addClass("favorite-active");
@@ -906,7 +906,7 @@ window.stopPlay = function() {
sendcmd("停止");
updatePlayingInfo(currentSong, false);
}
// 重置进度条和时间显示
$("#progress").val(0);
$("#current-time").text("00:00");
@@ -1051,7 +1051,7 @@ function adjustVolume(value) {
audio.volume = value;
localStorage.setItem('volume', value);
}
// 更新设备音量
if (window.did && window.did !== 'web_device') {
$.ajax({
@@ -1112,7 +1112,7 @@ document.getElementById('volume-slider')?.addEventListener('input', function() {
function renderDeviceButtons(devices, currentDid) {
const container = $("#device-buttons");
container.empty();
// 切换设备函数
function switchDevice(did) {
// 只有在切换到不同设备时才刷新页面
@@ -1122,7 +1122,7 @@ function renderDeviceButtons(devices, currentDid) {
location.reload();
}
}
// 添加设备按钮
Object.values(devices).forEach(device => {
const isActive = device.did === currentDid;
@@ -1148,14 +1148,14 @@ function renderDeviceButtons(devices, currentDid) {
${isActive ? '<span class="material-icons flex-shrink-0 text-blue-500 text-sm">check</span>' : ''}
</button>
`);
button.click(function() {
switchDevice(device.did);
});
container.append(button);
});
// 添加本机播放按钮
const isWebDevice = currentDid === 'web_device';
const webDeviceButton = $(`
@@ -1180,11 +1180,11 @@ function renderDeviceButtons(devices, currentDid) {
${isWebDevice ? '<span class="material-icons flex-shrink-0 text-blue-500 text-sm">check</span>' : ''}
</button>
`);
webDeviceButton.click(function() {
switchDevice('web_device');
});
container.append(webDeviceButton);
}
@@ -1194,7 +1194,7 @@ function showPlaylist(type) {
$('.playlist-button').removeClass('bg-blue-50 dark:bg-blue-900/20');
// 添加当前按钮的活动状态
$(`[data-playlist="${type}"]`).addClass('bg-blue-50 dark:bg-blue-900/20');
switch(type) {
case 'all':
// 显示所有歌曲
@@ -1268,12 +1268,12 @@ function renderSongList(songs) {
</div>
</div>
`);
// 添加播放按钮点击事件
songItem.find('.play-button').on('click', function() {
playMusic(song);
});
// 添加删除按钮点击事件
songItem.find('.delete-button').on('click', function() {
if (confirm(`确定要删除歌曲 "${song}" 吗?`)) {
@@ -1293,7 +1293,7 @@ function renderSongList(songs) {
});
}
});
container.append(songItem);
});
}
@@ -1302,18 +1302,18 @@ function renderSongList(songs) {
window.playMusic = function(songName) {
const currentPlaylist = localStorage.getItem("cur_playlist");
console.log(`播放音乐: ${songName}, 播放列表: ${currentPlaylist}`);
// 检查是否是当前播放的歌曲
const currentPlayingSong = localStorage.getItem("cur_music");
const isCurrentSong = currentPlayingSong === songName;
if (window.did === 'web_device') {
// Web播放模式
$.get(`/musicinfo?name=${songName}`, function (data, status) {
if (data.ret == "OK") {
if (validHost(data.url)) {
const audio = $("#audio")[0];
// 如果是同一首歌,切换播放/暂停状态
if (audio.src && audio.src === data.url) {
if (audio.paused) {
@@ -1362,25 +1362,25 @@ window.playMusic = function(songName) {
// 更新播放信息
function updatePlayingInfo(songName, isPlaying) {
if (!songName) return;
// 更新播放栏信息
const displayText = isPlaying ? `【播放中】 ${songName}` : `【暂停中】 ${songName}`;
$("#playering-music").text(displayText);
$("#playering-music-mobile").text(displayText);
// 更新播放按钮图标
$(".play").text(isPlaying ? "pause_circle_outline" : "play_circle_outline");
// 更新收藏状态
updateFavoriteStatus(songName);
// 高亮当前播放的歌曲
highlightPlayingSong(songName, isPlaying);
// 保存当前播放的歌曲
localStorage.setItem("cur_music", songName);
localStorage.setItem("is_playing", isPlaying);
// 根据播放状态控制进度条更新
if (isPlaying) {
startProgressUpdate();
@@ -1393,10 +1393,10 @@ function updatePlayingInfo(songName, isPlaying) {
function highlightPlayingSong(songName, isPlaying) {
// 移除所有歌曲的高亮状态
$(".song-item").removeClass("bg-blue-50 dark:bg-blue-900/20");
// 重置所有播放按钮为播放图标(只选择播放按钮中的图标)
$(".play-icon").text("play_arrow");
// 高亮当前歌曲,无论是播放还是暂停状态
$(".song-item").each(function() {
const itemSongName = $(this).find("h3").text();
@@ -1456,7 +1456,7 @@ window.stopPlay = function() {
sendcmd("停止");
updatePlayingInfo(currentSong, false);
}
// 重置进度条和时间显示
$("#progress").val(0);
$("#current-time").text("00:00");
@@ -1490,12 +1490,12 @@ window.prevTrack = function() {
const currentPlaylist = localStorage.getItem("cur_playlist");
$.get("/musiclist", function (data, status) {
if (!data || !data[currentPlaylist]) return;
const songs = data[currentPlaylist];
const currentSong = $("#playering-music").text().replace('当前播放歌曲:', '');
const currentIndex = songs.indexOf(currentSong);
const prevIndex = currentIndex > 0 ? currentIndex - 1 : songs.length - 1;
playMusic(songs[prevIndex]);
});
} else {
@@ -1510,12 +1510,12 @@ window.nextTrack = function() {
const currentPlaylist = localStorage.getItem("cur_playlist");
$.get("/musiclist", function (data, status) {
if (!data || !data[currentPlaylist]) return;
const songs = data[currentPlaylist];
const currentSong = $("#playering-music").text().replace('当前播放歌曲:', '');
const currentIndex = songs.indexOf(currentSong);
const nextIndex = currentIndex < songs.length - 1 ? currentIndex + 1 : 0;
playMusic(songs[nextIndex]);
});
} else {
@@ -1530,10 +1530,10 @@ window.toggleFavorite = function() {
const favoriteIcon = document.querySelector('.favorite-icon');
const isFavorite = favoriteIcon.textContent === 'favorite';
// 切换图标
favoriteIcon.textContent = isFavorite ? 'favorite_border' : 'favorite';
// 发送收藏/取消收藏请求
$.ajax({
type: "POST",

View File

@@ -200,7 +200,7 @@ def custom_sort_key(s):
def _get_depth_path(root, directory, depth):
# 计算当前目录的深度
relative_path = root[len(directory) :].strip(os.sep)
relative_path = root[len(directory):].strip(os.sep)
path_parts = relative_path.split(os.sep)
if len(path_parts) >= depth:
return os.path.join(directory, *path_parts[:depth])
@@ -231,7 +231,7 @@ def traverse_music_directory(directory, depth, exclude_dirs, support_extension):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
# 计算当前目录的深度
current_depth = root[len(directory) :].count(os.sep) + 1
current_depth = root[len(directory):].count(os.sep) + 1
if current_depth > depth:
depth_path = _get_depth_path(root, directory, depth - 1)
_append_files_result(result, depth_path, root, files, support_extension)
@@ -242,14 +242,14 @@ def traverse_music_directory(directory, depth, exclude_dirs, support_extension):
# 发送给网页3thplay用于三者设备播放
async def thdplay(
action, args="/static/3thdplay.mp3", target="HTTP://192.168.1.10:58090/thdaction"
action, args="/static/3thdplay.mp3", target="HTTP://192.168.1.10:58090/thdaction"
):
# 接口地址 target,在参数文件指定
data = {"action": action, "args": args}
try:
async with aiohttp.ClientSession() as session:
async with session.post(
target, json=data, timeout=5
target, json=data, timeout=5
) as response: # 增加超时以避免长时间挂起
# 如果响应不是200引发异常
response.raise_for_status()
@@ -276,7 +276,7 @@ async def downloadfile(url):
# 使用 aiohttp 创建一个客户端会话来发起请求
async with aiohttp.ClientSession() as session:
async with session.get(
cleaned_url, timeout=5
cleaned_url, timeout=5
) as response: # 增加超时以避免长时间挂起
# 如果响应不是200引发异常
response.raise_for_status()
@@ -345,11 +345,11 @@ async def get_web_music_duration(url, config):
cleaned_url = parsed_url.geturl()
async with aiohttp.ClientSession() as session:
async with session.get(
cleaned_url,
allow_redirects=True,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
},
cleaned_url,
allow_redirects=True,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
},
) as response:
url = str(response.url)
# 设置总超时时间为3秒
@@ -402,28 +402,39 @@ async def get_duration_by_mutagen(file_path):
def get_duration_by_ffprobe(file_path, ffmpeg_location):
duration = 0
try:
# 构造 ffprobe 命令参数
cmd_args = [
os.path.join(ffmpeg_location, "ffprobe"),
"-v",
"error", # 只输出错误信息,避免混杂在其他输出中
"-show_entries",
"format=duration", # 仅显示时长
"-of",
"json", # 以 JSON 格式输出
file_path,
]
# 输出待执行的完整命令
full_command = " ".join(cmd_args)
log.info(f"待执行的完整命令 ffprobe command: {full_command}")
# 使用 ffprobe 获取文件的元数据,并以 JSON 格式输出
result = subprocess.run(
[
os.path.join(ffmpeg_location, "ffprobe"),
"-v",
"error", # 只输出错误信息,避免混杂在其他输出中
"-show_entries",
"format=duration", # 仅显示时长
"-of",
"json", # 以 JSON 格式输出
file_path,
],
cmd_args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
# 输出命令执行结果
log.info(f"命令执行结果 command result - return code: {result.returncode}, stdout: {result.stdout}")
# 解析 JSON 输出
ffprobe_output = json.loads(result.stdout)
# 获取时长
duration = float(ffprobe_output["format"]["duration"])
log.info(f"Successfully extracted duration: {duration} seconds for file: {file_path}")
except Exception as e:
log.warning(f"Error getting local music {file_path} duration: {e}")
@@ -488,8 +499,8 @@ def remove_id3_tags(input_file: str, config) -> str:
# 检查是否存在ID3 v2.3或v2.4标签
if not (
audio.tags
and (audio.tags.version == (2, 3, 0) or audio.tags.version == (2, 4, 0))
audio.tags
and (audio.tags.version == (2, 3, 0) or audio.tags.version == (2, 4, 0))
):
return None
@@ -1137,7 +1148,7 @@ def remove_common_prefix(directory):
# 检查文件名是否以共同前缀开头
if filename.startswith(common_prefix):
# 构造新的文件名
new_filename = filename[len(common_prefix) :]
new_filename = filename[len(common_prefix):]
match = pattern.search(new_filename.strip())
if match:
num = match.group(1)
@@ -1328,10 +1339,10 @@ async def fetch_json_get(url, headers, config):
async with aiohttp.ClientSession(connector=connector) as session:
# 3. 发起带代理的GET请求
async with session.get(
url,
headers=headers,
proxy=proxy, # 传入格式化后的代理参数
timeout=10, # 超时时间(秒),避免无限等待
url,
headers=headers,
proxy=proxy, # 传入格式化后的代理参数
timeout=10, # 超时时间(秒),避免无限等待
) as response:
if response.status == 200:
data = await response.json()
@@ -1471,7 +1482,7 @@ class MusicUrlCache:
async def text_to_mp3(
text: str, save_dir: str, voice: str = "zh-CN-XiaoxiaoNeural"
text: str, save_dir: str, voice: str = "zh-CN-XiaoxiaoNeural"
) -> str:
"""
使用edge-tts将文本转换为MP3语音文件

View File

@@ -93,6 +93,7 @@ class XiaoMusic:
self.music_list = {} # 播放列表 key 为目录名, value 为 play_list
self.default_music_list_names = [] # 非自定义个歌单
self.devices = {} # key 为 did
self._cur_did = None # 当前设备did
self.running_task = []
self.all_music_tags = {} # 歌曲额外信息
self._tag_generation_task = False
@@ -108,6 +109,23 @@ class XiaoMusic:
# 计划任务
self.crontab = Crontab(self.log)
# 初始化 JS 插件管理器
try:
from xiaomusic.js_plugin_manager import JSPluginManager
self.js_plugin_manager = JSPluginManager(self)
self.log.info("JS Plugin Manager initialized successfully")
except Exception as e:
self.log.error(f"Failed to initialize JS Plugin Manager: {e}")
self.js_plugin_manager = None
# 初始化 JS 插件适配器
try:
from xiaomusic.js_adapter import JSAdapter
self.js_adapter = JSAdapter(self)
self.log.info("JS Adapter initialized successfully")
except Exception as e:
self.log.error(f"Failed to initialize JS Adapter: {e}")
# 尝试从设置里加载配置
self.try_init_setting()
@@ -129,6 +147,51 @@ class XiaoMusic:
if self.config.conf_path == self.music_path:
self.log.warning("配置文件目录和音乐目录建议设置为不同的目录")
# 私有方法:调用插件方法的通用封装
async def __call_plugin_method(self, plugin_name: str, method_name: str, music_item: dict, result_key: str,
required_field: str = None, **kwargs):
"""
通用方法:调用 JS 插件的方法并返回结果
Args:
plugin_name: 插件名称
method_name: 插件方法名(如 get_media_source 或 get_lyric
music_item: 音乐项数据
result_key: 返回结果中的字段名(如 'url''rawLrc'
required_field: 必须存在的字段(用于校验)
**kwargs: 传递给插件方法的额外参数
Returns:
dict: 包含 success 和对应字段的字典
"""
if not music_item:
return {"success": False, "error": "Music item required"}
# 检查插件管理器是否可用
if not self.js_plugin_manager:
return {"success": False, "error": "JS Plugin Manager not available"}
enabled_plugins = self.js_plugin_manager.get_enabled_plugins()
if plugin_name not in enabled_plugins:
return {"success": False, "error": f"Plugin {plugin_name} not enabled"}
try:
# 调用插件方法,传递额外参数
result = getattr(self.js_plugin_manager, method_name)(plugin_name, music_item, **kwargs)
if not result or not result.get(result_key) or result.get(result_key) == 'None':
return {"success": False, "error": f"Failed to get {result_key}"}
# 如果指定了必填字段,则额外校验
if required_field and not result.get(required_field):
return {"success": False, "error": f"Missing required field: {required_field}"}
# 追加属性后返回
result["success"] = True
return result
except Exception as e:
self.log.error(f"Plugin {plugin_name} {method_name} failed: {e}")
return {"success": False, "error": str(e)}
def init_config(self):
self.music_path = self.config.music_path
self.download_path = self.config.download_path
@@ -307,6 +370,8 @@ class XiaoMusic:
if device_id and hardware and did and (did in mi_dids):
device = self.config.devices.get(did, Device())
device.did = did
# 将did存一下 方便其他地方调用
self._cur_did = did
device.device_id = device_id
device.hardware = hardware
device.name = name
@@ -496,7 +561,7 @@ class XiaoMusic:
picture = tags["picture"]
if picture:
if picture.startswith(self.config.picture_cache_path):
picture = picture[len(self.config.picture_cache_path) :]
picture = picture[len(self.config.picture_cache_path):]
picture = picture.replace("\\", "/")
if picture.startswith("/"):
picture = picture[1:]
@@ -530,29 +595,35 @@ class XiaoMusic:
self.try_save_tag_cache()
return "OK"
async def get_music_sec_url(self, name):
async def get_music_sec_url(self, name, true_url):
"""获取歌曲播放时长和播放地址
Args:
name: 歌曲名称
true_url: 真实播放URL
Returns:
tuple: (播放时长(秒), 播放地址)
"""
url, origin_url = await self.get_music_url(name)
self.log.info(
f"get_music_sec_url. name:{name} url:{url} origin_url:{origin_url}"
)
# 电台直接返回
if self.is_web_radio_music(name):
self.log.info("电台不会有播放时长")
return 0, url
# 获取播放时长
if self.is_web_music(name):
sec = await self._get_web_music_duration(name, url, origin_url)
if true_url is not None:
url = true_url
sec = await self._get_online_music_duration(name, true_url)
self.log.info(f"在线歌曲时长获取::{name} sec{sec}")
else:
sec = await self._get_local_music_duration(name, url)
url, origin_url = await self.get_music_url(name)
self.log.info(
f"get_music_sec_url. name:{name} url:{url} origin_url:{origin_url}"
)
# 电台直接返回
if self.is_web_radio_music(name):
self.log.info("电台不会有播放时长")
return 0, url
if self.is_web_music(name):
sec = await self._get_web_music_duration(name, url, origin_url)
else:
sec = await self._get_local_music_duration(name, url)
if sec <= 0:
self.log.warning(f"获取歌曲时长失败 {name} {url}")
@@ -582,6 +653,14 @@ class XiaoMusic:
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec}")
return sec
async def _get_online_music_duration(self, name, url):
"""获取在线音乐时长"""
self.log.info(f"get_music_sec_url. name:{name}")
duration = await get_local_music_duration(url, self.config)
sec = math.ceil(duration)
self.log.info(f"在线歌曲 {name} : {url} 的时长 {sec}")
return sec
async def get_music_url(self, name):
"""获取音乐播放地址
@@ -633,7 +712,7 @@ class XiaoMusic:
# 处理文件路径
if filename.startswith(self.config.music_path):
filename = filename[len(self.config.music_path) :]
filename = filename[len(self.config.music_path):]
filename = filename.replace("\\", "/")
if filename.startswith("/"):
filename = filename[1:]
@@ -723,7 +802,7 @@ class XiaoMusic:
# TODO: 网络歌曲获取歌曲额外信息
pass
elif os.path.exists(file_or_url) and not_in_dirs(
file_or_url, ignore_tag_absolute_dirs
file_or_url, ignore_tag_absolute_dirs
):
all_music_tags[name] = extract_audio_metadata(
file_or_url, self.config.picture_cache_path
@@ -760,7 +839,7 @@ class XiaoMusic:
if dir_name == os.path.basename(self.music_path):
dir_name = "其他"
if self.music_path != self.download_path and dir_name == os.path.basename(
self.download_path
self.download_path
):
dir_name = "下载"
if dir_name not in all_music_by_dir:
@@ -934,7 +1013,7 @@ class XiaoMusic:
self.start_file_watch()
analytics_task = asyncio.create_task(self.analytics_task_daily())
assert (
analytics_task is not None
analytics_task is not None
) # to keep the reference to task, do not remove this
async with ClientSession() as session:
self.session = session
@@ -1047,11 +1126,11 @@ class XiaoMusic:
opvalue = self.config.key_word_dict.get(opkey)
if (
(not ctrl_panel)
and (not self.isplaying(did))
and self.active_cmd
and (opvalue not in self.active_cmd)
and (opkey not in self.active_cmd)
(not ctrl_panel)
and (not self.isplaying(did))
and self.active_cmd
and (opvalue not in self.active_cmd)
and (opkey not in self.active_cmd)
):
self.log.info(f"不在激活命令中 {opvalue}")
continue
@@ -1103,6 +1182,7 @@ class XiaoMusic:
# 播放一个 url
async def play_url(self, did="", arg1="", **kwargs):
self.log.info(f"手动播放链接:{arg1}")
url = arg1
return await self.devices[did].group_player_play(url)
@@ -1158,6 +1238,366 @@ class XiaoMusic:
# TODO: 这里可以优化性能
self._gen_all_music_list()
# ===========================MusicFree插件函数================================
# 在线获取歌曲列表
async def get_music_list_online(self, plugin="all", keyword="", page=1, limit=20, **kwargs):
self.log.info("在线获取歌曲列表!")
"""
在线获取歌曲列表
Args:
plugin: 插件名称,"OpenAPI"表示 通过开放接口获取,其他为插件在线搜索
keyword: 搜索关键词
page: 页码
limit: 每页数量
**kwargs: 其他参数
Returns:
dict: 搜索结果
"""
openapi_info = self.js_plugin_manager.get_openapi_info()
if openapi_info.get("enabled", False) and openapi_info.get("search_url", "") != "":
# 开放接口获取
return await self.js_plugin_manager.openapi_search(openapi_info.get("search_url"), keyword)
else:
if not self.js_plugin_manager:
return {"success": False, "error": "JS Plugin Manager not available"}
# 插件在线搜索
return await self.get_music_list_mf(plugin, keyword, page, limit)
@staticmethod
async def get_real_url_of_openapi(url: str, timeout: int = 10) -> dict:
"""
通过服务端代理获取开放接口真实的音乐播放URL避免CORS问题
Args:
url (str): 原始音乐URL
timeout (int): 请求超时时间(秒)
Returns:
dict: 包含success、realUrl、statusCode等信息的字典
"""
from urllib.parse import urlparse
import aiohttp
try:
# 验证URL格式
parsed_url = urlparse(url)
if not parsed_url.scheme or not parsed_url.netloc:
return {
"success": False,
"url": url,
"error": "Invalid URL format"
}
# 创建aiohttp客户端会话
async with aiohttp.ClientSession() as session:
# 发送HEAD请求跟随重定向
async with session.head(url, allow_redirects=True,
timeout=aiohttp.ClientTimeout(total=timeout)) as response:
# 获取最终重定向后的URL
final_url = str(response.url)
return {
"success": True,
"url": final_url,
"statusCode": response.status
}
except Exception as e:
return {
"success": False,
"url": url,
"error": f"Error occurred: {str(e)}"
}
# 调用MusicFree插件获取歌曲列表
async def get_music_list_mf(self, plugin="all", keyword="", page=1, limit=20, **kwargs):
self.log.info("通过MusicFree插件搜索音乐列表!")
"""
通过MusicFree插件搜索音乐列表
Args:
plugin: 插件名称,"all"表示所有插件
keyword: 搜索关键词
page: 页码
limit: 每页数量
**kwargs: 其他参数
Returns:
dict: 搜索结果
"""
# 检查JS插件管理器是否可用
if not self.js_plugin_manager:
return {"success": False, "error": "JS插件管理器不可用"}
# 如果关键词包含 '-',则提取歌手名、歌名
if '-' in keyword:
parts = keyword.split('-')
keyword = parts[0]
artist = parts[1]
else:
artist = ""
try:
if plugin == "all":
# 搜索所有启用的插件
return await self._search_all_plugins(keyword, artist, page, limit)
else:
# 搜索指定插件
return await self._search_specific_plugin(plugin, keyword, artist, page, limit)
except Exception as e:
self.log.error(f"搜索音乐时发生错误: {e}")
return {"success": False, "error": str(e)}
async def _search_all_plugins(self, keyword, artist, page, limit):
"""搜索所有启用的插件"""
enabled_plugins = self.js_plugin_manager.get_enabled_plugins()
if not enabled_plugins:
return {"success": False, "error": "没有可用的接口和插件,请先进行配置!"}
results = []
sources = {}
# 计算每个插件的限制数量
plugin_count = len(enabled_plugins)
item_limit = max(1, limit // plugin_count) if plugin_count > 0 else limit
# 并行搜索所有插件
search_tasks = [
self._search_plugin_task(plugin_name, keyword, page, item_limit)
for plugin_name in enabled_plugins
]
plugin_results = await asyncio.gather(*search_tasks, return_exceptions=True)
# 处理搜索结果
for i, result in enumerate(plugin_results):
plugin_name = list(enabled_plugins)[i]
# 检查是否为异常对象
if isinstance(result, Exception):
self.log.error(f"插件 {plugin_name} 搜索失败: {result}")
continue
# 检查是否为有效的搜索结果(修改这里的判断逻辑)
if result and isinstance(result, dict):
# 检查是否有错误信息
if "error" in result:
self.log.error(f"插件 {plugin_name} 搜索失败: {result.get('error', '未知错误')}")
continue
# 处理成功的搜索结果
data_list = result.get("data", [])
if data_list:
results.extend(data_list)
sources[plugin_name] = len(data_list)
# 如果没有data字段但有其他数据也认为是成功的结果
elif result: # 非空字典
results.append(result)
sources[plugin_name] = 1
# 统一排序并提取前limit条数据
if results:
unified_result = {"data": results}
optimized_result = self.js_plugin_manager.optimize_search_results(
unified_result,
search_keyword=keyword,
limit=limit,
search_artist=artist
)
results = optimized_result.get('data', [])
return {
"success": True,
"data": results,
"total": len(results),
"sources": sources,
"page": page,
"limit": limit
}
async def _search_specific_plugin(self, plugin, keyword, artist, page, limit):
"""搜索指定插件"""
try:
results = self.js_plugin_manager.search(plugin, keyword, page, limit)
# 额外检查 resources 字段
data_list = results.get('data', [])
if data_list:
# 优化搜索结果排序
results = self.js_plugin_manager.optimize_search_results(
results,
search_keyword=keyword,
limit=limit,
search_artist=artist
)
return {
"success": True,
"data": results.get('data', []),
"total": results.get('total', 0),
"page": page,
"limit": limit
}
except Exception as e:
self.log.error(f"插件 {plugin} 搜索失败: {e}")
return {"success": False, "error": str(e)}
async def _search_plugin_task(self, plugin_name, keyword, page, limit):
"""单个插件搜索任务"""
try:
return self.js_plugin_manager.search(plugin_name, keyword, page, limit)
except Exception as e:
# 直接抛出异常,让 asyncio.gather 处理
raise e
# 调用MusicFree插件获取真实播放url
async def get_media_source_url(self, music_item, quality: str = 'standard'):
"""获取音乐项的媒体源URL
Args:
music_item : MusicFree插件定义的 IMusicItem
quality: 音质参数
Returns:
dict: 包含成功状态和URL信息的字典
"""
# kwargs可追加
kwargs = {'quality': quality}
return await self.__call_plugin_method(
plugin_name=music_item.get('platform'),
method_name="get_media_source",
music_item=music_item,
result_key="url",
required_field="url",
**kwargs
)
# 调用MusicFree插件获取歌词
async def get_media_lyric(self, music_item):
"""获取音乐项的歌词 Lyric
Args:
music_item : MusicFree插件定义的 IMusicItem
Returns:
dict: 包含成功状态和URL信息的字典
"""
return await self.__call_plugin_method(
plugin_name=music_item.get('platform'),
method_name="get_lyric",
music_item=music_item,
result_key="rawLrc",
required_field="rawLrc"
)
# 调用在线搜索歌曲,并优化返回
async def search_music_online(self, search_key, name):
"""调用MusicFree插件搜索歌曲
Args:
search_key (str): 搜索关键词
name (str): 歌曲名
Returns:
dict: 包含成功状态和URL信息的字典
"""
try:
# 获取歌曲列表
result = await self.get_music_list_online(keyword=name, limit=10)
self.log.info(f"在线搜索歌曲列表: {result}")
if result.get('success') and result.get('total') > 0:
# 打印输出 result.data
self.log.info(f"歌曲列表: {result.get('data')}")
# 根据搜素关键字智能搜索出最符合的一条music_item
music_item = await self._search_top_one(result.get('data'), search_key, name)
# 验证 music_item 是否为字典类型
if not isinstance(music_item, dict):
self.log.error(f"music_item should be a dict, but got {type(music_item)}: {music_item}")
return {"success": False, "error": "Invalid music item format"}
# 如果是OpenAPI则需要转换播放链接
openapi_info = self.js_plugin_manager.get_openapi_info()
if openapi_info.get("enabled", False):
return await self.get_real_url_of_openapi(music_item.get('url'))
else:
media_source = await self.get_media_source_url(music_item)
if media_source.get('success'):
return {"success": True, "url": media_source.get('url')}
else:
return {"success": False, "error": media_source.get('error')}
else:
return {"success": False, "error": "未找到歌曲"}
except Exception as e:
# 记录错误日志
self.log.error(f"searchKey {search_key} get media source failed: {e}")
return {"success": False, "error": str(e)}
async def _search_top_one(self, music_items, search_key, name):
"""智能搜索出最符合的一条music_item"""
try:
# 如果没有音乐项目返回None
if not music_items:
return None
self.log.info(f"搜索关键字: {search_key};歌名:{name}")
# 如果只有一个项目,直接返回
if len(music_items) == 1:
return music_items[0]
# 计算每个项目的匹配分数
def calculate_match_score(item):
"""计算匹配分数"""
title = item.get('title', '').lower() if item.get('title') else ''
artist = item.get('artist', '').lower() if item.get('artist') else ''
keyword = search_key.lower()
if not keyword:
return 0
score = 0
# 歌曲名匹配权重
if keyword in title:
# 完全匹配得最高分
if title == keyword:
score += 90
# 开头匹配
elif title.startswith(keyword):
score += 70
# 结尾匹配
elif title.endswith(keyword):
score += 50
# 包含匹配
else:
score += 30
# 部分字符匹配
elif any(char in title for char in keyword.split()):
score += 10
# 艺术家名匹配权重
if keyword in artist:
# 完全匹配
if artist == keyword:
score += 9
# 开头匹配
elif artist.startswith(keyword):
score += 7
# 结尾匹配
elif artist.endswith(keyword):
score += 5
# 包含匹配
else:
score += 3
# 部分字符匹配
elif any(char in artist for char in keyword.split()):
score += 1
return score
# 按匹配分数排序,返回分数最高的项目
sorted_items = sorted(music_items, key=calculate_match_score, reverse=True)
return sorted_items[0]
except Exception as e:
self.log.error(f"_search_top_one error: {e}")
# 出现异常时返回第一个项目
return music_items[0] if music_items else None
# ===========================================================
def _find_real_music_list_name(self, list_name):
if not self.config.enable_fuzzy_match:
self.log.debug("没开启模糊匹配")
@@ -1188,11 +1628,14 @@ class XiaoMusic:
return await self.do_play_music_list(did, list_name, music_name)
async def do_play_music_list(self, did, list_name, music_name=""):
# 查找并获取真实的音乐列表名称
list_name = self._find_real_music_list_name(list_name)
# 检查音乐列表是否存在,如果不存在则进行语音提示并返回
if list_name not in self.music_list:
await self.do_tts(did, f"播放列表{list_name}不存在")
return
# 调用设备播放音乐列表的方法
await self.devices[did].play_music_list(list_name, music_name)
# 播放一个播放列表里第几个
@@ -1245,9 +1688,37 @@ class XiaoMusic:
did, name, search_key, exact=False, update_cur_list=False
)
# 在线播放:在线搜索、播放
async def online_play(self, did="", arg1="", **kwargs):
# 先推送默认【搜索中】音频搜索到播放url后推送给小爱
config = self.config
if config and hasattr(config, 'hostname') and hasattr(config, 'public_port'):
proxy_base = f"{config.hostname}:{config.public_port}"
else:
proxy_base = "http://192.168.31.241:8090"
search_audio = proxy_base + "/static/search.mp3"
proxy_base + "/static/silence.mp3"
await self.play_url(self.get_cur_did(), search_audio)
# TODO 添加一个定时器4秒后触发
# 获取搜索关键词
parts = arg1.split("|")
search_key = parts[0]
name = parts[1] if len(parts) > 1 else search_key
if not name:
name = search_key
self.log.info(f"搜索关键字{search_key},搜索歌名{name}")
result = await self.search_music_online(search_key, name)
# 搜索成功则直接推送url播放
if result.get("success", False):
url = result.get("url", "")
# 播放歌曲
await self.devices[did].play_music(name, true_url=url)
# 后台搜索播放
async def do_play(
self, did, name, search_key="", exact=False, update_cur_list=False
self, did, name, search_key="", exact=False, update_cur_list=False
):
return await self.devices[did].play(name, search_key, exact, update_cur_list)
@@ -1633,6 +2104,9 @@ class XiaoMusicDevice:
offset = time.time() - self._start_time - self._paused_time
return offset, duration
async def play_music(self, name, true_url=None):
return await self._playmusic(name, true_url=true_url)
# 初始化播放列表
def update_playlist(self, reorder=True):
# 没有重置 list 且非初始化
@@ -1640,7 +2114,7 @@ class XiaoMusicDevice:
# 更新总播放列表为了UI显示
self.xiaomusic.music_list["临时搜索列表"] = copy.copy(self._play_list)
elif (
self.device.cur_playlist == "临时搜索列表" and len(self._play_list) == 0
self.device.cur_playlist == "临时搜索列表" and len(self._play_list) == 0
) or (self.device.cur_playlist not in self.xiaomusic.music_list):
self.device.cur_playlist = "全部"
else:
@@ -1682,7 +2156,7 @@ class XiaoMusicDevice:
return
else:
name = self.get_cur_music()
self.log.info(f"play. search_key:{search_key} name:{name}")
self.log.info(f"play. search_key:{search_key} name:{name}: exact:{exact}")
# 本地歌曲不存在时下载
if exact:
@@ -1713,10 +2187,15 @@ class XiaoMusicDevice:
if self.config.disable_download:
await self.do_tts(f"本地不存在歌曲{name}")
return
await self.download(search_key, name)
# 把文件插入到播放列表里
await self.add_download_music(name)
await self._playmusic(name)
else:
# 如果插件播放失败,则执行下载流程
await self.download(search_key, name)
# 把文件插入到播放列表里
await self.add_download_music(name)
await self._playmusic(name)
else:
# 本地存在歌曲,直接播放
await self._playmusic(name)
# 下一首
async def play_next(self):
@@ -1726,18 +2205,18 @@ class XiaoMusicDevice:
self.log.info("开始播放下一首")
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or self.device.play_type == PLAY_TYPE_SEQ
or name == ""
or (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or self.device.play_type == PLAY_TYPE_SEQ
or name == ""
or (
(name not in self._play_list) and self.device.play_type != PLAY_TYPE_ONE
)
)
):
name = self.get_next_music()
self.log.info(f"_play_next. name:{name}, cur_music:{self.get_cur_music()}")
if name == "":
await self.do_tts("本地没有歌曲")
# await self.do_tts("本地没有歌曲")
return
await self._play(name, exact=True)
@@ -1749,10 +2228,10 @@ class XiaoMusicDevice:
self.log.info("开始播放上一首")
name = self.get_cur_music()
if (
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or name == ""
or (name not in self._play_list)
self.device.play_type == PLAY_TYPE_ALL
or self.device.play_type == PLAY_TYPE_RND
or name == ""
or (name not in self._play_list)
):
name = self.get_prev_music()
self.log.info(f"_play_prev. name:{name}, cur_music:{self.get_cur_music()}")
@@ -1803,7 +2282,7 @@ class XiaoMusicDevice:
return
await self._playmusic(name)
async def _playmusic(self, name):
async def _playmusic(self, name, true_url=None):
# 取消组内所有的下一首歌曲的定时器
self.cancel_group_next_timer()
@@ -1812,20 +2291,22 @@ class XiaoMusicDevice:
self.device.playlist2music[self.device.cur_playlist] = name
self.log.info(f"cur_music {self.get_cur_music()}")
sec, url = await self.xiaomusic.get_music_sec_url(name)
sec, url = await self.xiaomusic.get_music_sec_url(name, true_url)
await self.group_force_stop_xiaoai()
self.log.info(f"播放 {url}")
# 有3方设备打开 /static/3thplay.html 通过socketio连接返回true 忽律小爱音箱的播放
online = await thdplay("play", url, self.xiaomusic.thdtarget)
self.log.error(f"IS online {online}")
if not online:
results = await self.group_player_play(url, name)
if all(ele is None for ele in results):
self.log.info(f"播放 {name} 失败. 失败次数: {self._play_failed_cnt}")
await asyncio.sleep(1)
if (
self.isplaying()
and self._last_cmd != "stop"
and self._play_failed_cnt < 10
self.isplaying()
and self._last_cmd != "stop"
and self._play_failed_cnt < 10
):
self._play_failed_cnt = self._play_failed_cnt + 1
await self._play_next()
@@ -1879,8 +2360,8 @@ class XiaoMusicDevice:
self.log.info(playing_info)
# WTF xiaomi api
is_playing = (
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
== 1
json.loads(playing_info.get("data", {}).get("info", "{}")).get("status", -1)
== 1
)
return is_playing
@@ -1999,8 +2480,8 @@ class XiaoMusicDevice:
if direction == "next":
new_index = index + 1
if (
self.device.play_type == PLAY_TYPE_SEQ
and new_index >= play_list_len
self.device.play_type == PLAY_TYPE_SEQ
and new_index >= play_list_len
):
self.log.info("顺序播放结束")
return ""
@@ -2087,7 +2568,7 @@ class XiaoMusicDevice:
f"play_one_url continue_play device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
)
elif self.config.use_music_api or (
self.hardware in NEED_USE_PLAY_MUSIC_API
self.hardware in NEED_USE_PLAY_MUSIC_API
):
ret = await self.xiaomusic.mina_service.play_by_music_url(
device_id, url, audio_id=audio_id
@@ -2296,7 +2777,7 @@ class XiaoMusicDevice:
return "最近新增"
for list_name, play_list in self.xiaomusic.music_list.items():
if (list_name not in ["全部", "所有歌曲", "所有电台", "临时搜索列表"]) and (
name in play_list
name in play_list
):
return list_name
if name in self.xiaomusic.music_list.get("所有歌曲", []):
@@ -2344,3 +2825,5 @@ class XiaoMusicPathWatch(FileSystemEventHandler):
self._debounce_handle = self.loop.call_later(
self.debounce_delay, _execute_callback
)
# ===================================================================