Update Manhwa18/Manhwa18.net, Manhwaraw.com (#19516)

* Fix ManyToon.me & ManyToon.club

Fix issues in which both sources unable to load any mangas.

Closes #16612

* Update multisrc MyMangaCMS to support other site’s langugae

Also, support a second alternative name and remove generic words like “manhwa”, “engsub” from those names.
Also, mark TruyenTranhLH as NSFW

* Move Manhwa18 & Manhwa18.net to MyMangaCMS theme.
Also fix Manhwa18.net’s searching issue with the previous theme.
Also allow Manhwa18’s filter working since it was not with the previous theme.

Close #18818

* Remove unused import causing PR build check to fail

* MyMangaCms

Bump the `versionId` so the ID will change and users will be forced to migrate to update the URLs.

* fix locale friendly

* Fix Manhwaraw.com won’t show Popular & Latest manhwa

* revert bumping TruyenTranhLH’s version
This commit is contained in:
Cuong M. Tran 2024-01-02 04:03:26 +07:00 committed by GitHub
parent 7e4d35b066
commit e4a62d0fc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 218 additions and 198 deletions

View File

@ -1,90 +0,0 @@
package eu.kanade.tachiyomi.extension.en.manhwa18
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat
import java.util.Locale
class Manhwa18 : FMReader("Manhwa18", "https://manhwa18.com", "en") {
override val requestPath = "tim-kiem"
override val popularSort = "sort=top"
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(popularMangaSelector()).map { popularMangaFromElement(it) }
return MangasPage(mangas, document.select(".pagination_wrap .disabled").text() != "Bottom")
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/$requestPath?".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
return GET(url.toString(), headers)
}
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/$requestPath?listType=pagination&page=$page&sort=update&sort_type=DESC", headers)
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
title = document.select(".series-name").text()
thumbnail_url = document.select("meta[property='og:image']").attr("abs:content")
document.select(".series-information")?.let { info ->
author = info.select(".info-name:contains(Author:) + .info-value").text()
genre = info.select(".info-name:contains(Genre:) + .info-value > a")
.joinToString { it.text().trim() }
description = document.select(".summary-content").text().trim()
info.select(".info-name:contains(Other name:) + .info-value")
.firstOrNull()?.text()?.let {
val altName = removeGenericWords(it)
description = when (title.lowercase(Locale.US)) {
altName.lowercase(Locale.US) -> description
else -> description + "\n\n$altName"
}
}
status =
parseStatus(info.select(".info-name:contains(Status:) + .info-value").text())
}
}
}
private fun removeGenericWords(name: String): String {
val excludeList = listOf("manhwa", "engsub")
return name.split(' ').filterNot { word ->
word.lowercase(Locale.US) in excludeList
}.joinToString(" ")
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(".list-chapters > a").map { element ->
SChapter.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
name = element.attr("title")
date_upload =
SimpleDateFormat("dd/MM/yyyy", Locale.US).parse(
element.select(".chapter-time").text().substringAfter(" - "),
)?.time ?: 0L
chapter_number = element.attr("time").substringAfterLast(' ').toFloatOrNull() ?: -1f
}
}
}
override val pageListImageSelector = "#chapter-content > img"
override fun getFilterList() = FilterList()
}

View File

