diff --git a/multisrc/overrides/bakamanga/manhwaxxl/src/ManhwaXXL.kt b/multisrc/overrides/bakamanga/manhwaxxl/src/ManhwaXXL.kt deleted file mode 100644 index ba65d8f4f..000000000 --- a/multisrc/overrides/bakamanga/manhwaxxl/src/ManhwaXXL.kt +++ /dev/null @@ -1,68 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.manhwaxxl - -import eu.kanade.tachiyomi.multisrc.bakamanga.BakaManga - -class ManhwaXXL : BakaManga( - "Manhwa XXL", - "https://manhwaxxl.com", - "en", -) { - override fun getGenreList() = arrayOf( - Pair("All", ""), - Pair("Action", "action"), - Pair("Adaptation", "adaptation"), - Pair("Adult", "adult"), - Pair("Adventure", "adventure"), - Pair("BL", "bl"), - Pair("Comedy", "comedy"), - Pair("Cooking", "cooking"), - Pair("Demons", "demons"), - Pair("Drama", "drama"), - Pair("Ecchi", "ecchi"), - Pair("Fantasy", "fantasy"), - Pair("Full color", "full-color"), - Pair("Game", "game"), - Pair("Gender Bender", "gender-bender"), - Pair("GL", "gl"), - Pair("Harem", "harem"), - Pair("Historical", "historical"), - Pair("Horror", "horror"), - Pair("Isekai", "isekai"), - Pair("Josei", "josei"), - Pair("Live action", "live-action"), - Pair("Love & Romance", "love-romance"), - Pair("Magic", "magic"), - Pair("Manga", "manga"), - Pair("Manhua", "manhua"), - Pair("Manhwa", "manhwa"), - Pair("Martial Arts", "martial-arts"), - Pair("Mature", "mature"), - Pair("Mecha", "mecha"), - Pair("Mystery", "mystery"), - Pair("Omegaverse", "omegaverse"), - Pair("Psychological", "psychological"), - Pair("Raw", "raw"), - Pair("Reincarnation", "reincarnation"), - Pair("Romance", "romance"), - Pair("RPG", "rpg"), - Pair("School Life", "school-life"), - Pair("Sci-fi", "sci-fi"), - Pair("Seinen", "seinen"), - Pair("Shoujo", "shoujo"), - Pair("Shoujo Ai", "shoujo-ai"), - Pair("Shounen", "shounen"), - Pair("Slice of Life", "slice-of-life"), - Pair("Smut", "smut"), - Pair("Sports", "sports"), - Pair("Supernatural", "supernatural"), - Pair("Thriller", "thriller"), - Pair("Tragedy", "tragedy"), - Pair("Vampire", "vampire"), - Pair("Vanilla", "vanilla"), - Pair("Webtoon", "webtoon"), - Pair("Webtoons", "webtoons"), - Pair("Yaoi", "yaoi"), - Pair("Yuri", "yuri"), - Pair("Zombie", "zombie"), - ) -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakamanga/BakaManga.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakamanga/BakaManga.kt deleted file mode 100644 index c73850ca0..000000000 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakamanga/BakaManga.kt +++ /dev/null @@ -1,176 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.bakamanga - -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.model.Filter -import eu.kanade.tachiyomi.source.model.FilterList -import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.source.model.SChapter -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.source.online.ParsedHttpSource -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request -import okhttp3.Response -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element -import java.util.Calendar - -abstract class BakaManga( - override val name: String, - override val baseUrl: String, - override val lang: String, -) : ParsedHttpSource() { - override val supportsLatest = true - - // Popular - override fun popularMangaRequest(page: Int): Request = - GET("$baseUrl/most-views/page/$page", headers) - - override fun popularMangaSelector(): String = - ".li_truyen" - - override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { - setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) - title = element.selectFirst(".name")!!.text() - thumbnail_url = element.selectFirst("img")!!.absUrl("src") - } - - override fun popularMangaNextPageSelector(): String? = - ".page_redirect > a:last-child:not(.active)" - - // Search - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - return if (query.isNotEmpty()) { - val url = "$baseUrl/page/$page".toHttpUrl().newBuilder() - .addQueryParameter("s", query) - GET(url.build(), headers) - } else { - val filterList = if (filters.isEmpty()) getFilterList() else filters - val genreFilter = filterList.find { it is GenreFilter } as GenreFilter - val url = "$baseUrl/category/${genreFilter.toUriPart()}/page/$page" - GET(url, headers) - } - } - - override fun searchMangaSelector(): String = - popularMangaSelector() - - override fun searchMangaFromElement(element: Element): SManga = - popularMangaFromElement(element) - - override fun searchMangaNextPageSelector(): String? = - popularMangaNextPageSelector() - - // Latest - override fun latestUpdatesRequest(page: Int): Request = - GET("$baseUrl/latest-updates/page/$page", headers) - - override fun latestUpdatesSelector(): String = - popularMangaSelector() - - override fun latestUpdatesFromElement(element: Element): SManga = - popularMangaFromElement(element) - - override fun latestUpdatesNextPageSelector(): String? = - popularMangaNextPageSelector() - - // Details - override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { - val info = document.selectFirst(".box_info")!! - title = info.selectFirst("h1")!!.text() - artist = info.select(".info-item:contains(Artist:) > a").joinToString { it.text() } - - val descElements = info.select(".story-detail-info:matchText") - description = when { - descElements.size > 2 -> { - descElements.removeFirst() // "Summary:" - descElements.removeLast() // "-From example.com" - descElements.joinToString("\n") { it.text() } - } - else -> "" - } - - val altTitles = info.selectFirst(".info-item:contains(Alternate Title:)") - ?.text() - ?.removePrefix("Alternate Title:") - ?.trim() - - if (altTitles != null && altTitles.isNotEmpty()) { - description += "\n\nAlt title(s): $altTitles" - } - - genre = info.select(".post-categories > li > a").joinToString { it.text() } - status = info.selectFirst(".info-item:contains(Status:)")!!.text() - .removePrefix("Status:") - .trim() - .toStatus() - - thumbnail_url = info.selectFirst(".box_info img")!!.absUrl("src") - } - - // Chapters - override fun chapterListParse(response: Response): List = - super.chapterListParse(response).reversed() - - override fun chapterListSelector(): String = - ".list-chapters > .list-chapters > .box_list > .chapter-item" - - override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { - setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) - name = element.selectFirst(".chap_name")!!.text() - chapter_number = name - .substringAfter(' ') - .substringBefore(' ') - .toFloatOrNull() ?: -1f - - date_upload = parseRelativeDate(element.selectFirst(".chap_update")!!.text()) - } - - private fun parseRelativeDate(date: String): Long { - val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0 - val cal = Calendar.getInstance() - - return when { - date.contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis - date.contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis - date.contains("week") -> cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis - date.contains("day") -> cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis - date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis - date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis - date.contains("second") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis - else -> 0 - } - } - - // Pages - override fun pageListParse(document: Document): List { - return document.select("noscript > img").mapIndexed { i, img -> - Page(i, document.location(), img.absUrl("src")) - } - } - - override fun imageUrlParse(document: Document): String = - "" - - // Filter - override fun getFilterList() = FilterList( - Filter.Header("NOTE: Ignored if using text search!"), - Filter.Separator(), - GenreFilter(getGenreList()), - ) - - class GenreFilter(vals: Array>) : UriPartFilter("Category", vals) - - abstract fun getGenreList(): Array> - - open class UriPartFilter(displayName: String, private val vals: Array>) : - Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { - fun toUriPart() = vals[state].second - } - - // Other - private fun String.toStatus() = when (this) { - "Ongoing" -> SManga.ONGOING - "Completed" -> SManga.COMPLETED - else -> SManga.UNKNOWN - } -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakamanga/BakaMangaGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakamanga/BakaMangaGenerator.kt deleted file mode 100644 index b271dd90e..000000000 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/bakamanga/BakaMangaGenerator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package eu.kanade.tachiyomi.multisrc.bakamanga - -import generator.ThemeSourceData.SingleLang -import generator.ThemeSourceGenerator - -class BakaMangaGenerator : ThemeSourceGenerator { - override val themePkg = "bakamanga" - - override val themeClass = "BakaManga" - - override val baseVersionCode = 1 - - override val sources = listOf( - SingleLang("Manhwa XXL", "https://manhwaxxl.com", "en", isNsfw = true), - ) - - companion object { - @JvmStatic - fun main(args: Array) = BakaMangaGenerator().createAll() - } -} diff --git a/src/en/manhwaxxl/build.gradle b/src/en/manhwaxxl/build.gradle new file mode 100644 index 000000000..8ed4b0043 --- /dev/null +++ b/src/en/manhwaxxl/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = "Manhwa XXL" + extClass = ".ManhwaXXL" + extVersionCode = 2 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-hdpi/ic_launcher.png b/src/en/manhwaxxl/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-hdpi/ic_launcher.png rename to src/en/manhwaxxl/res/mipmap-hdpi/ic_launcher.png diff --git a/multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-mdpi/ic_launcher.png b/src/en/manhwaxxl/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-mdpi/ic_launcher.png rename to src/en/manhwaxxl/res/mipmap-mdpi/ic_launcher.png diff --git a/multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-xhdpi/ic_launcher.png b/src/en/manhwaxxl/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-xhdpi/ic_launcher.png rename to src/en/manhwaxxl/res/mipmap-xhdpi/ic_launcher.png diff --git a/multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-xxhdpi/ic_launcher.png b/src/en/manhwaxxl/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-xxhdpi/ic_launcher.png rename to src/en/manhwaxxl/res/mipmap-xxhdpi/ic_launcher.png diff --git a/multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/manhwaxxl/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/bakamanga/manhwaxxl/res/mipmap-xxxhdpi/ic_launcher.png rename to src/en/manhwaxxl/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/en/manhwaxxl/src/eu/kanade/tachiyomi/extension/en/manhwaxxl/ManhwaXXL.kt b/src/en/manhwaxxl/src/eu/kanade/tachiyomi/extension/en/manhwaxxl/ManhwaXXL.kt new file mode 100644 index 000000000..794804aaf --- /dev/null +++ b/src/en/manhwaxxl/src/eu/kanade/tachiyomi/extension/en/manhwaxxl/ManhwaXXL.kt @@ -0,0 +1,157 @@ +package eu.kanade.tachiyomi.extension.en.manhwaxxl + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +class ManhwaXXL : ParsedHttpSource() { + + override val name = "Manhwa XXL" + + override val lang = "en" + + override val baseUrl = "https://manhwaxxl.com" + + override val supportsLatest = true + + // Site changed from BakaManga + override val versionId = 2 + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + override fun popularMangaRequest(page: Int) = + GET("$baseUrl/popular" + (if (page > 1) "/page/$page" else "")) + + override fun popularMangaSelector() = "section#page ul.row li" + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.selectFirst("span.manga-name a")!!.attr("href")) + title = element.selectFirst("span.manga-name h2")!!.text() + thumbnail_url = element.selectFirst("img")?.absUrl("src") + } + + override fun popularMangaNextPageSelector() = "ul.pagination li.active:not(:last-child)" + + override fun latestUpdatesRequest(page: Int) = + GET("$baseUrl/latest" + (if (page > 1) "/page/$page" else "")) + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + if (query.isNotEmpty()) { + addQueryParameter("s", query) + } else { + val filterList = if (filters.isEmpty()) getFilterList() else filters + val genreFilter = filterList.find { it is GenreFilter } as GenreFilter + val genreId = genreFilter.genres[genreFilter.state].id + + if (genreId.isEmpty()) { + addPathSegment("popular") + } else { + addPathSegment("category") + addPathSegment(genreId) + } + } + + if (page > 1) { + addPathSegment("page") + addPathSegment(page.toString()) + } + }.build() + + return GET(url, headers) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val statusBadge = document.selectFirst("span.card-title i")?.classNames() ?: emptySet() + + title = document.selectFirst("span.card-title h1")!!.text() + author = document.selectFirst("div:has(> i.fa-user)")?.ownText() + description = document.selectFirst("div.manga-info")?.text() + genre = document.select("ul.post-categories li").joinToString { it.text() } + status = when { + statusBadge.contains("fa-circle-check") -> SManga.COMPLETED + statusBadge.contains("fa-rotate") -> SManga.ONGOING + else -> SManga.UNKNOWN + } + thumbnail_url = document.selectFirst("div.card div.manga-avatar img")?.absUrl("src") + } + + // Manga details page have paginated chapter list. We sacrifice `date_upload` + // but we save a bunch of calls, since each page is like 12 chapters. + override fun chapterListParse(response: Response): List { + val detailsDocument = response.asJsoup() + val firstChapter = detailsDocument.selectFirst("ul.chapters-list li.item-chapter a")?.absUrl("href") + ?: return emptyList() + val document = client.newCall(GET(firstChapter, headers)).execute().asJsoup() + + return document.select(chapterListSelector()).map { chapterFromElement(it) }.reversed() + } + + override fun chapterListSelector() = "ul#slide-out a.chapter-link" + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + setUrlWithoutDomain(element.attr("href")) + name = element.text() + } + + override fun pageListParse(document: Document) = + document.select("div#viewer img").mapIndexed { i, it -> + Page(i, imageUrl = it.absUrl("src")) + } + + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() + + override fun getFilterList() = FilterList( + Filter.Header("Ignored if using text search"), + GenreFilter(getGenreList()), + ) + + private data class Genre(val name: String, val id: String) { + override fun toString() = name + } + + private class GenreFilter(val genres: Array) : Filter.Select("Genre", genres.map { it.id }.toTypedArray()) + + // https://manhwaxxl.com/genres + // copy([...document.querySelectorAll("section#page ul li a:not([class])")].map((e) => `Genre("${e.textContent.trim()}", "${e.href.split("/").slice(-1)[0].replace(/#page$/u, "")}"),`).join("\n")) + private fun getGenreList() = arrayOf( + Genre("All", ""), + Genre("Action", "action"), + Genre("Adult", "adult"), + Genre("BL", "bl"), + Genre("Comedy", "comedy"), + Genre("Doujinshi", "doujinshi"), + Genre("Harem", "harem"), + Genre("Horror", "horror"), + Genre("Manga", "manga"), + Genre("Manhwa", "manhwa"), + Genre("Mature", "mature"), + Genre("NTR", "ntr"), + Genre("Romance", "romance"), + Genre("Uncensore", "uncensore"), + Genre("Webtoon", "webtoon"), + ) +}