AI 摘要
正在加载摘要...
去年年末时候接触到了某平台的夸克推广活动,就是别人转存你的链接你能从中受益,于是变有了这个项目
README
QuarkBot
⚠️ 该项目已经失效
因 Gewechat 框架已停止服务,所以此项目已无法正常使用
基于 Gewechat 框架二次开发
一款支持影视搜索及转存分享的多功能机器人
🌟 核心功能
- 🎥 影视/短剧搜索:基于个人聚合的接口和数据库快速查找全网影视资源。
🔑 夸克功能集成:
- 检测 Cookie 有效性。
- 检测夸克链接有效性。
- 转存资源并生成分享链接并推送。
- 监测转存更新。
📦 资源管理:
- 支持通过机器人口令快速转存
截图

项目分析
菜单
该项目主要利用了 Gewechat 框架接收发信息的功能,菜单功能如下
// 机器人命令
String newMsg = """
{{name}} {{description}}
{{link}}
""";
String menu = """
欢迎使用影视资源机器人!
""";
String adminMenu = """
管理员命令
----------------------
#ping
#开启/关闭提醒
#更新群组列表
#更新ck 你的COOKIE
#更新文件夹id 你的文件夹id
下面请使用空格隔开
----------------------
如果完结为 false 的话,则自动开启更新
#提交 名字 描述 链接 (是否完结 true|false)
#转存 名字 描述 链接 (是否完结 true|false)
#提交并群发 名字 描述 链接 (是否完结 true|false)
#转存并群发 名字 描述 链接 (是否完结 true|false)
#更新 你自己的分享链接 新的分享链接(没有则写 无 ) (是否完结 true|false)
-----------------------
""";因为项目的初衷是自用,所以写的简陋了些,直接将菜单项写死了
搜索
这个功能还是挺重要的,因为要实现搜剧,这个功能是必不可少的
private CommonVO<List<Map<String, String>>> baseSearch(String name,String type) {
List<Map<String, String>> data = Collections.synchronizedList(new ArrayList<>());
// 从 Redis 中获取缓存的 list 列表
List<Map<String, String>> o = (List<Map<String, String>>) redisTemplate.opsForHash().get("search-name-list", name);
if (o != null && !o.isEmpty()) {
log.info("命中缓存 search-name-list");
return CommonVO.OK(o);
}
// 从数据库中查询
List<QuarkEntity> list = quarkMapper.selectList(
new QueryWrapper<QuarkEntity>()
.like("name", name)
.eq("valid", true)
);
List<QuarkEntity> listDuanJu = new ArrayList<>();
if ("duanju".equals(type)) {
List<QuarkEntity> duanjuList = duanjuMapper.selectList(
new QueryWrapper<DuanjuEntity>().like("name", name)
).stream().map(duanju -> {
try {
return BeanConverter.convert(duanju, QuarkEntity.class);
} catch (Exception e) {
log.error("Bean转换失败", e);
}
return new QuarkEntity();
}).toList();
listDuanJu.addAll(duanjuList);
}
if (!listDuanJu.isEmpty()) {
// 使用CountDownLatch等待异步任务完成
CountDownLatch latch = new CountDownLatch(1);
threadExecutor.execute(() -> {
try {
List<Integer> idsToDelete = new ArrayList<>();
Iterator<QuarkEntity> iterator = listDuanJu.iterator();
while (iterator.hasNext()) {
QuarkEntity entity = iterator.next();
try {
String checked = quark.checkByUrl(entity.getUrl(), entity.getPassword());
if (StrUtil.isBlank(checked)) {
// 收集需要删除的ID
idsToDelete.add(entity.getId());
iterator.remove();
}
} catch (IOException e) {
log.error("检查URL失败,实体ID: {}, 错误: {}", entity.getId(), e.getMessage(), e);
}
}
// 批量删除失效记录
if (!idsToDelete.isEmpty()) {
try {
duanjuMapper.delete(
new QueryWrapper<DuanjuEntity>()
.in("id", idsToDelete)
);
log.info("成功删除 {} 条失效记录", idsToDelete.size());
} catch (Exception e) {
log.error("批量删除失效记录失败: {}", e.getMessage(), e);
}
}
} finally {
latch.countDown();
}
});
try {
// 等待异步任务完成,设置超时时间
if (!latch.await(30, TimeUnit.SECONDS)) {
log.warn("URL检查任务超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("等待URL检查任务被中断", e);
}
}
if (!list.isEmpty()) {
// 使用CountDownLatch等待异步任务完成
CountDownLatch latch = new CountDownLatch(1);
threadExecutor.execute(() -> {
try {
List<QuarkEntity> entitiesToUpdate = new ArrayList<>();
Iterator<QuarkEntity> iterator = list.iterator();
while (iterator.hasNext()) {
QuarkEntity entity = iterator.next();
try {
String checked = quark.checkByUrl(entity.getUrl(), entity.getPassword());
if (StrUtil.isBlank(checked)) {
// 收集需要更新的实体
entitiesToUpdate.add(entity.setValid(false));
iterator.remove();
}
} catch (IOException e) {
log.error("检查URL失败,实体ID: {}, URL: {}, 错误: {}",
entity.getId(), entity.getUrl(), e.getMessage(), e);
}
}
// 批量更新失效记录
if (!entitiesToUpdate.isEmpty()) {
try {
// 使用MyBatis-Plus的批量更新方法
for (int i = 0; i < entitiesToUpdate.size(); i += 1000) {
int endIndex = Math.min(i + 1000, entitiesToUpdate.size());
List<QuarkEntity> batch = entitiesToUpdate.subList(i, endIndex);
// 使用MyBatis-Plus的Service层批量更新
quarkMapper.updateBatchById(batch);
}
log.info("成功更新 {} 条记录为无效状态", entitiesToUpdate.size());
} catch (Exception e) {
log.error("批量更新记录状态失败: {}", e.getMessage(), e);
}
}
} finally {
latch.countDown();
}
});
try {
// 等待异步任务完成,设置合理的超时时间
if (!latch.await(30, TimeUnit.SECONDS)) {
log.warn("URL检查任务超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("等待URL检查任务被中断", e);
}
}
list.addAll(listDuanJu);
log.info("正在进行全网搜索: 【{}】",name);
data = this.searchAll(name).getData();
HashMap<String, String> map = new HashMap<>();
sortListByKeyCount(data);
if (!list.isEmpty()) {
list.forEach(item -> map.put(item.getName(),item.getUrl()));
data.add(map);
moveMapToFirst(data,map);
}
// 将查询结果存入 Redis 缓存
redisTemplate.opsForHash().put("search-name-list", name, data);
redisTemplate.expire("search-name-list", 30, TimeUnit.MINUTES);
return CommonVO.OK(data);
}这个即后端最重要的搜索接口,采用了Redis作为缓存。先去查询数据库中是否有匹配的结果,然后对其进行 失效性检测 若失效在进行 全网搜
全网搜接口
这个其实就是聚合了各种夸克的搜索接口,返回的结果由我做一个整合,并进行 失效性检测,具体代码可以去 QuarkBot/search-api at main · Liner03/QuarkBot 查看
夸克工具类
在转存及分享以及附带文件(广告)分享的时候必然少不了对夸克接口的分享,所以在我参考了其他同类项目,如 quark-auto-save
/**
* @Author Lin.
* @Date 2024/12/5
* 夸克网盘的一些方法
*/
@Slf4j
@Component
public class Quark {
@Value("${quark.config.cookie}")
private String cookie;
@Autowired
private OkhttpUtil okhttpUtil;
@Autowired
@Lazy
private FileSaveService fileSaveService;
@Autowired
@Lazy
private LinkService linkService;
@Autowired
private TempData data;
@Getter
private final String baseUrl = "https://drive-m.quark.cn";
@Autowired
@Lazy
private MessageService messageService;
@Value("${quark.config.save-path-fid}")
private String savePath;
@Autowired
private UserSettingMapper userSettingMapper;
@Getter
private final String rootDir = "bot测试";
private Notify notify;
@Autowired
private Environment environment;
@PostConstruct
private void init() {
renameFile(savePath,rootDir);
}
public HashMap<String,String> header() {
HashMap<String, String> map = new HashMap<>();
map.put("Cookie", cookie);
map.put("Content-Type", "application/json");
map.put("User-Agent", Common.randomUA());
return map;
}
/**
*
* @return boolean
* @author Lin.
* 验证cookie的有效性
*/
public boolean isValidCookie(String cookie) throws IOException {
String url = "https://pan.quark.cn/account/info?fr=pc&platform=pc";
HashMap<String, String> header = header();
header.put("Cookie", cookie);
JSONObject json = okhttpUtil.getJsonObject(url, header, okhttpUtil.getOkHttpClient());
return !StrUtil.hasEmpty(json.getString("data"));
}
public boolean isValidCookie() throws IOException {
String url = "https://pan.quark.cn/account/info?fr=pc&platform=pc";
JSONObject json = okhttpUtil.getJsonObject(url, header(), okhttpUtil.getOkHttpClient());
return !StrUtil.hasEmpty(json.getString("data"));
}
/**
* @return String stoken
* @author Lin.
* 验证资源是否失效
*/
public String checkByUrl(String panUrl,String passcode, String cookie) throws IOException {
String pwdId = getPwdId(panUrl);
if (pwdId == null) {
log.warn("错误的url传入{}",panUrl);
return null;
}
// 判断 cookie 有效性
boolean validCookie = false;
if(StrUtil.isBlankIfStr(cookie)){
validCookie = isValidCookie();
} else {
validCookie = isValidCookie(cookie);
}
if (!validCookie) {
UserSettingEntity entity = userSettingMapper.selectById(1);
if (entity.getCookieValid()) {
entity.setCookieValid(false);
userSettingMapper.updateById(entity);
String remindMsg = this.data.getDataByString("remindMsg");
if (StrUtil.isNotBlank(remindMsg) && Boolean.parseBoolean(remindMsg)) messageService.send_to_user(this.data.getDataByString("adminWxid"),"COOKIE已失效!");
log.error("COOKIE失效!");
notify.send("COOKIE失效!");
}
return null;
}
passcode = passcode == null ? "" : passcode;
String url = baseUrl + "/1/clouddrive/share/sharepage/token?pr=ucpro&fr=pc";
HashMap<String, String> body = new HashMap<>();
body.put("pwd_id", pwdId);
body.put("passcode", passcode);
JSONObject jsonObject = okhttpUtil.postJsonObject(url, header(), body, okhttpUtil.getOkHttpClient());
if (jsonObject != null && !jsonObject.isEmpty()) {
if (jsonObject.getInteger("status") != 200) {
log.warn("pwd_id: {}, 错误信息: {}", pwdId, jsonObject.getString("message") );
return null;
}
return jsonObject.getJSONObject("data").getString("stoken");
}
return null;
}
public String checkByUrl(String panUrl,String passcode) throws IOException {
return this.checkByUrl(panUrl, passcode, null);
}
/**
* 获取当前分享下大的 fid 下的所有json数据 pdirFid 初始为0
*/
public List<JSONObject> getFileList(String pwdId, String stoken, String pdirFid) {
return this.getFileList(pwdId, stoken, header().get("Cookie"), pdirFid);
}
public List<JSONObject> getFileList(String pwdId, String stoken, String cookie, String pdirFid) {
List<JSONObject> listMerge = new ArrayList<>();
int page = 1;
int fetchShare = 0;
try {
while (true) {
// 构建请求参数
String url = baseUrl + "/1/clouddrive/share/sharepage/detail";
// 构建查询参数
Map<String, String> queryParams = new HashMap<>();
queryParams.put("pr", "ucpro");
queryParams.put("fr", "pc");
queryParams.put("pwd_id", pwdId);
queryParams.put("stoken", stoken);
queryParams.put("pdir_fid", pdirFid);
queryParams.put("force", "0");
queryParams.put("_page", String.valueOf(page));
queryParams.put("_size", "50");
queryParams.put("_fetch_banner", "0");
queryParams.put("_fetch_share", String.valueOf(fetchShare));
queryParams.put("_fetch_total", "1");
queryParams.put("_sort", "file_type:asc,updated_at:desc");
// 将查询参数拼接到 URL
String fullUrl = url + "?" + buildQueryString(queryParams);
// 发送请求
HashMap<String, String> header = header();
header.put("cookie", cookie);
JSONObject response = okhttpUtil.getJsonObject(fullUrl, header, okhttpUtil.getOkHttpClient());
if (response == null) {
break;
}
// 获取当前页的数据列表
JSONObject data = response.getJSONObject("data");
if (data != null && data.getInteger("is_owner") == 1) {
log.error("❌网盘中已经存在该文件,无需再次转存");
return new ArrayList<>();
}
JSONArray currentList = response.getJSONObject("data").getJSONArray("list");
if (currentList == null || currentList.isEmpty()) {
break;
}
if (data != null && response.getJSONObject("metadata").getInteger("_total") == 1) {
List<JSONObject> list = currentList.toList(JSONObject.class);
if (list.get(0).getBoolean("dir")) {
listMerge.add(list.get(0));
return listMerge;
}else {
listMerge.addAll(list);
return listMerge;
}
}
// 添加到合并列表中
List<JSONObject> list = currentList.toList(JSONObject.class);
for (JSONObject json:list) {
if (json.getBoolean("dir")) {
listMerge.add(json);
}else {
listMerge.add(json);
}
}
// 检查是否获取完所有数据
int total = response.getJSONObject("metadata").getInteger("_total");
if (listMerge.size() >= total) {
break;
}
page++;
}
return listMerge;
} catch (Exception e) {
log.error("获取文件列表异常", e);
return null;
}
}
/**
* 获取短剧的基础信息 缩略图 集数
*/
public HashMap<String ,Object> getUrlInfo(String pwdId, String stoken, String cookie, String pdirFid) {
int retry = 5;
int fetchShare = 0;
boolean wrapDir = false;
HashMap<String, Object> map = new HashMap<>();
try {
while (retry > 0) {
// 构建请求参数
String url = baseUrl + "/1/clouddrive/share/sharepage/detail";
// 构建查询参数
Map<String, String> queryParams = new HashMap<>();
queryParams.put("pr", "ucpro");
queryParams.put("fr", "pc");
queryParams.put("pwd_id", pwdId);
queryParams.put("stoken", stoken);
queryParams.put("pdir_fid", pdirFid);
queryParams.put("force", "0");
queryParams.put("_page", "1");
queryParams.put("_size", "200");
queryParams.put("_fetch_banner", "0");
queryParams.put("_fetch_share", String.valueOf(fetchShare));
queryParams.put("_fetch_total", "1");
queryParams.put("_sort", "file_type:asc,updated_at:desc");
// 将查询参数拼接到 URL
String fullUrl = url + "?" + buildQueryString(queryParams);
// 发送请求
HashMap<String, String> header = header();
header.put("cookie", cookie);
JSONObject response = okhttpUtil.getJsonObject(fullUrl, header, okhttpUtil.getOkHttpClient());
if (response == null) {
retry--;
continue;
}
// 获取当前页的数据列表
JSONObject data = response.getJSONObject("data");
JSONArray list = data.getJSONArray("list");
JSONObject[] listArray = list.toArray(JSONObject.class);
if (!wrapDir) {
for(JSONObject json: listArray) {
if (json.getBoolean("dir")) {
pdirFid = json.getString("fid");
wrapDir = true;
break;
}
}
}
JSONObject metadata = response.getJSONObject("metadata");
JSONArray currentList = data.getJSONArray("list");
if (currentList == null || currentList.isEmpty()) {
retry--;
continue;
}
JSONObject[] array = currentList.toArray(JSONObject.class);
String total = metadata.getString("_total");
String thumb = null;
for (JSONObject json:array) {
if ("0.jpg".equals(json.getString("file_name")) || "0.png".equals(json.getString("file_name"))) {
thumb = json.getString("preview_url");
break;
}
}
map.put("thumb", thumb);
map.put("total", total);
retry--;
}
return map;
} catch (Exception e) {
log.error("获取文件列表异常", e);
return map;
}
}
/**
* @param url 分享url
* @param savePathFid 转存文件夹id
* @param expireDay 过期天数 默认 天
* @param b 是否自动删除
* @return String 分享链接
*/
public String saveAndShareAndDel(String url, String savePathFid,String title,String expireDay,boolean b) throws IOException, InterruptedException {
log.info("开始转存...");
String stoken = this.checkByUrl(url, null);
if (StrUtil.isBlankIfStr(stoken)) {
log.warn("❌链接已失效!");
return null;
}
String pwdId = getPwdId(url);
List<JSONObject> list = this.getFileList(pwdId, stoken, "0");
if (list == null) return null;
// 如果是自己的 url 则直接返回
if (list.isEmpty()) return url;
int dirCount = 0;
int fileCount = 0;
ArrayList<String> fidList = new ArrayList<>();
ArrayList<String> shareFidTokenList = new ArrayList<>();
for (JSONObject jsonObject : list) {
if (jsonObject.getBoolean("dir")) {
dirCount++;
}else {
fileCount++;
}
shareFidTokenList.add(jsonObject.getString("share_fid_token"));
fidList.add(jsonObject.getString("fid"));
}
log.info("⭕文件总数: {},文件数: {},文件夹数: {}",list.size(),fileCount,dirCount);
if (StrUtil.isBlankIfStr(savePathFid)) {
log.error("保存目录ID不合法,请重新获取,如果无法获取,请输入0作为文件夹ID");
return null;
}
String saveAsTopFids = fileSaveService.saveSharedFiles(pwdId, stoken, fidList, shareFidTokenList, savePathFid);
if (StrUtil.isBlankIfStr(saveAsTopFids)) return null;
ArrayList<String> fids = new ArrayList<>();
fids.add(saveAsTopFids);
if (StrUtil.isEmptyIfStr(expireDay)) expireDay = "2";
return this.share(fids,title,expireDay,b);
}
private final Lock lock = new ReentrantLock();
public String saveAndShareAndDelCustomize(String url, String savePathFid, String adFid,
String cookie, String title, String expireDay,
boolean delFlag) throws IOException, InterruptedException {
if (lock.tryLock()) {
try {
log.info("开始转存...");
String stoken = this.checkByUrl(url, null, cookie);
if (StrUtil.isBlankIfStr(stoken)) {
log.warn("❌链接已失效!");
return null;
}
String pwdId = getPwdId(url);
List<JSONObject> list = this.getFileList(pwdId, stoken, cookie, "0");
if (list == null) return null;
// 如果是自己的 url 则直接返回原始 url
if (list.isEmpty()) return url;
int dirCount = 0;
int fileCount = 0;
ArrayList<String> fidList = new ArrayList<>();
ArrayList<String> shareFidTokenList = new ArrayList<>();
for (JSONObject jsonObject : list) {
if (jsonObject.getBoolean("dir")) {
dirCount++;
} else {
fileCount++;
}
shareFidTokenList.add(jsonObject.getString("share_fid_token"));
fidList.add(jsonObject.getString("fid"));
}
log.info("⭕文件总数: {}, 文件数: {}, 文件夹数: {}", list.size(), fileCount, dirCount);
if (StrUtil.isBlankIfStr(savePathFid)) {
log.error("保存目录ID不合法,请重新获取,如果无法获取,请输入0作为文件夹ID");
return null;
}
String saveAsTopFids = fileSaveService.saveSharedFiles(pwdId, stoken, fidList, shareFidTokenList, savePathFid, cookie);
if (StrUtil.isBlankIfStr(saveAsTopFids)) return null;
ArrayList<String> fids = new ArrayList<>();
fids.add(saveAsTopFids);
if (StrUtil.isEmptyIfStr(expireDay)) expireDay = "2";
return this.share(fids, adFid, title, cookie, expireDay, delFlag);
} finally {
lock.unlock();
}
} else {
log.warn("转存任务已在执行,跳过本次请求");
}
return null;
}
public String saveAndShare(String url, String savePathFid,String title) throws IOException, InterruptedException {
return this.saveAndShareAndDel(url,savePathFid,title,null,true);
}
/**
* 分享文件 默认只有1天
* expired_type 1:永久 2:1天 3:7天 4:30天
*/
public String share(List<String> pFidList,String title,String expireDay,boolean b) throws IOException, InterruptedException {
log.info("开始生成分享链接");
String property = environment.getProperty("quark.config.ad-fid");
pFidList.add(property);
String url = baseUrl + "/1/clouddrive/share?pr=ucpro&fr=pc&uc_param_str";
HashMap<String, Object> data = new HashMap<>();
data.put("fid_list",pFidList);
data.put("title",title);
data.put("url_type","1");
data.put("expired_type",expireDay);
data.put("passcode","");
JSONObject jsonObject = okhttpUtil.postJsonObject(url, header(), data, okhttpUtil.getOkHttpClient());
if (jsonObject.isEmpty() || jsonObject.getInteger("status") != 200) return null;
String taskId = jsonObject.getJSONObject("data").getString("task_id");
url = baseUrl + "/1/clouddrive/task?pr=ucpro&fr=pc&uc_param_str&retry_index=0&task_id=" + taskId;
int retry = 10;
while (retry > 0) {
retry++;
Thread.sleep(200 + new Random().nextInt(500));
jsonObject = okhttpUtil.getJsonObject(url, header(), okhttpUtil.getOkHttpClient());
if (jsonObject.isEmpty() || jsonObject.getInteger("status") != 200) continue;
if (StrUtil.isBlankIfStr(jsonObject.getJSONObject("data").getString("share_id"))) continue;
break;
}
Thread.sleep(200 + new Random().nextInt(500));
String shareId = jsonObject.getJSONObject("data").getString("share_id");
url = baseUrl + "/1/clouddrive/share/password?pr=ucpro&fr=pc&uc_param_str";
data.clear();
data.put("share_id",shareId);
jsonObject = okhttpUtil.postJsonObject(url, header(), data, okhttpUtil.getOkHttpClient());
if (jsonObject.isEmpty() || jsonObject.getInteger("status") != 200) return null;
Thread.sleep(200 + new Random().nextInt(500));
String shareUrl = jsonObject.getJSONObject("data").getString("share_url");
log.info("分享链接: {}",shareUrl);
// 加入定时删除队列
pFidList.remove(property);
if (b) linkService.createLinks(pFidList);
return shareUrl;
}
public String share(List<String> pFidList, String adFid, String title, String cookie,
String expireDay, boolean delFlag) throws IOException, InterruptedException {
log.info("开始生成分享链接");
if (adFid != null) pFidList.add(adFid);
String url = baseUrl + "/1/clouddrive/share?pr=ucpro&fr=pc&uc_param_str";
HashMap<String, Object> data = new HashMap<>();
data.put("fid_list", pFidList);
data.put("title", title);
data.put("url_type", "1");
data.put("expired_type", expireDay);
data.put("passcode", "");
HashMap<String, String> header = header();
header.put("Cookie", cookie);
JSONObject jsonObject = okhttpUtil.postJsonObject(url, header, data, okhttpUtil.getOkHttpClient());
if (jsonObject.isEmpty() || jsonObject.getInteger("status") != 200) return null;
String taskId = jsonObject.getJSONObject("data").getString("task_id");
url = baseUrl + "/1/clouddrive/task?pr=ucpro&fr=pc&uc_param_str&retry_index=0&task_id=" + taskId;
int retry = 10;
while (retry > 0) {
retry--;
Thread.sleep(200 + new Random().nextInt(500));
jsonObject = okhttpUtil.getJsonObject(url, header, okhttpUtil.getOkHttpClient());
if (jsonObject.isEmpty() || jsonObject.getInteger("status") != 200) continue;
if (StrUtil.isBlankIfStr(jsonObject.getJSONObject("data").getString("share_id"))) continue;
break;
}
Thread.sleep(200 + new Random().nextInt(500));
String shareId = jsonObject.getJSONObject("data").getString("share_id");
url = baseUrl + "/1/clouddrive/share/password?pr=ucpro&fr=pc&uc_param_str";
data.clear();
data.put("share_id", shareId);
jsonObject = okhttpUtil.postJsonObject(url, header, data, okhttpUtil.getOkHttpClient());
if (jsonObject.isEmpty() || jsonObject.getInteger("status") != 200) return null;
Thread.sleep(200 + new Random().nextInt(500));
String shareUrl = jsonObject.getJSONObject("data").getString("share_url");
log.info("分享链接: {}", shareUrl);
HashMap<String, Object> map = getUrlInfo(getPwdId(shareUrl), checkByUrl(shareUrl,null), cookie, "0");
this.data.setMapData(shareUrl, map);
if (adFid != null) pFidList.remove(adFid);
if (delFlag) linkService.createLinks(pFidList, cookie);
return shareUrl;
}
public String share(List<String> pFidList,String title) throws IOException, InterruptedException {
return this.share(pFidList,title,"2",true);
}
/**
* 更新资源
*/
public String updateSave(String pwdId, String stoken, String pFid,String fid,String shareToken) {
return fileSaveService.saveSharedFiles(pwdId, stoken, Collections.singletonList(fid), Collections.singletonList(shareToken), pFid);
}
/**
* 构建查询字符串
*/
private String buildQueryString(Map<String, String> params) {
return params.entrySet().stream()
.map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
public String getPwdId(String url) {
String pattern = "(?<=s/)([^#]+)";
Pattern compiledPattern = Pattern.compile(pattern);
Matcher matcher = compiledPattern.matcher(url);
if (matcher.find()) {
return matcher.group(1); // 返回匹配的部分
} else {
return null; // 如果没有找到匹配,返回 null
}
}
/**
* 重命名文件
*/
public boolean renameFile(String fid, String newName) {
try {
Map<String, String> bodyMap = new HashMap<>();
bodyMap.put("fid", fid);
bodyMap.put("file_name", newName);
JSONObject response = okhttpUtil.postJsonObject(
baseUrl + "/1/clouddrive/file/rename?pr=ucpro&fr=pc&uc_param_str",
header(),
bodyMap,
okhttpUtil.getOkHttpClient()
);
if (response != null && response.getInteger("code") != 0) {
log.error("重命名文件失败: {}, 原因: {}", fid, response.getString("message"));
return false;
}
return true;
} catch (Exception e) {
log.error("重命名文件异常", e);
return false;
}
}
/**
* 删除文件夹
*/
public boolean delete(List<String> fid) throws IOException {
String url = baseUrl + "/1/clouddrive/file/delete?pr=ucpro&fr=pc&uc_param_str=";
HashMap<String, Object> data = new HashMap<>();
data.put("action_type","2");
data.put("filelist",fid);
JSONArray jsonArray = new JSONArray();
data.put("exclude_fids",jsonArray);
JSONObject jsonObject = okhttpUtil.postJsonObject(url, header(), data, okhttpUtil.getOkHttpClient());
return !jsonObject.isEmpty() && jsonObject.getInteger("status") == 200;
}
public boolean delete(List<String> fid, String cookie) throws IOException {
String url = baseUrl + "/1/clouddrive/file/delete?pr=ucpro&fr=pc&uc_param_str=";
HashMap<String, Object> data = new HashMap<>();
data.put("action_type","2");
data.put("filelist",fid);
JSONArray jsonArray = new JSONArray();
data.put("exclude_fids",jsonArray);
HashMap<String, String> header = header();
header.put("Cookie",cookie);
JSONObject jsonObject = okhttpUtil.postJsonObject(url, header, data, okhttpUtil.getOkHttpClient());
return !jsonObject.isEmpty() && jsonObject.getInteger("status") == 200;
}
/**
* 创建文件夹
* @param dirPath 文件夹绝对路径
* @return String 返回文件夹fid
* 对于已经存在的文件夹,则不会再创建,而是直接返回此文件夹的fid
*/
public String createDirectory(String dirPath) throws IOException {
String url = baseUrl + "/1/clouddrive/file?pr=ucpro&fr=pc&uc_param_str=";
HashMap<String, Object> body = new HashMap<>();
body.put("pdir_fid", "0");
body.put("file_name", "");
body.put("dir_path", dirPath);
body.put("dir_init_lock", false);
JSONObject response = okhttpUtil.postJsonObject(url, header(), body, okhttpUtil.getOkHttpClient());
if (response != null && response.getInteger("code") == 0) {
return response.getJSONObject("data").getString("fid");
}
return null;
}
/**
* @param fid 文件夹id
* @return JsonObject
*/
public JSONObject getDirDetails(String fid) throws IOException {
String url = baseUrl + "/1/clouddrive/file/sort?pr=ucpro&fr=pc&uc_param_str&pdir_fid=" + fid + "&_page=1&_size=50&_fetch_total=1&_fetch_sub_dirs=0&_sort=file_type:asc,updated_at:desc&_fetch_full_path=0";
return okhttpUtil.getJsonObject(url, header(), okhttpUtil.getOkHttpClient());
}
/**
* 递归获取文件列表 返回Tree
*/
public Tree<QuarkNode> getAllFileList(String pwdId, String stoken, String pdirFid) {
try {
Tree<QuarkNode> rootTree = new Tree<>();
rootTree.put("id", "root");
rootTree.put("name", "根目录");
rootTree.put("parentId", "0");
// 递归获取文件树
getFileListRecursive(pwdId, stoken, pdirFid, rootTree);
return rootTree;
} catch (Exception e) {
log.error("获取文件列表异常", e);
return null;
}
}
public Tree<QuarkNode> getAllFileList(String pdirFid) {
try {
Tree<QuarkNode> rootTree = new Tree<>();
rootTree.put("id", "root");
rootTree.put("name", "根目录");
rootTree.put("parentId", "0");
rootTree.put("shareFidToken",null);
// 递归获取文件树
getFileListRecursive(pdirFid, rootTree);
return rootTree;
} catch (Exception e) {
log.error("获取文件列表异常", e);
return null;
}
}
private void getFileListRecursive(String pwdId, String stoken, String pdirFid, Tree<QuarkNode> parentTree) throws IOException {
int page = 1;
Map<String, String> queryParams = buildInitialQueryParams(pwdId, stoken);
while (true) {
// 构建请求URL
String url = baseUrl + "/1/clouddrive/share/sharepage/detail";
queryParams.put("pdir_fid", pdirFid);
queryParams.put("_page", String.valueOf(page));
String fullUrl = url + "?" + buildQueryString(queryParams);
// 发送请求
JSONObject response = okhttpUtil.getJsonObject(fullUrl, header(), okhttpUtil.getOkHttpClient());
if (response == null) break;
JSONObject data = response.getJSONObject("data");
if (data == null) break;
JSONArray fileList = data.getJSONArray("list");
if (fileList == null || fileList.isEmpty()) break;
int total = response.getJSONObject("metadata").getInteger("_total");
// 处理单个文件/文件夹的情况
if (total == 1) {
JSONObject singleItem = fileList.getJSONObject(0);
// 在处理节点时的修改
QuarkNode node = createQuarkNode(singleItem);
Tree<QuarkNode> currentTree = node.toTree();
parentTree.setChildren(Collections.singletonList(currentTree));
if (node.isDir()) {
getFileListRecursive(pwdId, stoken, node.getFid(), currentTree);
}
return;
}
// 处理多个文件/文件夹的情况
for (int i = 0; i < fileList.size(); i++) {
JSONObject item = fileList.getJSONObject(i);
QuarkNode node = createQuarkNode(item);
Tree<QuarkNode> currentTree = node.toTree(); // 直接使用 QuarkNode 的 toTree 方法
// 将当前树添加到父节点的子节点列表中
if (parentTree.getChildren() == null) {
parentTree.setChildren(new ArrayList<>());
}
parentTree.getChildren().add(currentTree);
if (node.isDir()) {
// 递归处理子文件夹
getFileListRecursive(pwdId, stoken, node.getFid(), currentTree);
}
}
// 检查是否需要继续翻页
if (fileList.size() < total) {
page++;
} else {
break;
}
}
}
private void getFileListRecursive(String pdirFid, Tree<QuarkNode> parentTree) throws IOException {
int page = 1;
while (true) {
JSONObject response = this.getDirDetails(pdirFid);
// 发送请求
if (response == null) break;
JSONObject data = response.getJSONObject("data");
if (data == null) break;
JSONArray fileList = data.getJSONArray("list");
if (fileList == null || fileList.isEmpty()) break;
int total = response.getJSONObject("metadata").getInteger("_total");
// 处理单个文件/文件夹的情况
if (total == 1) {
JSONObject singleItem = fileList.getJSONObject(0);
// 在处理节点时的修改
QuarkNode node = createQuarkNode(singleItem);
Tree<QuarkNode> currentTree = node.toTree();
parentTree.setChildren(Collections.singletonList(currentTree));
if (node.isDir()) {
getFileListRecursive(node.getFid(), currentTree);
}
return;
}
// 处理多个文件/文件夹的情况
for (int i = 0; i < fileList.size(); i++) {
JSONObject item = fileList.getJSONObject(i);
QuarkNode node = createQuarkNode(item);
Tree<QuarkNode> currentTree = node.toTree();
// 将当前树添加到父节点的子节点列表中
if (parentTree.getChildren() == null) {
parentTree.setChildren(new ArrayList<>());
}
parentTree.getChildren().add(currentTree);
if (node.isDir()) {
// 递归处理子文件夹
getFileListRecursive(node.getFid(), currentTree);
}
}
// 检查是否需要继续翻页
if (fileList.size() < total) {
page++;
} else {
break;
}
}
}
private QuarkNode createQuarkNode(JSONObject json) {
return new QuarkNode()
.setFid(json.getString("fid"))
.setFileName(json.getString("file_name"))
.setPdirFid(json.getString("pdir_fid"))
.setShareFidToken(json.getString("share_fid_token"))
.setDir(json.getBoolean("dir"));
}
private Map<String, String> buildInitialQueryParams(String pwdId, String stoken) {
Map<String, String> queryParams = new HashMap<>();
queryParams.put("pr", "ucpro");
queryParams.put("fr", "pc");
queryParams.put("pwd_id", pwdId);
queryParams.put("stoken", stoken);
queryParams.put("force", "0");
queryParams.put("_size", "50");
queryParams.put("_fetch_banner", "0");
queryParams.put("_fetch_total", "1");
queryParams.put("_sort", "file_type:asc,updated_at:desc");
queryParams.put("_fetch_share", "0");
return queryParams;
}
}其中的方法包括但不限于
isValidCookie验证cookie有效性checkByUrl验证资源是否有效并返回 stokengetFileList获取指定分享Fid下的 所有/部分 json数据getUrlInfo获取短剧的 集数、缩略图saveAndShareAndDel转存并获取分享链接 可指定过期时间以及是否自动删除updateSave用于检测资源更新后更新保存的文件renameFile重命名文件delete删除文件createDirectory创建文件夹
更新检测
思路
因为有的资源是长期更新的,所以我就在想如何实现更新自动转存。我的思路是: 转存时记录下所有文件的目录树,利用SpringScheduled定时检测目录树,如果不相同,则更新。 但是有个缺点,如果资源失效了,就无法检测,并且替换成新的又很麻烦,算是个半成品。
代码
相应的代码可以从 UpdateListService 这个类看
一些其他的小功能
- 可指定转存的文件夹
- 可指定分享时附带某个文件
- 可自行配置匹配广告的正则表达式,并决定是替换还是删除
- 可开启信息通知功能:优先使用微信机器人通知,如果机器人掉线,可利用Mail通知
- 可通过web的方式进行机器人登录及注销
由于框架限制,机器人目前只实现了以下功能
- 是否自动添加好友
- 添加好友欢迎语
- 自动邀请进群的关键词(支持正则)
- 自动邀请进群的名称(支持多群,一个群满自动换成下一个)
- 指定机器人生效的群组
- 匹配搜剧的关键词(支持正则)
- 搜索各种夸克资源并转存分享(默认分享后半小时删除资源以节省空间)
- 推送每日新增短剧列表
- 支持 WebHook 转存到指定账号并分享
由于这个项目最开始是跟人使用,所以有些代码并不规范,有什么问题可以去GitHub提issue
如无特殊说明 夸克短剧机器人 为博主 Lin 原创,转载请注明原文链接: https://blog.lin03.cn/archives/19/