WPComics update & add new sources (#1909)

* remove non-relevant query

* WPComics: query for genres instead of hard-code

* language assets to support dual-lang

* update XoxoComics, Nhattruyen, Nettruyen to support updated WPComics

* remove unused status

* JManga with new WPComics

* Fix JManga NextPageSelector

* Allow override some more methods

* correct jmanga's location

* remove redundant XoxoComics override

* Get alternative name and JManga's description

* add sources:
- NetTruyenX
- NhatTruyenS
- NetTruyenCO

* revert format changes

* Update NetTruyen to latest domain

* Minor changes:

- Named parameters;
- intl, lazy;

* Remove NetTruyen’s replaceSearchPath. It’s not necessary

* remove the japanese’s mtl

* remove hard-code user-agent

* remove some unnecessary named parameters

* Use super.headersBuilder & fix Referer

* remove redundant import
This commit is contained in:
Cuong M. Tran 2024-03-19 23:06:03 +07:00 committed by Draff
parent 455f57d209
commit a0fa7fa458
30 changed files with 330 additions and 324 deletions

View File

@ -0,0 +1,7 @@
STATUS=Status
STATUS_ALL=All
STATUS_ONGOING=Ongoing
STATUS_COMPLETED=Completed
GENRE=Genre
GENRES_RESET=Tap 'Reset' to load genres
OTHER_NAME=Alternate Name

View File

@ -0,0 +1,5 @@
STATUS=状態
STATUS_ALL=全て
STATUS_ONGOING=連載中
STATUS_COMPLETED=完結済み
GENRE=ジャンル

View File

@ -0,0 +1,7 @@
STATUS=Trạng thái
STATUS_ALL=Tất cả
STATUS_ONGOING=Đang tiến hành
STATUS_COMPLETED=Hoàn thành
GENRE=Thể loại
GENRES_RESET=Ấn 'Reset' để tải danh sách thể loại
OTHER_NAME=Tên khác

View File

@ -2,4 +2,8 @@ plugins {
id("lib-multisrc") id("lib-multisrc")
} }
baseVersionCode = 4 baseVersionCode = 5
dependencies {
api(project(":lib:i18n"))
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.wpcomics package eu.kanade.tachiyomi.multisrc.wpcomics
import eu.kanade.tachiyomi.lib.i18n.Intl
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -7,7 +8,10 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.Headers import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -20,23 +24,28 @@ import java.util.Locale
abstract class WPComics( abstract class WPComics(
override val name: String, override val name: String,
override val baseUrl: String, override val baseUrl: String,
override val lang: String, final override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US), protected val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US),
private val gmtOffset: String? = "+0500", protected val gmtOffset: String? = "+0500",
) : ParsedHttpSource() { ) : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder(): Headers.Builder = Headers.Builder() override fun headersBuilder() = super.headersBuilder()
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0") .add("Referer", "$baseUrl/")
.add("Referer", baseUrl)
private fun List<String>.doesInclude(thisWord: String): Boolean = this.any { it.contains(thisWord, ignoreCase = true) } open val intl = Intl(
language = lang,
baseLanguage = "en",
availableLanguages = setOf("en", "vi", "ja"),
classLoader = this::class.java.classLoader!!,
)
protected fun List<String>.doesInclude(thisWord: String): Boolean = this.any { it.contains(thisWord, ignoreCase = true) }
// Popular // Popular
open val popularPath = "hot" open val popularPath = "hot"
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
@ -58,7 +67,6 @@ abstract class WPComics(
override fun popularMangaNextPageSelector() = "a.next-page, a[rel=next]" override fun popularMangaNextPageSelector() = "a.next-page, a[rel=next]"
// Latest // Latest
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
return GET(baseUrl + if (page > 1) "?page=$page" else "", headers) return GET(baseUrl + if (page > 1) "?page=$page" else "", headers)
} }
@ -70,35 +78,27 @@ abstract class WPComics(
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
// Search // Search
protected open val searchPath = "tim-truyen" protected open val searchPath = "tim-truyen"
protected open val queryParam = "keyword" protected open val queryParam = "keyword"
protected open fun String.replaceSearchPath() = this
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it } val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
return if (filterList.isEmpty()) {
GET("$baseUrl/?s=$query&post_type=comics&page=$page")
} else {
val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
filterList.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) } is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) } is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
else -> {} else -> {}
}
} }
url.apply {
addQueryParameter(queryParam, query)
addQueryParameter("page", page.toString())
addQueryParameter("sort", "0")
}
GET(url.toString().replaceSearchPath(), headers)
} }
url.apply {
addQueryParameter(queryParam, query)
addQueryParameter("page", page.toString())
addQueryParameter("sort", "0")
}
return GET(url.toString(), headers)
} }
override fun searchMangaSelector() = "div.items div.item" override fun searchMangaSelector() = "div.items div.item"
@ -116,22 +116,23 @@ abstract class WPComics(
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details // Details
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply { return SManga.create().apply {
document.select("article#item-detail").let { info -> document.select("article#item-detail").let { info ->
author = info.select("li.author p.col-xs-8").text() author = info.select("li.author p.col-xs-8").text()
status = info.select("li.status p.col-xs-8").text().toStatus() status = info.select("li.status p.col-xs-8").text().toStatus()
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() } genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
description = info.select("div.detail-content p").text() val otherName = info.select("h2.other-name").text()
description = info.select("div.detail-content p").text() +
if (otherName.isNotBlank()) "\n\n ${intl["OTHER_NAME"]}: $otherName" else ""
thumbnail_url = imageOrNull(info.select("div.col-image img").first()!!) thumbnail_url = imageOrNull(info.select("div.col-image img").first()!!)
} }
} }
} }
open fun String?.toStatus(): Int { open fun String?.toStatus(): Int {
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành") val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành", "連載中")
val completedWords = listOf("Complete", "Completed", "Hoàn thành") val completedWords = listOf("Complete", "Completed", "Hoàn thành", "完結済み")
return when { return when {
this == null -> SManga.UNKNOWN this == null -> SManga.UNKNOWN
ongoingWords.doesInclude(this) -> SManga.ONGOING ongoingWords.doesInclude(this) -> SManga.ONGOING
@ -141,7 +142,6 @@ abstract class WPComics(
} }
// Chapters // Chapters
override fun chapterListSelector() = "div.list-chapter li.row:not(.heading)" override fun chapterListSelector() = "div.list-chapter li.row:not(.heading)"
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter {
@ -154,10 +154,10 @@ abstract class WPComics(
} }
} }
private val currentYear by lazy { Calendar.getInstance(Locale.US)[1].toString().takeLast(2) } protected val currentYear by lazy { Calendar.getInstance(Locale.US)[1].toString().takeLast(2) }
protected fun String?.toDate(): Long { protected open fun String?.toDate(): Long {
this ?: return 0 this ?: return 0L
val secondWords = listOf("second", "giây") val secondWords = listOf("second", "giây")
val minuteWords = listOf("minute", "phút") val minuteWords = listOf("minute", "phút")
@ -182,10 +182,10 @@ abstract class WPComics(
(if (gmtOffset == null) this.substringAfterLast(" ") else "$this $gmtOffset").let { (if (gmtOffset == null) this.substringAfterLast(" ") else "$this $gmtOffset").let {
// timestamp has year // timestamp has year
if (Regex("""\d+/\d+/\d\d""").find(it)?.value != null) { if (Regex("""\d+/\d+/\d\d""").find(it)?.value != null) {
dateFormat.parse(it)?.time ?: 0 dateFormat.parse(it)?.time ?: 0L
} else { } else {
// MangaSum - timestamp sometimes doesn't have year (current year implied) // MangaSum - timestamp sometimes doesn't have year (current year implied)
dateFormat.parse("$it/$currentYear")?.time ?: 0 dateFormat.parse("$it/$currentYear")?.time ?: 0L
} }
} }
} }
@ -195,9 +195,8 @@ abstract class WPComics(
} }
// Pages // Pages
// sources sometimes have an image element with an empty attr that isn't really an image
open fun imageOrNull(element: Element): String? { open fun imageOrNull(element: Element): String? {
// sources sometimes have an image element with an empty attr that isn't really an image
fun Element.hasValidAttr(attr: String): Boolean { fun Element.hasValidAttr(attr: String): Boolean {
val regex = Regex("""https?://.*""", RegexOption.IGNORE_CASE) val regex = Regex("""https?://.*""", RegexOption.IGNORE_CASE)
return when { return when {
@ -226,80 +225,74 @@ abstract class WPComics(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException() override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// Filters // Filters
protected class StatusFilter(name: String, pairs: List<Pair<String?, String>>) : UriPartFilter(name, pairs)
protected class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals) protected class GenreFilter(name: String, pairs: List<Pair<String?, String>>) : UriPartFilter(name, pairs)
protected class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Genre", vals)
protected open fun getStatusList(): Array<Pair<String?, String>> = arrayOf( protected open fun getStatusList(): List<Pair<String?, String>> =
Pair(null, "Tất cả"), listOf(
Pair("1", "Đang tiến hành"), Pair(null, intl["STATUS_ALL"]),
Pair("2", "Đã hoàn thành"), Pair("1", intl["STATUS_ONGOING"]),
Pair("3", "Tạm ngừng"), Pair("2", intl["STATUS_COMPLETED"]),
) )
protected open fun getGenreList(): Array<Pair<String?, String>> = arrayOf(
null to "Tất cả", protected var genreList: List<Pair<String?, String>> = emptyList()
"action" to "Action",
"adult" to "Adult", private val scope = CoroutineScope(Dispatchers.IO)
"adventure" to "Adventure",
"anime" to "Anime", protected fun launchIO(block: () -> Unit) = scope.launch { block() }
"chuyen-sinh" to "Chuyển Sinh",
"comedy" to "Comedy", private var fetchGenresAttempts: Int = 0
"comic" to "Comic",
"cooking" to "Cooking", protected fun fetchGenres() {
"co-dai" to "Cổ Đại", if (fetchGenresAttempts < 3 && genreList.isEmpty()) {
"doujinshi" to "Doujinshi", try {
"drama" to "Drama", genreList =
"dam-my" to "Đam Mỹ", client.newCall(genresRequest()).execute()
"ecchi" to "Ecchi", .asJsoup()
"fantasy" to "Fantasy", .let(::parseGenres)
"gender-bender" to "Gender Bender", } catch (_: Exception) {
"harem" to "Harem", } finally {
"historical" to "Historical", fetchGenresAttempts++
"horror" to "Horror", }
"josei" to "Josei", }
"live-action" to "Live action", }
"manga" to "Manga",
"manhua" to "Manhua", protected open fun genresRequest() = GET("$baseUrl/$searchPath", headers)
"manhwa" to "Manhwa",
"martial-arts" to "Martial Arts", protected open val genresSelector = ".genres ul.nav li:not(.active) a"
"mature" to "Mature",
"mecha" to "Mecha", protected open val genresUrlDelimiter = "/"
"mystery" to "Mystery",
"ngon-tinh" to "Ngôn Tình", protected open fun parseGenres(document: Document): List<Pair<String?, String>> {
"one-shot" to "One shot", val items = document.select(genresSelector)
"psychological" to "Psychological", return buildList(items.size + 1) {
"romance" to "Romance", add(Pair(null, intl["STATUS_ALL"]))
"school-life" to "School Life", items.mapTo(this) {
"sci-fi" to "Sci-fi", Pair(
"seinen" to "Seinen", it.attr("href")
"shoujo" to "Shoujo", .removeSuffix("/")
"shoujo-ai" to "Shoujo Ai", .substringAfterLast(genresUrlDelimiter),
"shounen" to "Shounen", it.text(),
"shounen-ai" to "Shounen Ai", )
"slice-of-life" to "Slice of Life", }
"smut" to "Smut", }
"soft-yaoi" to "Soft Yaoi", }
"soft-yuri" to "Soft Yuri",
"sports" to "Sports",
"supernatural" to "Supernatural",
"thieu-nhi" to "Thiếu Nhi",
"tragedy" to "Tragedy",
"trinh-tham" to "Trinh Thám",
"truyen-scan" to "Truyện scan",
"truyen-mau" to "Truyện Màu",
"webtoon" to "Webtoon",
"xuyen-khong" to "Xuyên Không",
)
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
launchIO { fetchGenres() }
return FilterList( return FilterList(
StatusFilter(getStatusList()), StatusFilter(intl["STATUS"], getStatusList()),
GenreFilter(getGenreList()), if (genreList.isEmpty()) {
Filter.Header(intl["GENRES_RESET"])
} else {
GenreFilter(intl["GENRE"], genreList)
},
) )
} }
protected open class UriPartFilter(displayName: String, val vals: Array<Pair<String?, String>>) : protected open class UriPartFilter(displayName: String, private val pairs: List<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) { Filter.Select<String>(displayName, pairs.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first fun toUriPart() = pairs[state].first
} }
} }

View File

@ -15,7 +15,13 @@ import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class XoxoComics : WPComics("XOXO Comics", "https://xoxocomic.com", "en", SimpleDateFormat("MM/dd/yyyy", Locale.US), null) { class XoxoComics : WPComics(
"XOXO Comics",
"https://xoxocomic.com",
"en",
dateFormat = SimpleDateFormat("MM/dd/yyyy", Locale.US),
gmtOffset = null,
) {
override val searchPath = "search-comic" override val searchPath = "search-comic"
override val popularPath = "hot-comic" override val popularPath = "hot-comic"
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/comic-update?page=$page", headers) override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/comic-update?page=$page", headers)
@ -84,72 +90,20 @@ class XoxoComics : WPComics("XOXO Comics", "https://xoxocomic.com", "en", Simple
override fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + "${chapter.url}/all") override fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + "${chapter.url}/all")
override fun getStatusList(): Array<Pair<String?, String>> = arrayOf( override fun genresRequest() = GET("$baseUrl/comic-list", headers)
Pair(null, "All"),
Pair("ongoing", "Ongoing"), override val genresSelector = ".genres h2:contains(Genres) + ul.nav li a"
Pair("completed", "Completed"),
)
override fun getGenreList(): Array<Pair<String?, String>> = arrayOf(
null to "All",
"marvel-comic" to "Marvel",
"dc-comics-comic" to "DC Comics",
"dark-horse-comic" to "Dark Horse",
"action-comic" to "Action",
"adventure-comic" to "Adventure",
"anthology-comic" to "Anthology",
"anthropomorphic-comic" to "Anthropomorphic",
"biography-comic" to "Biography",
"children-comic" to "Children",
"comedy-comic" to "Comedy",
"crime-comic" to "Crime",
"drama-comic" to "Drama",
"family-comic" to "Family",
"fantasy-comic" to "Fantasy",
"fighting-comic" to "Fighting",
"graphic-novels-comic" to "Graphic Novels",
"historical-comic" to "Historical",
"horror-comic" to "Horror",
"leading-ladies-comic" to "Leading Ladies",
"lgbtq-comic" to "LGBTQ",
"literature-comic" to "Literature",
"manga-comic" to "Manga",
"martial-arts-comic" to "Martial Arts",
"military-comic" to "Military",
"mini-series-comic" to "Mini-Series",
"movies-tv-comic" to "Movies &amp; TV",
"music-comic" to "Music",
"mystery-comic" to "Mystery",
"mythology-comic" to "Mythology",
"personal-comic" to "Personal",
"political-comic" to "Political",
"post-apocalyptic-comic" to "Post-Apocalyptic",
"psychological-comic" to "Psychological",
"pulp-comic" to "Pulp",
"religious-comic" to "Religious",
"robots-comic" to "Robots",
"romance-comic" to "Romance",
"school-life-comic" to "School Life",
"sci-fi-comic" to "Sci-Fi",
"slice-of-life-comic" to "Slice of Life",
"sport-comic" to "Sport",
"spy-comic" to "Spy",
"superhero-comic" to "Superhero",
"supernatural-comic" to "Supernatural",
"suspense-comic" to "Suspense",
"teen-comic" to "Teen",
"thriller-comic" to "Thriller",
"vampires-comic" to "Vampires",
"video-games-comic" to "Video Games",
"war-comic" to "War",
"western-comic" to "Western",
"zombies-comic" to "Zombies",
)
override fun getFilterList(): FilterList { override fun getFilterList(): FilterList {
launchIO { fetchGenres() }
return FilterList( return FilterList(
Filter.Header("Search query won't use Genre/Status filter"), Filter.Header("Search query won't use Genre/Status filter"),
StatusFilter(getStatusList()), StatusFilter("Status", getStatusList()),
GenreFilter(getGenreList()), if (genreList.isEmpty()) {
Filter.Header("Tap 'Reset' to load genres")
} else {
GenreFilter("Genre", genreList)
},
) )
} }
} }

