Add EternalMangas and move MangaEsp to multisrc (#2127)
* I made this on termux * Fix regex and move dateFormat to DTO * Phase 1 * Phase 2: Prepare for intl * Phase 3: Add intl Builds are faster on my phone T.T * Apply suggestions from code review * bump
This commit is contained in:
		
							parent
							
								
									9637963a6c
								
							
						
					
					
						commit
						3dc97aaff8
					
				
							
								
								
									
										13
									
								
								lib-multisrc/mangaesp/assets/i18n/messages_en.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								lib-multisrc/mangaesp/assets/i18n/messages_en.properties
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| search_length_error=The query must have at least 2 characters | ||||
| comics_list_error=Comics list not found | ||||
| comic_data_error=Comic data not found | ||||
| sort_by_filter_title=Sort by | ||||
| sort_by_filter_name=Name | ||||
| sort_by_filter_views=Views | ||||
| sort_by_filter_updated=Updated | ||||
| sort_by_filter_added=Added | ||||
| status_filter_title=Status | ||||
| status_filter_ongoing=Ongoing | ||||
| status_filter_hiatus=Hiatus | ||||
| status_filter_dropped=Dropped | ||||
| status_filter_completed=Completed | ||||
							
								
								
									
										13
									
								
								lib-multisrc/mangaesp/assets/i18n/messages_es.properties
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								lib-multisrc/mangaesp/assets/i18n/messages_es.properties
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| search_length_error=La búsqueda debe tener al menos 2 caracteres | ||||
| comics_list_error=No se pudo encontrar la lista de comics | ||||
| comic_data_error=No se pudo encontrar los datos del comic | ||||
| sort_by_filter_title=Ordenar por | ||||
| sort_by_filter_name=Nombre | ||||
| sort_by_filter_views=Vistas | ||||
| sort_by_filter_updated=Actualización | ||||
| sort_by_filter_added=Agregado | ||||
| status_filter_title=Estado | ||||
| status_filter_ongoing=En curso | ||||
| status_filter_hiatus=En pausa | ||||
| status_filter_dropped=Abandonado | ||||
| status_filter_completed=Finalizado | ||||
							
								
								
									
										9
									
								
								lib-multisrc/mangaesp/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib-multisrc/mangaesp/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| plugins { | ||||
|     id("lib-multisrc") | ||||
| } | ||||
| 
 | ||||
| baseVersionCode = 1 | ||||
| 
 | ||||
| dependencies { | ||||
|   api(project(":lib:i18n")) | ||||
| } | ||||
| @ -0,0 +1,248 @@ | ||||
| package eu.kanade.tachiyomi.multisrc.mangaesp | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.lib.i18n.Intl | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.network.asObservableSuccess | ||||
| import eu.kanade.tachiyomi.network.interceptor.rateLimitHost | ||||
| 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 eu.kanade.tachiyomi.util.asJsoup | ||||
| 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 org.jsoup.nodes.Element | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import kotlin.math.min | ||||
| 
 | ||||
| abstract class MangaEsp( | ||||
|     override val name: String, | ||||
|     override val baseUrl: String, | ||||
|     override val lang: String, | ||||
|     protected val apiBaseUrl: String = baseUrl.replace("://", "://apis."), | ||||
| ) : HttpSource() { | ||||
| 
 | ||||
|     override val supportsLatest = true | ||||
| 
 | ||||
|     protected val json: Json by injectLazy() | ||||
| 
 | ||||
|     protected val intl = Intl( | ||||
|         language = lang, | ||||
|         baseLanguage = "en", | ||||
|         availableLanguages = setOf("en", "es"), | ||||
|         classLoader = this::class.java.classLoader!!, | ||||
|     ) | ||||
| 
 | ||||
|     override val client: OkHttpClient = network.client.newBuilder() | ||||
|         .rateLimitHost(baseUrl.toHttpUrl(), 2) | ||||
|         .build() | ||||
| 
 | ||||
|     override fun headersBuilder(): Headers.Builder = Headers.Builder() | ||||
|         .add("Referer", "$baseUrl/") | ||||
| 
 | ||||
|     override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl/api/topSerie", headers) | ||||
| 
 | ||||
|     override fun popularMangaParse(response: Response): MangasPage { | ||||
|         val responseData = json.decodeFromString<TopSeriesDto>(response.body.string()) | ||||
| 
 | ||||
|         val topDaily = responseData.response.topDaily.flatten().map { it.data } | ||||
|         val topWeekly = responseData.response.topWeekly.flatten().map { it.data } | ||||
|         val topMonthly = responseData.response.topMonthly.flatten().map { it.data } | ||||
| 
 | ||||
|         val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga() } | ||||
| 
 | ||||
|         return MangasPage(mangas, false) | ||||
|     } | ||||
| 
 | ||||
|     override fun latestUpdatesRequest(page: Int): Request = GET("$apiBaseUrl/api/lastUpdates", headers) | ||||
| 
 | ||||
|     override fun latestUpdatesParse(response: Response): MangasPage { | ||||
|         val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string()) | ||||
| 
 | ||||
|         val mangas = responseData.response.map { it.toSManga() } | ||||
| 
 | ||||
|         return MangasPage(mangas, false) | ||||
|     } | ||||
| 
 | ||||
