diff --git a/lib/eval/dart/bridge/m_provider.dart b/lib/eval/dart/bridge/m_provider.dart index 8bb59ec..795a728 100644 --- a/lib/eval/dart/bridge/m_provider.dart +++ b/lib/eval/dart/bridge/m_provider.dart @@ -330,6 +330,28 @@ class $MProvider extends MProvider with $Bridge { false), ]), ), + 'quarkFilesExtractor': BridgeMethodDef( + BridgeFunctionDef( + returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.future, [ + BridgeTypeRef(CoreTypes.list, [ + BridgeTypeRef(CoreTypes.map, [ + BridgeTypeRef(CoreTypes.string), + BridgeTypeRef(CoreTypes.string) + ]) + ]) + ])), + params: [ + BridgeParameter( + 'url', + BridgeTypeAnnotation(BridgeTypeRef( + CoreTypes.list, [BridgeTypeRef(CoreTypes.string)])), + false), + BridgeParameter( + 'cookie', + BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)), + false), + ]), + ), 'substringAfter': BridgeMethodDef( BridgeFunctionDef( returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)), @@ -904,6 +926,17 @@ class $MProvider extends MProvider with $Bridge { args[0]!.$value, args[1]?.$value ?? "", args[2]?.$value ?? "") .then((value) => $List.wrap(value.map((e) => _toMVideo(e)).toList())))), + "quarkFilesExtractor" => $Function((_, __, List<$Value?> args) => + $Future.wrap( + MBridge.quarkFilesExtractor(args[0]!.$value, args[1]!.$value) + .then((value) { + return $List.wrap(value + .map((e) => $Map.wrap({ + $String('name'): $String(e['name'] ?? ''), + $String('url'): $String(e['url'] ?? ''), + })) + .toList()); + }))), "toVideo" => $Function((_, __, List<$Value?> args) { final value = MBridge.toVideo( args[0]!.$value, diff --git a/lib/eval/dart/model/m_bridge.dart b/lib/eval/dart/model/m_bridge.dart index 12ec9ac..6389fe0 100644 --- a/lib/eval/dart/model/m_bridge.dart +++ b/lib/eval/dart/model/m_bridge.dart @@ -33,6 +33,7 @@ import 'package:mangayomi/utils/extensions/string_extensions.dart'; import 'package:mangayomi/utils/reg_exp_matcher.dart'; import 'package:xpath_selector_html_parser/xpath_selector_html_parser.dart'; import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:mangayomi/services/anime_extractors/quark_extractor.dart'; class WordSet { final List words; @@ -344,6 +345,13 @@ class MBridge { .videosFromUrl(url, newHeaders, prefix: prefix, suffix: suffix); } + static Future>> quarkFilesExtractor( + List url, String cookie) async { + QuarkExtractor quark = QuarkExtractor(); + await quark.initQuark(cookie); + return await quark.videoFilesFromUrl(url); + } + static Future> streamTapeExtractor( String url, String? quality) async { return await StreamTapeExtractor() diff --git a/lib/eval/javascript/extractors.dart b/lib/eval/javascript/extractors.dart index 2d8daf7..052f5ac 100644 --- a/lib/eval/javascript/extractors.dart +++ b/lib/eval/javascript/extractors.dart @@ -25,6 +25,10 @@ class JsVideosExtractors { runtime.onMessage('vidBomExtractor', (dynamic args) async { return (await MBridge.vidBomExtractor(args[0])).encodeToJson(); }); + runtime.onMessage('quarkFilesExtractor', (dynamic args) async { + List urls = (args[0] as List).cast(); + return (await MBridge.quarkFilesExtractor(urls, args[1])); + }); runtime.onMessage('streamlareExtractor', (dynamic args) async { return (await MBridge.streamlareExtractor( args[0], args[1] ?? "", args[2] ?? "")) @@ -178,6 +182,13 @@ async function filemoonExtractor(url, prefix, suffix) { ); return JSON.parse(result); } +async function quarkFilesExtractor(urls, cookie) { + const result = await sendMessage( + "quarkFilesExtractor", + JSON.stringify([urls, cookie]) + ); + return result; +} '''); } } diff --git a/lib/services/anime_extractors/quark_extractor.dart b/lib/services/anime_extractors/quark_extractor.dart new file mode 100644 index 0000000..66f81c1 --- /dev/null +++ b/lib/services/anime_extractors/quark_extractor.dart @@ -0,0 +1,548 @@ +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'; + +class QuarkExtractor { + final String apiUrl = "https://drive-pc.quark.cn/1/clouddrive/"; + String cookie = ""; + Map shareTokenCache = {}; + final String pr = "pr=ucpro&fr=pc"; + final List subtitleExts = ['.srt', '.ass', '.scc', '.stl', '.ttml']; + Map saveFileIdCaches = {}; + String? saveDirId; + final String saveDirName = 'TV'; + + Future initQuark(String cookie) async { + this.cookie = cookie; + print('夸克云盘初始化完成,Cookie为:$cookie'); + } + + Map getHeaders() { + return { + '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": cookie, + "Host": "drive-pc.quark.cn" + }; + } + + 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) { + final puus = resp.headers['set-cookie']! + .split(';;;') + .join() + .split(';') + .firstWhere((element) => element.startsWith('__puus='), + orElse: () => ''); + if (puus.isNotEmpty) { + final newPuus = puus.split('=')[1]; + if (cookie.contains('__puus=')) { + cookie = + cookie.replaceFirst(RegExp(r'__puus=[^;]+'), '__puus=$newPuus'); + } + } + } + return jsonDecode(resp.body); + } + + Map? getShareData(String url) { + final regex = RegExp(r'https://pan\.quark\.cn/s/([^\\|#/]+)'); + final matches = regex.firstMatch(url); + if (matches != null) { + return { + 'shareId': matches.group(1)!, + 'folderId': '0', + }; + } + return null; + } + + List getPlayFormatList() { + return ["4K", "超清", "高清", "普画"]; + } + + List getPlayFormtQuarkList() { + return ["4k", "2k", "super", "high", "normal", "low"]; + } + + 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:asc', + 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)); + } else if (item['type'] == 'file' && + subtitleExts.any((x) => item['file_name'].endsWith(x))) { + subtitles.add(Item.objectFrom(item, shareData['shareId']!, shareIndex)); + } + } + 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(Duration(seconds: 1)); + } + } + return null; + } + + Future getLiveTranscoding(String shareId, String stoken, + String fileId, String fileToken, String flag) 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) { + final flagId = flag.split("-").last; + final index = getPlayFormatList().indexOf(flagId); + final quarkFormat = getPlayFormtQuarkList()[index]; + for (final video in transcoding['data']['video_list']) { + if (video['resolution'] == quarkFormat) { + return video['video_info']['url']; + } + } + return transcoding['data']['video_list'][index]['video_info']['url']; + } + 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); + } + + if (videoItems.isNotEmpty) { + print('获取播放链接成功,分享链接为:${shareUrlList.join("\t")}'); + } else { + print('获取播放链接失败,检查分享链接为:${shareUrlList.join("\t")}'); + } + + 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> getVod(List videoItemList, + // List subItemList, String typeName) async { + // if (videoItemList.isEmpty) { + // return []; + // } + // List