@ -1,77 +0,0 @@
package eu.kanade.tachiyomi.extension.all.manhwa18net
import eu.kanade.tachiyomi.multisrc.fmreader.FMReader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.Request
import org.jsoup.nodes.Element
class Manhwa18NetFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Manhwa18Net(),
Manhwa18NetRaw(),
)
}
class Manhwa18Net : FMReader("Manhwa18.net", "https://manhwa18.net", "en") {
override val requestPath = "genre/manhwa"
override val popularSort = "sort=top"
override val pageListImageSelector = "div#chapter-content > img"
override fun latestUpdatesRequest(page: Int): Request =
GET(
"$baseUrl/$requestPath?listType=pagination&page=$page&sort=update&sort_type=DESC",
headers,
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val noRawsUrl = super.searchMangaRequest(page, query, filters).url.newBuilder().toString()
return GET(noRawsUrl, headers)
}
override fun getGenreList() = getAdultGenreList()
override fun chapterFromElement(element: Element, mangaTitle: String): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
name = element.attr("title")
date_upload = parseAbsoluteDate(
element.select(chapterTimeSelector).text().substringAfter(" - "),
)
}
}
}
class Manhwa18NetRaw : FMReader("Manhwa18.net", "https://manhwa18.net", "ko") {
override val requestPath = "genre/raw"
override val popularSort = "sort=top"
override val pageListImageSelector = "div#chapter-content > img"
override fun latestUpdatesRequest(page: Int): Request =
GET(
"$baseUrl/$requestPath?listType=pagination&page=$page&sort=update&sort_type=DESC",
headers,
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val onlyRawsUrl = super.searchMangaRequest(page, query, filters).url.newBuilder().toString()
return GET(onlyRawsUrl, headers)
}
override fun getFilterList() = FilterList(
super.getFilterList().filterNot { it == GenreList(getGenreList()) },
)
override fun chapterFromElement(element: Element, mangaTitle: String): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
name = element.attr("title")
date_upload = parseAbsoluteDate(
element.select(chapterTimeSelector).text().substringAfter(" - "),
)
}
}
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.ko.manhwaraw
import eu.kanade.tachiyomi.multisrc.madara.Madara
class ManhwaRaw : Madara("ManhwaRaw", "https://manhwaraw.com", "ko") {
override val mangaSubString = "manhwa-raw"
// The website does not flag the content.
override val filterNonMangaItems = false
}

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.extension.en.manhwa18
import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
import eu.kanade.tachiyomi.source.model.FilterList
class Manhwa18 : MyMangaCMS("Manhwa18", "https://manhwa18.com", "en") {
// Migrated from FMReader to MyMangaCMS.
override val versionId = 2
override val parseAuthorString = "Author"
override val parseAlternativeNameString = "Other name"
override val parseAlternative2ndNameString = "Doujinshi"
override val parseStatusString = "Status"
override val parseStatusOngoingStringLowerCase = "on going"
override val parseStatusOnHoldStringLowerCase = "on hold"
override val parseStatusCompletedStringLowerCase = "completed"
override fun getFilterList(): FilterList = FilterList(
Author("Author"),
Status(
"Status",
"All",
"Ongoing",
"On hold",
"Completed",
),
Sort(
"Order",
"A-Z",
"Z-A",
"Latest update",
"New manhwa",
"Most view",
"Most like",
),
GenreList(getGenreList(), "Genre"),
)
// To populate this list:
// console.log([...document.querySelectorAll("div.search-gerne_item")].map(elem => `Genre("${elem.textContent.trim()}", ${elem.querySelector("label").getAttribute("data-genre-id")}),`).join("\n"))
override fun getGenreList() = listOf(
Genre("Adult", 4),
Genre("Doujinshi", 9),
Genre("Harem", 17),
Genre("Manga", 24),
Genre("Manhwa", 26),
Genre("Mature", 28),
Genre("NTR", 33),
Genre("Romance", 36),
Genre("Webtoon", 57),
Genre("Action", 59),
Genre("Comedy", 60),
Genre("BL", 61),
Genre("Horror", 62),
Genre("Raw", 63),
Genre("Uncensore", 64),
)
override fun dateUpdatedParser(date: String): Long =
runCatching { dateFormatter.parse(date.substringAfter(" - "))?.time }.getOrNull() ?: 0L
}

View File

Before

