Add ManhwaWeb (#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>
This commit is contained in:
bapeey 2024-02-27 09:38:22 -05:00 committed by Draff
parent 4f3500d728
commit ee937137c3
9 changed files with 442 additions and 0 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,199 @@
package eu.kanade.tachiyomi.extension.es.manhwaweb
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class ManhwaWeb : HttpSource(), ConfigurableSource {
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val name = "ManhwaWeb"
override val baseUrl = "https://manhwaweb.com"
private val apiUrl = "https://manhwawebbackend-production.up.railway.app"
override val lang = "es"
override val supportsLatest = true
private val json: Json by injectLazy()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request = GET("$apiUrl/manhwa/nuevos", headers)
override fun popularMangaParse(response: Response): MangasPage {
val result = json.decodeFromString<PayloadPopularDto>(response.body.string())
val mangas = (result.data.weekly + result.data.total)
.distinctBy { it.slug }
.sortedByDescending { it.views }
.map { it.toSManga() }
return MangasPage(mangas, false)
}
override fun latestUpdatesRequest(page: Int): Request = GET("$apiUrl/latest/new-manhwa", headers)
override fun latestUpdatesParse(response: Response): MangasPage {
val result = json.decodeFromString<PayloadLatestDto>(response.body.string())
val mangas = (result.data.esp + result.data.raw18 + result.data.esp18)
.distinctBy { it.type + it.slug }
.sortedByDescending { it.latestChapterDate }
.map { it.toSManga() }
return MangasPage(mangas, false)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/manhwa/library".toHttpUrl().newBuilder()
.addQueryParameter("buscar", query)
filters.forEach { filter ->
when (filter) {
is TypeFilter -> url.addQueryParameter("tipo", filter.toUriPart())
is DemographyFilter -> url.addQueryParameter("demografia", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("estado", filter.toUriPart())
is EroticFilter -> url.addQueryParameter("erotico", filter.toUriPart())
is GenreFilter -> {
val genres = filter.state
.filter { it.state }
.joinToString("a") { it.id.toString() }
url.addQueryParameter("generes", genres)
}
is SortByFilter -> {
url.addQueryParameter(
"order_dir",
if (filter.state!!.ascending) "asc" else "desc",
)
url.addQueryParameter("order_item", filter.selected)
}
else -> {}
}
}
url.addQueryParameter("page", (page - 1).toString())
return GET(url.build(), headers)
}
override fun getFilterList(): FilterList {
return FilterList(
TypeFilter(),
DemographyFilter(),
StatusFilter(),
EroticFilter(),
Filter.Separator(),
GenreFilter("Géneros", getGenres()),
Filter.Separator(),
SortByFilter("Ordenar por", getSortProperties()),
)
}
override fun searchMangaParse(response: Response): MangasPage {
val result = json.decodeFromString<PayloadSearchDto>(response.body.string())
val mangas = result.data.map { it.toSManga() }
return MangasPage(mangas, result.hasNextPage)
}
override fun getMangaUrl(manga: SManga): String = "$baseUrl/${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request {
val slug = manga.url.removeSuffix("/").substringAfterLast("/")
return GET("$apiUrl/manhwa/see/$slug", headers)
}
override fun mangaDetailsParse(response: Response): SManga =
json.decodeFromString<ComicDetailsDto>(response.body.string()).toSManga()
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.decodeFromString<PayloadChapterDto>(response.body.string())
val chaptersEsp = result.esp.map { it.toSChapter("Esp") }
val chaptersRaw = result.raw.map { it.toSChapter("Raw") }
val filteredRaws = if (preferences.showAllRawsPref()) {
chaptersRaw
} else {
val chapterNumbers = chaptersEsp.map { it.chapter_number }.toSet()
chaptersRaw.filter { it.chapter_number !in chapterNumbers }
}
return (chaptersEsp + filteredRaws).sortedByDescending { it.chapter_number }
}
private fun ChapterDto.toSChapter(type: String) = SChapter.create().apply {
name = "Capítulo ${number.toString().removeSuffix(".0")}"
chapter_number = number
date_upload = createdAt ?: 0
setUrlWithoutDomain(url)
scanlator = type
}
override fun pageListRequest(chapter: SChapter): Request {
val slug = chapter.url.removeSuffix("/").substringAfterLast("/")
return GET("$apiUrl/chapters/see/$slug", headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = json.decodeFromString<PayloadPageDto>(response.body.string())
return result.data.images.filter { it.isNotBlank() }
.mapIndexed { i, img -> Page(i, imageUrl = img) }
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val showAllRawsPref = SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ALL_RAWS_PREF
title = SHOW_ALL_RAWS_TITLE
summary = SHOW_ALL_RAWS_SUMMARY
setDefaultValue(SHOW_ALL_RAWS_DEFAULT)
}
screen.addPreference(showAllRawsPref)
}
private fun SharedPreferences.showAllRawsPref() = getBoolean(SHOW_ALL_RAWS_PREF, SHOW_ALL_RAWS_DEFAULT)
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
companion object {
private const val SHOW_ALL_RAWS_PREF = "pref_show_all_raws_"
private const val SHOW_ALL_RAWS_TITLE = "Mostrar todos los capítulos \"Raw\""
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."
private const val SHOW_ALL_RAWS_DEFAULT = false
}
}

