夸克部分

This commit is contained in:
lushunming 2024-08-02 11:08:05 +08:00
parent a4bc07a717
commit 98cadeb8a5
2 changed files with 288 additions and 260 deletions

View File

@ -1,326 +1,254 @@
/*
package com.github.catvod.api;
import com.github.catvod.bean.quark.Item;
import com.github.catvod.utils.Util;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class QuarkApi {
/* constructor() {
this.apiUrl = "https://drive-pc.quark.cn/1/clouddrive/"
this.cookie = ""
this.ckey = ""
this.shareTokenCache = {}
this.pr = "pr=ucpro&fr=pc"
this.subtitleExts = ['.srt', '.ass', '.scc', '.stl', '.ttml'];
this.saveFileIdCaches = {}
this.saveDirId = null
this.saveDirName = 'TV';
private static final String API_URL = "https://drive-pc.QuarkApi.cn/1/clouddrive/";
private String cookie;
private String ckey;
private Map<String, String> shareTokenCache;
private String pr = "pr=ucpro&fr=pc";
private List<String> subtitleExts;
private Map<String, String> saveFileIdCaches;
private String saveDirId;
private String saveDirName = "TV";
public QuarkApi() {
this.shareTokenCache = new HashMap<>();
this.subtitleExts = List.of(".srt", ".ass", ".scc", ".stl", ".ttml");
this.saveFileIdCaches = new HashMap<>();
}
async initQuark(cookie) {
this.ckey = Crypto.enc.Hex.stringify(Crypto.MD5(cookie)).toString();
let localCfg = await local.get("quark", "cookie");
if (!_.isEmpty(localCfg)) {
this.cookie = JSON.parse(localCfg)[this.ckey]
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 void initQuark(String cookie) {
this.ckey = bytesToHex(Util.MD5(cookie).getBytes());
String localCfg = local.get("quark", "cookie");
if (!localCfg.isEmpty()) {
Map<String, String> cookieMap = new HashMap<>();
JSONObject localCookie = new JSONObject(localCfg);
cookieMap.put(this.ckey, localCookie.getString(this.ckey));
this.cookie = cookieMap.get(this.ckey);
} else {
this.cookie = cookie
this.cookie = cookie;
}
}
private Map<String, String> getHeaders() {
return Map.of(
"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",
"Referer", "https://pan.quark.cn/",
"Content-Type", "application/json",
"Cookie", this.cookie,
"Host", "drive-pc.quark.cn"
);
public Map<String, String> getHeaders() {
return Map.of("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", "Referer", "https://pan.QuarkApi.cn/", "Content-Type", "application/json", "Cookie", this.cookie, "Host", "drive-pc.QuarkApi.cn");
}
async api(url, data, retry, method) {
const leftRetry = retry || 3;
let resp = await req(this.apiUrl + url, {
method:method || "post",
data:data,
headers:this.getHeaders()
})
if (resp.headers['set-cookie']) {
const puus = [resp.headers['set-cookie']].join(';;;').match( / __puus = ([ ^;]+)/);
if (puus) {
if ( this.cookie.match( / __puus = ([ ^;]+)/)[1] !=puus[1]){
this.cookie = this.cookie.replace( / __puus =[^;]+ /, `__puus = $ {
puus[1]
}`);
let cookieDic = {}
cookieDic[this.ckey] = this.cookie
await local.set("quark", this.cookie, JSON.stringify(cookieDic));
}
public JSONObject api(String url, String data, int retry, String method) throws ExecutionException, InterruptedException {
int leftRetry = retry != null ? retry : 3;
JSONObject resp = req(API_URL + url, method != null ? method : "post", data, getHeaders());
if (resp.headers().get("set-cookie") != null) {
String[] puus = resp.headers().get("set-cookie").split(";;;").stream().map(String::trim).filter(s -> s.startsWith("__puus=")).map(s -> s.substring(s.indexOf('=') + 1)).collect(Collectors.toList()).toArray(new String[0]);
if (puus.length > 0 && !this.cookie.matches("__puus=[^;]+") || !this.cookie.replace("__puus=[^;]+", "__puus=" + puus[0]).equals(this.cookie)) {
this.cookie = this.cookie.replace("__puus=[^;]+", "__puus=" + puus[0]);
Map<String, String> cookieDic = new HashMap<>();
cookieDic.put(this.ckey, this.cookie);
local.set("quark", this.cookie, new JSONObject(cookieDic).toString());
}
}
if (resp.code != = 200 && leftRetry > 0) {
Utils.sleep(1)
return await this.api(url, data, leftRetry - 1);
if (resp.code() != 200 && leftRetry > 0) {
Utils.sleep(1);
return api(url, data, leftRetry - 1, method);
}
return JSON.parse(resp.content) || {};
return resp;
}
getShareData(url) {
let regex = /https:\/\/pan\.quark\.cn\/s\/([ ^\\|#/]+)/;
let matches = regex.exec(url);
if (matches) {
return {
shareId:matches[1],
folderId:'0',
};
public Map<String, String> getShareData(String url) {
Pattern regex = Pattern.compile("https://pan\\.quark\\.cn\\/s\\/([^\\|#/]+)");
Matcher matches = regex.matcher(url);
if (matches.find()) {
return Map.of("shareId", matches.group(1), "folderId", "0");
}
return null;
}
async getShareToken(shareData) {
if (!this.shareTokenCache[shareData.shareId]) {
delete this.shareTokenCache[shareData.shareId];
const shareToken = await this.api(`share / sharepage / token ? $ {
this.pr
}`,{
pwd_id:
shareData.shareId,
passcode:shareData.sharePwd || '',
});
if (shareToken.data && shareToken.data.stoken) {
this.shareTokenCache[shareData.shareId] = shareToken.data;
public void getShareToken(QuarkApi.ShareData shareData) throws ExecutionException, InterruptedException {
if (!shareTokenCache.containsKey(shareData.shareId)) {
JSONObject shareToken = api("share/sharepage/token?" + pr, Map.of("pwd_id", shareData.shareId, "passcode", shareData.sharePwd), null, "get");
if (shareToken != null && shareToken.has("data") && shareToken.getJSONObject("data").has("stoken")) {
shareTokenCache.put(shareData.shareId, shareToken.getJSONObject("data").getJSONObject("stoken").toString());
}
}
}
async 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));
public List<Item> listFile(QuarkApi.ShareData shareData, List<Item> videos, List<Item> subtitles, String shareId, String folderId, int page) throws ExecutionException, InterruptedException {
int prePage = 200;
page = page != 0 ? page : 1;
JSONObject listData = api("share/sharepage/detail?" + pr + "&pwd_id=" + shareId + "&stoken=" + URLEncoder.encode(shareTokenCache.get(shareData.shareId).get("stoken")) + "&pdir_fid=" + folderId + "&force=0&_page=" + page + "&_size=" + prePage + "&_sort=file_type:asc,file_name:asc", null, null, "get");
if (listData != null && listData.has("data") && listData.getJSONObject("data").has("list")) {
List<Item> items = listData.getJSONObject("data").getJSONArray("list").toList().stream().map(item -> Item.objectFrom(item, shareData, page)).collect(Collectors.toList());
if (page < Math.ceil(listData.getJSONObject("metadata").getJSONObject("_total").toString() / prePage)) {
List<Item> nextItems = listFile(shareData, videos, subtitles, shareId, folderId, page + 1);
items.addAll(nextItems);
}
for (Item dir : items) {
if (dir.dir()) {
List<Item> subItems = listFile(shareData, videos, subtitles, shareId, dir.fid);
items.addAll(subItems);
}
}
return items;
}
return new ArrayList<>();
}
public JSONObject findBestLCS(Item mainItem, List<Item> targetItems) {
List<JSONObject> results = targetItems.stream().map(targetItem -> new JSONObject().put("target", targetItem).put("lcs", Utils.lcs(mainItem.name, targetItem.name))).collect(Collectors.toList());
int bestMatchIndex = 0;
for (int i = 1; i < results.size(); i++) {
if (results.get(i).getJSONObject("lcs").length() > results.get(bestMatchIndex).getJSONObject("lcs").length()) {
bestMatchIndex = i;
}
}
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);
}
}
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);
}
return results.get(bestMatchIndex);
}
public List<Item> getFilesByShareUrl(int shareIndex, QuarkApi.ShareData shareInfo, List<Item> videos, List<Item> subtitles) throws ExecutionException, InterruptedException {
QuarkApi.ShareData shareData = shareInfo instanceof String ? getShareData((String) shareInfo) : (QuarkApi.ShareData) shareInfo;
if (shareData == null) return Collections.emptyList();
getShareToken(shareData);
if (shareTokenCache.get(shareData.shareId) == null) return Collections.emptyList();
List<Item> items = listFile(shareIndex, shareData, videos, subtitles, shareData.shareId, shareData.folderId);
if (subtitles.size() > 0) {
videos.forEach(item -> {
JSONObject matchSubtitle = findBestLCS(item, subtitles);
if (matchSubtitle != null) {
item.subtitle = matchSubtitle.getJSONObject("target");
}
});
}
return items;
}
;
findBestLCS(mainItem, targetItems) {
const results = [];
let bestMatchIndex = 0;
for (let i = 0; i < targetItems.length; i++) {
const currentLCS = Utils.lcs(mainItem.name, targetItems[i].name);
results.push({target:targetItems[i], lcs:currentLCS });
if (currentLCS.length > results[bestMatchIndex].lcs.length) {
bestMatchIndex = i;
}
}
const bestMatch = results[bestMatchIndex];
return {allLCS:results, bestMatch:bestMatch, bestMatchIndex:bestMatchIndex };
}
async getFilesByShareUrl(shareIndex, shareInfo, videos, subtitles) {
const shareData = typeof shareInfo == = 'string' ? this.getShareData(shareInfo) : shareInfo;
if (!shareData) return [];
await this.getShareToken(shareData);
if (!this.shareTokenCache[shareData.shareId]) return [];
await
this.listFile(shareIndex, shareData, videos, subtitles, shareData.shareId, shareData.folderId);
if (subtitles.length > 0) {
videos.forEach((item) = > {
var matchSubtitle = this.findBestLCS(item, subtitles);
if (matchSubtitle.bestMatch) {
item.subtitle = matchSubtitle.bestMatch.target;
}
});
public void clean() {
Map<String, String> saves = new HashMap<>(saveFileIdCaches);
for (String save : saves.keySet()) {
saveFileIdCaches.remove(save);
}
}
clean() {
const saves = Object.keys(this.saveFileIdCaches);
for (const save of saves){
delete this.saveFileIdCaches[save];
public void clearSaveDir() throws ExecutionException, InterruptedException {
JSONObject listData = api("file/sort?" + pr + "&pdir_fid=" + saveDirId + "&_page=1&_size=200&_sort=file_type:asc,updated_at:desc", null, null, "get");
if (listData != null && listData.has("data") && listData.getJSONObject("data").has("list") && listData.getJSONObject("data").getJSONArray("list").length() > 0) {
api("file/delete?" + pr, Map.of("action_type", 2, "filelist", listData.getJSONObject("data").getJSONArray("list").toList().stream().map(f -> f.getString("fid")).collect(Collectors.toList()), "exclude_fids", new JSONArray()));
}
}
async clearSaveDir() {
const listData = await this.api(`file / sort ? $ {
this.pr
}&pdir_fid = $ {
this.saveDirId
}&_page = 1 & _size = 200 & _sort = file_type:asc, updated_at:desc`,{
},{
},'get');
if (listData.data && listData.data.list && listData.data.list.length > 0) {
await this.api(`file / delete ? $ {
this.pr
}`,{
action_type:
2,
filelist:listData.data.list.map((v) = > v.fid),
exclude_fids: [],
});
}
}
async createSaveDir(clean) {
if (this.saveDirId) {
// 删除所有子文件
if (clean) await this.clearSaveDir();
public void createSaveDir(boolean clean) throws ExecutionException, InterruptedException {
if (saveDirId != null) {
if (clean) clearSaveDir();
return;
}
const listData = await this.api(`file / sort ? $ {
this.pr
}&pdir_fid = 0 & _page = 1 & _size = 200 & _sort = file_type:asc, updated_at:desc`,{
},{
},'get');
if (listData.data && listData.data.list)
for (const item of listData.data.list){
if (item.file_name == = this.saveDirName) {
this.saveDirId = item.fid;
await this.clearSaveDir();
break;
JSONObject listData = api("file/sort?" + pr + "&pdir_fid=0&_page=1&_size=200&_sort=file_type:asc,updated_at:desc", null, null, "get");
if (listData != null && listData.has("data") && listData.getJSONObject("data").has("list")) {
for (JSONObject item : listData.getJSONObject("data").getJSONArray("list").toList()) {
if (item.getString("file_name").equals(saveDirName)) {
saveDirId = item.getString("fid");
clearSaveDir();
break;
}
}
}
if (!this.saveDirId) {
const create = await this.api(`file ? $ {
this.pr
}`,{
pdir_fid:
'0',
file_name:this.saveDirName,
dir_path:'',
dir_init_lock:false,
});
if (create.data && create.data.fid) {
this.saveDirId = create.data.fid;
if (saveDirId == null) {
JSONObject create = api("file?" + pr, Map.of("pdir_fid", "0", "file_name", saveDirName, "dir_path", "", "dir_init_lock", false));
if (create != null && create.has("data") && create.getJSONObject("data").has("fid")) {
saveDirId = create.getJSONObject("data").getString("fid");
}
}
}
async save(shareId, stoken, fileId, fileToken, clean) {
await this.createSaveDir(clean);
if (clean) {
this.clean()
public String save(String shareId, String stoken, String fileId, String fileToken, boolean clean) throws ExecutionException, InterruptedException {
createSaveDir(clean);
if (clean) clean();
if (saveDirId == null) return null;
if (stoken == null) {
getShareToken(Map.of("shareId", shareId));
if (shareTokenCache.get(shareId) == null) return null;
}
if (!this.saveDirId) return null;
if (!stoken) {
await this.getShareToken({
shareId:shareId,
});
if (!this.shareTokenCache[shareId]) return null;
}
const saveResult = await this.api(`share / sharepage / save ? $ {
this.pr
}`,{
fid_list: [fileId],
fid_token_list: [fileToken],
to_pdir_fid:
this.saveDirId,
pwd_id:shareId,
stoken:stoken || this.shareTokenCache[shareId].stoken,
pdir_fid:'0',
scene:'link',
});
if (saveResult.data && saveResult.data.task_id) {
let retry = 0;
// wait task finish
JSONObject saveResult = api("share/sharepage/save?" + pr, Map.of("fid_list", new JSONArray().put(fileId), "fid_token_list", new JSONArray().put(fileToken), "to_pdir_fid", saveDirId, "pwd_id", shareId, "stoken", stoken != null ? stoken : shareTokenCache.get(shareId).getJSONObject("stoken").toString(), "pdir_fid", "0", "scene", "link"));
if (saveResult != null && saveResult.has("data") && saveResult.getJSONObject("data").has("task_id")) {
int retry = 0;
while (true) {
const taskResult = await this.api(`task ? $ {
this.pr
}&task_id = $ {
saveResult.data.task_id
}&retry_index = $ {
retry
}`,{
},{
},'get');
if (taskResult.data && taskResult.data.save_as && taskResult.data.save_as.save_as_top_fids && taskResult.data.save_as.save_as_top_fids.length > 0) {
return taskResult.data.save_as.save_as_top_fids[0];
JSONObject taskResult = api("task?" + pr + "&task_id=" + saveResult.getJSONObject("data").getString("task_id") + "&retry_index=" + retry, null, null, "get");
if (taskResult != null && taskResult.has("data") && taskResult.getJSONObject("data").has("save_as") && taskResult.getJSONObject("data").getJSONObject("save_as").has("save_as_top_fids") && taskResult.getJSONObject("data").getJSONObject("save_as").getJSONArray("save_as_top_fids").length() > 0) {
return taskResult.getJSONObject("data").getJSONObject("save_as").getJSONArray("save_as_top_fids").getJSONObject(0).getString("fid");
}
retry++;
if (retry > 2) break;
Utils.sleep(1);
TimeUnit.SECONDS.sleep(1);
}
}
return false;
return "false";
}
async getLiveTranscoding(shareId, stoken, fileId, fileToken) {
if (!this.saveFileIdCaches[fileId]) {
const saveFileId = await this.save(shareId, stoken, fileId, fileToken, true);
if (!saveFileId) return null;
this.saveFileIdCaches[fileId] = saveFileId;
public List<JSONObject> getLiveTranscoding(String shareId, String stoken, String fileId, String fileToken) throws ExecutionException, InterruptedException {
if (!saveFileIdCaches.containsKey(fileId)) {
String saveFileId = save(shareId, stoken, fileId, fileToken, true);
if (saveFileId == null) return null;
saveFileIdCaches.put(fileId, saveFileId);
}
const transcoding = await this.api(`file / v2 / play ? $ {
this.pr
}`,{
fid:
this.saveFileIdCaches[fileId],
resolutions:'normal,low,high,super,2k,4k',
supports:'fmp4',
});
if (transcoding.data && transcoding.data.video_list) {
return transcoding.data.video_list;
JSONObject transcoding = api("file/v2/play?" + pr, Map.of("fid", saveFileIdCaches.get(fileId), "resolutions", "normal,low,high,super,2k,4k", "supports", "fmp4"));
if (transcoding != null && transcoding.has("data") && transcoding.getJSONObject("data").has("video_list")) {
return transcoding.getJSONObject("data").getJSONArray("video_list").toList();
}
return null;
}
async getDownload(shareId, stoken, fileId, fileToken, clean) {
if (!this.saveFileIdCaches[fileId]) {
const saveFileId = await this.save(shareId, stoken, fileId, fileToken, clean);
if (!saveFileId) return null;
this.saveFileIdCaches[fileId] = saveFileId;
public JSONObject getDownload(String shareId, String stoken, String fileId, String fileToken, boolean clean) throws ExecutionException, InterruptedException {
if (!saveFileIdCaches.containsKey(fileId)) {
String saveFileId = save(shareId, stoken, fileId, fileToken, clean);
if (saveFileId == null) return null;
saveFileIdCaches.put(fileId, saveFileId);
}
const down = await this.api(`file / download ? $ {
this.pr
}&uc_param_str =`,{
fids: [this.saveFileIdCaches[fileId]],
});
if (down.data) {
return down.data[0];
JSONObject down = api("file/download?" + pr + "&uc_param_str", Map.of("fids", new JSONArray().put(saveFileIdCaches.get(fileId))));
if (down != null && down.has("data")) {
return down.getJSONObject("data").getJSONObject(0);
}
return null;
}
*/
public static class ShareData {
private String shareId;
private String folderId;
private String sharePwd;
public ShareData(String shareId, String folderId, String sharePwd) {
this.shareId = shareId;
this.folderId = folderId;
this.sharePwd = sharePwd;
}
}
}
*/

