mangayomi/lib/eval/model/m_bridge.dart
2024-12-14 13:55:40 +02:00

634 lines
23 KiB
Dart

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<String> 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<String> 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<String>
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<dynamic> values = [];
final val = jsonDecode(source) as List;
for (var element in val) {
final mMap = element as Map?;
Map<String, dynamic> map = {};
if (mMap != null) {
map = mMap.map((key, value) => MapEntry(key.toString(), value));
}
values.add(map);
}
List<String> 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<String, dynamic>;
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<dynamic> 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<String, dynamic> 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<String, dynamic> map = {};
if (mMap != null) {
map = mMap.map((key, value) => MapEntry(key.toString(), value));
}
values.add(map);
}
List<String> 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<dynamic> val = [];
for (var element in value) {
if (element is $Value) {
val.add(element.$reified.toString());
} else {
val.add(element);
}
}
bool error = false;
List<dynamic> 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<List<Video>> gogoCdnExtractor(String url) async {
return await GogoCdnExtractor().videosFromUrl(url);
}
static Future<List<Video>> doodExtractor(String url, String? quality) async {
return await DoodExtractor().videosFromUrl(url, quality: quality);
}
static Future<List<Video>> streamWishExtractor(String url, String prefix) async {
return await StreamWishExtractor().videosFromUrl(url, prefix);
}
static Future<List<Video>> filemoonExtractor(String url, String prefix, String suffix) async {
return await FilemoonExtractor().videosFromUrl(url, prefix, suffix);
}
static Future<List<Video>> mp4UploadExtractor(String url, String? headers, String prefix, String suffix) async {
Map<String, String> newHeaders = {};
if (headers != null) {
newHeaders = (jsonDecode(headers) as Map).toMapStringString!;
}
return await Mp4uploadExtractor().videosFromUrl(url, newHeaders, prefix: prefix, suffix: suffix);
}
static Future<List<Map<String, String>>> quarkFilesExtractor(List<String> url, String cookie) async {
QuarkUcExtractor quark = QuarkUcExtractor();
await quark.initCloudDrive(cookie, CloudDriveType.quark);
return await quark.videoFilesFromUrl(url);
}
static Future<List<Map<String, String>>> ucFilesExtractor(List<String> url, String cookie) async {
QuarkUcExtractor uc = QuarkUcExtractor();
await uc.initCloudDrive(cookie, CloudDriveType.uc);
return await uc.videoFilesFromUrl(url);
}
static Future<List<Video>> quarkVideosExtractor(String url, String cookie) async {
QuarkUcExtractor quark = QuarkUcExtractor();
await quark.initCloudDrive(cookie, CloudDriveType.quark);
return await quark.videosFromUrl(url);
}
static Future<List<Video>> ucVideosExtractor(String url, String cookie) async {
QuarkUcExtractor uc = QuarkUcExtractor();
await uc.initCloudDrive(cookie, CloudDriveType.uc);
return await uc.videosFromUrl(url);
}
static Future<List<Video>> 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<List<Video>> sibnetExtractor(String url, String prefix) async {
return await SibnetExtractor().videosFromUrl(url, prefix: prefix);
}
static Future<List<Video>> sendVidExtractor(String url, String? headers, String prefix) async {
Map<String, String> newHeaders = {};
if (headers != null) {
newHeaders = (jsonDecode(headers) as Map).toMapStringString!;
}
return await SendvidExtractor(newHeaders).videosFromUrl(url, prefix: prefix);
}
static Future<List<Video>> myTvExtractor(String url) async {
return await MytvExtractor().videosFromUrl(url);
}
static Future<List<Video>> okruExtractor(String url) async {
return await OkruExtractor().videosFromUrl(url);
}
static Future<List<Video>> yourUploadExtractor(String url, String? headers, String? name, String prefix) async {
Map<String, String> newHeaders = {};
if (headers != null) {
newHeaders = (jsonDecode(headers) as Map).toMapStringString!;
}
return await YourUploadExtractor().videosFromUrl(url, newHeaders, prefix: prefix, name: name ?? "YourUpload");
}
static Future<List<Video>> voeExtractor(String url, String? quality) async {
return await VoeExtractor().videosFromUrl(url, quality);
}
static Future<List<Video>> vidBomExtractor(String url) async {
return await VidBomExtractor().videosFromUrl(url);
}
static Future<List<Video>> 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<Track>? subtitles, List<Track>? audios) {
Map<String, String> 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<String> _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);
}