From a30b7789583858d00f910c986bce02efa51f2e81 Mon Sep 17 00:00:00 2001 From: lushunming <1357197829@qq.com> Date: Sat, 2 Nov 2024 10:53:12 +0800 Subject: [PATCH] uc --- .../java/com/github/catvod/api/UCApi.java | 695 ++++++++++++++++++ .../java/com/github/catvod/bean/uc/Cache.java | 38 + .../java/com/github/catvod/bean/uc/Item.java | 112 +++ .../com/github/catvod/bean/uc/ShareData.java | 36 + .../java/com/github/catvod/bean/uc/User.java | 30 + .../java/com/github/catvod/spider/UC.java | 86 +++ 6 files changed, 997 insertions(+) create mode 100644 app/src/main/java/com/github/catvod/api/UCApi.java create mode 100644 app/src/main/java/com/github/catvod/bean/uc/Cache.java create mode 100644 app/src/main/java/com/github/catvod/bean/uc/Item.java create mode 100644 app/src/main/java/com/github/catvod/bean/uc/ShareData.java create mode 100644 app/src/main/java/com/github/catvod/bean/uc/User.java create mode 100644 app/src/main/java/com/github/catvod/spider/UC.java diff --git a/app/src/main/java/com/github/catvod/api/UCApi.java b/app/src/main/java/com/github/catvod/api/UCApi.java new file mode 100644 index 00000000..261f133f --- /dev/null +++ b/app/src/main/java/com/github/catvod/api/UCApi.java @@ -0,0 +1,695 @@ +package com.github.catvod.api; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.github.catvod.bean.Result; +import com.github.catvod.bean.Vod; +import com.github.catvod.bean.uc.Cache; +import com.github.catvod.bean.uc.Item; +import com.github.catvod.bean.uc.ShareData; +import com.github.catvod.bean.uc.User; +import com.github.catvod.crawler.SpiderDebug; +import com.github.catvod.net.OkHttp; +import com.github.catvod.net.OkResult; +import com.github.catvod.spider.Init; +import com.github.catvod.spider.Proxy; +import com.github.catvod.utils.*; +import com.google.gson.Gson; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.charset.Charset; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UCApi { + private String apiUrl = "https://pc-api.uc.cn/1/clouddrive/"; + private String cookie = ""; + private String ckey = ""; + private Map> shareTokenCache = new HashMap<>(); + private String pr = "pr=UCBrowser&fr=pc"; + 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 final Cache cache; + private ScheduledExecutorService service; + + + private AlertDialog dialog; + private String serviceTicket; + + public Object[] proxyVideo(Map params) throws Exception { + String url = Util.base64Decode(params.get("url")); + Map header = new Gson().fromJson(Util.base64Decode(params.get("header")), Map.class); + if (header == null) header = new HashMap<>(); + List arr = List.of("Range", "Accept", "Accept-Encoding", "Accept-Language", "Cookie", "Origin", "Referer", "Sec-Ch-Ua", "Sec-Ch-Ua-Mobile", "Sec-Ch-Ua-Platform", "Sec-Fetch-Dest", "Sec-Fetch-Mode", "Sec-Fetch-Site", "User-Agent"); + for (String key : params.keySet()) { + for (String s : arr) { + if (s.toLowerCase().equals(key.toLowerCase())) { + header.put(key, params.get(key)); + } + } + + } + if (Util.getExt(url).contains("m3u8")) { + return getM3u8(url, header); + } + return ProxyVideo.proxy(url, header); + } + + /** + * 代理m3u8 + * + * @param url + * @param header + * @return + */ + private Object[] getM3u8(String url, Map header) { + + OkResult result = OkHttp.get(url, new HashMap<>(), header); + String[] m3u8Arr = result.getBody().split("\n"); + List listM3u8 = new ArrayList<>(); + + String site = url.substring(0, url.lastIndexOf("/")) + "/"; + int mediaId = 0; + for (String oneLine : m3u8Arr) { + String thisOne = oneLine; + if (oneLine.contains(".ts")) { + thisOne = proxyVideoUrl(site + thisOne, header); + mediaId++; + } + listM3u8.add(thisOne); + } + String m3u8Str = TextUtils.join("\n", listM3u8); + String contentType = result.getResp().get("Content-Type").get(0); + + Map respHeaders = new HashMap<>(); + for (String key : result.getResp().keySet()) { + respHeaders.put(key, result.getResp().get(key).get(0)); + } + return new Object[]{result.getCode(), contentType, new ByteArrayInputStream(m3u8Str.getBytes(Charset.forName("UTF-8"))), respHeaders}; + } + + private static class Loader { + static volatile UCApi INSTANCE = new UCApi(); + } + + public static UCApi get() { + return UCApi.Loader.INSTANCE; + } + + public void setCookie(String token) throws Exception { + if (StringUtils.isNoneBlank(token)) { + this.cookie = token; + initUserInfo(); + } + } + + private Map getHeaders() { + Map headers = new HashMap<>(); + headers.put("User-Agent", "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"); + headers.put("Referer", "https://drive.uc.cn"); + headers.put("Content-Type", "application/json"); + headers.put("Cookie", cookie); + //headers.put("Host", "drive-pc.quark.cn"); + return headers; + } + + private Map getWebHeaders() { + Map headers = new HashMap<>(); + headers.put("User-Agent", "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"); + headers.put("Referer", "https://drive.uc.cn"); + headers.put("Cookie", cookie); + return headers; + } + + public void initQuark(String cookie) throws Exception { + this.ckey = Util.MD5(cookie); + this.cookie = cookie; + this.isVip = getVip(); + } + + private UCApi() { + Init.checkPermission(); + + cache = Cache.objectFrom(Path.read(getCache())); + } + + public File getCache() { + return Path.tv("uc"); + } + + public Vod getVod(ShareData shareData) throws Exception { + getShareToken(shareData); + List files = new ArrayList<>(); + List subs = new ArrayList<>(); + List> listData = listFile(1, shareData, files, subs, shareData.getShareId(), shareData.getFolderId(), 1); + + List playFrom = UCApi.get().getPlayFormatList(); + playFrom = new ArrayList<>(playFrom); + playFrom.add("原画"); + + List playUrl = new ArrayList<>(); + + if (files.isEmpty()) { + return null; + } + for (int i = 0; i < files.get(files.size() - 1).getShareIndex(); i++) { + for (int index = 0; index < playFrom.size(); index++) { + List vodItems = new ArrayList<>(); + for (Item video_item : files) { + if (video_item.getShareIndex() == i + 1) { + vodItems.add(video_item.getEpisodeUrl("电影"));// + findSubs(video_item.getName(), subs)); + } + } + playUrl.add(TextUtils.join("#", vodItems)); + } + } + + + Vod vod = new Vod(); + vod.setVodId(""); + vod.setVodContent(""); + vod.setVodPic(""); + vod.setVodName(""); + vod.setVodPlayUrl(TextUtils.join("$$$", playUrl)); + vod.setVodPlayFrom(TextUtils.join("$$$", playFrom)); + vod.setTypeName("夸克云盘"); + return vod; + } + + public String playerContent(String[] split, String flag) throws Exception { + + String fileId = split[0], fileToken = split[1], shareId = split[2], stoken = split[3]; + String playUrl = ""; + if (flag.contains("uc原画")) { + playUrl = this.getDownload(shareId, stoken, fileId, fileToken, true); + } else { + playUrl = this.getLiveTranscoding(shareId, stoken, fileId, fileToken, flag); + } + Map header = getHeaders(); + header.remove("Host"); + header.remove("Content-Type"); + return Result.get().url(proxyVideoUrl(playUrl, header)).octet().header(header).string(); + } + + private String proxyVideoUrl(String url, Map header) { + return String.format(Proxy.getUrl() + "?do=uc&type=video&url=%s&header=%s", Util.base64Encode(url.getBytes(Charset.defaultCharset())), Util.base64Encode(Json.toJson(header).getBytes(Charset.defaultCharset()))); + } + + /** + * @param url + * @param params get 参数 + * @param data post json + * @param retry + * @param method + * @return + * @throws Exception + */ + private String api(String url, Map params, Map data, Integer retry, String method) throws Exception { + + + int leftRetry = retry != null ? retry : 3; + if (StringUtils.isAllBlank(cookie)) { + this.initUserInfo(); + return api(url, params, data, leftRetry - 1, method); + } + OkResult okResult; + if ("GET".equals(method)) { + okResult = OkHttp.get(this.apiUrl + url, params, getHeaders()); + } else { + okResult = OkHttp.post(this.apiUrl + url, Json.toJson(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)); + } else { + this.cookie = this.cookie + ";__puus=" + matcher.group(1); + } + } + } + + if (okResult.getCode() != 200 && leftRetry > 0) { + SpiderDebug.log("api error code:" + okResult.getCode()); + Thread.sleep(1000); + return api(url, params, data, leftRetry - 1, method); + } + return okResult.getBody(); + } + + private void initUserInfo() { + try { + SpiderDebug.log("initUserInfo..."); + + //extend没有cookie,从缓存中获取 + if (StringUtils.isAllBlank(cookie)) { + SpiderDebug.log(" cookie from ext is empty..."); + cookie = cache.getUser().getCookie(); + } + //获取到cookie,初始化uc,并且把cookie缓存一次 + if (StringUtils.isNoneBlank(cookie) && cookie.contains("__pus")) { + SpiderDebug.log(" initQuark ..."); + initQuark(this.cookie); + cache.setUser(User.objectFrom(this.cookie)); + return; + } + + //没有cookie,也没有serviceTicket,抛出异常,提示用户重新登录 + if (StringUtils.isAllBlank(cookie) && StringUtils.isAllBlank(serviceTicket)) { + SpiderDebug.log("cookie为空"); + throw new RuntimeException("cookie为空"); + } + + String token = serviceTicket; + OkResult result = OkHttp.get("https://drive.uc.cn/account/info?st=" + token + "", new HashMap<>(), getWebHeaders()); + Map json = Json.parseSafe(result.getBody(), Map.class); + if (json.get("success").equals(Boolean.TRUE)) { + List cookies = result.getResp().get("set-Cookie"); + List cookieList = new ArrayList<>(); + for (String cookie : cookies) { + cookieList.add(cookie.split(";")[0]); + } + this.cookie += TextUtils.join(";", cookieList); + + cache.setUser(User.objectFrom(this.cookie)); + if (cache.getUser().getCookie().isEmpty()) throw new Exception(this.cookie); + initQuark(this.cookie); + } + + } catch (Exception e) { + cache.getUser().clean(); + e.printStackTrace(); + stopService(); + startFlow(); + } finally { + while (cache.getUser().getCookie().isEmpty()) SystemClock.sleep(250); + } + } + + /** + * 获取二维码登录的令牌 + * + * @return 返回包含二维码登录令牌的字符串 + */ + private String getTokenForQrcodeLogin() { + Map params = new HashMap<>(); + params.put("client_id", "381"); + params.put("v", "1.2"); + params.put("request_id", UUID.randomUUID().toString()); + OkResult res = OkHttp.post("https://api.open.uc.cn/cas/ajax/getTokenForQrcodeLogin?__dt="+ RandomUtils.nextInt(1000,100000) +"&__t="+new Date().getTime(), params, new HashMap<>()); + if (this.cookie.isEmpty()) { + List cookies = res.getResp().get("set-Cookie"); + List cookieList = new ArrayList<>(); + for (String cookie : cookies) { + cookieList.add(cookie.split(";")[0]); + } + this.cookie = TextUtils.join(";", cookieList); + } + Map json = Json.parseSafe(res.getBody(), Map.class); + if (Objects.equals(json.get("message"), "ok")) { + return (String) ((Map) ((Map) json.get("data")).get("members")).get("token"); + } + return ""; + } + + + /** + * 获取二维码内容 + *

+ * 此方法用于生成二维码的URL内容该URL用于二维码登录,包含了登录所需的token和客户端信息 + * + * @return 返回包含token的二维码URL字符串 + */ + private String getQrCodeToken() { + // 获取用于二维码登录的token + String token = getTokenForQrcodeLogin(); + // 组装二维码URL,包含token和客户端标识 + return token; + } + + + public ShareData getShareData(String url) { + Pattern pattern = Pattern.compile("https://pan\\.quark\\.cn/s/([^\\\\|#/]+)"); + Matcher matcher = pattern.matcher(url); + if (matcher.find()) { + return new ShareData(matcher.group(1), "0"); + } + return null; + } + + 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, null, 0, "GET"), Map.class); + return ((Map) listData.get("data")).get("member_type").contains("VIP"); + } + + public List getPlayFormatList() { + if (this.isVip) { + return Arrays.asList("4K", "超清", "高清", "普画"); + } else { + return Collections.singletonList("普画"); + } + } + + private List getPlayFormatQuarkList() { + if (this.isVip) { + return Arrays.asList("4k", "2k", "super", "high", "normal", "low"); + } else { + return Collections.singletonList("low"); + } + } + + private void getShareToken(ShareData shareData) throws Exception { + if (!this.shareTokenCache.containsKey(shareData.getShareId())) { + this.shareTokenCache.remove(shareData.getShareId()); + Map shareToken = Json.parseSafe(api("share/sharepage/token?" + this.pr, Collections.emptyMap(), Map.of("pwd_id", shareData.getShareId(), "passcode", shareData.getSharePwd() == null ? "" : shareData.getSharePwd()), 0, "POST"), Map.class); + if (shareToken.containsKey("data") && ((Map) shareToken.get("data")).containsKey("stoken")) { + this.shareTokenCache.put(shareData.getShareId(), (Map) shareToken.get("data")); + } + } + } + + private List> listFile(int shareIndex, ShareData 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", Collections.emptyMap(), Collections.emptyMap(), 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 ((Double) item.get("size") < 1024 * 1024 * 5) continue; + item.put("stoken", this.shareTokenCache.get(shareData.getShareId()).get("stoken")); + videos.add(Item.objectFrom(item, shareData.getShareId(), shareIndex)); + } else if ("file".equals(item.get("type")) && this.subtitleExts.contains("." + Util.getExt((String) item.get("file_name")))) { + subtitles.add(Item.objectFrom(item, shareData.getShareId(), shareIndex)); + } + } + 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 (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, String shareInfo, List videos, List subtitles) throws Exception { + ShareData shareData = getShareData((String) shareInfo); + if (shareData == null) return; + getShareToken(shareData); + if (!this.shareTokenCache.containsKey(shareData.getShareId())) return; + listFile(shareIndex, shareData, videos, subtitles, shareData.getShareId(), shareData.getFolderId(), 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(), Collections.emptyMap(), 0, "GET"), Map.class); + if (listData.get("data") != null && ((List>) ((Map) listData.get("data")).get("list")).size() > 0) { + List list = new ArrayList<>(); + for (Map stringStringMap : ((List>) ((Map) listData.get("data")).get("list"))) { + list.add((String) stringStringMap.get("fid")); + } + api("file/delete?" + this.pr, Collections.emptyMap(), Map.of("action_type", "2", "filelist", Json.toJson(list), "exclude_fids", ""), 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(), 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, Collections.emptyMap(), 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(new ShareData(shareId, null)); + if (!this.shareTokenCache.containsKey(shareId)) return null; + } + + Map saveResult = Json.parseSafe(api("share/sharepage/save?" + this.pr, null, Map.of("fid_list", List.of(fileId), "fid_token_list", List.of(fileToken), "to_pdir_fid", this.saveDirId, "pwd_id", shareId, "stoken", stoken != null ? stoken : (String) 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(), 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, Collections.emptyMap(), 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 = Util.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 (String) ((Map) video.get("video_info")).get("url"); + } + } + return (String) ((Map) ((List>) ((Map) transcoding.get("data")).get("video_list")).get(index).get("video_info")).get("url"); + } + 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=", Collections.emptyMap(), Map.of("fids", List.of(this.saveFileIdCaches.get(fileId))), 0, "POST"), Map.class); + if (down.get("data") != null) { + return ((List>) down.get("data")).get(0).get("download_url").toString(); + } + 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; + } + } + + private void startFlow() { + Init.run(this::showInput); + } + + private void showInput() { + try { + int margin = ResUtil.dp2px(16); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + FrameLayout frame = new FrameLayout(Init.context()); + params.setMargins(margin, margin, margin, margin); + EditText input = new EditText(Init.context()); + frame.addView(input, params); + dialog = new AlertDialog.Builder(Init.getActivity()).setTitle("请输入cookie").setView(frame).setNeutralButton("QRCode", (dialog, which) -> onNeutral()).setNegativeButton(android.R.string.cancel, null).setPositiveButton(android.R.string.ok, (dialog, which) -> onPositive(input.getText().toString())).show(); + } catch (Exception ignored) { + } + } + + private void onNeutral() { + dismiss(); + Init.execute(this::getQRCode); + } + + private void onPositive(String text) { + dismiss(); + Init.execute(() -> { + if (text.startsWith("http")) setToken(OkHttp.string(text)); + else setToken(text); + }); + } + + private void getQRCode() { + String token = getQrCodeToken(); + + Init.run(() -> openApp(token)); + } + + private void openApp(String token) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setClassName("com.alicloud.databox", "com.taobao.login4android.scan.QrScanActivity"); + intent.putExtra("key_scanParam", token); + Init.getActivity().startActivity(intent); + } catch (Exception e) { + showQRCode("https://su.uc.cn/1_n0ZCv?uc_param_str=dsdnfrpfbivesscpgimibtbmnijblauputogpintnwktprchmt&token=" + token + "&client_id=381&uc_biz_str=S%3Acustom%7CC%3Atitlebar_fix"); + } finally { + Map map = new HashMap<>(); + map.put("token", token); + Init.execute(() -> startService(map)); + } + } + + private void showQRCode(String content) { + try { + int size = ResUtil.dp2px(240); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size); + ImageView image = new ImageView(Init.context()); + image.setScaleType(ImageView.ScaleType.CENTER_CROP); + image.setImageBitmap(QRCode.getBitmap(content, size, 2)); + FrameLayout frame = new FrameLayout(Init.context()); + params.gravity = Gravity.CENTER; + frame.addView(image, params); + dialog = new AlertDialog.Builder(Init.getActivity()).setView(frame).setOnCancelListener(this::dismiss).setOnDismissListener(this::dismiss).show(); + dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + Notify.show("请使用夸克网盘App扫描二维码"); + } catch (Exception ignored) { + } + } + + private void startService(Map params) { + SpiderDebug.log("----startservice"); + params.put("client_id", "381"); + params.put("v", "1.2"); + params.put("request_id", UUID.randomUUID().toString()); + service = Executors.newScheduledThreadPool(1); + + service.scheduleWithFixedDelay(() -> { + SpiderDebug.log("----scheduleAtFixedRate" + new Date().toString()); + String result = OkHttp.string("https://api.open.uc.cn/cas/ajax/getServiceTicketByQrcodeToken?__dt="+ RandomUtils.nextInt(1000,100000) +"&__t="+new Date().getTime(), params, getWebHeaders()); + Map json = Json.parseSafe(result, Map.class); + if (json.get("status").equals(new Double(2000000))) { + setToken((String) ((Map) ((Map) json.get("data")).get("members")).get("service_ticket")); + + } + + }, 1, 3, TimeUnit.SECONDS); + } + + private void setToken(String value) { + this.serviceTicket = value; + SpiderDebug.log("ServiceTicket:" + value); + Notify.show("ServiceTicket:" + value); + initUserInfo(); + stopService(); + } + + private void stopService() { + if (service != null) service.shutdownNow(); + + + Init.run(this::dismiss); + } + + private void dismiss(DialogInterface dialog) { + stopService(); + } + + private void dismiss() { + try { + if (dialog != null) dialog.dismiss(); + } catch (Exception ignored) { + } + } + +} + diff --git a/app/src/main/java/com/github/catvod/bean/uc/Cache.java b/app/src/main/java/com/github/catvod/bean/uc/Cache.java new file mode 100644 index 00000000..4c04e245 --- /dev/null +++ b/app/src/main/java/com/github/catvod/bean/uc/Cache.java @@ -0,0 +1,38 @@ +package com.github.catvod.bean.uc; + +import com.github.catvod.api.QuarkApi; +import com.github.catvod.spider.Init; +import com.github.catvod.utils.Path; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; + +public class Cache { + + @SerializedName("user") + private User user; + + + public static Cache objectFrom(String str) { + Cache item = new Gson().fromJson(str, Cache.class); + return item == null ? new Cache() : item; + } + + public User getUser() { + return user == null ? new User("") : user; + } + + public void setUser(User user) { + this.user = user; + this.save(); + } + + + public void save() { + Init.execute(() -> Path.write(QuarkApi.get().getCache(), toString())); + } + + @Override + public String toString() { + return new Gson().toJson(this); + } +} diff --git a/app/src/main/java/com/github/catvod/bean/uc/Item.java b/app/src/main/java/com/github/catvod/bean/uc/Item.java new file mode 100644 index 00000000..5f76d7ac --- /dev/null +++ b/app/src/main/java/com/github/catvod/bean/uc/Item.java @@ -0,0 +1,112 @@ +package com.github.catvod.bean.uc; + +import com.github.catvod.utils.Util; + +import java.util.Map; +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 Double size; + private String parent; + private String shareData; + private int shareIndex; + private Double lastUpdateAt; + private String subtitle; + + public Item() { + this.fileId = ""; + this.shareId = ""; + this.shareToken = ""; + this.shareFileToken = ""; + this.seriesId = ""; + this.name = ""; + this.type = ""; + this.formatType = ""; + this.size = 0d; + this.parent = ""; + this.shareData = null; + this.shareIndex = 0; + this.lastUpdateAt = 0d; + } + + public static Item objectFrom(Map item_json, String shareId, int shareIndex) { + Item item = new Item(); + item.fileId = item_json.get("fid") != null ? (String) item_json.get("fid") : ""; + item.shareId = shareId; + item.shareToken = item_json.get("stoken") != null ? (String) item_json.get("stoken") : ""; + item.shareFileToken = item_json.get("share_fid_token") != null ? (String) item_json.get("share_fid_token") : ""; + item.seriesId = item_json.get("series_id") != null ? (String) item_json.get("series_id") : ""; + item.name = item_json.get("file_name") != null ? (String) item_json.get("file_name") : ""; + item.type = item_json.get("obj_category") != null ? (String) item_json.get("obj_category") : ""; + item.formatType = item_json.get("format_type") != null ? (String) item_json.get("format_type") : ""; + item.size = item_json.get("size") != null ? (Double) item_json.get("size") : 0d; + item.parent = item_json.get("pdir_fid") != null ? (String) item_json.get("pdir_fid") : ""; + item.lastUpdateAt = item_json.get("last_update_at") != null ? (Double) item_json.get("last_update_at") : Double.valueOf(0d); + item.shareIndex = shareIndex; + return item; + } + + public String getFileExtension() { + String[] arr = name.split("\\."); + 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; + } + + 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 + " " + Util.getSize(size); + } + + public String getEpisodeUrl(String type_name) { + return getDisplayName(type_name) + "$" + getFileId() + "++" + shareFileToken + "++" + shareId + "++" + shareToken; + } +} + diff --git a/app/src/main/java/com/github/catvod/bean/uc/ShareData.java b/app/src/main/java/com/github/catvod/bean/uc/ShareData.java new file mode 100644 index 00000000..aad568f3 --- /dev/null +++ b/app/src/main/java/com/github/catvod/bean/uc/ShareData.java @@ -0,0 +1,36 @@ +package com.github.catvod.bean.uc; + +public class ShareData { + private String shareId; + private String folderId; + private String sharePwd ; + + public ShareData(String shareId, String folderId) { + this.shareId = shareId; + this.folderId = folderId; + } + + public String getSharePwd() { + return sharePwd; + } + + public void setSharePwd(String sharePwd) { + this.sharePwd = sharePwd; + } + + public String getShareId() { + return shareId; + } + + public void setShareId(String shareId) { + this.shareId = shareId; + } + + public String getFolderId() { + return folderId; + } + + public void setFolderId(String folderId) { + this.folderId = folderId; + } +} diff --git a/app/src/main/java/com/github/catvod/bean/uc/User.java b/app/src/main/java/com/github/catvod/bean/uc/User.java new file mode 100644 index 00000000..714f3739 --- /dev/null +++ b/app/src/main/java/com/github/catvod/bean/uc/User.java @@ -0,0 +1,30 @@ +package com.github.catvod.bean.uc; + +import com.google.gson.annotations.SerializedName; + +public class User { + public User(String cookie) { + this.cookie = cookie; + } + + @SerializedName("cookie") + private String cookie; + + public String getCookie() { + return cookie; + } + + public void setCookie(String cookie) { + this.cookie = cookie; + } + + public static User objectFrom(String cookie) { + return new User(cookie); + } + + + public void clean() { + this.cookie = ""; + + } +} diff --git a/app/src/main/java/com/github/catvod/spider/UC.java b/app/src/main/java/com/github/catvod/spider/UC.java new file mode 100644 index 00000000..114871e3 --- /dev/null +++ b/app/src/main/java/com/github/catvod/spider/UC.java @@ -0,0 +1,86 @@ +package com.github.catvod.spider; + +import android.content.Context; +import android.text.TextUtils; + +import com.github.catvod.api.UCApi; +import com.github.catvod.bean.Result; +import com.github.catvod.bean.uc.ShareData; +import com.github.catvod.crawler.Spider; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * @author ColaMint & Adam & FongMi + */ +public class UC extends Spider { + + + @Override + public void init(Context context, String extend) throws Exception { + + UCApi.get().setCookie(extend); + } + + @Override + public String detailContent(List ids) throws Exception { + + ShareData shareData = UCApi.get().getShareData(ids.get(0)); + return Result.string(UCApi.get().getVod(shareData)); + } + + + @Override + public String playerContent(String flag, String id, List vipFlags) throws Exception { + return UCApi.get().playerContent(id.split("\\+\\+"), flag); + + } + + /** + * 獲取詳情內容視頻播放來源(多 shared_link) + * + * @param ids share_link 集合 + * @return 詳情內容視頻播放來源 + */ + public String detailContentVodPlayFrom(List ids) { + List playFrom = new ArrayList<>(); + /* if (ids.size() < 2){ + return TextUtils.join("$$$", UCApi.get().getPlayFormatList()); + }*/ + + for (int i = 1; i <= ids.size(); i++) { + + for (String s : UCApi.get().getPlayFormatList()) { + playFrom.add(String.format(Locale.getDefault(), "uc" + s + "#%02d", i)); + + } + playFrom.add("uc原画"); + } + return TextUtils.join("$$$", playFrom); + } + + /** + * 獲取詳情內容視頻播放地址(多 share_link) + * + * @param ids share_link 集合 + * @return 詳情內容視頻播放地址 + */ + public String detailContentVodPlayUrl(List ids) throws Exception { + List playUrl = new ArrayList<>(); + for (String id : ids) { + ShareData shareData = UCApi.get().getShareData(id); + playUrl.add(UCApi.get().getVod(shareData).getVodPlayUrl()); + } + return TextUtils.join("$$$", playUrl); + } + + public static Object[] proxy(Map params) throws Exception { + String type = params.get("type"); + if ("video".equals(type)) return UCApi.get().proxyVideo(params); + //if ("sub".equals(type)) return AliYun.get().proxySub(params); + return null; + } +}