mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-17 21:03:03 +00:00
parser v10.1.17发布到maven central 允许开发者依赖
1. 添加自定义解析器扩展和相关示例 2. 优化pom结构
This commit is contained in:
222
parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java
Normal file
222
parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java
Normal file
@@ -0,0 +1,222 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 用户自定义解析器配置类
|
||||
* 用于描述自定义解析器的元信息
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class CustomParserConfig {
|
||||
|
||||
/**
|
||||
* 解析器类型标识(唯一,建议使用小写英文)
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
/**
|
||||
* 网盘显示名称
|
||||
*/
|
||||
private final String displayName;
|
||||
|
||||
/**
|
||||
* 解析工具实现类(必须实现 IPanTool 接口,且有 ShareLinkInfo 单参构造器)
|
||||
*/
|
||||
private final Class<? extends IPanTool> toolClass;
|
||||
|
||||
/**
|
||||
* 标准URL模板(可选,用于规范化分享链接)
|
||||
*/
|
||||
private final String standardUrlTemplate;
|
||||
|
||||
/**
|
||||
* 网盘域名(可选)
|
||||
*/
|
||||
private final String panDomain;
|
||||
|
||||
/**
|
||||
* 匹配正则表达式(可选,用于从分享链接中识别和提取信息)
|
||||
* 如果提供,则支持通过 fromShareUrl 方法自动识别自定义解析器
|
||||
* 正则表达式必须包含命名捕获组 KEY,用于提取分享键
|
||||
* 可选包含命名捕获组 PWD,用于提取分享密码
|
||||
*/
|
||||
private final Pattern matchPattern;
|
||||
|
||||
private CustomParserConfig(Builder builder) {
|
||||
this.type = builder.type;
|
||||
this.displayName = builder.displayName;
|
||||
this.toolClass = builder.toolClass;
|
||||
this.standardUrlTemplate = builder.standardUrlTemplate;
|
||||
this.panDomain = builder.panDomain;
|
||||
this.matchPattern = builder.matchPattern;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public Class<? extends IPanTool> getToolClass() {
|
||||
return toolClass;
|
||||
}
|
||||
|
||||
public String getStandardUrlTemplate() {
|
||||
return standardUrlTemplate;
|
||||
}
|
||||
|
||||
public String getPanDomain() {
|
||||
return panDomain;
|
||||
}
|
||||
|
||||
public Pattern getMatchPattern() {
|
||||
return matchPattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持从分享链接自动识别
|
||||
* @return true表示支持,false表示不支持
|
||||
*/
|
||||
public boolean supportsFromShareUrl() {
|
||||
return matchPattern != null;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 建造者类
|
||||
*/
|
||||
public static class Builder {
|
||||
private String type;
|
||||
private String displayName;
|
||||
private Class<? extends IPanTool> toolClass;
|
||||
private String standardUrlTemplate;
|
||||
private String panDomain;
|
||||
private Pattern matchPattern;
|
||||
|
||||
/**
|
||||
* 设置解析器类型标识(必填,唯一)
|
||||
* @param type 类型标识(建议使用小写英文)
|
||||
*/
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网盘显示名称(必填)
|
||||
* @param displayName 显示名称
|
||||
*/
|
||||
public Builder displayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置解析工具实现类(必填)
|
||||
* @param toolClass 工具类(必须实现 IPanTool 接口)
|
||||
*/
|
||||
public Builder toolClass(Class<? extends IPanTool> toolClass) {
|
||||
this.toolClass = toolClass;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标准URL模板(可选)
|
||||
* @param standardUrlTemplate URL模板
|
||||
*/
|
||||
public Builder standardUrlTemplate(String standardUrlTemplate) {
|
||||
this.standardUrlTemplate = standardUrlTemplate;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置网盘域名(可选)
|
||||
* @param panDomain 网盘域名
|
||||
*/
|
||||
public Builder panDomain(String panDomain) {
|
||||
this.panDomain = panDomain;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置匹配正则表达式(可选)
|
||||
* @param pattern 正则表达式Pattern对象
|
||||
*/
|
||||
public Builder matchPattern(Pattern pattern) {
|
||||
this.matchPattern = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置匹配正则表达式(可选)
|
||||
* @param regex 正则表达式字符串
|
||||
*/
|
||||
public Builder matchPattern(String regex) {
|
||||
if (regex != null && !regex.trim().isEmpty()) {
|
||||
this.matchPattern = Pattern.compile(regex);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建配置对象
|
||||
* @return CustomParserConfig
|
||||
*/
|
||||
public CustomParserConfig build() {
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("type不能为空");
|
||||
}
|
||||
if (displayName == null || displayName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("displayName不能为空");
|
||||
}
|
||||
if (toolClass == null) {
|
||||
throw new IllegalArgumentException("toolClass不能为空");
|
||||
}
|
||||
|
||||
// 验证toolClass是否实现了IPanTool接口
|
||||
if (!IPanTool.class.isAssignableFrom(toolClass)) {
|
||||
throw new IllegalArgumentException("toolClass必须实现IPanTool接口");
|
||||
}
|
||||
|
||||
// 验证toolClass是否有ShareLinkInfo单参构造器
|
||||
try {
|
||||
toolClass.getDeclaredConstructor(ShareLinkInfo.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException("toolClass必须有ShareLinkInfo单参构造器", e);
|
||||
}
|
||||
|
||||
// 验证正则表达式(如果提供)
|
||||
if (matchPattern != null) {
|
||||
// 检查正则表达式是否包含KEY命名捕获组
|
||||
String patternStr = matchPattern.pattern();
|
||||
if (!patternStr.contains("(?<KEY>")) {
|
||||
throw new IllegalArgumentException("正则表达式必须包含命名捕获组 KEY,用于提取分享键");
|
||||
}
|
||||
}
|
||||
|
||||
return new CustomParserConfig(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CustomParserConfig{" +
|
||||
"type='" + type + '\'' +
|
||||
", displayName='" + displayName + '\'' +
|
||||
", toolClass=" + toolClass.getName() +
|
||||
", standardUrlTemplate='" + standardUrlTemplate + '\'' +
|
||||
", panDomain='" + panDomain + '\'' +
|
||||
", matchPattern=" + (matchPattern != null ? matchPattern.pattern() : "null") +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
120
parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java
Normal file
120
parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 自定义解析器注册中心
|
||||
* 用户可以通过此类注册自己的解析器实现
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class CustomParserRegistry {
|
||||
|
||||
/**
|
||||
* 存储自定义解析器配置的Map,key为类型标识,value为配置对象
|
||||
*/
|
||||
private static final Map<String, CustomParserConfig> CUSTOM_PARSERS = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 注册自定义解析器
|
||||
*
|
||||
* @param config 解析器配置
|
||||
* @throws IllegalArgumentException 如果type已存在或与内置解析器冲突
|
||||
*/
|
||||
public static void register(CustomParserConfig config) {
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("config不能为空");
|
||||
}
|
||||
|
||||
String type = config.getType().toLowerCase();
|
||||
|
||||
// 检查是否与内置枚举冲突
|
||||
try {
|
||||
PanDomainTemplate.valueOf(type.toUpperCase());
|
||||
throw new IllegalArgumentException(
|
||||
"类型标识 '" + type + "' 与内置解析器冲突,请使用其他标识"
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 如果valueOf抛出异常,说明不存在该枚举,这是正常情况
|
||||
if (e.getMessage().startsWith("类型标识")) {
|
||||
throw e; // 重新抛出我们自己的异常
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已注册
|
||||
if (CUSTOM_PARSERS.containsKey(type)) {
|
||||
throw new IllegalArgumentException(
|
||||
"类型标识 '" + type + "' 已被注册,请先注销或使用其他标识"
|
||||
);
|
||||
}
|
||||
|
||||
CUSTOM_PARSERS.put(type, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销自定义解析器
|
||||
*
|
||||
* @param type 解析器类型标识
|
||||
* @return 是否注销成功
|
||||
*/
|
||||
public static boolean unregister(String type) {
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return CUSTOM_PARSERS.remove(type.toLowerCase()) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型获取自定义解析器配置
|
||||
*
|
||||
* @param type 解析器类型标识
|
||||
* @return 解析器配置,如果不存在则返回null
|
||||
*/
|
||||
public static CustomParserConfig get(String type) {
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return CUSTOM_PARSERS.get(type.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指定类型的解析器是否已注册
|
||||
*
|
||||
* @param type 解析器类型标识
|
||||
* @return 是否已注册
|
||||
*/
|
||||
public static boolean contains(String type) {
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return CUSTOM_PARSERS.containsKey(type.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有自定义解析器
|
||||
*/
|
||||
public static void clear() {
|
||||
CUSTOM_PARSERS.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的自定义解析器数量
|
||||
*
|
||||
* @return 数量
|
||||
*/
|
||||
public static int size() {
|
||||
return CUSTOM_PARSERS.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的自定义解析器配置(只读视图)
|
||||
*
|
||||
* @return 不可修改的Map
|
||||
*/
|
||||
public static Map<String, CustomParserConfig> getAll() {
|
||||
return Map.copyOf(CUSTOM_PARSERS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,19 +16,38 @@ import static cn.qaiu.parser.PanDomainTemplate.PWD;
|
||||
* 通过这种方式,应用程序可以更容易地处理和识别不同网盘服务的分享链接。
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2024/9/15 14:10
|
||||
* Create at 2024/9/15 14:10
|
||||
*/
|
||||
public class ParserCreate {
|
||||
private final PanDomainTemplate panDomainTemplate;
|
||||
private final ShareLinkInfo shareLinkInfo;
|
||||
|
||||
// 自定义解析器配置(与 panDomainTemplate 二选一)
|
||||
private final CustomParserConfig customParserConfig;
|
||||
|
||||
private String standardUrl;
|
||||
|
||||
// 标识是否为自定义解析器
|
||||
private final boolean isCustomParser;
|
||||
|
||||
public ParserCreate(PanDomainTemplate panDomainTemplate, ShareLinkInfo shareLinkInfo) {
|
||||
this.panDomainTemplate = panDomainTemplate;
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
this.customParserConfig = null;
|
||||
this.isCustomParser = false;
|
||||
this.standardUrl = panDomainTemplate.getStandardUrlTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义解析器专用构造器
|
||||
*/
|
||||
private ParserCreate(CustomParserConfig customParserConfig, ShareLinkInfo shareLinkInfo) {
|
||||
this.customParserConfig = customParserConfig;
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
this.panDomainTemplate = null;
|
||||
this.isCustomParser = true;
|
||||
this.standardUrl = customParserConfig.getStandardUrlTemplate();
|
||||
}
|
||||
|
||||
|
||||
// 解析并规范化分享链接
|
||||
@@ -36,6 +55,60 @@ public class ParserCreate {
|
||||
if (shareLinkInfo == null) {
|
||||
throw new IllegalArgumentException("ShareLinkInfo not init");
|
||||
}
|
||||
|
||||
// 自定义解析器处理
|
||||
if (isCustomParser) {
|
||||
if (!customParserConfig.supportsFromShareUrl()) {
|
||||
throw new UnsupportedOperationException(
|
||||
"自定义解析器不支持 normalizeShareLink 方法,请使用 shareKey 方法设置分享键");
|
||||
}
|
||||
|
||||
// 使用自定义解析器的正则表达式进行匹配
|
||||
String shareUrl = shareLinkInfo.getShareUrl();
|
||||
if (StringUtils.isEmpty(shareUrl)) {
|
||||
throw new IllegalArgumentException("ShareLinkInfo shareUrl is empty");
|
||||
}
|
||||
|
||||
java.util.regex.Matcher matcher = customParserConfig.getMatchPattern().matcher(shareUrl);
|
||||
if (matcher.matches()) {
|
||||
// 提取分享键
|
||||
try {
|
||||
String shareKey = matcher.group("KEY");
|
||||
if (shareKey != null) {
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 提取密码
|
||||
try {
|
||||
String pwd = matcher.group("PWD");
|
||||
if (StringUtils.isNotEmpty(pwd)) {
|
||||
shareLinkInfo.setSharePassword(pwd);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 设置标准URL
|
||||
if (customParserConfig.getStandardUrlTemplate() != null) {
|
||||
String standardUrl = customParserConfig.getStandardUrlTemplate()
|
||||
.replace("{shareKey}", shareLinkInfo.getShareKey() != null ? shareLinkInfo.getShareKey() : "");
|
||||
|
||||
// 处理密码替换
|
||||
if (shareLinkInfo.getSharePassword() != null && !shareLinkInfo.getSharePassword().isEmpty()) {
|
||||
standardUrl = standardUrl.replace("{pwd}", shareLinkInfo.getSharePassword());
|
||||
} else {
|
||||
// 如果密码为空,移除包含 {pwd} 的部分
|
||||
standardUrl = standardUrl.replaceAll("\\?pwd=\\{pwd\\}", "").replaceAll("&pwd=\\{pwd\\}", "");
|
||||
}
|
||||
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid share URL for " + customParserConfig.getDisplayName());
|
||||
}
|
||||
|
||||
// 内置解析器处理
|
||||
// 匹配并提取shareKey
|
||||
String shareUrl = shareLinkInfo.getShareUrl();
|
||||
if (StringUtils.isEmpty(shareUrl)) {
|
||||
@@ -72,6 +145,20 @@ public class ParserCreate {
|
||||
if (shareLinkInfo == null || StringUtils.isEmpty(shareLinkInfo.getType())) {
|
||||
throw new IllegalArgumentException("ShareLinkInfo not init or type is empty");
|
||||
}
|
||||
|
||||
// 自定义解析器处理
|
||||
if (isCustomParser) {
|
||||
try {
|
||||
return this.customParserConfig.getToolClass()
|
||||
.getDeclaredConstructor(ShareLinkInfo.class)
|
||||
.newInstance(shareLinkInfo);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("无法创建自定义工具实例: " +
|
||||
customParserConfig.getToolClass().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 内置解析器处理
|
||||
if (StringUtils.isEmpty(shareLinkInfo.getShareKey())) {
|
||||
this.normalizeShareLink();
|
||||
}
|
||||
@@ -86,6 +173,20 @@ public class ParserCreate {
|
||||
|
||||
// set share key
|
||||
public ParserCreate shareKey(String shareKey) {
|
||||
// 自定义解析器处理
|
||||
if (isCustomParser) {
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
if (standardUrl != null) {
|
||||
standardUrl = standardUrl.replace("{shareKey}", shareKey);
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
}
|
||||
if (StringUtils.isEmpty(shareLinkInfo.getShareUrl())) {
|
||||
shareLinkInfo.setShareUrl(standardUrl != null ? standardUrl : shareKey);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// 内置解析器处理
|
||||
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
|
||||
// 处理Cloudreve(ce)类: pan.huang1111.cn_s_wDz5TK _ -> /
|
||||
String[] s = shareKey.split("_");
|
||||
@@ -112,6 +213,9 @@ public class ParserCreate {
|
||||
}
|
||||
|
||||
public String getStandardUrlTemplate() {
|
||||
if (isCustomParser) {
|
||||
return this.customParserConfig.getStandardUrlTemplate();
|
||||
}
|
||||
return this.panDomainTemplate.getStandardUrlTemplate();
|
||||
}
|
||||
|
||||
@@ -131,8 +235,56 @@ public class ParserCreate {
|
||||
return this;
|
||||
}
|
||||
|
||||
// 根据分享链接获取PanDomainTemplate实例
|
||||
// 根据分享链接获取PanDomainTemplate实例(优先匹配自定义解析器)
|
||||
public synchronized static ParserCreate fromShareUrl(String shareUrl) {
|
||||
// 优先查找支持正则匹配的自定义解析器
|
||||
for (CustomParserConfig customConfig : CustomParserRegistry.getAll().values()) {
|
||||
if (customConfig.supportsFromShareUrl()) {
|
||||
java.util.regex.Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl);
|
||||
if (matcher.matches()) {
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type(customConfig.getType())
|
||||
.panName(customConfig.getDisplayName())
|
||||
.shareUrl(shareUrl)
|
||||
.build();
|
||||
|
||||
// 提取分享键和密码
|
||||
try {
|
||||
String shareKey = matcher.group("KEY");
|
||||
if (shareKey != null) {
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
try {
|
||||
String password = matcher.group("PWD");
|
||||
if (password != null) {
|
||||
shareLinkInfo.setSharePassword(password);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 设置标准URL(如果有模板)
|
||||
if (customConfig.getStandardUrlTemplate() != null) {
|
||||
String standardUrl = customConfig.getStandardUrlTemplate()
|
||||
.replace("{shareKey}", shareLinkInfo.getShareKey() != null ? shareLinkInfo.getShareKey() : "");
|
||||
|
||||
// 处理密码替换
|
||||
if (shareLinkInfo.getSharePassword() != null && !shareLinkInfo.getSharePassword().isEmpty()) {
|
||||
standardUrl = standardUrl.replace("{pwd}", shareLinkInfo.getSharePassword());
|
||||
} else {
|
||||
// 如果密码为空,移除包含 {pwd} 的部分
|
||||
standardUrl = standardUrl.replaceAll("\\?pwd=\\{pwd\\}", "").replaceAll("&pwd=\\{pwd\\}", "");
|
||||
}
|
||||
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
}
|
||||
|
||||
return new ParserCreate(customConfig, shareLinkInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找内置解析器
|
||||
for (PanDomainTemplate panDomainTemplate : PanDomainTemplate.values()) {
|
||||
if (panDomainTemplate.getPattern().matcher(shareUrl).matches()) {
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
@@ -149,25 +301,47 @@ public class ParserCreate {
|
||||
throw new IllegalArgumentException("Unsupported share URL");
|
||||
}
|
||||
|
||||
// 根据type获取枚举实例
|
||||
// 根据type获取枚举实例(优先查找自定义解析器)
|
||||
public synchronized static ParserCreate fromType(String type) {
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("type不能为空");
|
||||
}
|
||||
|
||||
String normalizedType = type.toLowerCase();
|
||||
|
||||
// 优先查找自定义解析器
|
||||
CustomParserConfig customConfig = CustomParserRegistry.get(normalizedType);
|
||||
if (customConfig != null) {
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type(normalizedType)
|
||||
.panName(customConfig.getDisplayName())
|
||||
.build();
|
||||
return new ParserCreate(customConfig, shareLinkInfo);
|
||||
}
|
||||
|
||||
// 查找内置解析器
|
||||
try {
|
||||
PanDomainTemplate panDomainTemplate = Enum.valueOf(PanDomainTemplate.class, type.toUpperCase());
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type(type.toLowerCase()).build();
|
||||
shareLinkInfo.setPanName(panDomainTemplate.getDisplayName());
|
||||
.type(normalizedType)
|
||||
.panName(panDomainTemplate.getDisplayName())
|
||||
.build();
|
||||
return new ParserCreate(panDomainTemplate, shareLinkInfo);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
// 如果没有找到对应的枚举实例,抛出异常
|
||||
throw new IllegalArgumentException("No enum constant for type name: " + type);
|
||||
// 如果没有找到对应的解析器,抛出异常
|
||||
throw new IllegalArgumentException("未找到类型为 '" + type + "' 的解析器," +
|
||||
"请检查是否已注册自定义解析器或使用正确的内置类型");
|
||||
}
|
||||
}
|
||||
|
||||
// 生成parser短链path(不包含domainName)
|
||||
public String genPathSuffix() {
|
||||
|
||||
String path;
|
||||
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
|
||||
|
||||
// 自定义解析器处理
|
||||
if (isCustomParser) {
|
||||
path = this.shareLinkInfo.getType() + "/" + this.shareLinkInfo.getShareKey();
|
||||
} else if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
|
||||
// 处理Cloudreve(ce)类: pan.huang1111.cn_s_wDz5TK _ -> /
|
||||
path = this.shareLinkInfo.getType() + "/"
|
||||
+ this.shareLinkInfo.getShareUrl()
|
||||
@@ -175,8 +349,33 @@ public class ParserCreate {
|
||||
} else {
|
||||
path = this.shareLinkInfo.getType() + "/" + this.shareLinkInfo.getShareKey();
|
||||
}
|
||||
|
||||
String sharePassword = this.shareLinkInfo.getSharePassword();
|
||||
return path + (StringUtils.isBlank(sharePassword) ? "" : ("@" + sharePassword));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否为自定义解析器
|
||||
* @return true表示自定义解析器,false表示内置解析器
|
||||
*/
|
||||
public boolean isCustomParser() {
|
||||
return isCustomParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义解析器配置(仅当isCustomParser为true时有效)
|
||||
* @return 自定义解析器配置,如果不是自定义解析器则返回null
|
||||
*/
|
||||
public CustomParserConfig getCustomParserConfig() {
|
||||
return customParserConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内置解析器模板(仅当isCustomParser为false时有效)
|
||||
* @return 内置解析器模板,如果是自定义解析器则返回null
|
||||
*/
|
||||
public PanDomainTemplate getPanDomainTemplate() {
|
||||
return panDomainTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
* 奶牛快传解析工具
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/4/21 21:19
|
||||
* Create at 2023/4/21 21:19
|
||||
*/
|
||||
public class CowTool extends PanBase {
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@ import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.ext.web.client.WebClientSession;
|
||||
import org.apache.commons.lang3.RegExUtils;
|
||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||
|
||||
import javax.script.ScriptException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -28,10 +30,9 @@ import java.util.regex.Pattern;
|
||||
public class LzTool extends PanBase {
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoum.com";
|
||||
|
||||
MultiMap headers0 = HeaderUtils.parseHeaders("""
|
||||
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
|
||||
Cache-Control: max-age=0
|
||||
Cookie: codelen=1; pc_ad1=1
|
||||
@@ -48,6 +49,7 @@ public class LzTool extends PanBase {
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0
|
||||
""");
|
||||
|
||||
|
||||
public LzTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
@@ -57,42 +59,49 @@ public class LzTool extends PanBase {
|
||||
String pwd = shareLinkInfo.getSharePassword();
|
||||
|
||||
WebClient client = clientNoRedirects;
|
||||
client.getAbs(sUrl).putHeaders(headers0).send().onSuccess(res -> {
|
||||
String html = res.bodyAsString();
|
||||
// 匹配iframe
|
||||
Pattern compile = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\"");
|
||||
Matcher matcher = compile.matcher(html);
|
||||
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
|
||||
if (!matcher.find()) {
|
||||
try {
|
||||
String jsText = getJsByPwd(pwd, html, "document.getElementById('rpt')");
|
||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
|
||||
getDownURL(sUrl, client, scriptObjectMirror);
|
||||
} catch (Exception e) {
|
||||
fail(e, "js引擎执行失败");
|
||||
}
|
||||
} else {
|
||||
// 没有密码
|
||||
String iframePath = matcher.group(1);
|
||||
client.getAbs(SHARE_URL_PREFIX + iframePath).send().onSuccess(res2 -> {
|
||||
String html2 = res2.bodyAsString();
|
||||
|
||||
// 去TMD正则
|
||||
// Matcher matcher2 = Pattern.compile("'sign'\s*:\s*'(\\w+)'").matcher(html2);
|
||||
String jsText = getJsText(html2);
|
||||
if (jsText == null) {
|
||||
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": js脚本匹配失败, 可能分享已失效");
|
||||
return;
|
||||
}
|
||||
client.getAbs(sUrl)
|
||||
.putHeaders(headers0)
|
||||
.send().onSuccess(res -> {
|
||||
String html = asText(res);
|
||||
try {
|
||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
|
||||
getDownURL(sUrl, client, scriptObjectMirror);
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
fail(e, "js引擎执行失败");
|
||||
setFileInfo(html, shareLinkInfo);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).onFailure(handleFail(SHARE_URL_PREFIX));
|
||||
}
|
||||
}).onFailure(handleFail(sUrl));
|
||||
// 匹配iframe
|
||||
Pattern compile = Pattern.compile("src=\"(/fn\\?[a-zA-Z\\d_+/=]{16,})\"");
|
||||
Matcher matcher = compile.matcher(html);
|
||||
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
|
||||
if (!matcher.find()) {
|
||||
try {
|
||||
String jsText = getJsByPwd(pwd, html, "document.getElementById('rpt')");
|
||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
|
||||
getDownURL(sUrl, client, scriptObjectMirror);
|
||||
} catch (Exception e) {
|
||||
fail(e, "js引擎执行失败");
|
||||
}
|
||||
} else {
|
||||
// 没有密码
|
||||
String iframePath = matcher.group(1);
|
||||
client.getAbs(SHARE_URL_PREFIX + iframePath).send().onSuccess(res2 -> {
|
||||
String html2 = res2.bodyAsString();
|
||||
|
||||
// 去TMD正则
|
||||
// Matcher matcher2 = Pattern.compile("'sign'\s*:\s*'(\\w+)'").matcher(html2);
|
||||
String jsText = getJsText(html2);
|
||||
if (jsText == null) {
|
||||
fail(SHARE_URL_PREFIX + iframePath + " -> " + sUrl + ": js脚本匹配失败, 可能分享已失效");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
|
||||
getDownURL(sUrl, client, scriptObjectMirror);
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
fail(e, "js引擎执行失败");
|
||||
}
|
||||
}).onFailure(handleFail(SHARE_URL_PREFIX));
|
||||
}
|
||||
}).onFailure(handleFail(sUrl));
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@@ -163,7 +172,10 @@ public class LzTool extends PanBase {
|
||||
return;
|
||||
}
|
||||
// 文件名
|
||||
((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setFileName(name);
|
||||
if (urlJson.containsKey("inf") && urlJson.getMap().get("inf") instanceof Character) {
|
||||
((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setFileName(name);
|
||||
}
|
||||
|
||||
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
|
||||
headers.remove("Referer");
|
||||
WebClientSession webClientSession = WebClientSession.create(client);
|
||||
@@ -186,17 +198,16 @@ public class LzTool extends PanBase {
|
||||
webClientSession.cookieStore().put(nettyCookie);
|
||||
webClientSession.getAbs(downUrl).putHeaders(headers).send()
|
||||
.onSuccess(res4 -> {
|
||||
String location0 = res4.headers().get("Location");
|
||||
if (location0 == null) {
|
||||
fail(downUrl + " -> 直链获取失败, 可能分享已失效");
|
||||
} else {
|
||||
promise.complete(location0);
|
||||
}
|
||||
}).onFailure(handleFail(downUrl));
|
||||
String location0 = res4.headers().get("Location");
|
||||
if (location0 == null) {
|
||||
fail(downUrl + " -> 直链获取失败, 可能分享已失效");
|
||||
} else {
|
||||
setDateAndComplate(location0);
|
||||
}
|
||||
}).onFailure(handleFail(downUrl));
|
||||
return;
|
||||
}
|
||||
|
||||
promise.complete(location);
|
||||
setDateAndComplate(location);
|
||||
})
|
||||
.onFailure(handleFail(downUrl));
|
||||
} catch (Exception e) {
|
||||
@@ -205,6 +216,17 @@ public class LzTool extends PanBase {
|
||||
}).onFailure(handleFail(url));
|
||||
}
|
||||
|
||||
private void setDateAndComplate(String location0) {
|
||||
// 分享时间 提取url中的时间戳格式:lanzoui.com/abc/abc/yyyy/mm/dd/
|
||||
String regex = "(\\d{4}/\\d{1,2}/\\d{1,2})";
|
||||
Matcher matcher = Pattern.compile(regex).matcher(location0);
|
||||
if (matcher.find()) {
|
||||
String dateStr = matcher.group().replace("/", "-");
|
||||
((FileInfo)shareLinkInfo.getOtherParam().get("fileInfo")).setCreateTime(dateStr);
|
||||
}
|
||||
promise.complete(location0);
|
||||
}
|
||||
|
||||
private static MultiMap getHeaders(String key) {
|
||||
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||
var userAgent2 = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, " +
|
||||
@@ -286,4 +308,36 @@ public class LzTool extends PanBase {
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
void setFileInfo(String html, ShareLinkInfo shareLinkInfo) {
|
||||
// 写入 fileInfo
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
||||
try {
|
||||
// 提取文件名
|
||||
String fileName = CommonUtils.extract(html, Pattern.compile("padding: 56px 0px 20px 0px;\">(.*?)<|filenajax\">(.*?)<"));
|
||||
String sizeStr = CommonUtils.extract(html, Pattern.compile(">文件大小:</span>(.*?)<br>|\"n_filesize\">大小:(.*?)</div>"));
|
||||
String createBy = CommonUtils.extract(html, Pattern.compile(">分享用户:</span><font>(.*?)</font>|获取<span>(.*?)</span>的文件|\"user-name\">(.*?)</"));
|
||||
String description = CommonUtils.extract(html, Pattern.compile("(?s)文件描述:</span><br>(.*?)</td>|class=\"n_box_des\">(.*?)</div>"));
|
||||
// String icon = CommonUtils.extract(html, Pattern.compile("class=\"n_file_icon\" src=\"(.*?)\""));
|
||||
String fileId = CommonUtils.extract(html, Pattern.compile("\\?f=(.*?)&|fid = (.*?);"));
|
||||
String createTime = CommonUtils.extract(html, Pattern.compile(">上传时间:</span>(.*?)<"));
|
||||
try {
|
||||
long bytes = FileSizeConverter.convertToBytes(sizeStr);
|
||||
fileInfo.setFileName(fileName)
|
||||
.setSize(bytes)
|
||||
.setSizeStr(FileSizeConverter.convertToReadableSize(bytes))
|
||||
.setCreateBy(createBy)
|
||||
.setPanType(shareLinkInfo.getType())
|
||||
.setDescription(description)
|
||||
.setFileType("file")
|
||||
.setFileId(fileId)
|
||||
.setCreateTime(createTime);
|
||||
} catch (Exception e) {
|
||||
log.warn("文件信息解析异常", e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("文件信息匹配异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CommonUtils {
|
||||
|
||||
@@ -44,4 +46,29 @@ public class CommonUtils {
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取第一个匹配的非空捕捉组
|
||||
* @param matcher 已创建的 Matcher
|
||||
* @return 第一个非空 group,或 "" 如果没有
|
||||
*/
|
||||
public static String firstNonEmptyGroup(Matcher matcher) {
|
||||
if (!matcher.find()) {
|
||||
return "";
|
||||
}
|
||||
for (int i = 1; i <= matcher.groupCount(); i++) {
|
||||
String g = matcher.group(i);
|
||||
if (g != null && !g.trim().isEmpty()) {
|
||||
return g.trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接传 html 和 regex,返回第一个非空捕捉组
|
||||
*/
|
||||
public static String extract(String input, Pattern pattern) {
|
||||
Matcher matcher = pattern.matcher(input);
|
||||
return firstNonEmptyGroup(matcher);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import static cn.qaiu.util.AESUtils.encrypt;
|
||||
* 执行Js脚本
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/7/29 17:35
|
||||
* Create at 2023/7/29 17:35
|
||||
*/
|
||||
public class JsExecUtils {
|
||||
private static final Invocable inv;
|
||||
|
||||
@@ -2,7 +2,7 @@ package cn.qaiu.util;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/7/16 1:53
|
||||
* Create at 2023/7/16 1:53
|
||||
*/
|
||||
public class PanExceptionUtils {
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2024/5/13 4:10
|
||||
* Create at 2024/5/13 4:10
|
||||
*/
|
||||
public class UUIDUtil {
|
||||
|
||||
|
||||
410
parser/src/test/java/cn/qaiu/parser/CustomParserTest.java
Normal file
410
parser/src/test/java/cn/qaiu/parser/CustomParserTest.java
Normal file
@@ -0,0 +1,410 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* 自定义解析器功能测试
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class CustomParserTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// 清空注册表,确保测试独立性
|
||||
CustomParserRegistry.clear();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
// 测试后清理
|
||||
CustomParserRegistry.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegisterCustomParser() {
|
||||
// 创建配置
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.standardUrlTemplate("https://testpan.com/s/{shareKey}")
|
||||
.panDomain("https://testpan.com")
|
||||
.build();
|
||||
|
||||
// 注册
|
||||
CustomParserRegistry.register(config);
|
||||
|
||||
// 验证
|
||||
assertTrue(CustomParserRegistry.contains("testpan"));
|
||||
assertEquals(1, CustomParserRegistry.size());
|
||||
|
||||
CustomParserConfig retrieved = CustomParserRegistry.get("testpan");
|
||||
assertNotNull(retrieved);
|
||||
assertEquals("testpan", retrieved.getType());
|
||||
assertEquals("测试网盘", retrieved.getDisplayName());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testRegisterDuplicateType() {
|
||||
CustomParserConfig config1 = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘1")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
|
||||
CustomParserConfig config2 = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘2")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
|
||||
// 第一次注册成功
|
||||
CustomParserRegistry.register(config1);
|
||||
|
||||
// 第二次注册应该失败,期望抛出 IllegalArgumentException
|
||||
CustomParserRegistry.register(config2);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testRegisterConflictWithBuiltIn() {
|
||||
// 尝试注册与内置类型冲突的解析器
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("lz") // 蓝奏云的类型
|
||||
.displayName("假蓝奏云")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
|
||||
// 应该抛出异常,期望抛出 IllegalArgumentException
|
||||
CustomParserRegistry.register(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnregisterParser() {
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
|
||||
CustomParserRegistry.register(config);
|
||||
assertTrue(CustomParserRegistry.contains("testpan"));
|
||||
|
||||
// 注销
|
||||
boolean result = CustomParserRegistry.unregister("testpan");
|
||||
assertTrue(result);
|
||||
assertFalse(CustomParserRegistry.contains("testpan"));
|
||||
assertEquals(0, CustomParserRegistry.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateToolFromCustomParser() {
|
||||
// 注册自定义解析器
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.standardUrlTemplate("https://testpan.com/s/{shareKey}")
|
||||
.build();
|
||||
CustomParserRegistry.register(config);
|
||||
|
||||
// 通过 fromType 创建
|
||||
ParserCreate parser = ParserCreate.fromType("testpan")
|
||||
.shareKey("abc123")
|
||||
.setShareLinkInfoPwd("1234");
|
||||
|
||||
// 验证是自定义解析器
|
||||
assertTrue(parser.isCustomParser());
|
||||
assertNotNull(parser.getCustomParserConfig());
|
||||
assertNull(parser.getPanDomainTemplate());
|
||||
|
||||
// 创建工具
|
||||
IPanTool tool = parser.createTool();
|
||||
assertNotNull(tool);
|
||||
assertTrue(tool instanceof TestPanTool);
|
||||
|
||||
// 验证解析
|
||||
String url = tool.parseSync();
|
||||
assertTrue(url.contains("abc123"));
|
||||
assertTrue(url.contains("1234"));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testCustomParserNotSupportFromShareUrl() {
|
||||
// 注册自定义解析器(不提供正则表达式)
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
CustomParserRegistry.register(config);
|
||||
|
||||
// fromShareUrl 不应该识别自定义解析器,期望抛出 IllegalArgumentException
|
||||
// 使用一个不会被任何内置解析器匹配的URL(不符合域名格式)
|
||||
ParserCreate.fromShareUrl("not-a-valid-url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomParserWithRegexSupportFromShareUrl() {
|
||||
// 注册支持正则匹配的自定义解析器
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.standardUrlTemplate("https://testpan.com/s/{shareKey}")
|
||||
.matchPattern("https://testpan\\.com/s/(?<KEY>[^?]+)(\\?pwd=(?<PWD>.+))?")
|
||||
.build();
|
||||
CustomParserRegistry.register(config);
|
||||
|
||||
// 测试 fromShareUrl 识别自定义解析器
|
||||
ParserCreate parser = ParserCreate.fromShareUrl("https://testpan.com/s/abc123?pwd=pass456");
|
||||
|
||||
// 验证是自定义解析器
|
||||
assertTrue(parser.isCustomParser());
|
||||
assertEquals("testpan", parser.getShareLinkInfo().getType());
|
||||
assertEquals("测试网盘", parser.getShareLinkInfo().getPanName());
|
||||
assertEquals("abc123", parser.getShareLinkInfo().getShareKey());
|
||||
assertEquals("pass456", parser.getShareLinkInfo().getSharePassword());
|
||||
assertEquals("https://testpan.com/s/abc123", parser.getShareLinkInfo().getStandardUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomParserSupportsFromShareUrl() {
|
||||
// 测试 supportsFromShareUrl 方法
|
||||
CustomParserConfig config1 = CustomParserConfig.builder()
|
||||
.type("test1")
|
||||
.displayName("测试1")
|
||||
.toolClass(TestPanTool.class)
|
||||
.matchPattern("https://test1\\.com/s/(?<KEY>.+)")
|
||||
.build();
|
||||
assertTrue(config1.supportsFromShareUrl());
|
||||
|
||||
CustomParserConfig config2 = CustomParserConfig.builder()
|
||||
.type("test2")
|
||||
.displayName("测试2")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
assertFalse(config2.supportsFromShareUrl());
|
||||
}
|
||||
|
||||
@Test(expected = UnsupportedOperationException.class)
|
||||
public void testCustomParserNotSupportNormalizeShareLink() {
|
||||
// 注册不支持正则匹配的自定义解析器
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
CustomParserRegistry.register(config);
|
||||
|
||||
ParserCreate parser = ParserCreate.fromType("testpan");
|
||||
|
||||
// 不支持正则匹配的自定义解析器不支持 normalizeShareLink,期望抛出 UnsupportedOperationException
|
||||
parser.normalizeShareLink();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomParserWithRegexSupportNormalizeShareLink() {
|
||||
// 注册支持正则匹配的自定义解析器
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.standardUrlTemplate("https://testpan.com/s/{shareKey}")
|
||||
.matchPattern("https://testpan\\.com/s/(?<KEY>[^?]+)(\\?pwd=(?<PWD>.+))?")
|
||||
.build();
|
||||
CustomParserRegistry.register(config);
|
||||
|
||||
// 通过 fromType 创建,然后设置分享URL
|
||||
ParserCreate parser = ParserCreate.fromType("testpan")
|
||||
.shareKey("abc123")
|
||||
.setShareLinkInfoPwd("pass456");
|
||||
|
||||
// 设置分享URL
|
||||
parser.getShareLinkInfo().setShareUrl("https://testpan.com/s/abc123?pwd=pass456");
|
||||
|
||||
// 支持正则匹配的自定义解析器支持 normalizeShareLink
|
||||
ParserCreate result = parser.normalizeShareLink();
|
||||
|
||||
// 验证结果
|
||||
assertTrue(result.isCustomParser());
|
||||
assertEquals("abc123", result.getShareLinkInfo().getShareKey());
|
||||
assertEquals("pass456", result.getShareLinkInfo().getSharePassword());
|
||||
assertEquals("https://testpan.com/s/abc123", result.getShareLinkInfo().getStandardUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenPathSuffix() {
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("testpan")
|
||||
.displayName("测试网盘")
|
||||
.toolClass(TestPanTool.class)
|
||||
.standardUrlTemplate("https://testpan.com/s/{shareKey}") // 添加URL模板
|
||||
.build();
|
||||
CustomParserRegistry.register(config);
|
||||
|
||||
ParserCreate parser = ParserCreate.fromType("testpan")
|
||||
.shareKey("abc123")
|
||||
.setShareLinkInfoPwd("pass123");
|
||||
|
||||
String pathSuffix = parser.genPathSuffix();
|
||||
assertEquals("testpan/abc123@pass123", pathSuffix);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAll() {
|
||||
CustomParserConfig config1 = CustomParserConfig.builder()
|
||||
.type("testpan1")
|
||||
.displayName("测试网盘1")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
|
||||
CustomParserConfig config2 = CustomParserConfig.builder()
|
||||
.type("testpan2")
|
||||
.displayName("测试网盘2")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
|
||||
CustomParserRegistry.register(config1);
|
||||
CustomParserRegistry.register(config2);
|
||||
|
||||
var allParsers = CustomParserRegistry.getAll();
|
||||
assertEquals(2, allParsers.size());
|
||||
assertTrue(allParsers.containsKey("testpan1"));
|
||||
assertTrue(allParsers.containsKey("testpan2"));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConfigBuilderValidationMissingType() {
|
||||
// 测试缺少 type,期望抛出 IllegalArgumentException
|
||||
CustomParserConfig.builder()
|
||||
.displayName("测试")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConfigBuilderValidationMissingDisplayName() {
|
||||
// 测试缺少 displayName,期望抛出 IllegalArgumentException
|
||||
CustomParserConfig.builder()
|
||||
.type("test")
|
||||
.toolClass(TestPanTool.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testConfigBuilderValidationMissingToolClass() {
|
||||
// 测试缺少 toolClass,期望抛出 IllegalArgumentException
|
||||
CustomParserConfig.builder()
|
||||
.type("test")
|
||||
.displayName("测试")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testConfigBuilderToolClassValidation() {
|
||||
// 测试工具类没有实现 IPanTool 接口,期望抛出 IllegalArgumentException
|
||||
// 使用类型转换绕过编译器检查,测试运行时验证
|
||||
Class<? extends IPanTool> invalidClass = (Class<? extends IPanTool>) (Class<?>) InvalidTool.class;
|
||||
CustomParserConfig.builder()
|
||||
.type("test")
|
||||
.displayName("测试")
|
||||
.toolClass(invalidClass)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigBuilderRegexValidationMissingKey() {
|
||||
// 测试正则表达式缺少KEY命名捕获组,期望抛出 IllegalArgumentException
|
||||
try {
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("test")
|
||||
.displayName("测试")
|
||||
.toolClass(TestPanTool.class)
|
||||
.matchPattern("https://test\\.com/s/(.+)") // 缺少 (?<KEY>)
|
||||
.build();
|
||||
|
||||
// 如果没有抛出异常,检查配置
|
||||
System.out.println("Pattern: " + config.getMatchPattern().pattern());
|
||||
System.out.println("Supports fromShareUrl: " + config.supportsFromShareUrl());
|
||||
fail("Should throw IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 期望抛出异常
|
||||
assertTrue(e.getMessage().contains("正则表达式必须包含命名捕获组 KEY"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigBuilderRegexValidationWithKey() {
|
||||
// 测试正则表达式包含KEY命名捕获组,应该成功
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("test")
|
||||
.displayName("测试")
|
||||
.toolClass(TestPanTool.class)
|
||||
.matchPattern("https://test\\.com/s/(?<KEY>.+)")
|
||||
.build();
|
||||
|
||||
assertNotNull(config);
|
||||
assertTrue(config.supportsFromShareUrl());
|
||||
assertEquals("https://test\\.com/s/(?<KEY>.+)", config.getMatchPattern().pattern());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConfigBuilderRegexValidationWithKeyAndPwd() {
|
||||
// 测试正则表达式包含KEY和PWD命名捕获组,应该成功
|
||||
CustomParserConfig config = CustomParserConfig.builder()
|
||||
.type("test")
|
||||
.displayName("测试")
|
||||
.toolClass(TestPanTool.class)
|
||||
.matchPattern("https://test\\.com/s/(?<KEY>.+)(\\?pwd=(?<PWD>.+))?")
|
||||
.build();
|
||||
|
||||
assertNotNull(config);
|
||||
assertTrue(config.supportsFromShareUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用的解析器实现
|
||||
*/
|
||||
public static class TestPanTool implements IPanTool {
|
||||
private final ShareLinkInfo shareLinkInfo;
|
||||
|
||||
public TestPanTool(ShareLinkInfo shareLinkInfo) {
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
Promise<String> promise = Promise.promise();
|
||||
|
||||
String shareKey = shareLinkInfo.getShareKey();
|
||||
String password = shareLinkInfo.getSharePassword();
|
||||
|
||||
String url = "https://testpan.com/download/" + shareKey;
|
||||
if (password != null && !password.isEmpty()) {
|
||||
url += "?pwd=" + password;
|
||||
}
|
||||
|
||||
promise.complete(url);
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 无效的工具类(未实现 IPanTool 接口)
|
||||
*/
|
||||
public static class InvalidTool {
|
||||
public InvalidTool(ShareLinkInfo shareLinkInfo) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2024/8/8 2:39
|
||||
* Create at 2024/8/8 2:39
|
||||
*/
|
||||
public class PanDomainTemplateTest {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user