- [汽水音乐-qishui_music](https://music.douyin.com/qishui/)

- [咪咕音乐-migu](https://music.migu.cn/)
- [一刻相册-baidu_photo](https://photo.baidu.com/)
This commit is contained in:
q
2025-11-15 21:49:40 +08:00
parent 0e2ca2f1ca
commit 584c075930
12 changed files with 1383 additions and 61 deletions

View File

@@ -12,7 +12,7 @@
<groupId>cn.qaiu</groupId>
<artifactId>parser</artifactId>
<version>10.2.1</version>
<version>10.2.3</version>
<packaging>jar</packaging>
<name>cn.qaiu:parser</name>

View File

@@ -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<HttpResponse<Buffer>> promise = Promise.promise();
Future<HttpResponse<Buffer>> 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<Buffer> response = promise.future().toCompletionStage()

View File

@@ -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<String> 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<String> 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<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> 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<FileInfo> 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<String> parseById() {
Promise<String> 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();
});
}
/**

View File

@@ -0,0 +1,205 @@
// ==UserScript==
// @name 咪咕音乐解析器
// @type migu
// @displayName 咪咕音乐
// @description 解析咪咕音乐分享链接,获取歌曲下载地址
// @match https?://c\.migu\.cn/(?<KEY>\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 +
"&copyrightId=" + 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);
}

View File

@@ -0,0 +1,231 @@
// ==UserScript==
// @name 汽水音乐解析器
// @type qishui_music
// @displayName 汽水音乐
// @description 解析汽水音乐分享链接,获取音乐文件下载链接
// @match https://music\.douyin\.com/qishui/share/track\?(.*&)?track_id=(?<KEY>\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 数据(音频地址和歌词)
// 匹配模式:<script async="" data-script-src="modern-inline">_ROUTER_DATA = {...};
var routerDataPattern = /<script\s+async=""\s+data-script-src="modern-inline">\s*_ROUTER_DATA\s*=\s*({[\s\S]*?});/;
var routerDataMatch = htmlContent.match(routerDataPattern);
if (routerDataMatch) {
try {
var jsonStr = routerDataMatch[1].trim();
var jsonData = JSON.parse(jsonStr);
logger.debug("解析_ROUTER_DATA成功");
// 提取音频URL
var audioOption = jsonData.loaderData &&
jsonData.loaderData.track_page &&
jsonData.loaderData.track_page.audioWithLyricsOption;
if (audioOption && audioOption.url) {
musicPlayUrl = audioOption.url;
logger.info("提取到音频URL: " + musicPlayUrl);
}
// 提取歌词(可选,用于日志)
if (audioOption && audioOption.lyrics && audioOption.lyrics.sentences) {
var sentences = audioOption.lyrics.sentences;
logger.debug("提取到歌词,共 " + sentences.length + " 句");
}
} catch (e) {
logger.warn("解析_ROUTER_DATA失败: " + e.message);
}
} else {
logger.warn("未找到_ROUTER_DATA");
}
// 如果未找到音频URL尝试从application/ld+json中提取备用方案
if (!musicPlayUrl) {
logger.warn("未从_ROUTER_DATA中提取到音频URL尝试备用方案");
// 提取 application/ld+json 数据
var ldJsonPattern = /<script\s+data-react-helmet="true"\s+type="application\/ld\+json">([\s\S]*?)<\/script>/;
var ldJsonMatch = htmlContent.match(ldJsonPattern);
if (ldJsonMatch) {
try {
var ldJsonStr = unquote(ldJsonMatch[1]);
var ldJsonData = JSON.parse(ldJsonStr);
logger.debug("解析ld+json成功标题: " + (ldJsonData.title || "无"));
} catch (e) {
logger.warn("解析ld+json失败: " + e.message);
}
}
}
if (!musicPlayUrl) {
throw new Error("没有找到相关音乐");
}
logger.info("解析成功: " + musicPlayUrl);
return musicPlayUrl;
} catch (e) {
logger.error("解析失败: " + e.message);
throw e;
}
}

View File

@@ -0,0 +1,282 @@
package cn.qaiu.parser;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.parser.customjs.JsHttpClient;
import io.vertx.core.Vertx;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* JsHttpClient 测试类
* 测试HTTP请求功能是否正常
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/11/15
*/
public class JsHttpClientTest {
private Vertx vertx;
private JsHttpClient httpClient;
@Before
public void setUp() {
// 初始化Vertx
vertx = Vertx.vertx();
WebClientVertxInit.init(vertx);
// 创建JsHttpClient实例
httpClient = new JsHttpClient();
System.out.println("=== 测试开始 ===");
}
@After
public void tearDown() {
// 清理资源
if (vertx != null) {
vertx.close();
}
System.out.println("=== 测试结束 ===\n");
}
@Test
public void testSimpleGetRequest() {
System.out.println("\n[测试1] 简单GET请求 - httpbin.org/get");
try {
String url = "https://httpbin.org/get";
System.out.println("请求URL: " + url);
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.get(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
System.out.println("响应头数量: " + response.headers().size());
String body = response.body();
System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符");
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
assertTrue("响应体应该包含url字段", body.contains("\"url\""));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("GET请求失败: " + e.getMessage());
}
}
@Test
public void testGetWithRedirect() {
System.out.println("\n[测试2] GET请求跟随重定向 - httpbin.org/redirect/1");
try {
String url = "https://httpbin.org/redirect/1";
System.out.println("请求URL: " + url);
System.out.println("开始请求(会自动跟随重定向)...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.getWithRedirect(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符");
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200重定向后", 200, response.statusCode());
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("GET重定向请求失败: " + e.getMessage());
}
}
@Test
public void testGetNoRedirect() {
System.out.println("\n[测试3] GET请求不跟随重定向 - httpbin.org/redirect/1");
try {
String url = "https://httpbin.org/redirect/1";
System.out.println("请求URL: " + url);
System.out.println("开始请求(不跟随重定向)...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.getNoRedirect(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String location = response.header("Location");
System.out.println("Location头: " + location);
// 验证结果
assertNotNull("响应不能为null", response);
assertTrue("状态码应该是3xx重定向",
response.statusCode() >= 300 && response.statusCode() < 400);
assertNotNull("应该有Location头", location);
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("GET不重定向请求失败: " + e.getMessage());
}
}
@Test
public void testGetWithHeaders() {
System.out.println("\n[测试4] GET请求带自定义请求头 - httpbin.org/headers");
try {
String url = "https://httpbin.org/headers";
System.out.println("请求URL: " + url);
// 设置自定义请求头
httpClient.putHeader("X-Custom-Header", "test-value");
httpClient.putHeader("X-Another-Header", "another-value");
System.out.println("设置请求头: X-Custom-Header=test-value, X-Another-Header=another-value");
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.get(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
String body = response.body();
System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符");
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("响应体不能为null", body);
assertTrue("响应体应该包含自定义请求头",
body.contains("X-Custom-Header") || body.contains("test-value"));
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("带请求头的GET请求失败: " + e.getMessage());
}
}
@Test
public void testGetJsonResponse() {
System.out.println("\n[测试5] GET请求JSON响应 - jsonplaceholder.typicode.com/posts/1");
try {
String url = "https://jsonplaceholder.typicode.com/posts/1";
System.out.println("请求URL: " + url);
System.out.println("开始请求...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.get(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
// 测试JSON解析
Object jsonData = response.json();
System.out.println("JSON数据: " + jsonData);
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertNotNull("JSON数据不能为null", jsonData);
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("JSON响应请求失败: " + e.getMessage());
}
}
@Test
public void testTimeout() {
System.out.println("\n[测试6] 超时测试 - httpbin.org/delay/5");
System.out.println("注意这个请求会延迟5秒应该在30秒内完成");
try {
String url = "https://httpbin.org/delay/5";
System.out.println("请求URL: " + url);
System.out.println("开始请求延迟5秒...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.get(url);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.println("请求完成,耗时: " + duration + "ms");
System.out.println("状态码: " + response.statusCode());
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是200", 200, response.statusCode());
assertTrue("应该在合理时间内完成5-10秒", duration >= 5000 && duration < 10000);
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("超时测试失败: " + e.getMessage());
}
}
@Test
public void testErrorResponse() {
System.out.println("\n[测试7] 错误响应测试 - httpbin.org/status/404");
try {
String url = "https://httpbin.org/status/404";
System.out.println("请求URL: " + url);
System.out.println("开始请求预期404错误...");
long startTime = System.currentTimeMillis();
JsHttpClient.JsHttpResponse response = httpClient.get(url);
long endTime = System.currentTimeMillis();
System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms");
System.out.println("状态码: " + response.statusCode());
// 验证结果
assertNotNull("响应不能为null", response);
assertEquals("状态码应该是404", 404, response.statusCode());
assertFalse("不应该成功", response.isSuccess());
System.out.println("✓ 测试通过");
} catch (Exception e) {
System.err.println("✗ 测试失败: " + e.getMessage());
e.printStackTrace();
fail("错误响应测试失败: " + e.getMessage());
}
}
}