import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:mangayomi/models/video.dart'; import 'package:mangayomi/services/http/m_client.dart'; enum CloudDriveType { quark, uc } class QuarkUcExtractor { late CloudDriveType cloudDriveType; String apiUrl = ""; // String cookie = ""; String refererUrl = ""; String ua = ""; String host = ""; Map shareTokenCache = {}; String pr = ""; final List subtitleExts = ['.srt', '.ass', '.scc', '.stl', '.ttml']; Map saveFileIdCaches = {}; String? saveDirId; final String saveDirName = 'TV'; String _lastCookie = ""; Future initCloudDrive( String cookie, CloudDriveType cloudDriveType, ) async { this.cloudDriveType = cloudDriveType; if (cloudDriveType == CloudDriveType.quark) { apiUrl = "https://drive-pc.quark.cn/1/clouddrive/"; pr = "pr=ucpro&fr=pc"; ua = "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"; refererUrl = "https://pan.quark.cn/"; host = "https://quark.cn"; _lastCookie = "https://quarkcookie.last"; } else { apiUrl = "https://pc-api.uc.cn/1/clouddrive/"; pr = "pr=UCBrowser&fr=pc"; ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uc-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch"; refererUrl = "https://drive.uc.cn/"; host = "https://uc.cn"; _lastCookie = "https://uccookie.last"; } if (cookie.isNotEmpty && getLastCookie() != cookie) { MClient.setCookie(host, ua, null, cookie: cookie); MClient.setCookie(_lastCookie, ua, null, cookie: cookie); } } String getLastCookie() { var cookie = MClient.getCookiesPref(_lastCookie); return cookie.isNotEmpty ? cookie.values.first : ""; } String getCurrentCookie() { var cookie = MClient.getCookiesPref(host); return cookie.isNotEmpty ? cookie.values.first : ""; } Map getHeaders() { return { 'User-Agent': ua, 'Referer': refererUrl, "Content-Type": "application/json", "Cookie": getCurrentCookie(), }; } Future> api( String url, dynamic data, String method, ) async { InterceptedClient client = MClient.init( reqcopyWith: {'useDartHttpClient': true}, ); late Response resp; if (method != "get") { resp = await client.post( Uri.parse(apiUrl + url), body: jsonEncode(data), headers: getHeaders(), ); } else { resp = await client.get(Uri.parse(apiUrl + url), headers: getHeaders()); } // if (resp.headers['set-cookie'] != null) { // print('headers: ${resp.headers}'); // final puus = resp.headers['set-cookie']! // .split(';;;') // .join() // .split(';') // .firstWhere((element) => element.startsWith('__puus='), // orElse: () => ''); // if (puus.isNotEmpty) { // final newPuus = puus.split('=')[1]; // var cookie = getCurrentCookie(); // if (cookie != null && cookie.contains('__puus=')) { // cookie = // cookie.replaceFirst(RegExp(r'__puus=[^;]+'), '__puus=$newPuus'); // } // MClient.setCookie(host, ua, cookie: cookie); // } // } // 处理 set-cookie if (resp.headers['set-cookie'] != null) { final cookies = resp.headers['set-cookie']!.split(';;;'); for (var cookie in cookies) { if (cookie.contains('__puus=')) { final newPuus = cookie.split(';')[0]; // 获取新的 __puus var currentCookie = getCurrentCookie(); if (currentCookie.isNotEmpty) { // 更新 __puus if (currentCookie.contains('__puus=')) { currentCookie = currentCookie.replaceFirst( RegExp(r'__puus=[^;]+'), newPuus, ); } else { currentCookie = '$currentCookie; $newPuus'; } MClient.setCookie(host, ua, null, cookie: currentCookie); } break; } } } return jsonDecode(resp.body); } Map? getShareData(String url) { RegExp regex; if (cloudDriveType == CloudDriveType.quark) { regex = RegExp(r'https://pan\.quark\.cn/s/([^\\|#/]+)'); } else { regex = RegExp(r'https://drive\.uc\.cn/s/([^?]+)'); } final matches = regex.firstMatch(url); if (matches != null) { return {'shareId': matches.group(1)!, 'folderId': '0'}; } return null; } Future getShareToken(Map shareData) async { if (!shareTokenCache.containsKey(shareData['shareId'])) { shareTokenCache.remove(shareData['shareId']); final shareToken = await api('share/sharepage/token?$pr', { 'pwd_id': shareData['shareId'], 'passcode': shareData['sharePwd'] ?? '', }, 'post'); if (shareToken['data'] != null && shareToken['data']['stoken'] != null) { shareTokenCache[shareData['shareId']!] = shareToken['data']; } } } Future> listFile( int shareIndex, Map shareData, List videos, List subtitles, String shareId, String folderId, { int page = 1, }) async { const int prePage = 200; final listData = await api( 'share/sharepage/detail?$pr&pwd_id=$shareId&stoken=${Uri.encodeComponent(shareTokenCache[shareId]['stoken'])}&pdir_fid=$folderId&force=0&_page=$page&_size=$prePage&_sort=file_type:asc,file_name:desc', null, 'get', ); if (listData['data'] == null) return []; final items = listData['data']['list']; if (items == null) return []; final subDir = []; for (final item in items) { if (item['dir'] == true) { subDir.add(item); } else if (item['file'] == true && item['obj_category'] == 'video') { if (item['size'] < 1024 * 1024 * 5) continue; item['stoken'] = shareTokenCache[shareData['shareId']]['stoken']; videos.add( Item.objectFrom( item, shareData['shareId']!, shareIndex, cloudDriveType, ), ); } else if (item['type'] == 'file' && subtitleExts.any((x) => item['file_name'].endsWith(x))) { subtitles.add( Item.objectFrom( item, shareData['shareId']!, shareIndex, cloudDriveType, ), ); } } if (page < (listData['metadata']['_total'] / prePage).ceil()) { final nextItems = await listFile( shareIndex, shareData, videos, subtitles, shareId, folderId, page: page + 1, ); items.addAll(nextItems); } for (final dir in subDir) { final subItems = await listFile( shareIndex, shareData, videos, subtitles, shareId, dir['fid'], ); items.addAll(subItems); } return items; } Map findBestLCS(Item mainItem, List targetItems) { final results = []; var bestMatchIndex = 0; for (var i = 0; i < targetItems.length; i++) { final currentLCS = lcs(mainItem.name, targetItems[i].name); results.add({'target': targetItems[i], 'lcs': currentLCS}); if (currentLCS['length'] > results[bestMatchIndex]['lcs']['length']) { bestMatchIndex = i; } } final bestMatch = results[bestMatchIndex]; return { 'allLCS': results, 'bestMatch': bestMatch, 'bestMatchIndex': bestMatchIndex, }; } Future getFilesByShareUrl( int shareIndex, dynamic shareInfo, List videos, List subtitles, ) async { final shareData = shareInfo is String ? getShareData(shareInfo) : shareInfo; if (shareData == null) return; await getShareToken(shareData); if (!shareTokenCache.containsKey(shareData['shareId'])) return; await listFile( shareIndex, shareData, videos, subtitles, shareData['shareId']!, shareData['folderId']!, ); if (subtitles.isNotEmpty) { for (var item in videos) { var matchSubtitle = findBestLCS(item, subtitles as List); if (matchSubtitle['bestMatch'] != null) { item.subtitle = matchSubtitle['bestMatch']['target']; } } } } void clean() { saveFileIdCaches.clear(); } Future clearSaveDir() async { final listData = await api( 'file/sort?$pr&pdir_fid=$saveDirId&_page=1&_size=200&_sort=file_type:asc,updated_at:desc', {}, 'get', ); if (listData['data'] != null && listData['data']['list'] != null && listData['data']['list'].isNotEmpty) { await api('file/delete?$pr', { 'action_type': 2, 'filelist': listData['data']['list'].map((v) => v['fid']).toList(), 'exclude_fids': [], }, 'post'); } } Future createSaveDir(bool clean) async { if (saveDirId != null) { if (clean) await clearSaveDir(); return; } final listData = await api( 'file/sort?$pr&pdir_fid=0&_page=1&_size=200&_sort=file_type:asc,updated_at:desc', {}, 'get', ); if (listData['data'] != null && listData['data']['list'] != null) { for (final item in listData['data']['list']) { if (item['file_name'] == saveDirName) { saveDirId = item['fid']; await clearSaveDir(); break; } } } if (saveDirId == null) { final create = await api('file?$pr', { 'pdir_fid': '0', 'file_name': saveDirName, 'dir_path': '', 'dir_init_lock': false, }, 'post'); if (create['data'] != null && create['data']['fid'] != null) { saveDirId = create['data']['fid']; } } } Future save( String shareId, String stoken, String fileId, String fileToken, bool clean, ) async { await createSaveDir(clean); if (clean) { this.clean(); } if (saveDirId == null) return null; if (stoken.isEmpty) { await getShareToken({'shareId': shareId}); if (!shareTokenCache.containsKey(shareId)) return null; } final saveResult = await api('share/sharepage/save?$pr', { 'fid_list': [fileId], 'fid_token_list': [fileToken], 'to_pdir_fid': saveDirId, 'pwd_id': shareId, 'stoken': stoken.isNotEmpty ? stoken : shareTokenCache[shareId]['stoken'], 'pdir_fid': '0', 'scene': 'link', }, 'post'); if (saveResult['data'] != null && saveResult['data']['task_id'] != null) { var retry = 0; while (true) { final taskResult = await api( 'task?$pr&task_id=${saveResult['data']['task_id']}&retry_index=$retry', {}, 'get', ); if (taskResult['data'] != null && taskResult['data']['save_as'] != null && taskResult['data']['save_as']['save_as_top_fids'] != null && taskResult['data']['save_as']['save_as_top_fids'].isNotEmpty) { return taskResult['data']['save_as']['save_as_top_fids'][0]; } retry++; if (retry > 2) break; await Future.delayed(const Duration(seconds: 1)); } } return null; } Future>?> getLiveTranscoding( String shareId, String stoken, String fileId, String fileToken, ) async { if (!saveFileIdCaches.containsKey(fileId)) { final saveFileId = await save(shareId, stoken, fileId, fileToken, true); if (saveFileId == null) return null; saveFileIdCaches[fileId] = saveFileId; } final transcoding = await api('file/v2/play?$pr', { 'fid': saveFileIdCaches[fileId], 'resolutions': 'normal,low,high,super,2k,4k', 'supports': 'fmp4', }, 'post'); if (transcoding['data'] != null && transcoding['data']['video_list'] != null) { List> qualityOptions = []; for (final video in transcoding['data']['video_list']) { qualityOptions.add({ 'url': video['video_info']['url'], 'quality': video['resolution'], }); } return qualityOptions; } return null; } Future?> getDownload( String shareId, String stoken, String fileId, String fileToken, bool clean, ) async { if (!saveFileIdCaches.containsKey(fileId)) { final saveFileId = await save(shareId, stoken, fileId, fileToken, clean); if (saveFileId == null) return null; saveFileIdCaches[fileId] = saveFileId; } final down = await api('file/download?$pr&uc_param_str=', { 'fids': [saveFileIdCaches[fileId]], }, 'post'); if (down['data'] != null) { return down['data'][0]; } return null; } Future>> videoFilesFromUrl( List shareUrlList, { String typeName = "电影", }) async { List videoItems = []; List subItems = []; for (int i = 0; i < shareUrlList.length; i++) { String shareUrl = shareUrlList[i]; await getFilesByShareUrl(i + 1, shareUrl, videoItems, subItems); } return await getVodFile(videoItems, subItems, typeName); } Future>> getVodFile( List videoItemList, List subItemList, String typeName, ) async { if (videoItemList.isEmpty) { return []; } List> vodItems = []; for (var videoItem in videoItemList) { String episodeUrl = videoItem.getEpisodeUrl(typeName); String subtitles = findSubs(videoItem.getName(), subItemList); String fullUrl = episodeUrl + subtitles; List parts = fullUrl.split('\$'); String name = parts[0].trim(); String url = parts[1]; vodItems.add({"name": name, "url": url}); } // print(vodItems); return vodItems; } Future> videosFromUrl(String url) async { List parts = url.split('++'); String fileId = parts[1]; String fileToken = parts[2]; String shareId = parts[3]; String stoken = parts[4]; // String type = parts[0]; List subtitleParts = parts.length > 5 ? parts[5].split('+') : []; // 获取可用的质量列表 //List qualities = getPlayFormtList(); List