diff --git a/lib-multisrc/lectormonline/build.gradle.kts b/lib-multisrc/lectormonline/build.gradle.kts new file mode 100644 index 000000000..dc076cc37 --- /dev/null +++ b/lib-multisrc/lectormonline/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + id("lib-multisrc") +} + +baseVersionCode = 1 diff --git a/lib-multisrc/lectormonline/res/mipmap-hdpi/ic_launcher.png b/lib-multisrc/lectormonline/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..e6f06f5c4 Binary files /dev/null and b/lib-multisrc/lectormonline/res/mipmap-hdpi/ic_launcher.png differ diff --git a/lib-multisrc/lectormonline/res/mipmap-mdpi/ic_launcher.png b/lib-multisrc/lectormonline/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..7904a7316 Binary files /dev/null and b/lib-multisrc/lectormonline/res/mipmap-mdpi/ic_launcher.png differ diff --git a/lib-multisrc/lectormonline/res/mipmap-xhdpi/ic_launcher.png b/lib-multisrc/lectormonline/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..46cbc524f Binary files /dev/null and b/lib-multisrc/lectormonline/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/lib-multisrc/lectormonline/res/mipmap-xxhdpi/ic_launcher.png b/lib-multisrc/lectormonline/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..035cb7ea6 Binary files /dev/null and b/lib-multisrc/lectormonline/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/lib-multisrc/lectormonline/res/mipmap-xxxhdpi/ic_launcher.png b/lib-multisrc/lectormonline/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..1aa427226 Binary files /dev/null and b/lib-multisrc/lectormonline/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnline.kt b/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnline.kt new file mode 100644 index 000000000..c30106e02 --- /dev/null +++ b/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnline.kt @@ -0,0 +1,193 @@ +package eu.kanade.tachiyomi.multisrc.lectormonline + +import eu.kanade.tachiyomi.network.GET +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 keiyoushi.utils.parseAs +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import kotlin.concurrent.thread + +open class LectorMOnline( + override val name: String, + override val baseUrl: String, + override val lang: String, +) : HttpSource() { + + override val supportsLatest = true + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/comics?sort=views&page=$page", headers) + } + + override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response) + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/comics?page=$page", headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder() + .addPathSegment("comics") + .addQueryParameter("q", query) + .addQueryParameter("page", page.toString()) + + filters.forEach { filter -> + when (filter) { + is SortByFilter -> { + if (filter.selected == "views") { + url.addQueryParameter("sort", "views") + } + if (filter.state!!.ascending) { + url.addQueryParameter("isDesc", "false") + } + } + is GenreFilter -> { + val selectedGenre = filter.toUriPart() + if (selectedGenre.isNotEmpty()) { + return GET("$baseUrl/genres/$selectedGenre?page=$page", headers) + } + } + else -> { } + } + } + + return GET(url.build(), headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + if (response.request.url.pathSegments[0] == "genres") { + return searchMangaGenreParse(document) + } + val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() } + val jsonData = COMICS_LIST_REGEX.find(script)?.groupValues?.get(1)?.unescape() + ?: throw Exception("No se pudo encontrar la lista de cómics") + val data = jsonData.parseAs() + return MangasPage(data.comics.map { it.toSManga() }, data.hasNextPage()) + } + + private fun searchMangaGenreParse(document: Document): MangasPage { + val mangas = document.select("div.grid.relative > a.group.relative").map { element -> + SManga.create().apply { + setUrlWithoutDomain(element.attr("href").substringAfter("/comics/").substringBefore("?")) + title = element.selectFirst("h3")!!.text() + thumbnail_url = element.selectFirst("img")?.attr("abs:src") + } + } + val hasNextPage = document.selectFirst("div.flex.items-center > a:has(> svg):last-child:not(.pointer-events-none)") != null + return MangasPage(mangas, hasNextPage) + } + + override fun getMangaUrl(manga: SManga) = "$baseUrl/comics/${manga.url}" + + override fun mangaDetailsRequest(manga: SManga): Request { + return GET("$baseUrl/api/app/comic/${manga.url}", headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + return response.parseAs().toSMangaDetails() + } + + override fun getChapterUrl(chapter: SChapter): String { + val mangaSlug = chapter.url.substringBefore("/") + val chapterNumber = chapter.url.substringAfter("/") + return "$baseUrl/comics/$mangaSlug/chapters/$chapterNumber" + } + + override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) + + override fun chapterListParse(response: Response): List { + return response.parseAs().getChapters() + } + + override fun pageListRequest(chapter: SChapter): Request { + val mangaSlug = chapter.url.substringBefore("/") + val chapterNumber = chapter.url.substringAfter("/") + return GET("$baseUrl/api/app/comic/$mangaSlug/chapter/$chapterNumber", headers) + } + + override fun pageListParse(response: Response): List { + val data = response.parseAs() + return data.chapter.urlImagesChapter.mapIndexed { index, image -> + Page(index, imageUrl = image) + } + } + + private var genresList: List> = emptyList() + private var fetchFiltersAttempts = 0 + private var filtersState = FiltersState.NOT_FETCHED + + private fun fetchFilters() { + if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return + filtersState = FiltersState.FETCHING + fetchFiltersAttempts++ + thread { + try { + val response = client.newCall(GET("$baseUrl/api/app/genres", headers)).execute() + val filters = response.parseAs() + + genresList = filters.genres.map { genre -> genre.name.lowercase().replaceFirstChar { it.uppercase() } to genre.name } + + filtersState = FiltersState.FETCHED + } catch (_: Throwable) { + filtersState = FiltersState.NOT_FETCHED + } + } + } + + override fun getFilterList(): FilterList { + fetchFilters() + + val filters = mutableListOf>( + Filter.Header("El filtro por género no funciona con los demas filtros"), + Filter.Separator(), + SortByFilter( + "Ordenar por", + listOf( + SortProperty("Más vistos", "views"), + SortProperty("Más recientes", "created_at"), + ), + 1, + ), + ) + + filters += if (filtersState == FiltersState.FETCHED) { + listOf( + Filter.Separator(), + Filter.Header("Filtrar por género"), + GenreFilter(genresList), + ) + } else { + listOf( + Filter.Separator(), + Filter.Header("Presione 'Reiniciar' para intentar cargar los filtros"), + ) + } + + return FilterList(filters) + } + + private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED } + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + + private fun String.unescape(): String { + return UNESCAPE_REGEX.replace(this, "$1") + } + + companion object { + private val UNESCAPE_REGEX = """\\(.)""".toRegex() + private val COMICS_LIST_REGEX = """\\"comicsData\\":(\{.*?\}),\\"searchParams""".toRegex() + } +} diff --git a/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnlineDto.kt b/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnlineDto.kt new file mode 100644 index 000000000..16f8c0802 --- /dev/null +++ b/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnlineDto.kt @@ -0,0 +1,93 @@ +package eu.kanade.tachiyomi.multisrc.lectormonline + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import keiyoushi.utils.tryParse +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonPrimitive +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +@Serializable +class ComicListDataDto( + val comics: List, + private val page: Int, + private val totalPages: Int, +) { + fun hasNextPage() = page < totalPages +} + +@Serializable +class ComicDto( + private val slug: String, + private val name: String, + private val state: String?, + private val urlCover: String, + private val description: String?, + private val author: String?, + private val chapters: List = emptyList(), +) { + fun toSManga() = SManga.create().apply { + url = slug + title = name.substringBeforeLast("-").trim() + thumbnail_url = urlCover + status = state.parseStatus() + } + + fun toSMangaDetails() = SManga.create().apply { + url = slug + title = name.substringBeforeLast("-").trim() + thumbnail_url = urlCover + description = this@ComicDto.description + status = state.parseStatus() + author = this@ComicDto.author + } + + fun getChapters(): List { + return chapters.map { it.toSChapter(slug) } + } + + private fun String?.parseStatus(): Int { + return when (this?.lowercase()) { + "ongoing" -> SManga.ONGOING + else -> SManga.UNKNOWN + } + } +} + +val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply { + timeZone = TimeZone.getTimeZone("UTC") +} + +@Serializable +class ChapterDto( + private val number: JsonPrimitive, + private val createdAt: String, +) { + fun toSChapter(mangaSlug: String) = SChapter.create().apply { + url = "$mangaSlug/$number" + name = "Capítulo $number" + date_upload = dateFormat.tryParse(createdAt) + } +} + +@Serializable +class ChapterPagesDataDto( + val chapter: ChapterPagesDto, +) + +@Serializable +class ChapterPagesDto( + val urlImagesChapter: List = emptyList(), +) + +@Serializable +class GenreListDto( + val genres: List, +) + +@Serializable +class GenreDto( + val name: String, +) diff --git a/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnlineFilters.kt b/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnlineFilters.kt new file mode 100644 index 000000000..6f2ac9078 --- /dev/null +++ b/lib-multisrc/lectormonline/src/eu/kanade/tachiyomi/multisrc/lectormonline/LectorMOnlineFilters.kt @@ -0,0 +1,29 @@ +package eu.kanade.tachiyomi.multisrc.lectormonline + +import eu.kanade.tachiyomi.source.model.Filter + +class SortByFilter(title: String, private val sortProperties: List, defaultIndex: Int) : Filter.Sort( + title, + sortProperties.map { it.name }.toTypedArray(), + Selection(defaultIndex, ascending = false), +) { + val selected: String + get() = sortProperties[state!!.index].value +} + +class SortProperty(val name: String, val value: String) { + override fun toString(): String = name +} + +class GenreFilter(genres: List>) : UriPartFilter( + "Género", + arrayOf( + Pair("Todos", ""), + *genres.toTypedArray(), + ), +) + +open class UriPartFilter(displayName: String, private val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second +} diff --git a/src/es/lectormonline/build.gradle b/src/es/lectormonline/build.gradle new file mode 100644 index 000000000..def292afe --- /dev/null +++ b/src/es/lectormonline/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'Lector MOnline' + extClass = '.LectorMOnline' + themePkg = 'lectormonline' + baseUrl = 'https://www.lectormangas.online' + overrideVersionCode = 0 + isNsfw = false +} + +apply from: "$rootDir/common.gradle" diff --git a/src/es/lectormonline/src/eu/kanade/tachiyomi/extension/es/lectormonline/LectorMOnline.kt b/src/es/lectormonline/src/eu/kanade/tachiyomi/extension/es/lectormonline/LectorMOnline.kt new file mode 100644 index 000000000..97cc596e9 --- /dev/null +++ b/src/es/lectormonline/src/eu/kanade/tachiyomi/extension/es/lectormonline/LectorMOnline.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.extension.es.lectormonline + +import eu.kanade.tachiyomi.multisrc.lectormonline.LectorMOnline +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import java.util.concurrent.TimeUnit + +class LectorMOnline : LectorMOnline( + name = "Lector MOnline", + baseUrl = "https://www.lectormangas.online", + lang = "es", +) { + override val client = network.cloudflareClient.newBuilder() + .rateLimit(3, 1, TimeUnit.SECONDS) + .build() +} diff --git a/src/es/mangasx/build.gradle b/src/es/mangasx/build.gradle new file mode 100644 index 000000000..8f96ae2d3 --- /dev/null +++ b/src/es/mangasx/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'MangasX' + extClass = '.MangasX' + themePkg = 'lectormonline' + baseUrl = 'https://mangasx.online' + overrideVersionCode = 0 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/es/mangasx/src/eu/kanade/tachiyomi/extension/es/mangasx/MangasX.kt b/src/es/mangasx/src/eu/kanade/tachiyomi/extension/es/mangasx/MangasX.kt new file mode 100644 index 000000000..d24e15b83 --- /dev/null +++ b/src/es/mangasx/src/eu/kanade/tachiyomi/extension/es/mangasx/MangasX.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.extension.es.mangasx + +import eu.kanade.tachiyomi.multisrc.lectormonline.LectorMOnline +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import java.util.concurrent.TimeUnit + +class MangasX : LectorMOnline( + name = "MangasX", + baseUrl = "https://mangasx.online", + lang = "es", +) { + override val client = network.cloudflareClient.newBuilder() + .rateLimit(3, 1, TimeUnit.SECONDS) + .build() +}