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 elements) { List mangaList = []; for (final el in elements) { final postTitle = el.selectFirst( "div.post-title a:not(:has(span.manga-title-badges))", ); final image = extractImageUrl(el.selectFirst("img")); MManga manga = MManga(); manga.name = postTitle.text; manga.imageUrl = substringBefore(image, " "); manga.link = postTitle.getHref; mangaList.add(manga); } return MPages(mangaList, true); } @override Future 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, div.manga__item"), ); } @override Future 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, div.manga__item"), ); } @override Future 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 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 getChapters(MDocument chapDoc) { List 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 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, div.c-page__content div.modal-contenido", ); 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("
", "\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> 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 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 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.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 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 buildPageUrls(List imgs, MDocument document) { List 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 _hexToBytes(String hex) { final List 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 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 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 l’URL', 'es': 'Editar URL', 'de': 'URL bearbeiten', 'tr': 'URL’yi 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 sourceTypeMap = { "Olaoe": "works", "Mangax Core": "works", "Azora": "series", "Manga Crab": "series", }; return sourceTypeMap[source.name] ?? "manga"; } } Madara main(MSource source) { return Madara(source: source); }