From ec15bcd86d939bd2203c2d99b424cc274d3eeb81 Mon Sep 17 00:00:00 2001 From: lushunming <1357197829@qq.com> Date: Tue, 13 Aug 2024 10:44:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=B8=E5=85=8B=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/github/catvod/api/QuarkApi.java | 385 ++++++++++++------ .../com/github/catvod/bean/quark/Item.java | 9 + .../java/com/github/catvod/net/OkHttp.java | 5 + .../java/com/github/catvod/utils/Json.java | 4 + .../java/com/github/catvod/utils/Util.java | 61 +++ 5 files changed, 337 insertions(+), 127 deletions(-) diff --git a/app/src/main/java/com/github/catvod/api/QuarkApi.java b/app/src/main/java/com/github/catvod/api/QuarkApi.java index 3bc53b8f..dbf51fe4 100644 --- a/app/src/main/java/com/github/catvod/api/QuarkApi.java +++ b/app/src/main/java/com/github/catvod/api/QuarkApi.java @@ -1,171 +1,302 @@ /* package com.github.catvod.api; -import com.github.catvod.bean.quark.ShareData; -import com.github.catvod.spider.Init; -import com.github.catvod.utils.Path; -import com.google.gson.Gson; -import com.google.gson.JsonObject; +import com.github.catvod.bean.quark.Item; +import com.github.catvod.net.OkHttp; +import com.github.catvod.net.OkResult; +import com.github.catvod.utils.Json; +import com.github.catvod.utils.Util; import org.apache.commons.lang3.StringUtils; -import java.io.File; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; public class QuarkApi { - - private static final String API_URL = "https://drive-pc.QuarkApi.cn/1/clouddrive/"; - private String cookie; - private String ckey; - private Map shareTokenCache; + private String apiUrl = "https://drive-pc.quark.cn/1/clouddrive/"; + private String cookie = ""; + private String ckey = ""; + private Map> shareTokenCache = new HashMap<>(); private String pr = "pr=ucpro&fr=pc"; - private List subtitleExts; - private Map saveFileIdCaches; - private String saveDirId; + private List subtitleExts = Arrays.asList(".srt", ".ass", ".scc", ".stl", ".ttml"); + private Map saveFileIdCaches = new HashMap<>(); + private String saveDirId = null; private String saveDirName = "TV"; private boolean isVip = false; - private static class Loader { - static volatile QuarkApi INSTANCE = new QuarkApi(); + public void initQuark(String cookie) throws Exception { + this.ckey = bytesToHex(MessageDigest.getInstance("MD5").digest(cookie.getBytes())); + this.cookie = cookie; + this.isVip = getVip(); } - public static QuarkApi get() { - return QuarkApi.Loader.INSTANCE; + private Map getHeaders() { + Map headers = new HashMap<>(); + headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch"); + headers.put("Referer", "https://pan.quark.cn/"); + headers.put("Content-Type", "application/json"); + headers.put("Cookie", this.cookie); + headers.put("Host", "drive-pc.quark.cn"); + return headers; } - public File getCache() { - return Path.tv("quark"); - } - - private QuarkApi() { - Init.checkPermission(); - this.shareTokenCache = new HashMap<>(); - this.subtitleExts = List.of(".srt", ".ass", ".scc", ".stl", ".ttml"); - this.saveFileIdCaches = new HashMap<>(); - } - - public void setRefreshToken(String token) { - this.cookie = token; - } + private String api(String url, Map data, Integer retry, String method) throws Exception { - public static String bytesToHex(byte[] bytes) { - StringBuilder hexString = new StringBuilder(); - for (byte b : bytes) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) { - hexString.append("0"); - } - hexString.append(hex); - } - return hexString.toString(); - } - - public JsonObject api(String url, String data, Integer retry, String method) { - - return null; - - } - - public ShareData getShareData(String url) { - String regex = "https:\\/\\/pan\\.quark\\.cn\\/s\\/([^\\|#/]+)"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(url); - - if (matcher.find()) { - - return new ShareData(matcher.group(1), "0"); - } - return null; - } - - boolean getVip() { - JsonObject listData = this.api("member?pr=ucpro&fr=pc&uc_param_str=&fetch_subscribe=true&_ch=home&fetch_identity=true", null, null, "get"); - return "EXP_SVIP".equals(listData.getAsJsonObject("data").getAsJsonObject("member_type").getAsString()); - } - - - List getPlayFormatList() { - if (this.isVip) { - return List.of("4K", "超清", "高清", "普画"); + int leftRetry = retry != null ? retry : 3; + OkResult okResult; + if ("get".equals(method)) { + okResult = OkHttp.get(this.apiUrl + url, data, getHeaders()); } else { - return List.of("普画"); + okResult = OkHttp.post(this.apiUrl + url, data, getHeaders()); } + + + if (okResult.getResp().get("Set-Cookie") != null) { + Matcher matcher = Pattern.compile("__puus=([^;]+)").matcher(StringUtils.join(okResult.getResp().get("Set-Cookie"), ";;;")); + if (matcher.find()) { + Matcher cookieMatcher = Pattern.compile("__puus=([^;]+)").matcher(this.cookie); + if (cookieMatcher.find() && !cookieMatcher.group(1).equals(matcher.group(1))) { + this.cookie = this.cookie.replaceAll("__puus=[^;]+", "__puus=" + matcher.group(1)); + } + } + } + + if (okResult.getCode() != 200 && leftRetry > 0) { + Thread.sleep(1000); + return api(url, data, leftRetry - 1, method); + } + return okResult.getBody(); } + private Map getShareData(String url) { + Pattern pattern = Pattern.compile("https://pan\\.quark\\.cn/s/([^\\\\|#/]+)"); + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + Map shareData = new HashMap<>(); + shareData.put("shareId", matcher.group(1)); + shareData.put("folderId", "0"); + return shareData; + } + return null; + } - List getPlayFormtQuarkList() { + private boolean getVip() throws Exception { + Map listData = Json.parseSafe(api("member?pr=ucpro&fr=pc&uc_param_str=&fetch_subscribe=true&_ch=home&fetch_identity=true", null, 0, "GET"), Map.class); + return "EXP_SVIP".equals(((Map) listData.get("data")).get("member_type")); + } + + private List getPlayFormatList() { if (this.isVip) { - return List.of("4k", "2k", "super", "high", "normal", "low"); - } - { - return List.of("low"); + return Arrays.asList("4K", "超清", "高清", "普画"); + } else { + return Collections.singletonList("普画"); } } - void getShareToken(ShareData shareData) { - if (null != this.shareTokenCache.get(shareData.getShareId())) { - this.shareTokenCache.put(shareData.getShareId(), null); - JsonObject shareToken = this.api("share/sharepage/token?" + this.pr, new Gson().toJson(Map.of("pwd_id", shareData.getFolderId(), "passcode", Objects.isNull(shareData.getSharePwd()) ? "" : shareData.getSharePwd())), null, null); - if (shareToken.getAsJsonObject("data") != null && StringUtils.isNoneBlank(shareToken.getAsJsonObject("data").getAsJsonObject("stoken").getAsString())) { - this.shareTokenCache.put(shareData.getShareId(), shareToken.getAsJsonObject("data")); + private List getPlayFormatQuarkList() { + if (this.isVip) { + return Arrays.asList("4k", "2k", "super", "high", "normal", "low"); + } else { + return Collections.singletonList("low"); + } + } + + private void getShareToken(Map shareData) throws Exception { + if (!this.shareTokenCache.containsKey(shareData.get("shareId"))) { + this.shareTokenCache.remove(shareData.get("shareId")); + Map shareToken = Json.parseSafe(api("share/sharepage/token?" + this.pr, Map.of("pwd_id", shareData.get("shareId"), "passcode", shareData.get("sharePwd")), 0, "POST"), Map.class); + if (shareToken.containsKey("data") && ((Map) shareToken.get("data")).containsKey("stoken")) { + this.shareTokenCache.put(shareData.get("shareId"), (Map) shareToken.get("data")); } } } - - listFile(shareIndex, shareData, videos, subtitles, shareId, folderId, page) { - const prePage = 200; - page = page || 1; - const listData = await this.api( - `share/sharepage/detail?${this.pr}&pwd_id=${shareId}&stoken=${encodeURIComponent(this.shareTokenCache[shareId].stoken)}&pdir_fid=${folderId}&force=0&_page=${page}&_size=${prePage}&_sort=file_type:asc,file_name:asc`, - null, - null, - 'get', - ); - if (!listData.data) return []; - const items = listData.data.list; - if (!items) return []; - const subDir = []; - for (const item of items) { - if (item.dir === true) { - subDir.push(item); - } else if (item.file === true && item.obj_category === 'video') { - if (item.size < 1024 * 1024 * 5) continue; - item.stoken = this.shareTokenCache[shareData.shareId].stoken; - videos.push(Item.objectFrom(item, shareData.shareId, shareIndex)); - } else if (item.type === 'file' && this.subtitleExts.some((x) => item.file_name.endsWith(x))) { - subtitles.push(Item.objectFrom(item, shareData, shareIndex)); + private List> listFile(int shareIndex, Map shareData, List videos, List subtitles, String shareId, String folderId, Integer page) throws Exception { + int prePage = 200; + page = page != null ? page : 1; + Map listData = Json.parseSafe(api("share/sharepage/detail?" + this.pr + "&pwd_id=" + shareId + "&stoken=" + encodeURIComponent((String) this.shareTokenCache.get(shareId).get("stoken")) + "&pdir_fid=" + folderId + "&force=0&_page=" + page + "&_size=" + prePage + "&_sort=file_type:asc,file_name:asc", null, 0, "GET"), Map.class); + if (listData.get("data") == null) return Collections.emptyList(); + List> items = (List>) ((Map) listData.get("data")).get("list"); + if (items == null) return Collections.emptyList(); + List> subDir = new ArrayList<>(); + for (Map item : items) { + if (Boolean.TRUE.equals(item.get("dir"))) { + subDir.add(item); + } else if (Boolean.TRUE.equals(item.get("file")) && "video".equals(item.get("obj_category"))) { + if ((int) item.get("size") < 1024 * 1024 * 5) continue; + item.put("stoken", this.shareTokenCache.get(shareData.get("shareId")).get("stoken")); + videos.add(Item.objectFrom(Json.toJson(item), shareData.get("shareId"), shareIndex)); + } else if ("file".equals(item.get("type")) && this.subtitleExts.contains("." + Util.getExt((String) item.get("file_name")))) { + subtitles.add(Item.objectFrom(Json.toJson(item), shareData.get("shareId"), shareIndex)); } } - if (page < Math.ceil(listData.metadata._total / prePage)) { - const nextItems = await this.listFile( - shareIndex, - shareData.shareId, - videos, - subtitles, - shareId, - folderId, - page + 1, - ); - for (const item of nextItems) { - items.push(item); - } + if (page < Math.ceil((double) ((Map) listData.get("metadata")).get("_total") / prePage)) { + List> nextItems = listFile(shareIndex, shareData, videos, subtitles, shareId, folderId, page + 1); + items.addAll(nextItems); } - for (const dir of subDir) { - const subItems = await this.listFile(shareIndex, shareData, videos, subtitles, shareId, dir.fid); - for (const item of subItems) { - items.push(item); - } + for (Map dir : subDir) { + List> subItems = listFile(shareIndex, shareData, videos, subtitles, shareId, dir.get("fid").toString(), null); + items.addAll(subItems); } return items; } -} + + private Map findBestLCS(Item mainItem, List targetItems) { + List> results = new ArrayList<>(); + int bestMatchIndex = 0; + for (int i = 0; i < targetItems.size(); i++) { + Util.LCSResult currentLCS = Util.lcs(mainItem.getName(), targetItems.get(i).getName()); + Map result = new HashMap<>(); + result.put("target", targetItems.get(i)); + result.put("lcs", currentLCS); + results.add(result); + if (currentLCS.length > results.get(bestMatchIndex).get("lcs").toString().length()) { + bestMatchIndex = i; + } + } + Map bestMatch = results.get(bestMatchIndex); + Map finalResult = new HashMap<>(); + finalResult.put("allLCS", results); + finalResult.put("bestMatch", bestMatch); + finalResult.put("bestMatchIndex", bestMatchIndex); + return finalResult; + } + + public void getFilesByShareUrl(int shareIndex, Object shareInfo, List videos, List subtitles) throws Exception { + Map shareData = shareInfo instanceof String ? getShareData((String) shareInfo) : (Map) shareInfo; + if (shareData == null) return; + getShareToken(shareData); + if (!this.shareTokenCache.containsKey(shareData.get("shareId"))) return; + listFile(shareIndex, shareData, videos, subtitles, shareData.get("shareId"), shareData.get("folderId"), 1); + if (!subtitles.isEmpty()) { + for (Item video : videos) { + Map matchSubtitle = findBestLCS(video, subtitles); + if (matchSubtitle.get("bestMatch") != null) { + video.setSubtitle((String) ((Map) matchSubtitle.get("bestMatch")).get("target")); + } + } + } + } + + private void clean() { + saveFileIdCaches.clear(); + } + + private void clearSaveDir() throws Exception { + Map listData = Json.parseSafe(api("file/sort?" + this.pr + "&pdir_fid=" + this.saveDirId + "&_page=1&_size=200&_sort=file_type:asc,updated_at:desc", Collections.emptyMap(), 0, "GET"), Map.class); + if (listData.get("data") != null && ((List>) ((Map) listData.get("data")).get("list")).size() > 0) { + api("file/delete?" + this.pr, Map.of("action_type", 2, "filelist", ((List>) ((Map) listData.get("data")).get("list")).stream().map(v -> v.get("fid")).toArray(), "exclude_fids", new Object[]{}), 0, "POST"); + } + } + + private void createSaveDir(boolean clean) throws Exception { + if (this.saveDirId != null) { + if (clean) clearSaveDir(); + return; + } + Map listData = Json.parseSafe(api("file/sort?" + this.pr + "&pdir_fid=0&_page=1&_size=200&_sort=file_type:asc,updated_at:desc", Collections.emptyMap(), 0, "GET"), Map.class); + if (listData.get("data") != null) { + for (Map item : (List>) ((Map) listData.get("data")).get("list")) { + if (this.saveDirName.equals(item.get("file_name"))) { + this.saveDirId = item.get("fid").toString(); + clearSaveDir(); + break; + } + } + } + if (this.saveDirId == null) { + Map create = Json.parseSafe(api("file?" + this.pr, Map.of("pdir_fid", "0", "file_name", this.saveDirName, "dir_path", "", "dir_init_lock", false), 0, "POST"), Map.class); + if (create.get("data") != null && ((Map) create.get("data")).get("fid") != null) { + this.saveDirId = ((Map) create.get("data")).get("fid").toString(); + } + } + } + + private String save(String shareId, String stoken, String fileId, String fileToken, boolean clean) throws Exception { + createSaveDir(clean); + if (clean) { + clean(); + } + if (this.saveDirId == null) return null; + if (stoken == null) { + getShareToken(Map.of("shareId", shareId)); + if (!this.shareTokenCache.containsKey(shareId)) return null; + } + Map saveResult = Json.parseSafe(api("share/sharepage/save?" + this.pr, Map.of("fid_list", new String[]{fileId}, "fid_token_list", new String[]{fileToken}, "to_pdir_fid", this.saveDirId, "pwd_id", shareId, "stoken", stoken != null ? stoken : this.shareTokenCache.get(shareId).get("stoken"), "pdir_fid", "0", "scene", "link"), 0, "POST"), Map.class); + if (saveResult.get("data") != null && ((Map) saveResult.get("data")).get("task_id") != null) { + int retry = 0; + while (true) { + Map taskResult = Json.parseSafe(api("task?" + this.pr + "&task_id=" + ((Map) saveResult.get("data")).get("task_id") + "&retry_index=" + retry, Collections.emptyMap(), 0, "GET"), Map.class); + if (taskResult.get("data") != null && ((Map) taskResult.get("data")).get("save_as") != null && ((Map) ((Map) taskResult.get("data")).get("save_as")).get("save_as_top_fids") != null && ((List) ((Map) ((Map) taskResult.get("data")).get("save_as")).get("save_as_top_fids")).size() > 0) { + return ((List) ((Map) ((Map) taskResult.get("data")).get("save_as")).get("save_as_top_fids")).get(0); + } + retry++; + if (retry > 2) break; + Thread.sleep(1000); + } + } + return null; + } + + private String getLiveTranscoding(String shareId, String stoken, String fileId, String fileToken, String flag) throws Exception { + if (!this.saveFileIdCaches.containsKey(fileId)) { + String saveFileId = save(shareId, stoken, fileId, fileToken, true); + if (saveFileId == null) return null; + this.saveFileIdCaches.put(fileId, saveFileId); + } + Map transcoding = Json.parseSafe(api("file/v2/play?" + this.pr, Map.of("fid", this.saveFileIdCaches.get(fileId), "resolutions", "normal,low,high,super,2k,4k", "supports", "fmp4"), 0, "POST"), Map.class); + if (transcoding.get("data") != null && ((Map) transcoding.get("data")).get("video_list") != null) { + String flagId = flag.split("-")[flag.split("-").length - 1]; + int index = Utils.findAllIndexes(getPlayFormatList(), flagId); + String quarkFormat = getPlayFormatQuarkList().get(index); + for (Map video : (List>) ((Map) transcoding.get("data")).get("video_list")) { + if (video.get("resolution").equals(quarkFormat)) { + return video.get("video_info").toString(); + } + } + return ((List>) ((Map) transcoding.get("data")).get("video_list")).get(index).get("video_info").toString(); + } + return null; + } + + private String getDownload(String shareId, String stoken, String fileId, String fileToken, boolean clean) throws Exception { + if (!this.saveFileIdCaches.containsKey(fileId)) { + String saveFileId = save(shareId, stoken, fileId, fileToken, clean); + if (saveFileId == null) return null; + this.saveFileIdCaches.put(fileId, saveFileId); + } + Map down = Json.parseSafe(api("file/download?" + this.pr + "&uc_param_str=", Map.of("fids", this.saveFileIdCaches.get(fileId)), 0, "POST"), Map.class); + if (down.get("data") != null) { + return ((List) down.get("data")).get(0); + } + return null; + } + + // Helper method to convert bytes to hex string + private String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + // Encoding helper method + private String encodeURIComponent(String value) { + try { + return java.net.URLEncoder.encode(value, "UTF-8"); + } catch (Exception e) { + return value; + } + } -*/ +}*/ diff --git a/app/src/main/java/com/github/catvod/bean/quark/Item.java b/app/src/main/java/com/github/catvod/bean/quark/Item.java index f5e6d096..7e10bb9f 100644 --- a/app/src/main/java/com/github/catvod/bean/quark/Item.java +++ b/app/src/main/java/com/github/catvod/bean/quark/Item.java @@ -16,6 +16,7 @@ public class Item { private String shareData; private int shareIndex; private long lastUpdateAt; + private String subtitle; public Item() { this.fileId = ""; @@ -55,6 +56,14 @@ public class Item { return arr[arr.length - 1]; } + public String getSubtitle() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle = subtitle; + } + public String getFileId() { return fileId.isEmpty() ? "" : fileId; } diff --git a/app/src/main/java/com/github/catvod/net/OkHttp.java b/app/src/main/java/com/github/catvod/net/OkHttp.java index 2e278438..029d6c94 100644 --- a/app/src/main/java/com/github/catvod/net/OkHttp.java +++ b/app/src/main/java/com/github/catvod/net/OkHttp.java @@ -21,6 +21,7 @@ public class OkHttp { private OkHttpClient client; + private static class Loader { static volatile OkHttp INSTANCE = new OkHttp(); } @@ -53,6 +54,10 @@ public class OkHttp { return url.startsWith("http") ? new OkRequest(GET, url, params, header).execute(client()).getBody() : ""; } + public static OkResult get(String url, Map params, Map header) { + return new OkRequest(GET, url, params, header).execute(client()); + } + public static String post(String url, Map params) { return post(url, params, null).getBody(); } diff --git a/app/src/main/java/com/github/catvod/utils/Json.java b/app/src/main/java/com/github/catvod/utils/Json.java index 0e04437b..711fa47a 100644 --- a/app/src/main/java/com/github/catvod/utils/Json.java +++ b/app/src/main/java/com/github/catvod/utils/Json.java @@ -28,6 +28,10 @@ public class Json { } } + public static String toJson(Object obj) { + return new Gson().toJson(obj); + } + public static T parseSafe(String json, Type t) { try { return new Gson().fromJson(json, t); diff --git a/app/src/main/java/com/github/catvod/utils/Util.java b/app/src/main/java/com/github/catvod/utils/Util.java index 2b3afd01..beeb26aa 100644 --- a/app/src/main/java/com/github/catvod/utils/Util.java +++ b/app/src/main/java/com/github/catvod/utils/Util.java @@ -274,4 +274,65 @@ public class Util { public static String base64Decode(String s) { return new String(android.util.Base64.decode(s, Base64.DEFAULT), Charset.defaultCharset()); } + + /** + * 字符串相似度匹配 + * + * @returns + */ + + public static LCSResult lcs(String str1, String str2) { + if (str1 == null || str2 == null) { + return new LCSResult(0, "", 0); + } + + StringBuilder sequence = new StringBuilder(); + int str1Length = str1.length(); + int str2Length = str2.length(); + int[][] num = new int[str1Length][str2Length]; + int maxlen = 0; + int lastSubsBegin = 0; + + for (int i = 0; i < str1Length; i++) { + for (int j = 0; j < str2Length; j++) { + if (str1.charAt(i) != str2.charAt(j)) { + num[i][j] = 0; + } else { + if (i == 0 || j == 0) { + num[i][j] = 1; + } else { + num[i][j] = 1 + num[i - 1][j - 1]; + } + + if (num[i][j] > maxlen) { + maxlen = num[i][j]; + int thisSubsBegin = i - num[i][j] + 1; + if (lastSubsBegin == thisSubsBegin) { + // if the current LCS is the same as the last time this block ran + sequence.append(str1.charAt(i)); + } else { + // this block resets the string builder if a different LCS is found + lastSubsBegin = thisSubsBegin; + sequence.setLength(0); // clear it + sequence.append(str1.substring(lastSubsBegin, i + 1)); + } + } + } + } + } + return new LCSResult(maxlen, sequence.toString(), lastSubsBegin); + } + + public static class LCSResult { + public int length; + public String sequence; + public int offset; + + public LCSResult(int length, String sequence, int offset) { + this.length = length; + this.sequence = sequence; + this.offset = offset; + } + } + }