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 数据(音频地址和歌词) + // 匹配模式: