diff --git a/README.md b/README.md
index ea2ddee..69d72c2 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -73,11 +73,13 @@ main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/
- [酷狗音乐分享链接-mkgs](https://www.kugou.com)
- [酷我音乐分享链接-mkws](https://kuwo.cn)
- [QQ音乐分享链接-mqqs](https://y.qq.com)
-- 咪咕音乐分享链接(开发中)
- [Cloudreve自建网盘-ce](https://github.com/cloudreve/Cloudreve)
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
- [WPS云文档-pwps](https://www.kdocs.cn/)
+- [汽水音乐-qishui_music](https://music.douyin.com/qishui/)
+- [咪咕音乐-migu](https://music.migu.cn/)
+- [一刻相册-baidu_photo](https://photo.baidu.com/)
- Google云盘-pgd
- Onedrive-pod
- Dropbox-pdp
diff --git a/parser/pom.xml b/parser/pom.xml
index a210e43..5733122 100644
--- a/parser/pom.xml
+++ b/parser/pom.xml
@@ -12,7 +12,7 @@
cn.qaiu
parser
- 10.2.1
+ 10.2.3
jar
cn.qaiu:parser
diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
index cefcd2a..d42fe95 100644
--- a/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
@@ -6,7 +6,6 @@ import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
-import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import io.vertx.core.net.ProxyType;
@@ -40,7 +39,7 @@ public class JsHttpClient {
private MultiMap headers;
public JsHttpClient() {
- this.client = WebClient.create(WebClientVertxInit.get());
+ this.client = WebClient.create(WebClientVertxInit.get(), new WebClientOptions());;
this.clientSession = WebClientSession.create(client);
this.headers = MultiMap.caseInsensitiveMultiMap();
// 设置默认的Accept-Encoding头以支持压缩响应
@@ -264,7 +263,13 @@ public class JsHttpClient {
Promise> promise = Promise.promise();
Future> future = executor.execute();
- future.onComplete(promise);
+ future.onComplete(result -> {
+ if (result.succeeded()) {
+ promise.complete(result.result());
+ } else {
+ promise.fail(result.cause());
+ }
+ }).onFailure(Throwable::printStackTrace);
// 等待响应完成(最多30秒)
HttpResponse response = promise.future().toCompletionStage()
diff --git a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
index 81715f1..7ec7da0 100644
--- a/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
@@ -1,23 +1,21 @@
package cn.qaiu.parser.customjs;
+import cn.qaiu.WebClientVertxInit;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.custom.CustomParserConfig;
import io.vertx.core.Future;
-import io.vertx.core.Promise;
+import io.vertx.core.WorkerExecutor;
import io.vertx.core.json.JsonObject;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
-import javax.script.ScriptException;
import java.util.ArrayList;
import java.util.List;
-import java.util.Map;
/**
* JavaScript解析器执行器
@@ -30,13 +28,14 @@ public class JsParserExecutor implements IPanTool {
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
+ private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get().createSharedWorkerExecutor("parser-executor", 32);
+
private final CustomParserConfig config;
private final ShareLinkInfo shareLinkInfo;
private final ScriptEngine engine;
private final JsHttpClient httpClient;
private final JsLogger jsLogger;
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
- private final Promise promise = Promise.promise();
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
this.config = config;
@@ -58,6 +57,7 @@ public class JsParserExecutor implements IPanTool {
* 获取ShareLinkInfo对象
* @return ShareLinkInfo对象
*/
+ @Override
public ShareLinkInfo getShareLinkInfo() {
return shareLinkInfo;
}
@@ -93,47 +93,40 @@ public class JsParserExecutor implements IPanTool {
@Override
public Future parse() {
- try {
- jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
-
+ jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
+
+ // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
+ return EXECUTOR.executeBlocking(() -> {
// 直接调用全局parse函数
Object parseFunction = engine.get("parse");
if (parseFunction == null) {
throw new RuntimeException("JavaScript代码中未找到parse函数");
}
- if (parseFunction instanceof ScriptObjectMirror) {
- ScriptObjectMirror parseMirror = (ScriptObjectMirror) parseFunction;
-
+ if (parseFunction instanceof ScriptObjectMirror parseMirror) {
+
Object result = parseMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
if (result instanceof String) {
jsLogger.info("解析成功: {}", result);
- promise.complete((String) result);
+ return (String) result;
} else {
jsLogger.error("parse方法返回值类型错误,期望String,实际: {}",
result != null ? result.getClass().getSimpleName() : "null");
- promise.fail("parse方法返回值类型错误");
+ throw new RuntimeException("parse方法返回值类型错误");
}
} else {
throw new RuntimeException("parse函数类型错误");
}
-
- } catch (Exception e) {
- jsLogger.error("JavaScript解析器执行失败", e);
- promise.fail("JavaScript解析器执行失败: " + e.getMessage());
- }
-
- return promise.future();
+ });
}
@Override
public Future> parseFileList() {
- Promise> promise = Promise.promise();
+ jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
- try {
- jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
-
+ // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
+ return EXECUTOR.executeBlocking(() -> {
// 直接调用全局parseFileList函数
Object parseFileListFunction = engine.get("parseFileList");
if (parseFileListFunction == null) {
@@ -141,41 +134,32 @@ public class JsParserExecutor implements IPanTool {
}
// 调用parseFileList方法
- if (parseFileListFunction instanceof ScriptObjectMirror) {
- ScriptObjectMirror parseFileListMirror = (ScriptObjectMirror) parseFileListFunction;
-
+ if (parseFileListFunction instanceof ScriptObjectMirror parseFileListMirror) {
+
Object result = parseFileListMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
- if (result instanceof ScriptObjectMirror) {
- ScriptObjectMirror resultMirror = (ScriptObjectMirror) result;
+ if (result instanceof ScriptObjectMirror resultMirror) {
List fileList = convertToFileInfoList(resultMirror);
jsLogger.info("文件列表解析成功,共 {} 个文件", fileList.size());
- promise.complete(fileList);
+ return fileList;
} else {
jsLogger.error("parseFileList方法返回值类型错误,期望数组,实际: {}",
result != null ? result.getClass().getSimpleName() : "null");
- promise.fail("parseFileList方法返回值类型错误");
+ throw new RuntimeException("parseFileList方法返回值类型错误");
}
} else {
throw new RuntimeException("parseFileList函数类型错误");
}
-
- } catch (Exception e) {
- jsLogger.error("JavaScript文件列表解析失败", e);
- promise.fail("JavaScript文件列表解析失败: " + e.getMessage());
- }
-
- return promise.future();
+ });
}
@Override
public Future parseById() {
- Promise promise = Promise.promise();
+ jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
- try {
- jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
-
+ // 使用executeBlocking在工作线程上执行,避免阻塞EventLoop线程
+ return EXECUTOR.executeBlocking(() -> {
// 直接调用全局parseById函数
Object parseByIdFunction = engine.get("parseById");
if (parseByIdFunction == null) {
@@ -183,29 +167,22 @@ public class JsParserExecutor implements IPanTool {
}
// 调用parseById方法
- if (parseByIdFunction instanceof ScriptObjectMirror) {
- ScriptObjectMirror parseByIdMirror = (ScriptObjectMirror) parseByIdFunction;
-
+ if (parseByIdFunction instanceof ScriptObjectMirror parseByIdMirror) {
+
Object result = parseByIdMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
if (result instanceof String) {
jsLogger.info("按ID解析成功: {}", result);
- promise.complete((String) result);
+ return (String) result;
} else {
jsLogger.error("parseById方法返回值类型错误,期望String,实际: {}",
result != null ? result.getClass().getSimpleName() : "null");
- promise.fail("parseById方法返回值类型错误");
+ throw new RuntimeException("parseById方法返回值类型错误");
}
} else {
throw new RuntimeException("parseById函数类型错误");
}
-
- } catch (Exception e) {
- jsLogger.error("JavaScript按ID解析失败", e);
- promise.fail("JavaScript按ID解析失败: " + e.getMessage());
- }
-
- return promise.future();
+ });
}
/**
diff --git a/parser/src/main/resources/custom-parsers/migu-music.js b/parser/src/main/resources/custom-parsers/migu-music.js
new file mode 100644
index 0000000..8b02f0d
--- /dev/null
+++ b/parser/src/main/resources/custom-parsers/migu-music.js
@@ -0,0 +1,205 @@
+// ==UserScript==
+// @name 咪咕音乐解析器
+// @type migu
+// @displayName 咪咕音乐
+// @description 解析咪咕音乐分享链接,获取歌曲下载地址
+// @match https?://c\.migu\.cn/(?\w+)(\?.*)?
+// @author qaiu
+// @version 2.0.0
+// ==/UserScript==
+
+/**
+ * 从URL中提取参数值
+ * @param {string} url - URL字符串
+ * @param {string} paramName - 参数名
+ * @returns {string|null} 参数值
+ */
+function getUrlParam(url, paramName) {
+ var match = url.match(new RegExp("[?&]" + paramName + "=([^&]*)"));
+ return match ? match[1] : null;
+}
+
+/**
+ * 获取302重定向地址
+ * @param {string} url - 原始URL
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 重定向后的URL
+ */
+function getRedirectUrl(url, http, logger) {
+ try {
+ logger.debug("获取重定向地址: " + url);
+
+ // 清理URL,移除?后面的参数
+ var cleanUrl = url;
+ var questionMarkIndex = url.indexOf("?");
+ if (questionMarkIndex !== -1) {
+ cleanUrl = url.substring(0, questionMarkIndex);
+ }
+ logger.debug("清理后的URL: " + cleanUrl);
+
+ // 使用getNoRedirect获取Location头
+ var response = http.getNoRedirect(cleanUrl);
+ var statusCode = response.statusCode();
+
+ // 检查是否是重定向状态码
+ if (statusCode >= 300 && statusCode < 400) {
+ var location = response.header("Location");
+ if (location) {
+ // 处理相对路径
+ if (location.indexOf("http") !== 0) {
+ var baseUrl = cleanUrl.substring(0, cleanUrl.indexOf("/", 8));
+ if (location.indexOf("/") === 0) {
+ location = baseUrl + location;
+ } else {
+ location = baseUrl + "/" + location;
+ }
+ }
+ logger.info("重定向到: " + location);
+ return location;
+ }
+ }
+
+ // 如果没有重定向,返回原URL
+ logger.warn("未获取到重定向地址,状态码: " + statusCode);
+ return cleanUrl;
+
+ } catch (e) {
+ logger.error("获取重定向地址失败: " + e.message);
+ throw e;
+ }
+}
+
+/**
+ * 解析单个文件下载链接
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 下载链接
+ */
+function parse(shareLinkInfo, http, logger) {
+ logger.info("===== 开始解析咪咕音乐 =====");
+
+ try {
+ var shareUrl = shareLinkInfo.getShareUrl();
+ logger.info("分享URL: " + shareUrl);
+
+ if (!shareUrl || shareUrl.indexOf("c.migu.cn") === -1) {
+ throw new Error("无效的咪咕音乐分享链接");
+ }
+
+ // 设置请求头
+ http.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
+ http.putHeader("Referer", "https://music.migu.cn/");
+ http.putHeader("Accept", "application/json, text/plain, */*");
+
+ // 步骤1: 获取302重定向地址
+ logger.info("步骤1: 获取302重定向地址...");
+ var redirectUrl = getRedirectUrl(shareUrl, http, logger);
+ logger.info("重定向地址: " + redirectUrl);
+
+ // 步骤2: 从重定向地址中提取contentId (id参数)
+ var contentId = getUrlParam(redirectUrl, "id");
+ if (!contentId) {
+ throw new Error("无法从重定向地址中提取contentId (id参数)");
+ }
+ logger.info("提取到contentId: " + contentId);
+
+ // 步骤3: 调用API获取文件信息
+ logger.info("步骤2: 获取文件信息...");
+ var fileInfoUrl = "https://c.musicapp.migu.cn/MIGUM3.0/resource/song/by-contentids/v2.0?contentId=" + contentId;
+ logger.debug("请求URL: " + fileInfoUrl);
+
+ var fileInfoResponse = http.get(fileInfoUrl);
+ if (fileInfoResponse.statusCode() !== 200) {
+ throw new Error("获取文件信息失败,状态码: " + fileInfoResponse.statusCode());
+ }
+
+ var fileInfoData = fileInfoResponse.json();
+ logger.debug("文件信息响应: " + JSON.stringify(fileInfoData));
+
+ // 提取ringCopyrightId
+ var ringCopyrightId = null;
+ if (fileInfoData.data && fileInfoData.data.length > 0) {
+ var songInfo = fileInfoData.data[0];
+ ringCopyrightId = songInfo.ringCopyrightId;
+ logger.info("歌曲名称: " + (songInfo.songName || "未知"));
+ logger.info("提取到ringCopyrightId: " + ringCopyrightId);
+ }
+
+ if (!ringCopyrightId) {
+ throw new Error("响应中未找到ringCopyrightId");
+ }
+
+ // 步骤4: 调用下载接口获取下载链接
+ logger.info("步骤3: 获取下载链接...");
+
+ // 设置完整的请求头(Referer使用302重定向地址)
+ http.putHeader("Accept", "application/json, text/plain, */*");
+ http.putHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7");
+ http.putHeader("Referer", redirectUrl);
+ http.putHeader("Sec-Fetch-Dest", "empty");
+ http.putHeader("Sec-Fetch-Mode", "cors");
+ http.putHeader("Sec-Fetch-Site", "same-site");
+ http.putHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36");
+ http.putHeader("channel", "014021I");
+ http.putHeader("subchannel", "014021I");
+
+ var downloadApiUrl = "https://c.musicapp.migu.cn/MIGUM3.0/strategy/listen-url/v2.4" +
+ "?contentId=" + contentId +
+ "©rightId=" + ringCopyrightId +
+ "&resourceType=2" +
+ "&netType=01" +
+ "&toneFlag=PQ" +
+ "&scene=" +
+ "&lowerQualityContentId=" + contentId;
+
+ logger.debug("请求URL: " + downloadApiUrl);
+ logger.debug("Referer: " + redirectUrl);
+
+ var downloadResponse = http.get(downloadApiUrl);
+ if (downloadResponse.statusCode() !== 200) {
+ throw new Error("获取下载链接失败,状态码: " + downloadResponse.statusCode());
+ }
+
+ var downloadData = downloadResponse.json();
+ logger.info("下载链接响应: " + JSON.stringify(downloadData));
+
+ // 提取最终下载链接
+ if (downloadData.data && downloadData.data.url) {
+ var downloadUrl = downloadData.data.url;
+ logger.info("解析成功,下载链接: " + downloadUrl);
+ return downloadUrl;
+ } else {
+ throw new Error("响应中未找到下载链接");
+ }
+
+ } catch (e) {
+ logger.error("解析失败: " + e.message);
+ throw e;
+ }
+}
+
+/**
+ * 解析文件列表(可选)
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {FileInfo[]} 文件信息列表
+ */
+function parseFileList(shareLinkInfo, http, logger) {
+ // 咪咕音乐通常是单曲,不需要实现文件列表
+ return [];
+}
+
+/**
+ * 根据文件ID获取下载链接(可选)
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 下载链接
+ */
+function parseById(shareLinkInfo, http, logger) {
+ // 使用相同的解析逻辑
+ return parse(shareLinkInfo, http, logger);
+}
diff --git a/parser/src/main/resources/custom-parsers/qishui-music.js b/parser/src/main/resources/custom-parsers/qishui-music.js
new file mode 100644
index 0000000..6e931a6
--- /dev/null
+++ b/parser/src/main/resources/custom-parsers/qishui-music.js
@@ -0,0 +1,231 @@
+// ==UserScript==
+// @name 汽水音乐解析器
+// @type qishui_music
+// @displayName 汽水音乐
+// @description 解析汽水音乐分享链接,获取音乐文件下载链接
+// @match https://music\.douyin\.com/qishui/share/track\?(.*&)?track_id=(?\d+)
+// @author qaiu
+// @version 2.0.1
+// ==/UserScript==
+
+/**
+ * 跟踪302重定向,获取真实URL
+ * @param {string} url - 原始URL
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 真实URL
+ */
+function getRealUrl(url, http, logger) {
+ try {
+ logger.debug("跟踪重定向: " + url);
+ // 使用getNoRedirect获取Location头
+ var response = http.getNoRedirect(url);
+ var statusCode = response.statusCode();
+
+ // 检查是否是重定向状态码 (301, 302, 303, 307, 308)
+ if (statusCode >= 300 && statusCode < 400) {
+ var location = response.header("Location");
+ if (location) {
+ // 处理相对路径
+ if (location.indexOf("http") !== 0) {
+ var baseUrl = url.substring(0, url.indexOf("/", 8)); // 获取协议和域名部分
+ if (location.indexOf("/") === 0) {
+ location = baseUrl + location;
+ } else {
+ location = baseUrl + "/" + location;
+ }
+ }
+ logger.debug("重定向到: " + location);
+ return location;
+ }
+ }
+ // 如果没有重定向或无法获取Location头,返回原URL
+ logger.debug("无需重定向或无法获取重定向信息");
+ return url;
+ } catch (e) {
+ logger.warn("获取真实链接失败: " + e.message);
+ return url;
+ }
+}
+
+/**
+ * 从URL中提取track_id
+ * @param {string} url - URL字符串
+ * @returns {string|null} track_id
+ */
+function extractTrackId(url) {
+ var match = url.match(/track_id=(\d+)/);
+ return match ? match[1] : null;
+}
+
+/**
+ * URL解码
+ * @param {string} str - 编码的字符串
+ * @returns {string} 解码后的字符串
+ */
+function unquote(str) {
+ try {
+ return decodeURIComponent(str);
+ } catch (e) {
+ return str;
+ }
+}
+
+/**
+ * 格式化时间标签(毫秒转LRC格式)
+ * @param {number} startMs - 开始时间(毫秒)
+ * @returns {string} LRC格式时间标签 [mm:ss.fff]
+ */
+function formatTimeTag(startMs) {
+ var minutes = Math.floor(startMs / 60000);
+ var seconds = Math.floor((startMs % 60000) / 1000);
+ var milliseconds = startMs % 1000;
+
+ var minutesStr = (minutes < 10 ? "0" : "") + minutes;
+ var secondsStr = (seconds < 10 ? "0" : "") + seconds;
+ var millisecondsStr = (milliseconds < 10 ? "00" : (milliseconds < 100 ? "0" : "")) + milliseconds;
+
+ return "[" + minutesStr + ":" + secondsStr + "." + millisecondsStr + "]";
+}
+
+/**
+ * 解析单个文件下载链接
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 下载链接
+ */
+function parse(shareLinkInfo, http, logger) {
+ logger.info("===== 开始解析汽水音乐 =====");
+
+ try {
+ // 优先从ShareKey获取track_id(最快方式)
+ var trackId = shareLinkInfo.getShareKey();
+
+ // 如果ShareKey为空,尝试从URL中提取
+ if (!trackId) {
+ var shareUrl = shareLinkInfo.getShareUrl();
+ logger.info("分享URL: " + shareUrl);
+
+ if (shareUrl) {
+ // 先尝试直接从URL提取track_id(避免重定向超时)
+ trackId = extractTrackId(shareUrl);
+
+ // 如果是短链接且仍未提取到track_id,才进行重定向处理
+ if (!trackId && shareUrl.indexOf("qishui.douyin.com") !== -1) {
+ logger.info("检测到短链接,尝试获取真实URL...");
+ try {
+ shareUrl = getRealUrl(shareUrl, http, logger);
+ logger.info("重定向后URL: " + shareUrl);
+ trackId = extractTrackId(shareUrl);
+ } catch (e) {
+ logger.warn("短链接重定向处理失败: " + e.message);
+ }
+ }
+ }
+ }
+
+ logger.info("歌曲ID: " + trackId);
+
+ if (!trackId) {
+ throw new Error("无法提取track_id");
+ }
+
+ // 设置必要的浏览器请求头(最小化,避免触发反爬虫)
+ http.putHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ http.putHeader("Accept-Language", "zh-CN,zh;q=0.9");
+ http.putHeader("Referer", "https://music.douyin.com/");
+ http.putHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
+
+ // 请求音乐页面
+ var musicUrl = "https://music.douyin.com/qishui/share/track?track_id=" + trackId;
+ logger.info("请求音乐页面: " + musicUrl);
+ logger.debug("开始请求,请等待...");
+
+ // 使用getWithRedirect自动处理重定向
+ // 注意:如果超时,可能是网络问题或目标网站响应慢
+ var response = http.getWithRedirect(musicUrl);
+
+ logger.debug("请求完成,状态码: " + response.statusCode());
+
+ if (response.statusCode() !== 200) {
+ throw new Error("获取页面内容失败,状态码: " + response.statusCode());
+ }
+
+ var htmlContent = response.body();
+
+ if (!htmlContent) {
+ throw new Error("页面内容为空");
+ }
+
+ logger.debug("页面内容长度: " + htmlContent.length);
+
+ // 初始化结果
+ var musicPlayUrl = "";
+
+ // 提取 _ROUTER_DATA 数据(音频地址和歌词)
+ // 匹配模式: