diff --git a/multisrc/overrides/madara/mizumangas/src/MizuMangas.kt b/multisrc/overrides/madara/mizumangas/src/MizuMangas.kt deleted file mode 100644 index 548843850..000000000 --- a/multisrc/overrides/madara/mizumangas/src/MizuMangas.kt +++ /dev/null @@ -1,20 +0,0 @@ -package eu.kanade.tachiyomi.extension.pt.mizumangas - -import eu.kanade.tachiyomi.multisrc.madara.Madara -import eu.kanade.tachiyomi.network.interceptor.rateLimit -import okhttp3.OkHttpClient -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.concurrent.TimeUnit - -class MizuMangas : Madara( - "Mizu Mangás", - "https://mizumangas.com.br", - "pt-BR", - SimpleDateFormat("dd/MM/yyyy", Locale("pt", "BR")), -) { - - override val client: OkHttpClient = super.client.newBuilder() - .rateLimit(1, 2, TimeUnit.SECONDS) - .build() -} diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt index 9b3284bdd..03d84470c 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt @@ -304,7 +304,6 @@ class MadaraGenerator : ThemeSourceGenerator { SingleLang("MiniTwo Scan", "https://minitwoscan.com", "pt-BR"), SingleLang("Mirad Scanlator", "https://miradscanlator.site", "pt-BR", overrideVersionCode = 1), SingleLang("Mixed Manga", "https://mixedmanga.com", "en", overrideVersionCode = 1), - SingleLang("Mizu Mangás", "https://mizumangas.com.br", "pt-BR", isNsfw = true, className = "MizuMangas"), SingleLang("MMScans", "https://mm-scans.org", "en", overrideVersionCode = 5), SingleLang("Momo no Hana Scan", "https://momonohanascan.com", "pt-BR", className = "MomoNoHanaScan", overrideVersionCode = 1), SingleLang("MonarcaManga", "https://monarcamanga.com", "es"), diff --git a/src/pt/mizumangas/AndroidManifest.xml b/src/pt/mizumangas/AndroidManifest.xml new file mode 100644 index 000000000..389c51256 --- /dev/null +++ b/src/pt/mizumangas/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/src/pt/mizumangas/build.gradle b/src/pt/mizumangas/build.gradle new file mode 100644 index 000000000..6e9b52310 --- /dev/null +++ b/src/pt/mizumangas/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Mizu Mangás' + pkgNameSuffix = 'pt.mizumangas' + extClass = '.MizuMangas' + extVersionCode = 30 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-hdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/mizumangas/res/mipmap-hdpi/ic_launcher.png rename to src/pt/mizumangas/res/mipmap-hdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-mdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/mizumangas/res/mipmap-mdpi/ic_launcher.png rename to src/pt/mizumangas/res/mipmap-mdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-xhdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/mizumangas/res/mipmap-xhdpi/ic_launcher.png rename to src/pt/mizumangas/res/mipmap-xhdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/mizumangas/res/mipmap-xxhdpi/ic_launcher.png rename to src/pt/mizumangas/res/mipmap-xxhdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from multisrc/overrides/madara/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png rename to src/pt/mizumangas/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/multisrc/overrides/madara/mizumangas/res/web_hi_res_512.png b/src/pt/mizumangas/res/web_hi_res_512.png similarity index 100% rename from multisrc/overrides/madara/mizumangas/res/web_hi_res_512.png rename to src/pt/mizumangas/res/web_hi_res_512.png diff --git a/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangas.kt b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangas.kt new file mode 100644 index 000000000..497a823f0 --- /dev/null +++ b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangas.kt @@ -0,0 +1,151 @@ +package eu.kanade.tachiyomi.extension.pt.mizumangas + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +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 rx.Observable +import uy.kohesive.injekt.injectLazy +import java.util.concurrent.TimeUnit + +class MizuMangas : HttpSource() { + + override val name = "Mizu Mangás" + + override val baseUrl = "https://mizumangas.com.br" + + override val lang = "pt-BR" + + override val supportsLatest = true + + // Migrated from Madara to a custom CMS. + override val versionId = 2 + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 1, 1, TimeUnit.SECONDS) + .rateLimitHost(API_URL.toHttpUrl(), 1, 1, TimeUnit.SECONDS) + .rateLimitHost(CDN_URL.toHttpUrl(), 1, 2, TimeUnit.SECONDS) + .build() + + private val json: Json by injectLazy() + + private val apiHeaders: Headers by lazy { apiHeadersBuilder().build() } + + override fun headersBuilder(): Headers.Builder = Headers.Builder() + .add("Origin", baseUrl) + .add("Referer", baseUrl) + + private fun apiHeadersBuilder(): Headers.Builder = headersBuilder() + .add("Accept", ACCEPT_JSON) + + /** + * The site doesn't have a popular section, so we use latest instead. + */ + override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page) + + override fun popularMangaParse(response: Response) = latestUpdatesParse(response) + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$API_URL/manga?page=$page&per_page=60", apiHeaders) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val result = response.parseAs>() + val workList = result.data.map(MizuMangasWorkDto::toSManga) + + return MangasPage(workList, result.hasNextPage) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + // The search with query isn't working in their direct API for some reason, + // so we use their site wrapped API instead for now. + val apiUrl = "$API_URL/search".toHttpUrl().newBuilder() + .addPathSegment(query) + .build() + + return GET(apiUrl, apiHeaders) + } + + override fun searchMangaParse(response: Response): MangasPage { + val result = response.parseAs() + val workList = result.mangas.map(MizuMangasWorkDto::toSManga) + + return MangasPage(workList, hasNextPage = false) + } + + override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url + + override fun mangaDetailsRequest(manga: SManga): Request { + val id = manga.url.substringAfter("/manga/") + + return GET("$API_URL/manga/$id", apiHeaders) + } + + override fun mangaDetailsParse(response: Response): SManga { + return response.parseAs().toSManga() + } + + override fun chapterListRequest(manga: SManga): Request { + val id = manga.url.substringAfter("/manga/") + + return GET("$API_URL/chapter/manga/all/$id", apiHeaders) + } + + override fun chapterListParse(response: Response): List { + return response.parseAs>() + .map(MizuMangasChapterDto::toSChapter) + } + + override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url + + override fun pageListRequest(chapter: SChapter): Request { + val id = chapter.url.substringAfter("/reader/") + + return GET("$API_URL/chapter/$id") + } + + override fun pageListParse(response: Response): List { + val result = response.parseAs() + val chapterUrl = "$baseUrl/manga/reader/${result.id}" + + return result.pages.mapIndexed { i, pageDto -> + Page(i, chapterUrl, "$CDN_URL/${pageDto.page}") + } + } + + override fun fetchImageUrl(page: Page): Observable = Observable.just(page.imageUrl!!) + + override fun imageUrlParse(response: Response): String = "" + + override fun imageRequest(page: Page): Request { + val newHeaders = headersBuilder() + .add("Accept", ACCEPT_IMAGE) + .set("Referer", page.url) + .build() + + return GET(page.imageUrl!!, newHeaders) + } + + private inline fun Response.parseAs(): T = use { + json.decodeFromString(it.body.string()) + } + + companion object { + private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8" + private const val ACCEPT_JSON = "application/json" + + private const val API_URL = "https://api.mizumangas.com.br" + const val CDN_URL = "https://cdn.mizumangas.com.br" + } +} diff --git a/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangasDto.kt b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangasDto.kt new file mode 100644 index 000000000..b3a41391b --- /dev/null +++ b/src/pt/mizumangas/src/eu/kanade/tachiyomi/extension/pt/mizumangas/MizuMangasDto.kt @@ -0,0 +1,85 @@ +package eu.kanade.tachiyomi.extension.pt.mizumangas + +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 +import java.util.TimeZone + +@Serializable +data class MizuMangasPaginatedContent( + @SerialName("current_page") val currentPage: Int, + val data: List = emptyList(), + @SerialName("last_page") val lastPage: Int, +) { + + val hasNextPage: Boolean + get() = currentPage < lastPage +} + +@Serializable +data class MizuMangasSearchDto(val mangas: List) + +@Serializable +data class MizuMangasWorkDto( + val id: Int, + val photo: String? = null, + val synopsis: String? = null, + val name: String, + val status: MizuMangasStatusDto? = null, + val categories: List = emptyList(), + val people: List = emptyList(), +) { + + fun toSManga(): SManga = SManga.create().apply { + title = name + author = people.joinToString { it.name } + description = synopsis + genre = categories.joinToString { it.name } + status = when (this@MizuMangasWorkDto.status?.name) { + "Ativo" -> SManga.ONGOING + "Completo" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + thumbnail_url = "${MizuMangas.CDN_URL}/$photo" + url = "/manga/$id" + } +} + +@Serializable +data class MizuMangasStatusDto(val name: String) + +@Serializable +data class MizuMangasCategoryDto(val name: String) + +@Serializable +data class MizuMangasStaffDto(val name: String) + +@Serializable +data class MizuMangasChapterDto( + val id: Int, + @SerialName("chapter") val number: String, + @SerialName("created_at") val createdAt: String? = null, + @SerialName("manga_pages") val pages: List = emptyList(), +) { + + fun toSChapter(): SChapter = SChapter.create().apply { + name = "Capítulo $number" + chapter_number = number.toFloatOrNull() ?: -1f + date_upload = runCatching { DATE_FORMATTER.parse(createdAt!!)?.time } + .getOrNull() ?: 0L + url = "/manga/reader/$id" + } + + companion object { + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.US) + .apply { timeZone = TimeZone.getTimeZone("UTC") } + } + } +} + +@Serializable +data class MizuMangasPageDto(val page: String)