View File

@ -13,27 +13,36 @@ import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
class JManga : WPComics("JManga", "https://jmanga.vip", "ja", SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.JAPANESE), null) { class JManga : WPComics(
"JManga",
"https://jmanga.vip",
"ja",
dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.JAPANESE),
gmtOffset = null,
) {
override fun popularMangaSelector() = "div.items article.item" override fun popularMangaSelector() = "div.items article.item"
override fun popularMangaNextPageSelector() = "li:nth-last-child(2) a.page-link"
override fun popularMangaNextPageSelector() = "li.active + li.page-item a.page-link"
override fun mangaDetailsParse(document: Document): SManga { override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply { return SManga.create().apply {
document.select("article#item-detail").let { info -> document.select("article#item-detail").let { info ->
author = info.select("li.author p.col-xs-8").text() author = info.select("li.author p.col-xs-8").text()
status = when { status = info.select("li.status p.col-xs-8").text().toStatus()
info.select("li.status p.col-xs-8").text().contains("連載中", true) -> SManga.ONGOING
info.select("li.status p.col-xs-8").text().contains("完結済み", true) -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() } genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
description = info.select("div.detail-content").text() val otherName = info.select("h2.other-name").text()
description = info.select("div.detail-content").text() +
if (otherName.isNotBlank()) "\n\n ${intl["OTHER_NAME"]}: $otherName" else ""
thumbnail_url = imageOrNull(info[0].selectFirst("div.col-image img")!!) thumbnail_url = imageOrNull(info[0].selectFirst("div.col-image img")!!)
} }
} }
} }
override val searchPath = "search/manga"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it } val filterList = filters.let { if (it.isEmpty()) getFilterList() else it }
val url = "$baseUrl/search/manga".toHttpUrl().newBuilder() val url = "$baseUrl/$searchPath".toHttpUrl().newBuilder()
filterList.forEach { filter -> filterList.forEach { filter ->
when (filter) { when (filter) {
@ -51,6 +60,7 @@ class JManga : WPComics("JManga", "https://jmanga.vip", "ja", SimpleDateFormat("
return GET(url.build(), headers) return GET(url.build(), headers)
} }
override fun chapterFromElement(element: Element): SChapter { override fun chapterFromElement(element: Element): SChapter {
val minuteWords = listOf("minute", "") val minuteWords = listOf("minute", "")
val hourWords = listOf("hour", "時間") val hourWords = listOf("hour", "時間")
@ -111,28 +121,15 @@ class JManga : WPComics("JManga", "https://jmanga.vip", "ja", SimpleDateFormat("
} }
} }
} }
override fun getStatusList(): Array<Pair<String?, String>> {
return arrayOf( override fun getStatusList(): List<Pair<String?, String>> =
listOf(
Pair("-1", "全て"), Pair("-1", "全て"),
Pair("0", "完結済み"), Pair("0", "完結済み"),
Pair("1", "連載中"), Pair("1", "連載中"),
) )
}
override fun getGenreList(): Array<Pair<String?, String>> { override val genresSelector = ".genres ul.nav li:not(.active) a"
return arrayOf(
null to "全てのジャンル", override val genresUrlDelimiter = "="
"TL" to "TL",
"BL" to "BL",
" ファンタジー " to " ファンタジー ",
"恋愛" to "恋愛",
"ドラマ" to "ドラマ",
"アクション" to "アクション",
"ホラー・ミステリー" to "ホラー・ミステリー",
"裏社会・アングラ" to "裏社会・アングラ",
"スポーツ" to "スポーツ",
"グルメ" to "グルメ",
"日常" to "日常",
"SF" to "SF",
)
}
} }