View File

@ -0,0 +1,135 @@
package eu.kanade.tachiyomi.extension.es.manhwaweb
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class PayloadPopularDto(
@SerialName("top") val data: PopularDto,
)
@Serializable
class PopularDto(
@SerialName("manhwas_esp") val weekly: List<PopularComicDto>,
@SerialName("manhwas_raw") val total: List<PopularComicDto>,
)
@Serializable
class PopularComicDto(
@SerialName("link") val slug: String,
@SerialName("numero") val views: Int,
private val name: String,
@SerialName("imagen") private val thumbnail: String,
) {
fun toSManga() = SManga.create().apply {
title = name
thumbnail_url = thumbnail
url = slug
}
}
@Serializable
class PayloadLatestDto(
@SerialName("manhwas") val data: LatestDto,
)
@Serializable
class LatestDto(
@SerialName("manhwas_esp") val esp: List<LatestComicDto>,
@SerialName("manhwas_raw") val raw18: List<LatestComicDto>,
@SerialName("_manhwas") val esp18: List<LatestComicDto>,
)
@Serializable
class LatestComicDto(
@SerialName("create") val latestChapterDate: Long,
@SerialName("id_manhwa") val slug: String,
@SerialName("_tipo") val type: String,
@SerialName("name_manhwa") private val name: String,
@SerialName("img") private val thumbnail: String,
) {
fun toSManga() = SManga.create().apply {
title = name
thumbnail_url = thumbnail
url = "$type/$slug"
}
}
@Serializable
class PayloadSearchDto(
val data: List<SearchComicDto>,
@SerialName("next") val hasNextPage: Boolean,
)
@Serializable
class SearchComicDto(
@SerialName("_id") val slug: String,
@SerialName("_tipo") val type: String,
@SerialName("the_real_name") private val name: String,
@SerialName("_imagen") private val thumbnail: String,
) {
fun toSManga() = SManga.create().apply {
title = name
thumbnail_url = thumbnail
url = "$type/$slug"
}
}
@Serializable
class ComicDetailsDto(
@SerialName("name_esp") private val title: String,
@SerialName("_sinopsis") private val description: String? = null,
@SerialName("_status") private val status: String,
@SerialName("_name") private val alternativeName: String? = null,
@SerialName("_imagen") private val thumbnail: String,
@SerialName("_categoris") private val genres: List<Map<Int, String>>,
@SerialName("_extras") private val extras: ComicDetailsExtrasDto,
) {
fun toSManga() = SManga.create().apply {
title = this@ComicDetailsDto.title
thumbnail_url = thumbnail
description = this@ComicDetailsDto.description
if (!alternativeName.isNullOrBlank()) {
if (!description.isNullOrBlank()) description += "\n\n"
description += "Nombres alternativos: $alternativeName"
}
status = parseStatus(this@ComicDetailsDto.status)
genre = genres.joinToString { it.values.first() }
author = extras.authors.joinToString()
}
private fun parseStatus(status: String) = when (status) {
"publicandose" -> SManga.ONGOING
"finalizado" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class ComicDetailsExtrasDto(
@SerialName("autores") val authors: List<String>,
)
@Serializable
class PayloadChapterDto(
@SerialName("chapters_esp") val esp: List<ChapterDto>,
@SerialName("chapters_raw") val raw: List<ChapterDto>,
)
@Serializable
class ChapterDto(
@SerialName("chapter") val number: Float,
@SerialName("link") val url: String,
@SerialName("create") val createdAt: Long?,
)
@Serializable
class PayloadPageDto(
@SerialName("chapter") val data: PageDto,
)
@Serializable
class PageDto(
@SerialName("img") val images: List<String>,
)

View File

@ -0,0 +1,100 @@
package eu.kanade.tachiyomi.extension.es.manhwaweb
import eu.kanade.tachiyomi.source.model.Filter
class TypeFilter : UriPartFilter(
"Tipo",
arrayOf(
Pair("Ver todo", ""),
Pair("Manhwa", "manhwa"),
Pair("Manga", "manga"),
Pair("Manhua", "manhua"),
),
)
class DemographyFilter : UriPartFilter(
"Demografía",
arrayOf(
Pair("Ver todo", ""),
Pair("Seinen", "seinen"),
Pair("Shonen", "shonen"),
Pair("Josei", "josei"),
Pair("Shojo", "shojo"),
),
)
class StatusFilter : UriPartFilter(
"Estado",
arrayOf(
Pair("Ver todo", ""),
Pair("Publicándose", "publicandose"),
Pair("Finalizado", "finalizado"),
),
)
class EroticFilter : UriPartFilter(
"Erótico",
arrayOf(
Pair("Ver todo", ""),
Pair("", "si"),
Pair("No", "no"),
),
)
class Genre(title: String, val id: Int) : Filter.CheckBox(title)
class GenreFilter(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
fun getGenres(): List<Genre> = listOf(
Genre("Acción", 3),
Genre("Aventura", 29),
Genre("Comedia", 18),
Genre("Drama", 1),
Genre("Recuentos de la vida", 42),
Genre("Romance", 2),
Genre("Venganza", 5),
Genre("Harem", 6),
Genre("Fantasia", 23),
Genre("Sobrenatural", 31),
Genre("Tragedia", 25),
Genre("Psicológico", 43),
Genre("Horror", 32),
Genre("Thriller", 44),
Genre("Historias cortas", 28),
Genre("Ecchi", 30),
Genre("Gore", 34),
Genre("Girls love", 27),
Genre("Boys love", 45),
Genre("Reencarnación", 41),
Genre("Sistema de niveles", 37),
Genre("Ciencia ficción", 33),
Genre("Apocalíptico", 38),
Genre("Artes Marciales", 39),
Genre("Superpoderes", 40),
Genre("Cultivación (cultivo)", 35),
Genre("Milf", 8),
)
class SortProperty(val name: String, val value: String) {
override fun toString(): String = name
}
fun getSortProperties(): List<SortProperty> = listOf(
SortProperty("Alfabético", "alfabetico"),
SortProperty("Creación", "creacion"),
SortProperty("Num. Capítulos", "num_chapter"),
)
class SortByFilter(title: String, private val sortProperties: List<SortProperty>) : Filter.Sort(
title,
sortProperties.map { it.name }.toTypedArray(),
Selection(0, ascending = false),
) {
val selected: String
get() = sortProperties[state!!.index].value
}
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}