Skip to content

Commit 0b66027

Browse files
kanoouAwkwardPeak7
andauthored
Add ManhwaWeb (keiyoushi#1542)
* Add manhwaweb * Lint * Remove data class * Oops * Oppsi Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Remove "Not used" Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Requested changes * Fix chapter url * Create SChapter in main class * make extension function --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
1 parent d0f80ac commit 0b66027

File tree

9 files changed

+442
-0
lines changed

9 files changed

+442
-0
lines changed

src/es/manhwaweb/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ext {
2+
extName = 'ManhwaWeb'
3+
extClass = '.ManhwaWeb'
4+
extVersionCode = 1
5+
isNsfw = true
6+
}
7+
8+
apply from: "$rootDir/common.gradle"
4.83 KB
Loading
2.72 KB
Loading
Loading
Loading
Loading
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package eu.kanade.tachiyomi.extension.es.manhwaweb
2+
3+
import android.app.Application
4+
import android.content.SharedPreferences
5+
import androidx.preference.PreferenceScreen
6+
import androidx.preference.SwitchPreferenceCompat
7+
import eu.kanade.tachiyomi.network.GET
8+
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
9+
import eu.kanade.tachiyomi.source.ConfigurableSource
10+
import eu.kanade.tachiyomi.source.model.Filter
11+
import eu.kanade.tachiyomi.source.model.FilterList
12+
import eu.kanade.tachiyomi.source.model.MangasPage
13+
import eu.kanade.tachiyomi.source.model.Page
14+
import eu.kanade.tachiyomi.source.model.SChapter
15+
import eu.kanade.tachiyomi.source.model.SManga
16+
import eu.kanade.tachiyomi.source.online.HttpSource
17+
import kotlinx.serialization.decodeFromString
18+
import kotlinx.serialization.json.Json
19+
import okhttp3.Headers
20+
import okhttp3.HttpUrl.Companion.toHttpUrl
21+
import okhttp3.OkHttpClient
22+
import okhttp3.Request
23+
import okhttp3.Response
24+
import uy.kohesive.injekt.Injekt
25+
import uy.kohesive.injekt.api.get
26+
import uy.kohesive.injekt.injectLazy
27+
28+
class ManhwaWeb : HttpSource(), ConfigurableSource {
29+
30+
private val preferences by lazy {
31+
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
32+
}
33+
34+
override val name = "ManhwaWeb"
35+
36+
override val baseUrl = "https://manhwaweb.com"
37+
38+
private val apiUrl = "https://manhwawebbackend-production.up.railway.app"
39+
40+
override val lang = "es"
41+
42+
override val supportsLatest = true
43+
44+
private val json: Json by injectLazy()
45+
46+
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
47+
.rateLimitHost(baseUrl.toHttpUrl(), 2)
48+
.build()
49+
50+
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
51+
.add("Referer", "$baseUrl/")
52+
53+
override fun popularMangaRequest(page: Int): Request = GET("$apiUrl/manhwa/nuevos", headers)
54+
55+
override fun popularMangaParse(response: Response): MangasPage {
56+
val result = json.decodeFromString<PayloadPopularDto>(response.body.string())
57+
val mangas = (result.data.weekly + result.data.total)
58+
.distinctBy { it.slug }
59+
.sortedByDescending { it.views }
60+
.map { it.toSManga() }
61+
62+
return MangasPage(mangas, false)
63+
}
64+
65+
override fun latestUpdatesRequest(page: Int): Request = GET("$apiUrl/latest/new-manhwa", headers)
66+
67+
override fun latestUpdatesParse(response: Response): MangasPage {
68+
val result = json.decodeFromString<PayloadLatestDto>(response.body.string())
69+
val mangas = (result.data.esp + result.data.raw18 + result.data.esp18)
70+
.distinctBy { it.type + it.slug }
71+
.sortedByDescending { it.latestChapterDate }
72+
.map { it.toSManga() }
73+
74+
return MangasPage(mangas, false)
75+
}
76+
77+
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
78+
val url = "$apiUrl/manhwa/library".toHttpUrl().newBuilder()
79+
.addQueryParameter("buscar", query)
80+
81+
filters.forEach { filter ->
82+
when (filter) {
83+
is TypeFilter -> url.addQueryParameter("tipo", filter.toUriPart())
84+
is DemographyFilter -> url.addQueryParameter("demografia", filter.toUriPart())
85+
is StatusFilter -> url.addQueryParameter("estado", filter.toUriPart())
86+
is EroticFilter -> url.addQueryParameter("erotico", filter.toUriPart())
87+
is GenreFilter -> {
88+
val genres = filter.state
89+
.filter { it.state }
90+
.joinToString("a") { it.id.toString() }
91+
url.addQueryParameter("generes", genres)
92+
}
93+
94+
is SortByFilter -> {
95+
url.addQueryParameter(
96+
"order_dir",
97+
if (filter.state!!.ascending) "asc" else "desc",
98+
)
99+
url.addQueryParameter("order_item", filter.selected)
100+
}
101+
102+
else -> {}
103+
}
104+
}
105+
106+
url.addQueryParameter("page", (page - 1).toString())
107+
108+
return GET(url.build(), headers)
109+
}
110+
111+
override fun getFilterList(): FilterList {
112+
return FilterList(
113+
TypeFilter(),
114+
DemographyFilter(),
115+
StatusFilter(),
116+
EroticFilter(),
117+
Filter.Separator(),
118+
GenreFilter("Géneros", getGenres()),
119+
Filter.Separator(),
120+
SortByFilter("Ordenar por", getSortProperties()),
121+
)
122+
}
123+
124+
override fun searchMangaParse(response: Response): MangasPage {
125+
val result = json.decodeFromString<PayloadSearchDto>(response.body.string())
126+
val mangas = result.data.map { it.toSManga() }
127+
return MangasPage(mangas, result.hasNextPage)
128+
}
129+
130+
override fun getMangaUrl(manga: SManga): String = "$baseUrl/${manga.url}"
131+
132+
override fun mangaDetailsRequest(manga: SManga): Request {
133+
val slug = manga.url.removeSuffix("/").substringAfterLast("/")
134+
return GET("$apiUrl/manhwa/see/$slug", headers)
135+
}
136+
137+
override fun mangaDetailsParse(response: Response): SManga =
138+
json.decodeFromString<ComicDetailsDto>(response.body.string()).toSManga()
139+
140+
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
141+
142+
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
143+
144+
override fun chapterListParse(response: Response): List<SChapter> {
145+
val result = json.decodeFromString<PayloadChapterDto>(response.body.string())
146+
val chaptersEsp = result.esp.map { it.toSChapter("Esp") }
147+
val chaptersRaw = result.raw.map { it.toSChapter("Raw") }
148+
149+
val filteredRaws = if (preferences.showAllRawsPref()) {
150+
chaptersRaw
151+
} else {
152+
val chapterNumbers = chaptersEsp.map { it.chapter_number }.toSet()
153+
chaptersRaw.filter { it.chapter_number !in chapterNumbers }
154+
}
155+
156+
return (chaptersEsp + filteredRaws).sortedByDescending { it.chapter_number }
157+
}
158+
159+
private fun ChapterDto.toSChapter(type: String) = SChapter.create().apply {
160+
name = "Capítulo ${number.toString().removeSuffix(".0")}"
161+
chapter_number = number
162+
date_upload = createdAt ?: 0
163+
setUrlWithoutDomain(url)
164+
scanlator = type
165+
}
166+
167+
override fun pageListRequest(chapter: SChapter): Request {
168+
val slug = chapter.url.removeSuffix("/").substringAfterLast("/")
169+
return GET("$apiUrl/chapters/see/$slug", headers)
170+
}
171+
172+
override fun pageListParse(response: Response): List<Page> {
173+
val result = json.decodeFromString<PayloadPageDto>(response.body.string())
174+
return result.data.images.filter { it.isNotBlank() }
175+
.mapIndexed { i, img -> Page(i, imageUrl = img) }
176+
}
177+
178+
override fun setupPreferenceScreen(screen: PreferenceScreen) {
179+
val showAllRawsPref = SwitchPreferenceCompat(screen.context).apply {
180+
key = SHOW_ALL_RAWS_PREF
181+
title = SHOW_ALL_RAWS_TITLE
182+
summary = SHOW_ALL_RAWS_SUMMARY
183+
setDefaultValue(SHOW_ALL_RAWS_DEFAULT)
184+
}
185+
186+
screen.addPreference(showAllRawsPref)
187+
}
188+
189+
private fun SharedPreferences.showAllRawsPref() = getBoolean(SHOW_ALL_RAWS_PREF, SHOW_ALL_RAWS_DEFAULT)
190+
191+
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
192+
193+
companion object {
194+
private const val SHOW_ALL_RAWS_PREF = "pref_show_all_raws_"
195+
private const val SHOW_ALL_RAWS_TITLE = "Mostrar todos los capítulos \"Raw\""
196+
private const val SHOW_ALL_RAWS_SUMMARY = "Mostrar todos los capítulos \"Raw\" en la lista de capítulos, a pesar de que ya exista una versión en español."
197+
private const val SHOW_ALL_RAWS_DEFAULT = false
198+
}
199+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package eu.kanade.tachiyomi.extension.es.manhwaweb
2+
3+
import eu.kanade.tachiyomi.source.model.SManga
4+
import kotlinx.serialization.SerialName
5+
import kotlinx.serialization.Serializable
6+
7+
@Serializable
8+
class PayloadPopularDto(
9+
@SerialName("top") val data: PopularDto,
10+
)
11+
12+
@Serializable
13+
class PopularDto(
14+
@SerialName("manhwas_esp") val weekly: List<PopularComicDto>,
15+
@SerialName("manhwas_raw") val total: List<PopularComicDto>,
16+
)
17+
18+
@Serializable
19+
class PopularComicDto(
20+
@SerialName("link") val slug: String,
21+
@SerialName("numero") val views: Int,
22+
private val name: String,
23+
@SerialName("imagen") private val thumbnail: String,
24+
) {
25+
fun toSManga() = SManga.create().apply {
26+
title = name
27+
thumbnail_url = thumbnail
28+
url = slug
29+
}
30+
}
31+
32+
@Serializable
33+
class PayloadLatestDto(
34+
@SerialName("manhwas") val data: LatestDto,
35+
)
36+
37+
@Serializable
38+
class LatestDto(
39+
@SerialName("manhwas_esp") val esp: List<LatestComicDto>,
40+
@SerialName("manhwas_raw") val raw18: List<LatestComicDto>,
41+
@SerialName("_manhwas") val esp18: List<LatestComicDto>,
42+
)
43+
44+
@Serializable
45+
class LatestComicDto(
46+
@SerialName("create") val latestChapterDate: Long,
47+
@SerialName("id_manhwa") val slug: String,
48+
@SerialName("_tipo") val type: String,
49+
@SerialName("name_manhwa") private val name: String,
50+
@SerialName("img") private val thumbnail: String,
51+
) {
52+
fun toSManga() = SManga.create().apply {
53+
title = name
54+
thumbnail_url = thumbnail
55+
url = "$type/$slug"
56+
}
57+
}
58+
59+
@Serializable
60+
class PayloadSearchDto(
61+
val data: List<SearchComicDto>,
62+
@SerialName("next") val hasNextPage: Boolean,
63+
)
64+
65+
@Serializable
66+
class SearchComicDto(
67+
@SerialName("_id") val slug: String,
68+
@SerialName("_tipo") val type: String,
69+
@SerialName("the_real_name") private val name: String,
70+
@SerialName("_imagen") private val thumbnail: String,
71+
) {
72+
fun toSManga() = SManga.create().apply {
73+
title = name
74+
thumbnail_url = thumbnail
75+
url = "$type/$slug"
76+
}
77+
}
78+
79+
@Serializable
80+
class ComicDetailsDto(
81+
@SerialName("name_esp") private val title: String,
82+
@SerialName("_sinopsis") private val description: String? = null,
83+
@SerialName("_status") private val status: String,
84+
@SerialName("_name") private val alternativeName: String? = null,
85+
@SerialName("_imagen") private val thumbnail: String,
86+
@SerialName("_categoris") private val genres: List<Map<Int, String>>,
87+
@SerialName("_extras") private val extras: ComicDetailsExtrasDto,
88+
) {
89+
fun toSManga() = SManga.create().apply {
90+
title = this@ComicDetailsDto.title
91+
thumbnail_url = thumbnail
92+
description = this@ComicDetailsDto.description
93+
if (!alternativeName.isNullOrBlank()) {
94+
if (!description.isNullOrBlank()) description += "\n\n"
95+
description += "Nombres alternativos: $alternativeName"
96+
}
97+
status = parseStatus(this@ComicDetailsDto.status)
98+
genre = genres.joinToString { it.values.first() }
99+
author = extras.authors.joinToString()
100+
}
101+
102+
private fun parseStatus(status: String) = when (status) {
103+
"publicandose" -> SManga.ONGOING
104+
"finalizado" -> SManga.COMPLETED
105+
else -> SManga.UNKNOWN
106+
}
107+
}
108+
109+
@Serializable
110+
class ComicDetailsExtrasDto(
111+
@SerialName("autores") val authors: List<String>,
112+
)
113+
114+
@Serializable
115+
class PayloadChapterDto(
116+
@SerialName("chapters_esp") val esp: List<ChapterDto>,
117+
@SerialName("chapters_raw") val raw: List<ChapterDto>,
118+
)
119+
120+
@Serializable
121+
class ChapterDto(
122+
@SerialName("chapter") val number: Float,
123+
@SerialName("link") val url: String,
124+
@SerialName("create") val createdAt: Long?,
125+
)
126+
127+
@Serializable
128+
class PayloadPageDto(
129+
@SerialName("chapter") val data: PageDto,
130+
)
131+
132+
@Serializable
133+
class PageDto(
134+
@SerialName("img") val images: List<String>,
135+
)

0 commit comments

Comments
 (0)