diff --git a/dart/manga/src/en/mangabuddy/icon.png b/dart/manga/src/en/mangabuddy/icon.png new file mode 100644 index 00000000..9f7260b2 Binary files /dev/null and b/dart/manga/src/en/mangabuddy/icon.png differ diff --git a/dart/manga/src/en/mangabuddy/mangabuddy.dart b/dart/manga/src/en/mangabuddy/mangabuddy.dart new file mode 100644 index 00000000..df9a5572 --- /dev/null +++ b/dart/manga/src/en/mangabuddy/mangabuddy.dart @@ -0,0 +1,309 @@ +import 'package:mangayomi/bridge_lib.dart'; +import 'dart:convert'; + +class MangaBuddy extends MProvider { + MangaBuddy({required this.source}); + + MSource source; + + final Client client = Client(source); + + @override + bool get supportsLatest => true; + + @override + Map get headers => {}; + + @override + MPages mangaFromElements(List elements, bool hasNextPage) { + List mangaList = []; + + for (var i = 0; i < elements.length; i++) { + final title = elements[i].selectFirst("div.meta > div.title > h3 > a"); + final imageElement = elements[i].selectFirst("div.thumb > a > img"); + final image = imageElement?.attr("data-src") ?? + imageElement?.getSrc ?? + ""; + + MManga manga = MManga(); + manga.name = title.text ?? title.attr("title"); + manga.imageUrl = image; + manga.link = title.attr("href").contains(source.baseUrl) ? title.attr("href") : "${source.baseUrl}${title.attr("href")}"; + mangaList.add(manga); + } + + + return MPages(mangaList, hasNextPage); + } + + @override + Future getPopular(int page) async { + final res = await client.get(Uri.parse("${source.baseUrl}/popular?page=$page")); + final doc = parseHtml(res.body); + + final nextElement = doc.selectFirst("a.page-link[title='Next']"); + bool hasNext = nextElement.text != null; + + return mangaFromElements(doc.select("div.list.manga-list > div.book-item > div.book-detailed-item"), true); + } + + @override + Future getLatestUpdates(int page) async { + final res = await client.get(Uri.parse("${source.baseUrl}/latest?page=$page")); + final doc = parseHtml(res.body); + + final nextElement = doc.selectFirst("a.page-link[title='Next']"); + bool hasNext = nextElement.text != null; + + return mangaFromElements(doc.select("div.list.manga-list > div.book-item > div.book-detailed-item"), true); + } + + @override + Future search(String initialQuery, int page, FilterList filterList) async { + final filters = filterList.filters; + final filterString = ""; + final query = initialQuery; + for (var filter in filters) { + + if (filter.type == "SearchFilter"){ + query = filter.state.toString(); + } else if (filter.type == "GenresFilter") { + for (var genre in filter.state) { + if (genre.state == true) { + filterString += ("&genre[]=${genre.value.toString()}"); + } + } + } else if (filter.type == "StatusFilter") { + filterString += ("&status=${filter.values[filter.state].value.toString()}"); + } else if (filter.type == "OrderFilter") { + filterString += ("&sort=${filter.values[filter.state].value.toString()}"); + } + } + + + final res = await client.get(Uri.parse("${source.baseUrl}/search?$filterString&q=$query&page=$page")); + final doc = parseHtml(res.body); + + return mangaFromElements(doc.select("div.list.manga-list > div.book-item > div.book-detailed-item"), true); + } + + @override + Future getDetail(String url) async { + final statusList = [{ + "Ongoing": 0, + "Completed": 1, + }]; + final res = await client.get(Uri.parse(url)); + final doc = parseHtml(res.body); + + MManga manga = MManga(); + + final chapterIdElement = doc.selectFirst("div.layout > script"); + final idRegex = RegExp(r"var\s+bookId\s*=\s*(\d+);"); + + final imageElement = doc.selectFirst("div.book-info div.img-cover > img"); + final statusElement = doc.selectFirst("div.book-info div.detail > div.meta.box.mt-1.p-10 > p > a[href^='/status/'] > span"); + final authorElements = doc.select("div.book-info div.detail > div.meta.box.mt-1.p-10 > p > a[href^='/authors/'] > span"); + final genreList = doc.select("div.book-info div.detail > div.meta.box.mt-1.p-10 > p > a[href^='/genres/']"); + final descriptionElement = doc.selectFirst("div.section-body.summary > p.content"); + + final chapterIdMatch = idRegex.firstMatch(chapterIdElement?.text); + final chapterId = chapterIdMatch != null ? chapterIdMatch.group(1) ?? "" : ""; + + final image = imageElement?.attr("data-src") ?? imageElement?.getSrc ?? ""; + final status = statusElement.text ?? "Ongoing"; + final author = authorElements.isNotEmpty ? authorElements.map((e) => e.text).join(" | ") : "unknown"; + final genres = genreList.map((e) => (e.text as String).replaceAll(",", "").trim()).toList(); + + final description = descriptionElement?.text ?? ""; + + manga.author = author; + manga.description = description; + manga.imageUrl = image; + manga.genre = genres; + + manga.chapters = await getChapters(chapterId); + manga.status = parseStatus(status, statusList); + return manga; + } + + @override + Future> getChapters(String chapterId) async { + List chapters = []; + + final res = await client.get(Uri.parse("${source.baseUrl}/api/manga/$chapterId/chapters?source=detail")); + MDocument doc = parseHtml(res.body); + + MElement chapterList = doc.selectFirst("ul.chapter-list"); + + for (MElement chapterElement in chapterList.select("li")) { + var chapter = MChapter(); + + final name = chapterElement.selectFirst("strong.chapter-title")?.text; + final url = chapterElement.selectFirst("a")?.attr("href"); + final uploadDate = chapterElement.selectFirst("time.chapter-update")?.text; + + chapter.name = name; + chapter.url = url; + chapter.dateUpload = parseDateToUnix(uploadDate).toString(); + + chapters.add(chapter); + } + + return chapters; + } + + @override + int parseDateToUnix(String dateStr) { + + const monthMap = { + 'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, + 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, + 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12, + }; + + + final parts = dateStr.split(' '); + if (parts.length != 3) return DateTime.now().millisecondsSinceEpoch; + + final monthStr = parts[0]; + final dayStr = parts[1].replaceAll(',', ''); + final yearStr = parts[2]; + + final month = monthMap[monthStr] ?? 1; + final day = int.tryParse(dayStr) ?? 1; + final year = int.tryParse(yearStr) ?? DateTime.now().year; + + final dt = DateTime(year, month, day); + return dt.millisecondsSinceEpoch; + } + + @override + Future>> getPageList(String url) async { + List> images = []; + + final res = await client.get(Uri.parse("${source.baseUrl}$url")); + final doc = parseHtml(res.body); + + final imageScript = doc.select("div#viewer-page.main-container.viewer > script"); + + final rawImageText = imageScript[imageScript.length-1]?.text ?? ""; + + final imageList = rawImageText.replaceAll("var chapImages =", "").replaceAll("'", "").trim().split(","); + + for (final image in imageList) { + images.add({ + "url": image.trim(), + "headers": { + "Referer": source.baseUrl, + } + }); + } + + return images; + } + + + + @override + List getFilterList() { + return [ + TextFilter("SearchFilter", "Search..."), + GroupFilter("GenresFilter", "Genres A-M", [ + CheckBoxFilter("Action", "action"), + CheckBoxFilter("Adaptation", "adaptation"), + CheckBoxFilter("Adult", "adult"), + CheckBoxFilter("Adventure", "adventure"), + CheckBoxFilter("Animal", "animal"), + CheckBoxFilter("Anthology", "anthology"), + CheckBoxFilter("Cartoon", "cartoon"), + CheckBoxFilter("Comedy", "comedy"), + CheckBoxFilter("Comic", "comic"), + CheckBoxFilter("Cooking", "cooking"), + CheckBoxFilter("Demons", "demons"), + CheckBoxFilter("Doujinshi", "doujinshi"), + CheckBoxFilter("Drama", "drama"), + CheckBoxFilter("Ecchi", "ecchi"), + CheckBoxFilter("Fantasy", "fantasy"), + CheckBoxFilter("Full Color", "full-color"), + CheckBoxFilter("Game", "game"), + CheckBoxFilter("Gender bender", "gender-bender"), + CheckBoxFilter("Ghosts", "ghosts"), + CheckBoxFilter("Harem", "harem"), + CheckBoxFilter("Historical", "historical"), + CheckBoxFilter("Horror", "horror"), + CheckBoxFilter("Isekai", "isekai"), + CheckBoxFilter("Josei", "josei"), + CheckBoxFilter("Long strip", "long-strip"), + CheckBoxFilter("Mafia", "mafia"), + CheckBoxFilter("Magic", "magic"), + CheckBoxFilter("Manga", "manga"), + CheckBoxFilter("Manhua", "manhua"), + CheckBoxFilter("Manhwa", "manhwa"), + CheckBoxFilter("Martial arts", "martial-arts"), + CheckBoxFilter("Mature", "mature"), + CheckBoxFilter("Mecha", "mecha"), + CheckBoxFilter("Medical", "medical"), + CheckBoxFilter("Military", "military"), + CheckBoxFilter("Monster", "monster"), + CheckBoxFilter("Monster girls", "monster-girls"), + CheckBoxFilter("Monsters", "monsters"), + CheckBoxFilter("Music", "music"), + CheckBoxFilter("Mystery", "mystery"), + ]), + GroupFilter("GenresFilter", "Genres N-Z", [ + CheckBoxFilter("Office", "office"), + CheckBoxFilter("Office workers", "office-workers"), + CheckBoxFilter("One shot", "one-shot"), + CheckBoxFilter("Police", "police"), + CheckBoxFilter("Psychological", "psychological"), + CheckBoxFilter("Reincarnation", "reincarnation"), + CheckBoxFilter("Romance", "romance"), + CheckBoxFilter("School life", "school-life"), + CheckBoxFilter("Sci fi", "sci-fi"), + CheckBoxFilter("Science fiction", "science-fiction"), + CheckBoxFilter("Seinen", "seinen"), + CheckBoxFilter("Shoujo", "shoujo"), + CheckBoxFilter("Shoujo ai", "shoujo-ai"), + CheckBoxFilter("Shounen", "shounen"), + CheckBoxFilter("Shounen ai", "shounen-ai"), + CheckBoxFilter("Slice of life", "slice-of-life"), + CheckBoxFilter("Smut", "smut"), + CheckBoxFilter("Soft Yaoi", "soft-yaoi"), + CheckBoxFilter("Sports", "sports"), + CheckBoxFilter("Super Power", "super-power"), + CheckBoxFilter("Superhero", "superhero"), + CheckBoxFilter("Supernatural", "supernatural"), + CheckBoxFilter("Thriller", "thriller"), + CheckBoxFilter("Time travel", "time-travel"), + CheckBoxFilter("Tragedy", "tragedy"), + CheckBoxFilter("Vampire", "vampire"), + CheckBoxFilter("Vampires", "vampires"), + CheckBoxFilter("Video games", "video-games"), + CheckBoxFilter("Villainess", "villainess"), + CheckBoxFilter("Web comic", "web-comic"), + CheckBoxFilter("Webtoons", "webtoons"), + CheckBoxFilter("Yaoi", "yaoi"), + CheckBoxFilter("Yuri", "yuri"), + CheckBoxFilter("Zombies", "zombies"), + ]), + SeparatorFilter(), + SelectFilter("StatusFilter", "Status", 0, [ + SelectFilterOption("All (Default)", "all"), + SelectFilterOption("Ongoing", "ongoing"), + SelectFilterOption("Completed", "completed"), + ]), + SelectFilter("OrderFilter", "Order By", 0, [ + SelectFilterOption("Views (Default)", "views"), + SelectFilterOption("Latest Updated", "updated_at"), + SelectFilterOption("Creation Date", "created_at"), + SelectFilterOption("Name A-Z", "name"), + SelectFilterOption("Rating", "rating"), + ]), + ]; + } +} + +MangaBuddy main(MSource source) { + return MangaBuddy(source:source); +} diff --git a/dart/manga/src/en/mangabuddy/source.dart b/dart/manga/src/en/mangabuddy/source.dart new file mode 100644 index 00000000..064de9d0 --- /dev/null +++ b/dart/manga/src/en/mangabuddy/source.dart @@ -0,0 +1,19 @@ +import '../../../../../model/source.dart'; + +Source get mangabuddySource => _mangabuddySource; +const _mangabuddyVersion = "0.0.1"; +const _mangabuddySourceCodeUrl = + "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/dart/manga/src/en/mangabuddy/mangabuddy.dart"; +Source _mangabuddySource = Source( + name: "MangaBuddy", + baseUrl: "http://www.mangabuddy.com", + lang: "en", + typeSource: "single", + isNsfw: true, + iconUrl: "https://github.com/KptnFishy/mangayomi-extensions/blob/patch-1/dart/manga/src/en/mangabuddy/icon.png", + sourceCodeUrl: _mangabuddySourceCodeUrl, + itemType: ItemType.manga, + version: _mangabuddyVersion, + dateFormat: "MMM dd,yyyy", + dateFormatLocale: "en", +);