Width:  |  Height:  |  Size: 212 KiB

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.extension.all.manhwa18net
import eu.kanade.tachiyomi.multisrc.mymangacms.MyMangaCMS
import eu.kanade.tachiyomi.source.model.FilterList
class Manhwa18Net : MyMangaCMS("Manhwa18.net", "https://manhwa18.net", "en") {
// Migrated from FMReader to MyMangaCms.
override val versionId = 2
override val parseAuthorString = "Author"
override val parseAlternativeNameString = "Other name"
override val parseAlternative2ndNameString = "Doujinshi"
override val parseStatusString = "Status"
override val parseStatusOngoingStringLowerCase = "on going"
override val parseStatusOnHoldStringLowerCase = "on hold"
override val parseStatusCompletedStringLowerCase = "completed"
override fun getFilterList(): FilterList = FilterList(
Author("Author"),
Status(
"Status",
"All",
"Ongoing",
"On hold",
"Completed",
),
Sort(
"Order",
"A-Z",
"Z-A",
"Latest update",
"New manhwa",
"Most view",
"Most like",
),
GenreList(getGenreList(), "Genre"),
)
// To populate this list:
// console.log([...document.querySelectorAll("div.search-gerne_item")].map(elem => `Genre("${elem.textContent.trim()}", ${elem.querySelector("label").getAttribute("data-genre-id")}),`).join("\n"))
override fun getGenreList() = listOf(
Genre("Adult", 4),
Genre("Doujinshi", 9),
Genre("Harem", 17),
Genre("Manga", 24),
Genre("Manhwa", 26),
Genre("Mature", 28),
Genre("NTR", 33),
Genre("Romance", 36),
Genre("Webtoon", 57),
Genre("Action", 59),
Genre("Comedy", 60),
Genre("BL", 61),
Genre("Horror", 62),
Genre("Raw", 63),
Genre("Uncensore", 64),
)
override fun dateUpdatedParser(date: String): Long =
runCatching { dateFormatter.parse(date.substringAfter(" - "))?.time }.getOrNull() ?: 0L
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.multisrc.fmreader
import generator.ThemeSourceData.MultiLang
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
@ -13,12 +12,10 @@ class FMReaderGenerator : ThemeSourceGenerator {
override val baseVersionCode: Int = 8
override val sources = listOf(
MultiLang("Manhwa18.net", "https://manhwa18.net", listOf("en", "ko"), className = "Manhwa18NetFactory", isNsfw = true, overrideVersionCode = 1),
SingleLang("Epik Manga", "https://www.epikmanga.com", "tr"),
SingleLang("KissLove", "https://klz9.com", "ja", isNsfw = true, overrideVersionCode = 4),
SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR", overrideVersionCode = 2),
SingleLang("ManhuaRock", "https://manhuarock.net", "vi", overrideVersionCode = 1),
SingleLang("Manhwa18", "https://manhwa18.com", "en", isNsfw = true, overrideVersionCode = 2),
SingleLang("Say Truyen", "https://saytruyenvip.com", "vi", overrideVersionCode = 3),
SingleLang("WeLoveManga", "https://weloma.art", "ja", pkgName = "rawlh", isNsfw = true, overrideVersionCode = 5),
SingleLang("Manga1000", "https://manga1000.top", "ja"),

View File

@ -329,7 +329,7 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("ManhuaScan.info (unoriginal)", "https://manhuascan.info", "en", isNsfw = true, className = "ManhuaScanInfo"),
SingleLang("ManhuaUS", "https://manhuaus.com", "en", overrideVersionCode = 5),
SingleLang("ManhuaZone", "https://manhuazone.org", "en", overrideVersionCode = 1),
SingleLang("Manhwa Raw", "https://manhwaraw.com", "ko", isNsfw = true, overrideVersionCode = 1),
SingleLang("Manhwa Raw", "https://manhwaraw.com", "ko", isNsfw = true, overrideVersionCode = 2),
SingleLang("Manhwa-Latino", "https://manhwa-latino.com", "es", isNsfw = true, className = "ManhwaLatino", overrideVersionCode = 7),
SingleLang("Manhwa-raw", "https://manhwa-raw.com", "all", isNsfw = true, className = "ManhwaDashRaw", overrideVersionCode = 1),
SingleLang("Manhwa18.app", "https://manhwa18.app", "en", isNsfw = true, className = "Manhwa18app"),

View File

@ -29,6 +29,19 @@ abstract class MyMangaCMS(
override val lang: String,
) : ParsedHttpSource() {
protected open val parseAuthorString = "Tác giả"
protected open val parseAlternativeNameString = "Tên khác"
protected open val parseAlternative2ndNameString = "Tên gốc"
protected open val parseStatusString = "Tình trạng"
protected open val parseStatusOngoingStringLowerCase = "đang tiến hành"
protected open val parseStatusOnHoldStringLowerCase = "tạm ngưng"
protected open val parseStatusCompletedStringLowerCase = "đã hoàn thành"
/**
* List of words to be removed when parsing alternative names
*/
protected open val removeGenericWords = listOf("manhwa", "engsub")
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder().apply {
@ -184,20 +197,23 @@ abstract class MyMangaCMS(
)
title = document.select(".series-name").first()!!.text().trim()
var alternativeNames: String? = null
var alternativeNames: String = ""
document.select(".info-item").forEach {
val value = it.select(".info-value")
when (it.select(".info-name").text().trim()) {
"Tên khác:" -> alternativeNames = value.joinToString(", ") { name ->
name.text().trim()
"$parseAlternativeNameString:" -> alternativeNames += value.joinToString(", ") { name ->
removeGenericWords(name.text()).trim() + ", "
}
"Tác giả:" -> author = value.joinToString(", ") { auth ->
"$parseAlternative2ndNameString:" -> alternativeNames += value.joinToString(", ") { name ->
removeGenericWords(name.text()).trim() + ", "
}
"$parseAuthorString:" -> author = value.joinToString(", ") { auth ->
auth.text().trim()
}
"Tình trạng:" -> status = when (value.first()!!.text().lowercase().trim()) {
"đang tiến hành" -> SManga.ONGOING
"tạm ngưng" -> SManga.ON_HIATUS
"đã hoàn thành" -> SManga.COMPLETED
"$parseStatusString:" -> status = when (value.first()!!.text().lowercase().trim()) {
parseStatusOngoingStringLowerCase -> SManga.ONGOING
parseStatusOnHoldStringLowerCase -> SManga.ON_HIATUS
parseStatusCompletedStringLowerCase -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@ -217,8 +233,8 @@ abstract class MyMangaCMS(
descElem.text().trim()
}
if (!alternativeNames.isNullOrEmpty()) {
description = "Tên khác: ${alternativeNames}\n\n" + description
if (alternativeNames.isNotEmpty()) {
description = "$parseAlternativeNameString: ${alternativeNames}\n\n" + description
}
genre = document.select("a[href*=the-loai] span.badge")
@ -230,6 +246,14 @@ abstract class MyMangaCMS(
.attr("style")
.let { backgroundImageRegex.find(it)?.groups?.get(1)?.value }
}
private fun removeGenericWords(name: String): String {
val locale = Locale.forLanguageTag(lang)
return name.split(' ')
.filterNot { word -> word.lowercase(locale) in removeGenericWords }
.joinToString(" ")
}
//endregion
//region Chapter list
@ -294,30 +318,44 @@ abstract class MyMangaCMS(
) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
fun toUriPart() = vals[state].second
}
private class Status : Filter.Select<String>(
"Tình trạng",
protected class Status(
displayName: String = "Tình trạng",
statusAll: String = "Tất cả",
statusOngoing: String = "Đang tiến hành",
statusOnHold: String = "Tạm ngưng",
statusCompleted: String = "Hoàn thành",
) : Filter.Select<String>(
displayName,
arrayOf(
"Tất cả",
"Đang tiến hành",
"Tạm ngưng",
"Hoàn thành",
statusAll,
statusOngoing,
statusOnHold,
statusCompleted,
),
)
private class Sort : UriPartFilter(
"Sắp xếp",
protected class Sort(
displayName: String = "Sắp xếp",
sortAZ: String = "A-Z",
sortZA: String = "Z-A",
sortUpdate: String = "Mới cập nhật",
sortNew: String = "Truyện mới",
sortPopular: String = "Xem nhiều",
sortLike: String = "Được thích nhiều",
) : UriPartFilter(
displayName,
arrayOf(
Pair("A-Z", "az"),
Pair("Z-A", "za"),
Pair("Mới cập nhật", "update"),
Pair("Truyện mới", "new"),
Pair("Xem nhiều", "top"),
Pair("Được thích nhiều", "like"),
Pair(sortAZ, "az"),
Pair(sortZA, "za"),
Pair(sortUpdate, "update"),
Pair(sortNew, "new"),
Pair(sortPopular, "top"),
Pair(sortLike, "like"),
),
4,
)
open class Genre(name: String, val id: Int) : Filter.TriState(name)
private class Author : Filter.Text("Tác giả")
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Thể loại", genres)
protected class Author(displayName: String = "Tác giả") : Filter.Text(displayName)
protected class GenreList(genres: List<Genre>, displayName: String = "Thể loại") : Filter.Group<Genre>(displayName, genres)
override fun getFilterList(): FilterList = FilterList(
Author(),

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.mymangacms
import generator.ThemeSourceData.MultiLang
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
@ -9,15 +10,31 @@ class MyMangaCMSGenerator : ThemeSourceGenerator {
override val themeClass = "MyMangaCMS"
override val baseVersionCode: Int = 1
override val baseVersionCode: Int = 2
override val sources = listOf(
SingleLang(
"TruyenTranhLH",
"https://truyentranhlh.net",
"vi",
isNsfw = true,
overrideVersionCode = 9,
),
SingleLang(
"Manhwa18",
"https://manhwa18.com",
"en",
isNsfw = true,
overrideVersionCode = 9,
),
MultiLang(
"Manhwa18.net",
"https://manhwa18.net",
listOf("en"),
className = "Manhwa18Net",
isNsfw = true,
overrideVersionCode = 8,
),
)
companion object {