Files
kodjodevf-mangayomi-extensions/dart/manga/multisrc/madara/madara.dart

512 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:mangayomi/bridge_lib.dart';
import 'dart:convert';
class Madara extends MProvider {
Madara({required this.source});
MSource source;
final Client client = Client();
MPages mangaFromElements(List<MElement> elements) {
List<MManga> mangaList = [];
for (var i = 0; i < elements.length; i++) {
final postTitle = elements[i].selectFirst("div.post-title a");
final imageElement = elements[i].selectFirst("img");
final image = extractImageUrl(imageElement);
MManga manga = MManga();
manga.name = postTitle.text;
manga.imageUrl = substringBefore(image, " ");
manga.link = postTitle.getHref;
mangaList.add(manga);
}
return MPages(mangaList, true);
}
@override
Future<MPages> getPopular(int page) async {
final res = (await client.get(
Uri.parse(
"${getBaseUrl()}/${getMangaSubString()}/page/$page/?m_orderby=views",
),
)).body;
final document = parseHtml(res);
return mangaFromElements(document.select("div.page-item-detail"));
}
@override
Future<MPages> getLatestUpdates(int page) async {
final res = (await client.get(
Uri.parse(
"${getBaseUrl()}/${getMangaSubString()}/page/$page/?m_orderby=latest",
),
)).body;
final document = parseHtml(res);
return mangaFromElements(document.select("div.page-item-detail"));
}
@override
Future<MPages> search(String query, int page, FilterList filterList) async {
final filters = filterList.filters;
String url = "${getBaseUrl()}/?s=$query&post_type=wp-manga";
for (var filter in filters) {
if (filter.type == "AuthorFilter") {
if (filter.state.isNotEmpty) {
url += "${ll(url)}author=${Uri.encodeComponent(filter.state)}";
}
} else if (filter.type == "ArtistFilter") {
if (filter.state.isNotEmpty) {
url += "${ll(url)}artist=${Uri.encodeComponent(filter.state)}";
}
} else if (filter.type == "YearFilter") {
if (filter.state.isNotEmpty) {
url += "${ll(url)}release=${Uri.encodeComponent(filter.state)}";
}
} else if (filter.type == "StatusFilter") {
List<String> status = filter.state
.where((item) => item.state)
.map((item) => item.value.toString())
.toList();
if (status.isNotEmpty) {
url += "${ll(url)}status[]=${status.join('&status[]=')}";
}
} else if (filter.type == "OrderByFilter") {
if (filter.state != 0) {
final order = filter.values[filter.state].value;
url += "${ll(url)}m_orderby=$order";
}
} else if (filter.type == "AdultContentFilter") {
final ctn = filter.values[filter.state].value;
if (ctn.isNotEmpty) {
url += "${ll(url)}adult=$ctn";
}
} else if (filter.type == "GenreListFilter") {
final genres = (filter.state as List).where((e) => e.state).toList();
if (genres.isNotEmpty) {
for (var genre in genres) {
url += "${ll(url)}genre[]=${genre.value},";
}
}
}
}
final res = (await client.get(Uri.parse(url))).body;
final document = parseHtml(res);
return mangaFromElements(document.select("div.c-tabs-item__content"));
}
List<MChapter> getChapters(MDocument chapDoc) {
List<MChapter> chapters = [];
for (MElement element in chapDoc.select("li.wp-manga-chapter") ?? []) {
var ch = element.selectFirst("a");
if (ch != null) {
var url = ch.attr("href");
if (url != null && url.isNotEmpty) {
url = substringBefore(url, "?style=paged");
if (url.endsWith("?style=paged")) {
url = url + "?style=paged";
}
var chapter = MChapter();
chapter.url = url;
chapter.name = ch.text;
if (source.dateFormat.isNotEmpty) {
var chd = element.selectFirst("span.chapter-release-date");
if (chd != null && chd.text.isNotEmpty) {
var dates = parseDates(
[chd.text],
source.dateFormat,
source.dateFormatLocale,
);
chapter.dateUpload = dates[0];
} else {
chapter.dateUpload = DateTime.now().millisecondsSinceEpoch
.toString();
}
}
chapters.add(chapter);
}
}
}
return chapters;
}
@override
Future<MManga> getDetail(String url) async {
final statusList = [
{
// Ongoing
"OnGoing": 0,
"Продолжается": 0,
"Updating": 0,
"Em Lançamento": 0,
"Em lançamento": 0,
"Em andamento": 0,
"Em Andamento": 0,
"En cours": 0,
"En Cours": 0,
"En cours de publication": 0,
"Ativo": 0,
"Lançando": 0,
"Đang Tiến Hành": 0,
"Devam Ediyor": 0,
"Devam ediyor": 0,
"Devam Ediyo": 0,
"Devam Eden": 0,
"In Corso": 0,
"In Arrivo": 0,
"مستمرة": 0,
"مستمر": 0,
"En Curso": 0,
"En curso": 0,
"Curso": 0,
"Emision": 0,
"En marcha": 0,
"Publicandose": 0,
"Publicándose": 0,
"En emision": 0,
"连载中": 0,
"Đang làm": 0,
"Em postagem": 0,
"Em progresso": 0,
"Em curso": 0,
"Atualizações Semanais": 0,
// Completed
"Completed": 1,
"Completo": 1,
"Completado": 1,
"Concluído": 1,
"Concluido": 1,
"Finalizado": 1,
"Achevé": 1,
"Terminé": 1,
"Complété": 1,
"Hoàn Thành": 1,
"Tamamlandı": 1,
"Tamamlanan": 1,
"Đã hoàn thành": 1,
"Завершено": 1,
"مكتملة": 1,
"مكتمل": 1,
"已完结": 1,
// On Hold
"On Hold": 2,
"Pausado": 2,
"En espera": 2,
"Durduruldu": 2,
"Beklemede": 2,
"Đang chờ": 2,
"متوقف": 2,
"En Pause": 2,
"Заморожено": 2,
"En attente": 2,
// Canceled
"Canceled": 3,
"Cancelado": 3,
"İptal Edildi": 3,
"Güncel": 3,
"Đã hủy": 3,
"ملغي": 3,
"Abandonné": 3,
"Заброшено": 3,
"Annulé": 3,
// Publishing Finished 4
},
];
MManga manga = MManga();
String res = "";
res = (await client.get(Uri.parse(url))).body;
final document = parseHtml(res);
manga.author = document.selectFirst("div.author-content > a")?.text ?? "";
final descriptionElement = document.select(
"div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt, .manga-summary",
);
if (descriptionElement.isNotEmpty) {
final paragraphs = descriptionElement
.expand((e) => e.select("p"))
.toList();
if (paragraphs.isNotEmpty &&
paragraphs.any((p) => p.text.trim().isNotEmpty)) {
manga.description = paragraphs
.map((p) => p.text.replaceAll("<br>", "\n").trim())
.join("\n\n");
} else {
manga.description = descriptionElement
.map((e) => e.text.trim())
.join("\n\n");
}
}
final imageElement = document.selectFirst("div.summary_image img");
manga.imageUrl = extractImageUrl(imageElement);
final id =
document
.selectFirst("div[id^=manga-chapters-holder]")
?.attr("data-id") ??
"";
String mangaId = "";
if (id.isNotEmpty) {
mangaId = id;
}
final status =
document
.selectFirst(
".summary-content > .tags-content, div.summary-content, div.summary-heading:contains(Status) + div",
)
?.text ??
"";
manga.status = parseStatus(status, statusList);
manga.genre =
document.select("div.genres-content a")?.map((e) => e.text).toList() ??
[];
final baseUrl = "${getBaseUrl()}/";
final headers = {"Referer": baseUrl, "X-Requested-With": "XMLHttpRequest"};
final oldXhrChaptersRequest = await client.post(
Uri.parse("${baseUrl}wp-admin/admin-ajax.php"),
headers: headers,
body: {"action": "manga_get_chapters", "manga": mangaId},
);
if (oldXhrChaptersRequest.statusCode == 400) {
res = (await client.post(
Uri.parse("${url}ajax/chapters"),
headers: headers,
)).body;
} else {
res = oldXhrChaptersRequest.body;
}
MDocument chapDoc = parseHtml(res);
manga.chapters = getChapters(chapDoc);
if (manga.chapters.isEmpty) {
res = (await client.post(
Uri.parse("${url}ajax/chapters"),
headers: headers,
)).body;
chapDoc = parseHtml(res);
manga.chapters = getChapters(chapDoc);
}
return manga;
}
@override
Future<List<String>> getPageList(String url) async {
final res = (await client.get(Uri.parse(url)));
final document = parseHtml(res.body);
var images = getImagesFromPage(document);
if (images.length == 1) {
images = buildPageUrls(images, document);
}
return images.isNotEmpty ? images : parseProtectorImage(document);
}
List<String> getImagesFromPage(MDocument doc) {
final elements = doc.select(
"div.page-break img, li.blocks-gallery-item img, .reading-content .text-left:not(:has(.blocks-gallery-item)) img",
);
return elements.map((e) => extractImageUrl(e)?.trim()).toList();
}
List<String> parseProtectorImage(Document doc) {
final protectorData = doc.selectFirst(chapterProtectorSelector)?.innerHtml;
if (protectorData == null) return [];
final password = getPasswordFromProtector(protectorData);
final chapterDataStr = getChapterDataStr(protectorData);
if (chapterDataStr == null || password == null) return [];
final decryptedText = decryptChapterData(chapterDataStr, password);
if (decryptedText == null) return [];
return List<String>.from(jsonDecode(jsonDecode(decryptedText)));
}
// Extract password
String? getPasswordFromProtector(String protectorData) {
final regex = RegExp(r"wpmangaprotectornonce='(.*?)';");
final match = regex.firstMatch(protectorData);
return match != null ? match.group(1) : null;
}
// Extract encrypted chapter string
String? getChapterDataStr(String protectorData) {
final regex = RegExp(r"chapter_data='(.*?)';");
final match = regex.firstMatch(protectorData);
return match != null ? match.group(1)!.replaceAll(r"\/", "/") : null;
}
// Decrypt AES encrypted string
String? decryptChapterData(String chapterDataStr, String password) {
try {
final Map<String, dynamic> chapterData = jsonDecode(chapterDataStr);
final salt = _hexToBytes(chapterData["s"]);
final ct = base64Decode(chapterData["ct"]);
// Prefix with 'Salted__' and salt
final completeCipher = Uint8List.fromList([
...utf8.encode('Salted__'),
...salt,
...ct,
]);
// Encode completeCipher again to base64
final base64Cipher = base64Encode(completeCipher);
// Decrypt
final decrypted = decryptAESCryptoJS(base64Cipher, password);
return decrypted;
} catch (e) {
print("Error decrypting chapter data: $e");
return null;
}
}
List<String> buildPageUrls(List<String> imgs, MDocument document) {
List<String> pageUrls = [];
final pagesNumber = document
.selectFirst("#single-pager")
.select("option")
.length;
final imgUrl = imgs.first;
for (var i = 0; i < pagesNumber; i++) {
final val = i + 1;
if (i.toString().length == 1) {
pageUrls.add(imgUrl.replaceAll("01", '0$val'));
} else {
pageUrls.add(imgUrl.replaceAll("01", val.toString()));
}
}
return pageUrls;
}
// Convert hex string to bytes
List<int> _hexToBytes(String hex) {
final List<int> result = [];
for (var i = 0; i < hex.length; i += 2) {
result.add(int.parse(hex.substring(i, i + 2), radix: 16));
}
return result;
}
@override
List<dynamic> getFilterList() {
return [
TextFilter("AuthorFilter", "Author"),
TextFilter("ArtistFilter", "Artist"),
TextFilter("YearFilter", "Year of Released"),
GroupFilter("StatusFilter", "Status", [
CheckBoxFilter("Completed", "end"),
CheckBoxFilter("Ongoing", "on-going"),
CheckBoxFilter("Canceled", "canceled"),
CheckBoxFilter("On Hold", "on-hold"),
]),
SelectFilter("OrderByFilter", "Order By", 0, [
SelectFilterOption("Relevance", ""),
SelectFilterOption("Latest", "latest"),
SelectFilterOption("A-Z", "alphabet"),
SelectFilterOption("Rating", "rating"),
SelectFilterOption("Trending", "trending"),
SelectFilterOption("Most Views", "views"),
SelectFilterOption("New", "new-manga"),
]),
SelectFilter("AdultContentFilter", "Adult Content", 0, [
SelectFilterOption("All", ""),
SelectFilterOption("None", "0"),
SelectFilterOption("Only", "1"),
]),
];
}
@override
List<dynamic> getSourcePreferences() {
return [
EditTextPreference(
key: "domain_url",
title: getTitleByLang(source.lang),
summary: "",
value: source.baseUrl,
dialogTitle: "URL",
dialogMessage: "",
),
];
}
String getBaseUrl() {
final baseUrl = getPreferenceValue(source.id, "domain_url")?.trim();
if (baseUrl == null || baseUrl.isEmpty) {
return source.baseUrl;
}
return baseUrl.endsWith("/")
? baseUrl.substring(0, baseUrl.length - 1)
: baseUrl;
}
String getTitleByLang(String? lang) {
const titles = {
'ar': 'تحرير الرابط',
'en': 'Edit URL',
'fr': 'Modifier lURL',
'es': 'Editar URL',
'de': 'URL bearbeiten',
'tr': 'URLyi düzenle',
'ru': 'Редактировать URL',
'id': 'Edit URL',
'pt': 'Editar URL',
'it': 'Modifica URL',
'ja': 'URLを編集',
'zh': '编辑网址',
'ko': 'URL 편집',
'fa': 'ویرایش نشانی',
};
return titles[lang?.toLowerCase()] ?? titles['en']!;
}
String ll(String url) {
if (url.contains("?")) {
return "&";
}
return "?";
}
String? extractImageUrl(Element? imageElement) {
if (imageElement == null) return "";
return imageElement.attr("data-src") ??
imageElement.attr("data-lazy-src") ??
imageElement.attr("srcset")?.split(" ")?.first ??
imageElement.getSrc ??
"";
}
String getMangaSubString() {
const worksSources = {"Olaoe", "Mangax Core"};
return worksSources.contains(source.name) ? "works" : "manga";
}
}
Madara main(MSource source) {
return Madara(source: source);
}