MENU

夸克短剧机器人

June 2, 2025 • Read: 69 • Java,Python

AI 摘要

正在加载摘要...
去年年末时候接触到了某平台的夸克推广活动,就是别人转存你的链接你能从中受益,于是变有了这个项目

README

QuarkBot

⚠️ 该项目已经失效

Gewechat 框架已停止服务,所以此项目已无法正常使用

基于 Gewechat 框架二次开发

一款支持影视搜索及转存分享的多功能机器人

🌟 核心功能

  • 🎥 影视/短剧搜索:基于个人聚合的接口和数据库快速查找全网影视资源。
  • 🔑 夸克功能集成

    • 检测 Cookie 有效性。
    • 检测夸克链接有效性。
    • 转存资源并生成分享链接并推送。
    • 监测转存更新。
  • 📦 资源管理

    • 支持通过机器人口令快速转存

截图

image.png


项目分析

菜单

该项目主要利用了 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 验证资源是否有效并返回 stoken
  • getFileList 获取指定分享Fid下的 所有/部分 json数据
  • getUrlInfo 获取短剧的 集数、缩略图
  • saveAndShareAndDel 转存并获取分享链接 可指定过期时间以及是否自动删除
  • updateSave 用于检测资源更新后更新保存的文件
  • renameFile 重命名文件
  • delete 删除文件
  • createDirectory 创建文件夹

更新检测

思路

因为有的资源是长期更新的,所以我就在想如何实现更新自动转存。我的思路是: 转存时记录下所有文件的目录树,利用SpringScheduled定时检测目录树,如果不相同,则更新。 但是有个缺点,如果资源失效了,就无法检测,并且替换成新的又很麻烦,算是个半成品

代码

相应的代码可以从 UpdateListService 这个类看

一些其他的小功能

  • 可指定转存的文件夹
  • 可指定分享时附带某个文件
  • 可自行配置匹配广告的正则表达式,并决定是替换还是删除
  • 可开启信息通知功能:优先使用微信机器人通知,如果机器人掉线,可利用Mail通知
  • 可通过web的方式进行机器人登录及注销

由于框架限制,机器人目前只实现了以下功能

  • 是否自动添加好友
  • 添加好友欢迎语
  • 自动邀请进群的关键词(支持正则)
  • 自动邀请进群的名称(支持多群,一个群满自动换成下一个)
  • 指定机器人生效的群组
  • 匹配搜剧的关键词(支持正则)
  • 搜索各种夸克资源并转存分享(默认分享后半小时删除资源以节省空间)
  • 推送每日新增短剧列表
  • 支持 WebHook 转存到指定账号并分享

由于这个项目最开始是跟人使用,所以有些代码并不规范,有什么问题可以去GitHub提issue