View File

@ -6,9 +6,13 @@ import okhttp3.Response
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class NetTruyen : WPComics("NetTruyen", "https://www.nettruyenff.com", "vi", SimpleDateFormat("dd/MM/yy", Locale.getDefault()), null) { class NetTruyen : WPComics(
override fun String.replaceSearchPath() = replace("/$searchPath?status=2&", "/truyen-full?") "NetTruyen",
"https://www.nettruyenff.com",
"vi",
dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()),
gmtOffset = null,
) {
/** /**
* NetTruyen/NhatTruyen redirect back to catalog page if searching query is not found. * NetTruyen/NhatTruyen redirect back to catalog page if searching query is not found.
* That makes both sites always return un-relevant results when searching should return empty. * That makes both sites always return un-relevant results when searching should return empty.
@ -19,62 +23,4 @@ class NetTruyen : WPComics("NetTruyen", "https://www.nettruyenff.com", "vi", Sim
} }
return super.searchMangaParse(response) return super.searchMangaParse(response)
} }
override fun getGenreList(): Array<Pair<String?, String>> = arrayOf(
null to "Tất cả",
"action-95" to "Action",
"truong-thanh" to "Adult",
"adventure" to "Adventure",
"anime" to "Anime",
"chuyen-sinh-2131" to "Chuyển Sinh",
"comedy-99" to "Comedy",
"comic" to "Comic",
"cooking-101" to "Cooking",
"co-dai-207" to "Cổ Đại",
"doujinshi" to "Doujinshi",
"drama-103" to "Drama",
"dam-my" to "Đam Mỹ",
"ecchi-104" to "Ecchi",
"fantasy-1050" to "Fantasy",
"gender-bender-106" to "Gender Bender",
"harem-107" to "Harem",
"historical" to "Historical",
"horror" to "Horror",
"josei" to "Josei",
"live-action" to "Live action",
"manga-112" to "Manga",
"manhua" to "Manhua",
"manhwa-11400" to "Manhwa",
"martial-arts" to "Martial Arts",
"mature" to "Mature",
"mecha-117" to "Mecha",
"mystery" to "Mystery",
"ngon-tinh" to "Ngôn Tình",
"one-shot" to "One shot",
"psychological" to "Psychological",
"romance-121" to "Romance",
"school-life" to "School Life",
"sci-fi" to "Sci-fi",
"seinen" to "Seinen",
"shoujo" to "Shoujo",
"shoujo-ai-126" to "Shoujo Ai",
"shounen-127" to "Shounen",
"shounen-ai" to "Shounen Ai",
"slice-of-life" to "Slice of Life",
"smut" to "Smut",
"soft-yaoi" to "Soft Yaoi",
"soft-yuri" to "Soft Yuri",
"sports" to "Sports",
"supernatural" to "Supernatural",
"tap-chi-truyen-tranh" to "Tạp chí truyện tranh",
"thieu-nhi" to "Thiếu Nhi",
"tragedy-136" to "Tragedy",
"trinh-tham" to "Trinh Thám",
"truyen-scan" to "Truyện scan",
"truyen-mau-214" to "Truyện Màu",
"viet-nam" to "Việt Nam",
"webtoon-140" to "Webtoon",
"xuyen-khong-205" to "Xuyên Không",
"16" to "16+",
)
} }

View File

@ -0,0 +1,9 @@
ext {
extName = 'NetTruyenCO (unoriginal)'
extClass = '.NetTruyenCO'
themePkg = 'wpcomics'
baseUrl = 'https://nettruyenco.vn'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.extension.vi.nettruyenco
import eu.kanade.tachiyomi.multisrc.wpcomics.WPComics
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat
import java.util.Locale
class NetTruyenCO : WPComics(
"NetTruyenCO (unoriginal)",
"https://nettruyenco.vn",
"vi",
dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()),
gmtOffset = null,
) {
override val popularPath = "truyen-tranh-hot"
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("article#item-detail").let { info ->
author = info.select("li.author p.col-xs-8").text()
status = info.select("li.status p.col-xs-8").text().toStatus()
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
val otherName = info.select("h2.other-name").text()
description = info.select("div.detail-content div div.nth-child(3)").text() +
if (otherName.isNotBlank()) "\n\n ${intl["OTHER_NAME"]}: $otherName" else ""
thumbnail_url = imageOrNull(info.select("div.col-image img").first()!!)
}
}
}
}

View File

@ -0,0 +1,9 @@
ext {
extName = 'NetTruyenX (unoriginal)'
extClass = '.NetTruyenX'
themePkg = 'wpcomics'
baseUrl = 'https://nettruyenx.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.extension.vi.nettruyenx
import eu.kanade.tachiyomi.multisrc.wpcomics.WPComics
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat
import java.util.Locale
class NetTruyenX : WPComics(
"NetTruyenX (unoriginal)",
"https://nettruyenx.com",
"vi",
dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()),
gmtOffset = null,
) {
override val popularPath = "truyen-tranh-hot"
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("article#item-detail").let { info ->
author = info.select("li.author p.col-xs-8").text()
status = info.select("li.status p.col-xs-8").text().toStatus()
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
val otherName = info.select("h2.other-name").text()
description = info.select("div.detail-content div div:nth-child(4)").text() +
if (otherName.isNotBlank()) "\n\n ${intl["OTHER_NAME"]}: $otherName" else ""
thumbnail_url = imageOrNull(info.select("div.col-image img").first()!!)
}
}
}
}

View File

@ -6,7 +6,13 @@ import okhttp3.Response
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class NhatTruyen : WPComics("NhatTruyen", "https://nhattruyenup.com", "vi", SimpleDateFormat("dd/MM/yy", Locale.getDefault()), null) { class NhatTruyen : WPComics(
"NhatTruyen",
"https://nhattruyenup.com",
"vi",
dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()),
gmtOffset = null,
) {
override val searchPath = "the-loai" override val searchPath = "the-loai"
/** /**
@ -19,62 +25,4 @@ class NhatTruyen : WPComics("NhatTruyen", "https://nhattruyenup.com", "vi", Simp
} }
return super.searchMangaParse(response) return super.searchMangaParse(response)
} }
override fun getGenreList(): Array<Pair<String?, String>> = arrayOf(
null to "Tất cả",
"action" to "Action",
"adult" to "Adult",
"adventure" to "Adventure",
"anime" to "Anime",
"chuyen-sinh" to "Chuyển Sinh",
"comedy" to "Comedy",
"comic" to "Comic",
"cooking" to "Cooking",
"co-dai" to "Cổ Đại",
"doujinshi" to "Doujinshi",
"drama" to "Drama",
"dam-my" to "Đam Mỹ",
"ecchi" to "Ecchi",
"fantasy" to "Fantasy",
"gender-bender" to "Gender Bender",
"harem" to "Harem",
"historical" to "Historical",
"horror" to "Horror",
"josei" to "Josei",
"live-action" to "Live action",
"manga-241" to "Manga",
"manhua" to "Manhua",
"manhwa-2431" to "Manhwa",
"martial-arts" to "Martial Arts",
"mature" to "Mature",
"mecha" to "Mecha",
"mystery" to "Mystery",
"ngon-tinh" to "Ngôn Tình",
"one-shot" to "One shot",
"psychological" to "Psychological",
"romance" to "Romance",
"school-life" to "School Life",
"sci-fi" to "Sci-fi",
"seinen" to "Seinen",
"shoujo" to "Shoujo",
"shoujo-ai" to "Shoujo Ai",
"shounen" to "Shounen",
"shounen-ai" to "Shounen Ai",
"slice-of-life" to "Slice of Life",
"smut" to "Smut",
"soft-yaoi" to "Soft Yaoi",
"soft-yuri" to "Soft Yuri",
"sports" to "Sports",
"supernatural" to "Supernatural",
"tap-chi-truyen-tranh" to "Tạp chí truyện tranh",
"thieu-nhi" to "Thiếu Nhi",
"tragedy" to "Tragedy",
"trinh-tham" to "Trinh Thám",
"truyen-scan" to "Truyện scan",
"truyen-mau" to "Truyện Màu",
"viet-nam" to "Việt Nam",
"webtoon" to "Webtoon",
"xuyen-khong" to "Xuyên Không",
"16" to "16+",
)
} }

View File

@ -0,0 +1,9 @@
ext {
extName = 'NhatTruyenS (unoriginal)'
extClass = '.NhatTruyenS'
themePkg = 'wpcomics'
baseUrl = 'https://nhattruyens.com'
overrideVersionCode = 0
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.extension.vi.nhattruyens
import eu.kanade.tachiyomi.multisrc.wpcomics.WPComics
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import java.text.SimpleDateFormat
import java.util.Locale
class NhatTruyenS : WPComics(
"NhatTruyenS (unoriginal)",
"https://nhattruyens.com",
"vi",
dateFormat = SimpleDateFormat("dd/MM/yy", Locale.getDefault()),
gmtOffset = null,
) {
override val popularPath = "truyen-hot"
/**
* Remove fake-manga ads
*/
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(searchMangaSelector())
.filter { element -> element.select("figure > div > a[rel='nofollow']").isNullOrEmpty() }
.map { element ->
searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector().let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select("article#item-detail").let { info ->
author = info.select("li.author p.col-xs-8").text()
status = info.select("li.status p.col-xs-8").text().toStatus()
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
val otherName = info.select("h2.other-name").text()
description = info.select("div.detail-content div.about:nth-child(3)").text() +
if (otherName.isNotBlank()) "\n\n ${intl["OTHER_NAME"]}: $otherName" else ""
thumbnail_url = imageOrNull(info.select("div.col-image img").first()!!)
}
}
}
}