ManhwaXXL: Move off BakaManga (#818)

* Move ManhwaXXL off BakaManga

* It's joever

* isNsfw = true

* Allow genre matching
This commit is contained in:
beerpsi 2024-01-31 08:26:37 +07:00 committed by GitHub
parent 59db21ae35
commit f4f9edb035
10 changed files with 165 additions and 265 deletions

View File

@ -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"),
)
}

View File

@ -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<SChapter> =
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<Page> {
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<Pair<String, String>>) : UriPartFilter("Category", vals)
abstract fun getGenreList(): Array<Pair<String, String>>
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(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
}
}

View File

@ -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<String>) = BakaMangaGenerator().createAll()
}
}

View File

@ -0,0 +1,8 @@
ext {
extName = "Manhwa XXL"
extClass = ".ManhwaXXL"
extVersionCode = 2
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -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<SChapter> {
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<Genre>) : Filter.Select<String>("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"),
)
}