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:
parent
4f3500d728
commit
ee937137c3
|
@ -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 |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>,
|
||||||
|
)
|
|
@ -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("Sí", "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
|
||||||
|
}
|
Loading…
Reference in New Issue