From 02581c9e2d7b9e10eea391ecec7ac7b36bf3a592 Mon Sep 17 00:00:00 2001 From: devi Date: Sat, 19 Aug 2023 15:38:40 +0200 Subject: [PATCH] Change and add Manga4Life --- .../kotatsu/parsers/site/en/ComicExtra.kt | 7 +- .../kotatsu/parsers/site/en/Comicastle.kt | 3 +- .../kotatsu/parsers/site/en/Manga4Life.kt | 280 ++++++++++++++++++ .../parsers/site/madara/en/CoffeeMangaTop.kt | 2 +- .../parsers/site/madara/en/MangaPure.kt | 4 +- .../parsers/site/mangabox/en/Mangakakalot.kt | 4 +- .../parsers/site/mangareader/en/RealmScans.kt | 4 +- .../kotatsu/parsers/site/pt/BrMangas.kt | 8 +- .../kotatsu/parsers/site/sinmh/SinmhParser.kt | 6 +- 9 files changed, 298 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manga4Life.kt diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt index 9e89a034..a25e2b7d 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/ComicExtra.kt @@ -86,7 +86,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex val doc = webClient.httpGet("https://$domain/popular-comic").parseHtml() return doc.select("li.tag-item a").mapNotNullToSet { a -> MangaTag( - key = a.attr("href").substringAfterLast("/"), + key = a.attr("href").substringAfterLast('/'), title = a.text(), source = source, ) @@ -95,6 +95,7 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex override suspend fun getDetails(manga: Manga): Manga { val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + val dateFormat = SimpleDateFormat("MM/dd/yy", sourceLocale) return manga.copy( altTitle = doc.selectFirstOrThrow("dt.movie-dt:contains(Alternate name:) + dd").text(), state = when (doc.selectFirstOrThrow("dt.movie-dt:contains(Status:) + dd a").text()) { @@ -113,16 +114,14 @@ internal class ComicExtra(context: MangaLoaderContext) : PagedMangaParser(contex description = doc.getElementById("film-content")?.text(), chapters = doc.requireElementById("list").select("tr") .mapChapters(reversed = true) { i, tr -> - val a = tr.selectFirstOrThrow("a") val url = a.attrAsRelativeUrl("href") + "/full" val name = a.text() val dateText = tr.select("td").last()?.text() - val dateFormat = SimpleDateFormat("MM/dd/yy", sourceLocale) MangaChapter( id = generateUid(url), name = name, - number = i, + number = i + 1, url = url, scanlator = null, uploadDate = dateFormat.tryParse(dateText), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Comicastle.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Comicastle.kt index eaf9da4f..22213527 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Comicastle.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Comicastle.kt @@ -126,13 +126,12 @@ internal class Comicastle(context: MangaLoaderContext) : PagedMangaParser(contex description = root.getElementById("comic-desc")?.text(), chapters = root.select("div.card-body > .table-responsive tr a") .mapChapters { i, a -> - val url = a.attrAsRelativeUrl("href") val name = a.text() MangaChapter( id = generateUid(url), name = name, - number = i, + number = i + 1, url = url, scanlator = null, uploadDate = 0, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manga4Life.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manga4Life.kt new file mode 100644 index 00000000..ee132482 --- /dev/null +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/en/Manga4Life.kt @@ -0,0 +1,280 @@ +package org.koitharu.kotatsu.parsers.site.en + +import okhttp3.Headers +import org.json.JSONArray +import org.json.JSONObject +import org.koitharu.kotatsu.parsers.MangaLoaderContext +import org.koitharu.kotatsu.parsers.MangaSourceParser +import org.koitharu.kotatsu.parsers.PagedMangaParser +import org.koitharu.kotatsu.parsers.config.ConfigKey +import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.network.UserAgents +import org.koitharu.kotatsu.parsers.util.* +import org.koitharu.kotatsu.parsers.util.json.* +import java.text.SimpleDateFormat +import java.util.EnumSet + + +@MangaSourceParser("MANGA4LIFE", "Manga4Life", "en") +internal class Manga4Life(context: MangaLoaderContext) : PagedMangaParser(context, MangaSource.MANGA4LIFE, 0) { + + override val sortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL) + + override val configKeyDomain = ConfigKey.Domain("manga4life.com") + + override val headers: Headers = Headers.Builder() + .add("User-Agent", UserAgents.CHROME_DESKTOP) + .build() + + + override suspend fun getListPage( + page: Int, + query: String?, + tags: Set?, + sortOrder: SortOrder, + ): List { + + if (page > 1) { + return emptyList() + } + + val url = buildString { + append("https://$domain/search/") + } + val doc = webClient.httpGet(url).parseHtml() + val json = JSONArray( + doc.selectFirstOrThrow("script:containsData(MainFunction)").data() + .substringAfter("vm.Directory = ").substringBefore("vm.GetIntValue").trim() + .replace(";", " "), + ) + + + val manga = ArrayList() + + for (i in 0 until json.length()) { + val m = json.getJSONObject(i) + val href = "/manga/" + m.getString("i") + val imgUrl = "https://temp.compsci88.com/cover/" + m.getString("i") + ".jpg" + if (!query.isNullOrEmpty()) { + if (m.getString("i").contains(query.urlEncoded(), ignoreCase = true)) { + manga.add( + Manga( + id = generateUid(href), + title = m.getString("i").replace("-", " "), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = imgUrl, + tags = emptySet(), + state = null, + author = null, + source = source, + ), + ) + } + + } else if (!tags.isNullOrEmpty()) { + + val a = m.getJSONArray("g").toString() + var found = true + tags.forEach { + if (!a.contains(it.key, ignoreCase = true)) { + found = false + } + } + if (found) { + manga.add( + Manga( + id = generateUid(href), + title = m.getString("i").replace("-", " "), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = imgUrl, + tags = emptySet(), + state = null, + author = null, + source = source, + ), + ) + } + } else { + manga.add( + Manga( + id = generateUid(href), + title = m.getString("i").replace("-", " "), + altTitle = null, + url = href, + publicUrl = href.toAbsoluteUrl(domain), + rating = RATING_UNKNOWN, + isNsfw = false, + coverUrl = imgUrl, + tags = emptySet(), + state = null, + author = null, + source = source, + ), + ) + } + + + } + + return manga + } + + override suspend fun getTags(): Set { + val doc = webClient.httpGet("https://$domain/search/").parseHtml() + val tags = doc.selectFirstOrThrow("script:containsData(vm.AvailableFilters)").data() + .substringAfter("\"Genre\" \t\t: [").substringBefore("]").replace("'", "").split(",") + + return tags.mapNotNullToSet { tag -> + MangaTag( + key = tag, + title = tag, + source = source, + ) + } + } + + override suspend fun getDetails(manga: Manga): Manga { + val doc = webClient.httpGet(manga.url.toAbsoluteUrl(domain)).parseHtml() + + val chapter = JSONArray( + JSONArray( + doc.selectFirstOrThrow("script:containsData(MainFunction)").data() + .substringAfter("vm.Chapters = ").substringBefore(";"), + ).toJSONList().reversed(), + ) + + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:SS", sourceLocale) + + return manga.copy( + altTitle = null, + state = when (doc.selectFirstOrThrow(".list-group-item:contains(Status:) a").text()) { + "Ongoing (Scan)", "Ongoing (Publish)" -> MangaState.ONGOING + "Complete (Scan)", "Complete (Publish)", + "Cancelled (Scan)", "Cancelled (Publish)", + "Discontinued (Scan)", "Discontinued (Publish)", + "Hiatus (Scan)", "Hiatus (Publish)", + -> MangaState.FINISHED + + else -> null + }, + tags = doc.select(".list-group-item:contains(Genre(s):) a").mapNotNullToSet { a -> + MangaTag( + key = a.attr("href").substringAfterLast("="), + title = a.text(), + source = source, + ) + }, + author = doc.select(".list-group-item:contains(Author(s):) a").text(), + description = doc.selectFirstOrThrow(".top-5.Content").text(), + + chapters = chapter.mapJSONIndexed { i, j -> + val indexChapter = j.getString("Chapter")!! + val url = "/read-online/" + manga.url.substringAfter("/manga/") + chapterURLEncode(indexChapter) + val name = j.getString("ChapterName").let { + if (it.isNullOrEmpty() || it == "null") "${j.getString("Type")} ${ + chapterImage( + indexChapter, + true, + ) + }" else it + } + val date = j.getString("Date") + MangaChapter( + id = generateUid(url), + name = name, + number = i + 1, + url = url, + scanlator = null, + uploadDate = dateFormat.tryParse(date), + branch = null, + source = source, + ) + }, + ) + } + + private fun chapterURLEncode(e: String): String { + var index = "" + val t = e.substring(0, 1).toInt() + if (1 != t) { + index = "-index-$t" + } + val dgt = if (e.toInt() < 100100) { + 4 + } else if (e.toInt() < 101000) { + 3 + } else if (e.toInt() < 110000) { + 2 + } else { + 1 + } + val n = e.substring(dgt, e.length - 1) + var suffix = "" + val path = e.substring(e.length - 1).toInt() + if (0 != path) { + suffix = ".$path" + } + return "-chapter-$n$suffix$index.html" + } + + private val chapterImageRegex = Regex("""^0+""") + + private fun chapterImage(e: String, cleanString: Boolean = false): String { + // cleanString will result in an empty string if chapter number is 0, hence the else if below + val a = e.substring(1, e.length - 1).let { if (cleanString) it.replace(chapterImageRegex, "") else it } + // If b is not zero, indicates chapter has decimal numbering + val b = e.substring(e.length - 1).toInt() + return if (b == 0 && a.isNotEmpty()) { + a + } else if (b == 0 && a.isEmpty()) { + "0" + } else { + "$a.$b" + } + } + + override suspend fun getPages(chapter: MangaChapter): List { + val fullUrl = chapter.url.toAbsoluteUrl(domain) + val doc = webClient.httpGet(fullUrl).parseHtml() + val script = doc.selectFirstOrThrow("script:containsData(MainFunction)").data() + val curChapter = JSONObject( + doc.selectFirstOrThrow("script:containsData(MainFunction)").data().substringAfter("vm.CurChapter = ") + .substringBefore(";"), + ) + val pageTotal = curChapter.getString("Page")!!.toInt() + val host = "https://" + + script + .substringAfter("vm.CurPathName = \"", "") + .substringBefore("\"") + .also { + if (it.isEmpty()) { + throw Exception("Manga4Life is overloaded and blocking Tachiyomi right now. Wait for unblock.") + } + } + val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"") + val seasonURI = curChapter.getString("Directory")!! + .let { if (it.isEmpty()) "" else "$it/" } + val path = "$host/manga/$titleURI/$seasonURI" + val chNum = chapterImage(curChapter.getString("Chapter")!!) + + return IntRange(1, pageTotal).mapIndexed { i, _ -> + val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) } + val url = "$path$chNum-$imageNum.png" + MangaPage( + id = generateUid(url), + url = url, + preview = null, + source = source, + ) + } + } + +} diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/CoffeeMangaTop.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/CoffeeMangaTop.kt index 0b3c21b2..f824e025 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/CoffeeMangaTop.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/CoffeeMangaTop.kt @@ -130,7 +130,7 @@ internal class CoffeeMangaTop(context: MangaLoaderContext) : override suspend fun getPages(chapter: MangaChapter): List { val fullUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() - val urlarray = doc.select("p#arraydata").text().split(",").toTypedArray() + val urlarray = doc.select("p#arraydata").text().split(',').toTypedArray() return urlarray.map { url -> MangaPage( id = generateUid(url), diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt index 605da5ae..f735fc92 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/madara/en/MangaPure.kt @@ -90,8 +90,8 @@ internal class MangaPure(context: MangaLoaderContext) : author = summary?.selectFirst(".mg_author")?.selectFirst("a")?.ownText(), state = when (summary?.selectFirst(".mg_status")?.selectFirst(".summary-content")?.ownText()?.trim() ?.lowercase()) { - "Ongoing" -> MangaState.ONGOING - "Completed " -> MangaState.FINISHED + "ongoing" -> MangaState.ONGOING + "completed" -> MangaState.FINISHED else -> null }, source = source, diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt index 8623f32d..5539c0d9 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangabox/en/Mangakakalot.kt @@ -42,9 +42,9 @@ internal class Mangakakalot(context: MangaLoaderContext) : append("$listUrl/") when (sortOrder) { SortOrder.POPULARITY -> append("?type=topview") - SortOrder.UPDATED -> append("") + SortOrder.UPDATED -> append("?type=latest") SortOrder.NEWEST -> append("?type=newest") - else -> append("") + else -> append("?type=latest") } if (!tags.isNullOrEmpty()) { append("&category=") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RealmScans.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RealmScans.kt index d071f531..adfa4484 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RealmScans.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/mangareader/en/RealmScans.kt @@ -68,9 +68,9 @@ internal class RealmScans(context: MangaLoaderContext) : val selectTag = docs.select(".wd-full .mgen > a") val tags = selectTag.mapNotNullToSet { tagMap[it.text()] } val mangaState = docs.selectFirst(".bs-status")?.let { - when (it.text()) { + when (it.text().lowercase()) { "ongoing" -> MangaState.ONGOING - "Completed" -> MangaState.FINISHED + "completed" -> MangaState.FINISHED else -> null } } diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt index 9a9e106f..6f5c5aae 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/pt/BrMangas.kt @@ -86,7 +86,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, val doc = webClient.httpGet("https://$domain/lista-de-generos-de-manga/").parseHtml() return doc.select(".genres_page a").mapNotNullToSet { a -> MangaTag( - key = a.attr("href").removeSuffix("/").substringAfterLast("/"), + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), title = a.text(), source = source, ) @@ -100,7 +100,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, state = null, tags = doc.select("div.serie-infos li:contains(Categorias:) a").mapNotNullToSet { a -> MangaTag( - key = a.attr("href").removeSuffix("/").substringAfterLast("/"), + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), title = a.text(), source = source, ) @@ -115,7 +115,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, MangaChapter( id = generateUid(url), name = name, - number = i, + number = i + 1, url = url, scanlator = null, uploadDate = 0, @@ -130,7 +130,7 @@ internal class BrMangas(context: MangaLoaderContext) : PagedMangaParser(context, val fullUrl = chapter.url.toAbsoluteUrl(domain) val doc = webClient.httpGet(fullUrl).parseHtml() val scriptData = - doc.selectFirstOrThrow("script:containsData(imageArray)").data().substringAfter("[").substringBefore("]") + doc.selectFirstOrThrow("script:containsData(imageArray)").data().substringAfter('[').substringBefore(']') .split(",") return scriptData.map { data -> val url = data.replace("\\\"", "").replace("\\/", "/") diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt index 95fac21a..6f3499f1 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/sinmh/SinmhParser.kt @@ -104,7 +104,7 @@ internal abstract class SinmhParser( override suspend fun getTags(): Set { val doc = webClient.httpGet("https://$domain/$listUrl").parseHtml() return doc.select(".filter-item:contains(按剧情) li a:not(.active)").mapNotNullToSet { a -> - val href = a.attr("href").removeSuffix("/").substringAfterLast("/") + val href = a.attr("href").removeSuffix('/').substringAfterLast('/') MangaTag( key = href, title = a.text(), @@ -136,7 +136,7 @@ internal abstract class SinmhParser( manga.copy( tags = doc.body().select(selectGenre).mapNotNullToSet { a -> MangaTag( - key = a.attr("href").removeSuffix("/").substringAfterLast('/'), + key = a.attr("href").removeSuffix('/').substringAfterLast('/'), title = a.text().toTitleCase(), source = source, ) @@ -152,7 +152,7 @@ internal abstract class SinmhParser( protected open suspend fun getChapters(manga: Manga, doc: Document): List { return doc.body().select(selectChapter).mapChapters { i, li -> - val href = li.selectFirstOrThrow("a").attrAsRelativeUrlOrNull("href") ?: li.parseFailed("Link is missing") + val href = li.selectFirstOrThrow("a").attrAsRelativeUrl("href") val name = li.selectFirstOrThrow("a").text() MangaChapter( id = generateUid(href),