|     private var comicsList = mutableListOf<SeriesDto>() | ||||
| 
 | ||||
|     override fun fetchSearchManga( | ||||
|         page: Int, | ||||
|         query: String, | ||||
|         filters: FilterList, | ||||
|     ): Observable<MangasPage> { | ||||
|         return if (comicsList.isEmpty()) { | ||||
|             client.newCall(searchMangaRequest(page, query, filters)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { searchMangaParse(it, page, query, filters) } | ||||
|         } else { | ||||
|             Observable.just(parseComicsList(page, query, filters)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/comics", headers) | ||||
| 
 | ||||
|     override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() | ||||
| 
 | ||||
|     private fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage { | ||||
|         val document = response.asJsoup() | ||||
|         val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() } | ||||
|         val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1) | ||||
|             ?: throw Exception(intl["comics_list_error"]) | ||||
|         val unescapedJson = jsonString.unescape() | ||||
|         comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList() | ||||
|         return parseComicsList(page, query, filters) | ||||
|     } | ||||
| 
 | ||||
|     private var filteredList = mutableListOf<SeriesDto>() | ||||
| 
 | ||||
|     private fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage { | ||||
|         if (page == 1) { | ||||
|             filteredList.clear() | ||||
| 
 | ||||
|             if (query.isNotBlank()) { | ||||
|                 if (query.length < 2) throw Exception(intl["search_length_error"]) | ||||
|                 filteredList.addAll( | ||||
|                     comicsList.filter { | ||||
|                         it.name.contains(query, ignoreCase = true) || it.alternativeName?.contains(query, ignoreCase = true) == true | ||||
|                     }, | ||||
|                 ) | ||||
|             } else { | ||||
|                 filteredList.addAll(comicsList) | ||||
|             } | ||||
| 
 | ||||
|             val statusFilter = filterList.firstInstanceOrNull<StatusFilter>() | ||||
| 
 | ||||
|             if (statusFilter != null) { | ||||
|                 filteredList = filteredList.filter { it.status == statusFilter.toUriPart() }.toMutableList() | ||||
|             } | ||||
| 
 | ||||
|             val sortByFilter = filterList.firstInstanceOrNull<SortByFilter>() | ||||
| 
 | ||||
|             if (sortByFilter != null) { | ||||
|                 if (sortByFilter.state?.ascending == true) { | ||||
|                     when (sortByFilter.selected) { | ||||
|                         "name" -> filteredList.sortBy { it.name } | ||||
|                         "views" -> filteredList.sortBy { it.trending?.views } | ||||
|                         "updated_at" -> filteredList.sortBy { it.lastChapterDate } | ||||
|                         "created_at" -> filteredList.sortBy { it.createdAt } | ||||
|                     } | ||||
|                 } else { | ||||
|                     when (sortByFilter.selected) { | ||||
|                         "name" -> filteredList.sortByDescending { it.name } | ||||
|                         "views" -> filteredList.sortByDescending { it.trending?.views } | ||||
|                         "updated_at" -> filteredList.sortByDescending { it.lastChapterDate } | ||||
|                         "created_at" -> filteredList.sortByDescending { it.createdAt } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         val hasNextPage = filteredList.size > page * MANGAS_PER_PAGE | ||||
| 
 | ||||
|         return MangasPage( | ||||
|             filteredList.subList((page - 1) * MANGAS_PER_PAGE, min(page * MANGAS_PER_PAGE, filteredList.size)) | ||||
|                 .map { it.toSManga() }, | ||||
|             hasNextPage, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun mangaDetailsParse(response: Response): SManga { | ||||
|         val responseBody = response.body.string() | ||||
|         val mangaDetailsJson = MANGA_DETAILS_REGEX.find(responseBody)?.groupValues?.get(1) | ||||
|             ?: throw Exception(intl["comic_data_error"]) | ||||
|         val unescapedJson = mangaDetailsJson.unescape() | ||||
| 
 | ||||
|         return json.decodeFromString<SeriesDto>(unescapedJson).toSMangaDetails() | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         val responseBody = response.body.string() | ||||
|         val mangaDetailsJson = MANGA_DETAILS_REGEX.find(responseBody)?.groupValues?.get(1) | ||||
|             ?: throw Exception(intl["comic_data_error"]) | ||||
|         val unescapedJson = mangaDetailsJson.unescape() | ||||
|         val series = json.decodeFromString<SeriesDto>(unescapedJson) | ||||
|         return series.chapters.map { it.toSChapter(series.slug) } | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         val document = response.asJsoup() | ||||
|         return document.select("main.contenedor.read img").mapIndexed { i, img -> | ||||
|             Page(i, imageUrl = img.imgAttr()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getFilterList() = FilterList( | ||||
|         SortByFilter(intl["sort_by_filter_title"], getSortProperties()), | ||||
|         StatusFilter(intl["status_filter_title"], getStatusList()), | ||||
|     ) | ||||
| 
 | ||||
|     protected open fun getSortProperties(): List<SortProperty> = listOf( | ||||
|         SortProperty(intl["sort_by_filter_name"], "name"), | ||||
|         SortProperty(intl["sort_by_filter_views"], "views"), | ||||
|         SortProperty(intl["sort_by_filter_updated"], "updated_at"), | ||||
|         SortProperty(intl["sort_by_filter_added"], "created_at"), | ||||
|     ) | ||||
| 
 | ||||
|     data class SortProperty(val name: String, val value: String) { | ||||
|         override fun toString(): String = name | ||||
|     } | ||||
| 
 | ||||
|     class SortByFilter(title: String, private val sortProperties: List<SortProperty>) : Filter.Sort( | ||||
|         title, | ||||
|         sortProperties.map { it.name }.toTypedArray(), | ||||
|         Selection(2, ascending = false), | ||||
|     ) { | ||||
|         val selected: String | ||||
|             get() = sortProperties[state!!.index].value | ||||
|     } | ||||
| 
 | ||||
|     private class StatusFilter(title: String, statusList: Array<Pair<String, Int>>) : UriPartFilter( | ||||
|         title, | ||||
|         statusList, | ||||
|     ) | ||||
| 
 | ||||
|     protected open fun getStatusList() = arrayOf( | ||||
|         Pair(intl["status_filter_ongoing"], 1), | ||||
|         Pair(intl["status_filter_hiatus"], 2), | ||||
|         Pair(intl["status_filter_dropped"], 3), | ||||
|         Pair(intl["status_filter_completed"], 4), | ||||
|     ) | ||||
| 
 | ||||
|     private open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, Int>>) : | ||||
|         Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { | ||||
|         fun toUriPart() = vals[state].second | ||||
|     } | ||||
| 
 | ||||
|     private inline fun <reified R> List<*>.firstInstanceOrNull(): R? = | ||||
|         filterIsInstance<R>().firstOrNull() | ||||
| 
 | ||||
|     override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() | ||||
| 
 | ||||
|     private fun Element.imgAttr(): String = when { | ||||
|         hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") | ||||
|         hasAttr("data-src") -> attr("abs:data-src") | ||||
|         hasAttr("data-cfsrc") -> attr("abs:data-cfsrc") | ||||
|         else -> attr("abs:src") | ||||
|     } | ||||
| 
 | ||||
|     private fun String.unescape(): String { | ||||
|         return UNESCAPE_REGEX.replace(this, "$1") | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val UNESCAPE_REGEX = """\\(.)""".toRegex() | ||||
|         private val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex() | ||||
|         private val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*\\"numFollow""".toRegex() | ||||
|         private const val MANGAS_PER_PAGE = 15 | ||||
|     } | ||||
| } | ||||
| @ -1,10 +1,11 @@ | ||||
| package eu.kanade.tachiyomi.extension.es.mangaesp | ||||
| package eu.kanade.tachiyomi.multisrc.mangaesp | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.source.model.SChapter | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| 
 | ||||
| @Serializable | ||||
| class TopSeriesDto( | ||||
| @ -101,18 +102,22 @@ class ChapterDto( | ||||
|     private val slug: String, | ||||
|     @SerialName("created_at") private val date: String, | ||||
| ) { | ||||
|     fun toSChapter(seriesSlug: String, dateFormat: SimpleDateFormat): SChapter { | ||||
|     fun toSChapter(seriesSlug: String): SChapter { | ||||
|         return SChapter.create().apply { | ||||
|             name = "Capítulo ${number.toString().removeSuffix(".0")}" | ||||
|             if (!this@ChapterDto.name.isNullOrBlank()) { | ||||
|                 name += " - ${this@ChapterDto.name}" | ||||
|             } | ||||
|             date_upload = try { | ||||
|                 dateFormat.parse(date)?.time ?: 0L | ||||
|                 DATE_FORMATTER.parse(date)?.time ?: 0L | ||||
|             } catch (e: Exception) { | ||||
|                 0L | ||||
|             } | ||||
|             url = "/ver/$seriesSlug/$slug" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/es/eternalmangas/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/es/eternalmangas/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| ext { | ||||
|     extName = 'EternalMangas' | ||||
|     extClass = '.EternalMangas' | ||||
|     themePkg = 'mangaesp' | ||||
|     baseUrl = 'https://eternalmangas.com' | ||||
|     overrideVersionCode = 0 | ||||
|     isNsfw = true | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
							
								
								
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 7.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 11 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/es/eternalmangas/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 32 KiB | 
| @ -0,0 +1,5 @@ | ||||
| package eu.kanade.tachiyomi.extension.es.eternalmangas | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp | ||||
| 
 | ||||
| class EternalMangas : MangaEsp("EternalMangas", "https://eternalmangas.com", "es") | ||||
| @ -1,8 +1,9 @@ | ||||
| ext { | ||||
|     extName = 'MangaEsp' | ||||
|     extClass = '.MangaEsp' | ||||
|     extVersionCode = 2 | ||||
|     isNsfw = false | ||||
|     themePkg = 'mangaesp' | ||||
|     baseUrl = 'https://mangaesp.net' | ||||
|     overrideVersionCode = 2 | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
|  | ||||
| @ -1,245 +1,5 @@ | ||||
| package eu.kanade.tachiyomi.extension.es.mangaesp | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.network.asObservableSuccess | ||||
| import eu.kanade.tachiyomi.network.interceptor.rateLimitHost | ||||
| 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 eu.kanade.tachiyomi.util.asJsoup | ||||
| 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 org.jsoup.nodes.Element | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| import kotlin.math.min | ||||
| import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp | ||||
| 
 | ||||
| class MangaEsp : HttpSource() { | ||||
| 
 | ||||
|     override val name = "MangaEsp" | ||||
| 
 | ||||
|     override val baseUrl = "https://mangaesp.net" | ||||
| 
 | ||||
|     private val apiBaseUrl = "https://apis.mangaesp.net" | ||||
| 
 | ||||
|     override val lang = "es" | ||||
| 
 | ||||
|     override val supportsLatest = true | ||||
| 
 | ||||
|     private val json: Json by injectLazy() | ||||
| 
 | ||||
|     private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) | ||||
| 
 | ||||
|     override val client: OkHttpClient = network.client.newBuilder() | ||||
|         .rateLimitHost(baseUrl.toHttpUrl(), 2) | ||||
|         .build() | ||||
| 
 | ||||
|     override fun headersBuilder(): Headers.Builder = Headers.Builder() | ||||
|         .add("Referer", "$baseUrl/") | ||||
| 
 | ||||
|     override fun popularMangaRequest(page: Int): Request = GET("$apiBaseUrl/api/topSerie", headers) | ||||
| 
 | ||||
|     override fun popularMangaParse(response: Response): MangasPage { | ||||
|         val responseData = json.decodeFromString<TopSeriesDto>(response.body.string()) | ||||
| 
 | ||||
|         val topDaily = responseData.response.topDaily.flatten().map { it.data } | ||||
|         val topWeekly = responseData.response.topWeekly.flatten().map { it.data } | ||||
|         val topMonthly = responseData.response.topMonthly.flatten().map { it.data } | ||||
| 
 | ||||
|         val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga() } | ||||
| 
 | ||||
|         return MangasPage(mangas, false) | ||||
|     } | ||||
| 
 | ||||
|     override fun latestUpdatesRequest(page: Int): Request = GET("$apiBaseUrl/api/lastUpdates", headers) | ||||
| 
 | ||||
|     override fun latestUpdatesParse(response: Response): MangasPage { | ||||
|         val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string()) | ||||
| 
 | ||||
|         val mangas = responseData.response.map { it.toSManga() } | ||||
| 
 | ||||
|         return MangasPage(mangas, false) | ||||
|     } | ||||
| 
 | ||||
|     private var comicsList = mutableListOf<SeriesDto>() | ||||
| 
 | ||||
|     override fun fetchSearchManga( | ||||
|         page: Int, | ||||
|         query: String, | ||||
|         filters: FilterList, | ||||
|     ): Observable<MangasPage> { | ||||
|         return if (comicsList.isEmpty()) { | ||||
|             client.newCall(searchMangaRequest(page, query, filters)) | ||||
|                 .asObservableSuccess() | ||||
|                 .map { searchMangaParse(it, page, query, filters) } | ||||
|         } else { | ||||
|             Observable.just(parseComicsList(page, query, filters)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/comics", headers) | ||||
| 
 | ||||
|     override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException() | ||||
| 
 | ||||
|     private fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage { | ||||
|         val document = response.asJsoup() | ||||
|         val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() } | ||||
|         val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1) | ||||
|             ?: throw Exception("No se pudo encontrar la lista de comics") | ||||
|         val unescapedJson = jsonString.unescape() | ||||
|         comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList() | ||||
|         return parseComicsList(page, query, filters) | ||||
|     } | ||||
| 
 | ||||
|     private var filteredList = mutableListOf<SeriesDto>() | ||||
| 
 | ||||
|     private fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage { | ||||
|         if (page == 1) { | ||||
|             filteredList.clear() | ||||
| 
 | ||||
|             if (query.isNotBlank()) { | ||||
|                 if (query.length < 2) throw Exception("La búsqueda debe tener al menos 2 caracteres") | ||||
|                 filteredList.addAll( | ||||
|                     comicsList.filter { | ||||
|                         it.name.contains(query, ignoreCase = true) || it.alternativeName?.contains(query, ignoreCase = true) == true | ||||
|                     }, | ||||
|                 ) | ||||
|             } else { | ||||
|                 filteredList.addAll(comicsList) | ||||
|             } | ||||
| 
 | ||||
|             val statusFilter = filterList.firstInstanceOrNull<StatusFilter>() | ||||
| 
 | ||||
|             if (statusFilter != null) { | ||||
|                 filteredList = filteredList.filter { it.status == statusFilter.toUriPart() }.toMutableList() | ||||
|             } | ||||
| 
 | ||||
|             val sortByFilter = filterList.firstInstanceOrNull<SortByFilter>() | ||||
| 
 | ||||
|             if (sortByFilter != null) { | ||||
|                 if (sortByFilter.state?.ascending == true) { | ||||
|                     when (sortByFilter.selected) { | ||||
|                         "name" -> filteredList.sortBy { it.name } | ||||
|                         "views" -> filteredList.sortBy { it.trending?.views } | ||||
|                         "updated_at" -> filteredList.sortBy { it.lastChapterDate } | ||||
|                         "created_at" -> filteredList.sortBy { it.createdAt } | ||||
|                     } | ||||
|                 } else { | ||||
|                     when (sortByFilter.selected) { | ||||
|                         "name" -> filteredList.sortByDescending { it.name } | ||||
|                         "views" -> filteredList.sortByDescending { it.trending?.views } | ||||
|                         "updated_at" -> filteredList.sortByDescending { it.lastChapterDate } | ||||
|                         "created_at" -> filteredList.sortByDescending { it.createdAt } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         val hasNextPage = filteredList.size > page * MANGAS_PER_PAGE | ||||
| 
 | ||||
|         return MangasPage( | ||||
|             filteredList.subList((page - 1) * MANGAS_PER_PAGE, min(page * MANGAS_PER_PAGE, filteredList.size)) | ||||
|                 .map { it.toSManga() }, | ||||
|             hasNextPage, | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     override fun mangaDetailsParse(response: Response): SManga { | ||||
|         val responseBody = response.body.string() | ||||
|         val mangaDetailsJson = MANGA_DETAILS_REGEX.find(responseBody)?.groupValues?.get(1) | ||||
|             ?: throw Exception("No se pudo encontrar los detalles del manga") | ||||
|         val unescapedJson = mangaDetailsJson.unescape() | ||||
| 
 | ||||
|         return json.decodeFromString<SeriesDto>(unescapedJson).toSMangaDetails() | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         val responseBody = response.body.string() | ||||
|         val mangaDetailsJson = MANGA_DETAILS_REGEX.find(responseBody)?.groupValues?.get(1) | ||||
|             ?: throw Exception("No se pudo encontrar la lista de capítulos") | ||||
|         val unescapedJson = mangaDetailsJson.unescape() | ||||
|         val series = json.decodeFromString<SeriesDto>(unescapedJson) | ||||
|         return series.chapters.map { it.toSChapter(series.slug, dateFormat) } | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         val document = response.asJsoup() | ||||
|         return document.select("main.contenedor.read img").mapIndexed { i, img -> | ||||
|             Page(i, imageUrl = img.imgAttr()) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getFilterList() = FilterList( | ||||
|         SortByFilter("Ordenar por", getSortProperties()), | ||||
|         StatusFilter(), | ||||
|     ) | ||||
| 
 | ||||
|     private fun getSortProperties(): List<SortProperty> = listOf( | ||||
|         SortProperty("Nombre", "name"), | ||||
|         SortProperty("Visitas", "views"), | ||||
|         SortProperty("Actualización", "updated_at"), | ||||
|         SortProperty("Agregado", "created_at"), | ||||
|     ) | ||||
| 
 | ||||
|     data class SortProperty(val name: String, val value: String) { | ||||
|         override fun toString(): String = name | ||||
|     } | ||||
| 
 | ||||
|     class SortByFilter(title: String, private val sortProperties: List<SortProperty>) : Filter.Sort( | ||||
|         title, | ||||
|         sortProperties.map { it.name }.toTypedArray(), | ||||
|         Selection(2, ascending = false), | ||||
|     ) { | ||||
|         val selected: String | ||||
|             get() = sortProperties[state!!.index].value | ||||
|     } | ||||
| 
 | ||||
|     private class StatusFilter : UriPartFilter( | ||||
|         "Estado", | ||||
|         arrayOf( | ||||
|             Pair("En emisión", 1), | ||||
|             Pair("En pausa", 2), | ||||
|             Pair("Abandonado", 3), | ||||
|             Pair("Finalizado", 4), | ||||
|         ), | ||||
|     ) | ||||
| 
 | ||||
|     private open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, Int>>) : | ||||
|         Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { | ||||
|         fun toUriPart() = vals[state].second | ||||
|     } | ||||
| 
 | ||||
|     private inline fun <reified R> List<*>.firstInstanceOrNull(): R? = | ||||
|         filterIsInstance<R>().firstOrNull() | ||||
| 
 | ||||
|     override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() | ||||
| 
 | ||||
|     private fun Element.imgAttr(): String = when { | ||||
|         hasAttr("data-lazy-src") -> attr("abs:data-lazy-src") | ||||
|         hasAttr("data-src") -> attr("abs:data-src") | ||||
|         hasAttr("data-cfsrc") -> attr("abs:data-cfsrc") | ||||
|         else -> attr("abs:src") | ||||
|     } | ||||
| 
 | ||||
|     private fun String.unescape(): String { | ||||
|         return UNESCAPE_REGEX.replace(this, "$1") | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private val UNESCAPE_REGEX = """\\(.)""".toRegex() | ||||
|         private val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex() | ||||
|         private val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*numFollow""".toRegex() | ||||
|         private const val MANGAS_PER_PAGE = 15 | ||||
|     } | ||||
| } | ||||
| class MangaEsp : MangaEsp("MangaEsp", "https://mangaesp.net", "es") | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 bapeey
						bapeey