|
| 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 | +} |
0 commit comments