import 'dart:convert'; import 'package:bot_toast/bot_toast.dart'; import 'package:dart_eval/dart_eval_bridge.dart'; import 'package:dart_eval/stdlib/core.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:html/dom.dart' hide Text; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:js_packer/js_packer.dart'; import 'package:json_path/json_path.dart'; import 'package:mangayomi/eval/model/document.dart'; import 'package:mangayomi/eval/javascript/http.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/router/router.dart'; import 'package:mangayomi/services/anime_extractors/dood_extractor.dart'; import 'package:mangayomi/services/anime_extractors/filemoon.dart'; import 'package:mangayomi/services/anime_extractors/gogocdn_extractor.dart'; import 'package:mangayomi/services/anime_extractors/mp4upload_extractor.dart'; import 'package:mangayomi/services/anime_extractors/mytv_extractor.dart'; import 'package:mangayomi/services/anime_extractors/okru_extractor.dart'; import 'package:mangayomi/services/anime_extractors/sendvid_extractor.dart'; import 'package:mangayomi/services/anime_extractors/sibnet_extractor.dart'; import 'package:mangayomi/services/anime_extractors/streamlare_extractor.dart'; import 'package:mangayomi/services/anime_extractors/streamtape_extractor.dart'; import 'package:mangayomi/models/video.dart'; import 'package:mangayomi/services/anime_extractors/streamwish_extractor.dart'; import 'package:mangayomi/services/anime_extractors/vidbom_extractor.dart'; import 'package:mangayomi/services/anime_extractors/voe_extractor.dart'; import 'package:mangayomi/services/anime_extractors/your_upload_extractor.dart'; import 'package:mangayomi/utils/cryptoaes/crypto_aes.dart'; import 'package:mangayomi/utils/cryptoaes/deobfuscator.dart'; import 'package:mangayomi/utils/cryptoaes/js_unpacker.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; 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/quarkuc_extractor.dart'; class WordSet { final List words; WordSet(this.words); bool anyWordIn(String dateString) { return words.any((word) => dateString.toLowerCase().contains(word.toLowerCase())); } bool startsWith(String dateString) { return words.any((word) => dateString.toLowerCase().startsWith(word.toLowerCase())); } bool endsWith(String dateString) { return words.any((word) => dateString.toLowerCase().endsWith(word.toLowerCase())); } } class MBridge { static MDocument parsHtml(String html) { return MDocument(Document.html(html)); } ///Create query by html string static const $Function xpath = $Function(_xpath); static $Value? _xpath(_, __, List<$Value?> args) { String html = args[0]!.$reified; String xpath = args[1]!.$reified; List attrs = []; try { var htmlXPath = HtmlXPath.html(html); var query = htmlXPath.query(xpath); if (query.nodes.length > 1) { for (var element in query.attrs) { attrs.add(element!.trim().trimLeft().trimRight()); } } //Return one attr else if (query.nodes.length == 1) { String attr = query.attr != null ? query.attr!.trim().trimLeft().trimRight() : ""; if (attr.isNotEmpty) { attrs = [attr]; } } return $List.wrap(attrs.map((e) => $String(e)).toList()); } catch (_) { return $List.wrap([]); } } ///Convert serie status to int ///[status] contains the current status of the serie ///[statusList] contains a list of map of many static status static Status parseStatus(String status, List statusList) { for (var element in statusList) { Map statusMap = {}; if (element is $Map<$Value, $Value>) { statusMap = element.$reified; } else { statusMap = element; } for (var element in statusMap.entries) { if (element.key.toString().toLowerCase().contains(status.toLowerCase().trim().trimLeft().trimRight())) { return switch (element.value as int) { 0 => Status.ongoing, 1 => Status.completed, 2 => Status.onHiatus, 3 => Status.canceled, 4 => Status.publishingFinished, _ => Status.unknown, }; } } } return Status.unknown; } ///Unpack a JS code static const $Function unpackJs = $Function(_unpackJs); static $Value? _unpackJs(_, __, List<$Value?> args) { String code = args[0]!.$reified; try { final jsPacker = JSPacker(code); return $String(jsPacker.unpack() ?? ""); } catch (_) { return $String(""); } } ///Unpack a JS code static const $Function unpackJsAndCombine = $Function(_unpackJsAndCombine); static $Value? _unpackJsAndCombine(_, __, List<$Value?> args) { String code = args[0]!.$reified; try { return $String(JsUnpacker.unpackAndCombine(code) ?? ""); } catch (_) { return $String(""); } } ///Read values in parsed JSON object and return resut to List static const $Function jsonPathToList = $Function(_jsonPathToList); static $Value? _jsonPathToList(_, __, List<$Value?> args) { String source = args[0]!.$reified; String expression = args[1]!.$reified; int type = args[2]!.$reified; try { //Check jsonDecode(source) is list value if (jsonDecode(source) is List) { List values = []; final val = jsonDecode(source) as List; for (var element in val) { final mMap = element as Map?; Map map = {}; if (mMap != null) { map = mMap.map((key, value) => MapEntry(key.toString(), value)); } values.add(map); } List list = []; for (var data in values) { final jsonRes = JsonPath(expression).read(data); String val = ""; //Get jsonRes first string value if (type == 0) { val = jsonRes.first.value.toString(); } //Decode jsonRes first map value else { val = jsonEncode(jsonRes.first.value); } list.add(val); } return $List.wrap(list.map((e) => $String(e)).toList()); } // else jsonDecode(source) is Map value else { var map = json.decode(source); var values = JsonPath(expression).readValues(map); return $List.wrap(values.map((e) { return $String(e == null ? "{}" : json.encode(e)); }).toList()); } } catch (_) { return $List.wrap([]); } } ///GetMapValue static String getMapValue(String source, String attr, bool encode) { try { var map = json.decode(source) as Map; if (!encode) { return map[attr] != null ? map[attr].toString() : ""; } return map[attr] != null ? jsonEncode(map[attr]) : ""; } catch (_) { return ""; } } ///Read values in parsed JSON object and return resut to String static const $Function jsonPathToString = $Function(_jsonPathToString); static $Value? _jsonPathToString(_, __, List<$Value?> args) { String source = args[0]!.$reified; String expression = args[1]!.$reified; String join = args[2]!.$reified; try { List values = []; //Check jsonDecode(source) is list value if (jsonDecode(source) is List) { final val = jsonDecode(source) as List; for (var element in val) { final mMap = element as Map?; Map map = {}; if (mMap != null) { map = mMap.map((key, value) => MapEntry(key.toString(), value)); } values.add(map); } } // else jsonDecode(source) is Map value else { final mMap = jsonDecode(source) as Map?; Map map = {}; if (mMap != null) { map = mMap.map((key, value) => MapEntry(key.toString(), value)); } values.add(map); } List listRg = []; for (var data in values) { final jsonRes = JsonPath(expression).readValues(data); List list = []; for (var element in jsonRes) { list.add(element); } //join the list into listRg listRg.add(list.join(join)); } return $String(listRg.first); } catch (_) { return $String(""); } } //Parse a list of dates to millisecondsSinceEpoch static List parseDates(List value, String dateFormat, String dateFormatLocale) { List val = []; for (var element in value) { if (element is $Value) { val.add(element.$reified.toString()); } else { val.add(element); } } bool error = false; List valD = []; for (var date in val) { if (date.toString().isNotEmpty) { String dateStr = ""; if (error) { dateStr = DateTime.now().millisecondsSinceEpoch.toString(); } else { dateStr = parseChapterDate( date, dateFormat, dateFormatLocale, (val) { dateFormat = val.$1; dateFormatLocale = val.$2; error = val.$3; }, ); } valD.add(dateStr); } } return valD; } static List sortMapList(List list, String value, int type) { if (type == 0) { list.sort((a, b) => a[value].compareTo(b[value])); } else if (type == 1) { list.sort((a, b) => b[value].compareTo(a[value])); } return list; } //Utility to use RegExp static String regExp(String expression, String source, String replace, int type, int group) { if (type == 0) { return expression.replaceAll(RegExp(source), replace); } return regCustomMatcher(expression, source, group); } static Future> gogoCdnExtractor(String url) async { return await GogoCdnExtractor().videosFromUrl(url); } static Future> doodExtractor(String url, String? quality) async { return await DoodExtractor().videosFromUrl(url, quality: quality); } static Future> streamWishExtractor(String url, String prefix) async { return await StreamWishExtractor().videosFromUrl(url, prefix); } static Future> filemoonExtractor(String url, String prefix, String suffix) async { return await FilemoonExtractor().videosFromUrl(url, prefix, suffix); } static Future> mp4UploadExtractor(String url, String? headers, String prefix, String suffix) async { Map newHeaders = {}; if (headers != null) { newHeaders = (jsonDecode(headers) as Map).toMapStringString!; } return await Mp4uploadExtractor().videosFromUrl(url, newHeaders, prefix: prefix, suffix: suffix); } static Future>> quarkFilesExtractor(List url, String cookie) async { QuarkUcExtractor quark = QuarkUcExtractor(); await quark.initCloudDrive(cookie, CloudDriveType.quark); return await quark.videoFilesFromUrl(url); } static Future>> ucFilesExtractor(List url, String cookie) async { QuarkUcExtractor uc = QuarkUcExtractor(); await uc.initCloudDrive(cookie, CloudDriveType.uc); return await uc.videoFilesFromUrl(url); } static Future> quarkVideosExtractor(String url, String cookie) async { QuarkUcExtractor quark = QuarkUcExtractor(); await quark.initCloudDrive(cookie, CloudDriveType.quark); return await quark.videosFromUrl(url); } static Future> ucVideosExtractor(String url, String cookie) async { QuarkUcExtractor uc = QuarkUcExtractor(); await uc.initCloudDrive(cookie, CloudDriveType.uc); return await uc.videosFromUrl(url); } static Future> streamTapeExtractor(String url, String? quality) async { return await StreamTapeExtractor().videosFromUrl(url, quality: quality ?? "StreamTape"); } //Utility to use substring static String substringAfter(String text, String pattern) { return text.substringAfter(pattern); } //Utility to use substring static String substringBefore(String text, String pattern) { return text.substringBefore(pattern); } //Utility to use substring static String substringBeforeLast(String text, String pattern) { return text.substringBeforeLast(pattern); } static String substringAfterLast(String text, String pattern) { return text.split(pattern).last; } //Parse a chapter date to millisecondsSinceEpoch static String parseChapterDate( String date, String dateFormat, String dateFormatLocale, Function((String, String, bool)) newLocale) { int parseRelativeDate(String date) { final number = int.tryParse(RegExp(r"(\d+)").firstMatch(date)!.group(0)!); if (number == null) return 0; final cal = DateTime.now(); if (WordSet(["hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "天"]).anyWordIn(date)) { return cal.subtract(Duration(days: number)).millisecondsSinceEpoch; } else if (WordSet(["jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时"]) .anyWordIn(date)) { return cal.subtract(Duration(hours: number)).millisecondsSinceEpoch; } else if (WordSet(["menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق"]).anyWordIn(date)) { return cal.subtract(Duration(minutes: number)).millisecondsSinceEpoch; } else if (WordSet(["detik", "segundo", "second", "วินาที", "sec"]).anyWordIn(date)) { return cal.subtract(Duration(seconds: number)).millisecondsSinceEpoch; } else if (WordSet(["week", "semana"]).anyWordIn(date)) { return cal.subtract(Duration(days: number * 7)).millisecondsSinceEpoch; } else if (WordSet(["month", "mes"]).anyWordIn(date)) { return cal.subtract(Duration(days: number * 30)).millisecondsSinceEpoch; } else if (WordSet(["year", "año"]).anyWordIn(date)) { return cal.subtract(Duration(days: number * 365)).millisecondsSinceEpoch; } else { return 0; } } try { if (WordSet(["yesterday", "يوم واحد"]).startsWith(date)) { DateTime cal = DateTime.now().subtract(const Duration(days: 1)); cal = DateTime(cal.year, cal.month, cal.day); return cal.millisecondsSinceEpoch.toString(); } else if (WordSet(["today"]).startsWith(date)) { DateTime cal = DateTime.now(); cal = DateTime(cal.year, cal.month, cal.day); return cal.millisecondsSinceEpoch.toString(); } else if (WordSet(["يومين"]).startsWith(date)) { DateTime cal = DateTime.now().subtract(const Duration(days: 2)); cal = DateTime(cal.year, cal.month, cal.day); return cal.millisecondsSinceEpoch.toString(); } else if (WordSet(["ago", "atrás", "önce", "قبل"]).endsWith(date)) { return parseRelativeDate(date).toString(); } else if (WordSet(["hace"]).startsWith(date)) { return parseRelativeDate(date).toString(); } else if (date.contains(RegExp(r"\d(st|nd|rd|th)"))) { final cleanedDate = date .split(" ") .map((it) => it.contains(RegExp(r"\d\D\D")) ? it.replaceAll(RegExp(r"\D"), "") : it) .join(" "); return DateFormat(dateFormat, dateFormatLocale).parse(cleanedDate).millisecondsSinceEpoch.toString(); } else { return DateFormat(dateFormat, dateFormatLocale).parse(date).millisecondsSinceEpoch.toString(); } } catch (e) { final supportedLocales = DateFormat.allLocalesWithSymbols(); for (var locale in supportedLocales) { for (var dateFormat in _dateFormats) { newLocale((dateFormat, locale, false)); try { initializeDateFormatting(locale); if (WordSet(["yesterday", "يوم واحد"]).startsWith(date)) { DateTime cal = DateTime.now().subtract(const Duration(days: 1)); cal = DateTime(cal.year, cal.month, cal.day); return cal.millisecondsSinceEpoch.toString(); } else if (WordSet(["today"]).startsWith(date)) { DateTime cal = DateTime.now(); cal = DateTime(cal.year, cal.month, cal.day); return cal.millisecondsSinceEpoch.toString(); } else if (WordSet(["يومين"]).startsWith(date)) { DateTime cal = DateTime.now().subtract(const Duration(days: 2)); cal = DateTime(cal.year, cal.month, cal.day); return cal.millisecondsSinceEpoch.toString(); } else if (WordSet(["ago", "atrás", "önce", "قبل"]).endsWith(date)) { return parseRelativeDate(date).toString(); } else if (WordSet(["hace"]).startsWith(date)) { return parseRelativeDate(date).toString(); } else if (date.contains(RegExp(r"\d(st|nd|rd|th)"))) { final cleanedDate = date .split(" ") .map((it) => it.contains(RegExp(r"\d\D\D")) ? it.replaceAll(RegExp(r"\D"), "") : it) .join(" "); return DateFormat(dateFormat, locale).parse(cleanedDate).millisecondsSinceEpoch.toString(); } else { return DateFormat(dateFormat, locale).parse(date).millisecondsSinceEpoch.toString(); } } catch (_) {} } } newLocale((dateFormat, dateFormatLocale, true)); return DateTime.now().millisecondsSinceEpoch.toString(); } } static String deobfuscateJsPassword(String inputString) { return Deobfuscator.deobfuscateJsPassword(inputString); } static Future> sibnetExtractor(String url, String prefix) async { return await SibnetExtractor().videosFromUrl(url, prefix: prefix); } static Future> sendVidExtractor(String url, String? headers, String prefix) async { Map newHeaders = {}; if (headers != null) { newHeaders = (jsonDecode(headers) as Map).toMapStringString!; } return await SendvidExtractor(newHeaders).videosFromUrl(url, prefix: prefix); } static Future> myTvExtractor(String url) async { return await MytvExtractor().videosFromUrl(url); } static Future> okruExtractor(String url) async { return await OkruExtractor().videosFromUrl(url); } static Future> yourUploadExtractor(String url, String? headers, String? name, String prefix) async { Map newHeaders = {}; if (headers != null) { newHeaders = (jsonDecode(headers) as Map).toMapStringString!; } return await YourUploadExtractor().videosFromUrl(url, newHeaders, prefix: prefix, name: name ?? "YourUpload"); } static Future> voeExtractor(String url, String? quality) async { return await VoeExtractor().videosFromUrl(url, quality); } static Future> vidBomExtractor(String url) async { return await VidBomExtractor().videosFromUrl(url); } static Future> streamlareExtractor(String url, String prefix, String suffix) async { return await StreamlareExtractor().videosFromUrl(url, prefix: prefix, suffix: suffix); } static String encryptAESCryptoJS(String plainText, String passphrase) { return CryptoAES.encryptAESCryptoJS(plainText, passphrase); } static String decryptAESCryptoJS(String encrypted, String passphrase) { return CryptoAES.decryptAESCryptoJS(encrypted, passphrase); } static Video toVideo( String url, String quality, String originalUrl, String? headers, List? subtitles, List? audios) { Map newHeaders = {}; if (headers != null) { newHeaders = (jsonDecode(headers) as Map).toMapStringString!; } return Video(url, quality, originalUrl, headers: newHeaders, subtitles: subtitles ?? [], audios: audios ?? []); } static String cryptoHandler(String text, String iv, String secretKeyString, bool encrypt) { try { if (encrypt) { final encryptt = _encrypt(secretKeyString, iv); final en = encryptt.$1.encrypt(text, iv: encryptt.$2); return en.base64; } else { final encryptt = _encrypt(secretKeyString, iv); final en = encryptt.$1.decrypt64(text, iv: encryptt.$2); return en; } } catch (_) { return text; } } } final List _dateFormats = [ 'dd/MM/yyyy', 'MM/dd/yyyy', 'yyyy/MM/dd', 'dd-MM-yyyy', 'MM-dd-yyyy', 'yyyy-MM-dd', 'dd.MM.yyyy', 'MM.dd.yyyy', 'yyyy.MM.dd', 'dd MMMM yyyy', 'MMMM dd, yyyy', 'yyyy MMMM dd', 'dd MMM yyyy', 'MMM dd yyyy', 'yyyy MMM dd', 'dd MMMM, yyyy', 'yyyy, MMMM dd', 'MMMM dd yyyy', 'MMM dd, yyyy', 'dd LLLL yyyy', 'LLLL dd, yyyy', 'yyyy LLLL dd', 'LLLL dd yyyy', "MMMMM dd, yyyy", "MMM d, yyy", "MMM d, yyyy", "dd/mm/yyyy", "d MMMM yyyy", "dd 'de' MMMM 'de' yyyy", "d MMMM'،' yyyy", "yyyy'年'M'月'd", "d MMMM, yyyy", "dd 'de' MMMMM 'de' yyyy", "dd MMMMM, yyyy", "MMMM d, yyyy", "MMM dd,yyyy" ]; void botToast(String title, {int second = 10, double? fontSize, double alignX = 0, double alignY = 0.99, bool hasCloudFlare = false, String? url}) { final context = navigatorKey.currentState?.context; final assets = ['assets/app_icons/icon-black.png', 'assets/app_icons/icon-red.png']; BotToast.showNotification( onlyOne: true, dismissDirections: [DismissDirection.horizontal, DismissDirection.down], align: Alignment(alignX, alignY), duration: Duration(seconds: second), animationDuration: const Duration(milliseconds: 200), animationReverseDuration: const Duration(milliseconds: 200), leading: (_) => Image.asset((assets..shuffle()).first, height: 25), title: (_) => Text(title, style: TextStyle(fontSize: fontSize)), trailing: hasCloudFlare ? (_) => OutlinedButton.icon( style: OutlinedButton.styleFrom(elevation: 10), onPressed: () { context?.push("/mangawebview", extra: {'url': url, 'title': ''}); }, label: Text("Resolve Cloudflare challenge", style: TextStyle(color: context?.secondaryColor)), icon: const Icon(Icons.public), ) : null, ); } (encrypt.Encrypter, encrypt.IV) _encrypt(String keyy, String ivv) { final key = encrypt.Key.fromUtf8(keyy); final iv = encrypt.IV.fromUtf8(ivv); final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: 'PKCS7')); return (encrypter, iv); }