View File

@ -0,0 +1,100 @@
package com.github.catvod.bean.quark;
import java.util.regex.Pattern;
public class Item {
private String fileId;
private String shareId;
private String shareToken;
private String shareFileToken;
private String seriesId;
private String name;
private String type;
private String formatType;
private String size;
private String parent;
private String shareData;
private int shareIndex;
private long lastUpdateAt;
public Item() {
this.fileId = "";
this.shareId = "";
this.shareToken = "";
this.shareFileToken = "";
this.seriesId = "";
this.name = "";
this.type = "";
this.formatType = "";
this.size = "";
this.parent = "";
this.shareData = null;
this.shareIndex = 0;
this.lastUpdateAt = 0;
}
public static Item objectFrom(String item_json, String shareId, int shareIndex) {
Item item = new Item();
item.fileId = item_json.contains("fid") ? item_json.split("fid=")[1].split("&")[0] : "";
item.shareId = shareId;
item.shareToken = item_json.contains("stoken") ? item_json.split("stoken=")[1].split("&")[0] : "";
item.shareFileToken = item_json.contains("share_fid_token") ? item_json.split("share_fid_token=")[1].split("&")[0] : "";
item.seriesId = item_json.contains("series_id") ? item_json.split("series_id=")[1].split("&")[0] : "";
item.name = item_json.contains("file_name") ? item_json.split("file_name=")[1].split("&")[0] : "";
item.type = item_json.contains("obj_category") ? item_json.split("obj_category=")[1].split("&")[0] : "";
item.formatType = item_json.contains("format_type") ? item_json.split("format_type=")[1].split("&")[0] : "";
item.size = item_json.contains("size") ? item_json.split("size=")[1].split("&")[0] : "";
item.parent = item_json.contains("pdir_fid") ? item_json.split("pdir_fid=")[1].split("&")[0] : "";
item.lastUpdateAt = item_json.contains("last_update_at") ? Long.parseLong(item_json.split("last_update_at=")[1].split("&")[0]) : 0;
item.shareIndex = shareIndex;
return item;
}
public String getFileExtension() {
String[] arr = name.split("\\.");
return arr[arr.length - 1];
}
public String getFileId() {
return fileId.isEmpty() ? "" : fileId;
}
public String getName() {
return name.isEmpty() ? "" : name;
}
public String getParent() {
return parent.isEmpty() ? "" : "[" + parent + "]";
}
public String getSize() {
return size.equals("0") ? "" : "[" + size + "]";
}
public int getShareIndex() {
return shareIndex;
}
public String getDisplayName(String type_name) {
String name = getName();
if (type_name.equals("电视剧")) {
String[] replaceNameList = {"4k", "4K"};
name = name.replaceAll("\\." + getFileExtension(), "");
for (String replaceName : replaceNameList) {
name = name.replaceAll(replaceName, "");
}
name = Pattern.compile("/\\.S01E(.*?)\\./").matcher(name).find() ? name.split("/\\.S01E(.*?)\\./")[1] : name;
String[] numbers = name.split("\\d+");
if (numbers.length > 0) {
name = numbers[0];
}
}
return name + " " + size;
}
public String getEpisodeUrl(String type_name) {
return getDisplayName(type_name) + "$" + getFileId() + "++" + shareFileToken + "++" + shareId + "++" + shareToken;
}
}