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