diff --git a/src/es/senshimanga/build.gradle b/src/es/senshimanga/build.gradle new file mode 100644 index 000000000..c1da971cb --- /dev/null +++ b/src/es/senshimanga/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Senshi Manga' + extClass = '.SenshiManga' + extVersionCode = 1 + isNsfw = false +} + +apply from: "$rootDir/common.gradle" diff --git a/src/es/senshimanga/res/mipmap-hdpi/ic_launcher.png b/src/es/senshimanga/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..6690098e6 Binary files /dev/null and b/src/es/senshimanga/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/es/senshimanga/res/mipmap-mdpi/ic_launcher.png b/src/es/senshimanga/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..68cd6fe3d Binary files /dev/null and b/src/es/senshimanga/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/es/senshimanga/res/mipmap-xhdpi/ic_launcher.png b/src/es/senshimanga/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..bb7682046 Binary files /dev/null and b/src/es/senshimanga/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/es/senshimanga/res/mipmap-xxhdpi/ic_launcher.png b/src/es/senshimanga/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..19ab52de2 Binary files /dev/null and b/src/es/senshimanga/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/es/senshimanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/es/senshimanga/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..705a0cd15 Binary files /dev/null and b/src/es/senshimanga/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/es/senshimanga/src/eu/kanade/tachiyomi/extension/es/senshimanga/SenshiManga.kt b/src/es/senshimanga/src/eu/kanade/tachiyomi/extension/es/senshimanga/SenshiManga.kt new file mode 100644 index 000000000..3a17e353a --- /dev/null +++ b/src/es/senshimanga/src/eu/kanade/tachiyomi/extension/es/senshimanga/SenshiManga.kt @@ -0,0 +1,145 @@ +package eu.kanade.tachiyomi.extension.es.senshimanga + +import eu.kanade.tachiyomi.network.GET +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 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.injectLazy + +class SenshiManga : HttpSource() { + + override val name = "Senshi Manga" + + override val baseUrl = "https://senshimanga.com" + + override val lang = "es" + + override val supportsLatest = true + + private val apiBaseUrl = "https://lat-manga.com" + + private val json: Json by injectLazy() + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 3) + .rateLimitHost(apiBaseUrl.toHttpUrl(), 3) + .build() + + override fun headersBuilder(): Headers.Builder = super.headersBuilder() + .add("Referer", "$baseUrl/") + + private val apiHeaders: Headers = headersBuilder() + .add("Organization-Domain", "senshimanga.com") + .build() + + override fun popularMangaRequest(page: Int): Request = + GET("$apiBaseUrl/api/manga-custom?page=$page&limit=$PAGE_LIMIT&order=popular", apiHeaders) + + override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response) + + override fun latestUpdatesRequest(page: Int): Request = + GET("$apiBaseUrl/api/manga-custom?page=$page&limit=$PAGE_LIMIT&order=latest", apiHeaders) + + override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$apiBaseUrl/api/manga-custom".toHttpUrl().newBuilder() + + url.setQueryParameter("page", page.toString()) + url.setQueryParameter("limit", PAGE_LIMIT.toString()) + + filters.forEach { filter -> + when (filter) { + is SortByFilter -> url.setQueryParameter("order", filter.toUriPart()) + else -> {} + } + } + + if (query.isNotBlank()) url.setQueryParameter("q", query) + + return GET(url.build(), apiHeaders) + } + + override fun searchMangaParse(response: Response): MangasPage { + val page = response.request.url.queryParameter("page")!!.toInt() + val result = json.decodeFromString>(response.body.string()) + + val mangas = result.data.series.map { it.toSManga() } + val hasNextPage = page < result.data.maxPage + + return MangasPage(mangas, hasNextPage) + } + + override fun getFilterList() = FilterList( + SortByFilter("Ordenar por", getSortList()), + ) + + private fun getSortList() = arrayOf( + Pair("Popularidad", "popular"), + Pair("Recientes", "latest"), + ) + + override fun getMangaUrl(manga: SManga): String = "$baseUrl/manga/${manga.url}" + + override fun mangaDetailsRequest(manga: SManga): Request = + GET("$apiBaseUrl/api/manga-custom/${manga.url}", apiHeaders) + + override fun mangaDetailsParse(response: Response): SManga { + val result = json.decodeFromString>(response.body.string()) + return result.data.toSMangaDetails() + } + + override fun getChapterUrl(chapter: SChapter): String { + val seriesSlug = chapter.url.substringBefore("/") + val chapterSlug = chapter.url.substringAfter("/") + + return "$baseUrl/manga/$seriesSlug/chapters/$chapterSlug" + } + + override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) + + override fun chapterListParse(response: Response): List { + val result = json.decodeFromString>(response.body.string()) + val seriesSlug = result.data.slug + return result.data.chapters?.map { it.toSChapter(seriesSlug) } ?: emptyList() + } + + override fun pageListRequest(chapter: SChapter): Request { + val seriesSlug = chapter.url.substringBefore("/") + val chapterSlug = chapter.url.substringAfter("/") + + return GET("$apiBaseUrl/api/manga-custom/$seriesSlug/chapter/$chapterSlug/pages", apiHeaders) + } + + override fun pageListParse(response: Response): List { + val result = json.decodeFromString>>(response.body.string()) + return result.data.mapIndexed { i, page -> + Page(i, imageUrl = page.imageUrl) + } + } + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + + class SortByFilter(title: String, list: Array>) : UriPartFilter(title, list) + + open class UriPartFilter(displayName: String, val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + companion object { + private const val PAGE_LIMIT = 36 + } +} diff --git a/src/es/senshimanga/src/eu/kanade/tachiyomi/extension/es/senshimanga/SenshiMangaDto.kt b/src/es/senshimanga/src/eu/kanade/tachiyomi/extension/es/senshimanga/SenshiMangaDto.kt new file mode 100644 index 000000000..78f8e99df --- /dev/null +++ b/src/es/senshimanga/src/eu/kanade/tachiyomi/extension/es/senshimanga/SenshiMangaDto.kt @@ -0,0 +1,78 @@ +package eu.kanade.tachiyomi.extension.es.senshimanga + +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.ParseException +import java.text.SimpleDateFormat +import java.util.Locale + +@Serializable +class Data(val data: T) + +@Serializable +class SeriesListDataDto( + @SerialName("data") val series: List = emptyList(), + val maxPage: Int = 0, +) + +@Serializable +class SeriesDto( + val slug: String, + private val imageUrl: String, + private val title: String, + private val status: String? = null, + private val description: String? = null, + private val authors: List? = emptyList(), + val chapters: List? = emptyList(), +) { + fun toSManga() = SManga.create().apply { + title = this@SeriesDto.title + thumbnail_url = imageUrl + url = slug + } + + fun toSMangaDetails() = toSManga().apply { + status = parseStatus(this@SeriesDto.status) + description = this@SeriesDto.description + title = this@SeriesDto.title + author = authors?.joinToString { it.name } + } + + private fun parseStatus(status: String?) = when (status) { + "ongoing" -> SManga.ONGOING + "hiatus" -> SManga.ON_HIATUS + "finished" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } +} + +@Serializable +class SeriesAuthorDto( + val name: String, +) + +private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + +@Serializable +class SeriesChapterDto( + private val title: String, + private val number: Float, + private val createdAt: String, +) { + fun toSChapter(seriesSlug: String) = SChapter.create().apply { + name = "CapĂ­tulo ${number.toString().removeSuffix(".0")} - $title" + date_upload = try { + dateFormat.parse(createdAt)?.time ?: 0L + } catch (_: ParseException) { + 0L + } + url = "$seriesSlug/$number" + } +} + +@Serializable +class PageDto( + val imageUrl: String, +)