diff --git a/src/vi/mimihentai/build.gradle b/src/vi/mimihentai/build.gradle new file mode 100644 index 000000000..7f7ee97c3 --- /dev/null +++ b/src/vi/mimihentai/build.gradle @@ -0,0 +1,7 @@ +ext { + extName = "MiMiHentai" + extClass = ".MiMiHentai" + extVersionCode = 1 + isNsfw = true +} +apply from: "$rootDir/common.gradle" diff --git a/src/vi/mimihentai/res/mipmap-hdpi/ic_launcher.png b/src/vi/mimihentai/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..f9dd79537 Binary files /dev/null and b/src/vi/mimihentai/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/vi/mimihentai/res/mipmap-mdpi/ic_launcher.png b/src/vi/mimihentai/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ef0a3a6c8 Binary files /dev/null and b/src/vi/mimihentai/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/vi/mimihentai/res/mipmap-xhdpi/ic_launcher.png b/src/vi/mimihentai/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..b453b2e1b Binary files /dev/null and b/src/vi/mimihentai/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/vi/mimihentai/res/mipmap-xxhdpi/ic_launcher.png b/src/vi/mimihentai/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..0f3f09ec2 Binary files /dev/null and b/src/vi/mimihentai/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/vi/mimihentai/res/mipmap-xxxhdpi/ic_launcher.png b/src/vi/mimihentai/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..47ef71842 Binary files /dev/null and b/src/vi/mimihentai/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/vi/mimihentai/src/eu/kanade/tachiyomi/extension/vi/mimihentai/Dto.kt b/src/vi/mimihentai/src/eu/kanade/tachiyomi/extension/vi/mimihentai/Dto.kt new file mode 100644 index 000000000..bc8d6662b --- /dev/null +++ b/src/vi/mimihentai/src/eu/kanade/tachiyomi/extension/vi/mimihentai/Dto.kt @@ -0,0 +1,64 @@ +package eu.kanade.tachiyomi.extension.vi.mimihentai + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import keiyoushi.utils.tryParse +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Locale + +@Serializable +class ListingDto( + val data: List, + val totalPage: Long, + val currentPage: Long, +) + +@Serializable +class MangaDto( + private val id: Long, + private val title: String, + private val coverUrl: String, + private val description: String, + private val authors: List, + private val genres: List, +) { + fun toSManga(): SManga = SManga.create().apply { + title = this@MangaDto.title + thumbnail_url = coverUrl + url = "$id" + description = this@MangaDto.description + author = authors.joinToString { i -> i.name } + genre = genres.joinToString { i -> i.name } + initialized = true + } +} + +@Serializable +class Author( + val name: String, +) + +@Serializable +class Genre( + val name: String, +) + +@Serializable +class ChapterDto( + private val id: Long, + private val title: String, + private val createdAt: String, +) { + fun toSChapter(mangaId: String): SChapter = SChapter.create().apply { + name = title + date_upload = dateFormat.tryParse(createdAt) + url = "$mangaId/$id" + } +} +private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.US) + +@Serializable +class PageListDto( + val pages: List, +) diff --git a/src/vi/mimihentai/src/eu/kanade/tachiyomi/extension/vi/mimihentai/MiMiHentai.kt b/src/vi/mimihentai/src/eu/kanade/tachiyomi/extension/vi/mimihentai/MiMiHentai.kt new file mode 100644 index 000000000..809e384d0 --- /dev/null +++ b/src/vi/mimihentai/src/eu/kanade/tachiyomi/extension/vi/mimihentai/MiMiHentai.kt @@ -0,0 +1,182 @@ +package eu.kanade.tachiyomi.extension.vi.mimihentai + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimit +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 keiyoushi.utils.parseAs +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable + +class MiMiHentai : HttpSource() { + override val name: String = "MiMiHentai" + + override val lang: String = "vi" + + override val baseUrl: String = "https://mimihentai.com" + + private val apiUrl: String = "$baseUrl/api/v1/manga" + + override val supportsLatest: Boolean = true + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .rateLimit(3) + .build() + + override fun latestUpdatesRequest(page: Int): Request = GET( + apiUrl.toHttpUrl().newBuilder().apply { + addPathSegments("tatcatruyen") + addQueryParameter("page", page.toString()) + addQueryParameter("sort", "updated_at") + }.build(), + headers, + ) + + override fun latestUpdatesParse(response: Response): MangasPage { + val res = response.parseAs() + val hasNextPage = res.currentPage != res.totalPage + return MangasPage(res.data.map { it.toSManga() }, hasNextPage) + } + + override fun getMangaUrl(manga: SManga): String = "$baseUrl/g/${manga.url}" + + override fun chapterListRequest(manga: SManga): Request { + val id = manga.url.substringAfterLast("/") + val url = apiUrl.toHttpUrl().newBuilder() + .addPathSegment("gallery") + .addPathSegment(id) + .build() + return GET(url, headers) + } + + override fun chapterListParse(response: Response): List { + val segments = response.request.url.pathSegments + val mangaId = segments.last() + val res = response.parseAs>() + return res.map { it.toSChapter(mangaId) } + } + + override fun getChapterUrl(chapter: SChapter): String { + val mangaId = chapter.url.substringBefore('/') + val chapterId = chapter.url.substringAfter('/') + return "$baseUrl/g/$mangaId/chapter/$chapterId" + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + override fun mangaDetailsRequest(manga: SManga): Request { + val id = manga.url.substringAfterLast("/") + val url = apiUrl.toHttpUrl().newBuilder() + .addPathSegment("info") + .addPathSegment(id) + .build() + return GET(url, headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + val res = response.parseAs() + return res.toSManga() + } + + override fun pageListRequest(chapter: SChapter): Request { + return GET("$apiUrl/chapter?id=${chapter.url.substringAfterLast("/")}", headers) + } + + override fun pageListParse(response: Response): List { + val res = response.parseAs() + return res.pages.mapIndexed { index, url -> + Page(index, imageUrl = url) + } + } + + override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response) + + override fun popularMangaRequest(page: Int): Request = GET( + apiUrl.toHttpUrl().newBuilder().apply { + addPathSegments("tatcatruyen") + addQueryParameter("page", page.toString()) + addQueryParameter("sort", "views") + }.build(), + headers, + ) + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return when { + query.startsWith(PREFIX_ID_SEARCH) -> { + val id = query.removePrefix(PREFIX_ID_SEARCH) + client.newCall(searchMangaByIdRequest(id)) + .asObservableSuccess() + .map { response -> searchMangaByIdParse(response) } + } + query.toIntOrNull() != null -> { + client.newCall(searchMangaByIdRequest(query)) + .asObservableSuccess() + .map { response -> searchMangaByIdParse(response) } + } + else -> super.fetchSearchManga(page, query, filters) + } + } + private fun searchMangaByIdRequest(id: String) = GET("$apiUrl/info/$id", headers) + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + private fun searchMangaByIdParse(response: Response): MangasPage { + val details = mangaDetailsParse(response) + return MangasPage(listOf(details), false) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = apiUrl.toHttpUrl().newBuilder().apply { + addPathSegments("search") + addQueryParameter("page", page.toString()) + addQueryParameter("name", query) + + (if (filters.isEmpty()) getFilterList() else filters).forEach { filters -> + when (filters) { + is SortByList -> + { + val sort = getSortByList()[filters.state] + addQueryParameter("sort", sort.id) + } + + else -> {} + } + } + }.build() + return GET(url, headers) + } + + private class SortByList(sort: Array) : Filter.Select("Sắp xếp", sort) + private class SortBy(name: String, val id: String) : Filter.CheckBox(name) { + override fun toString(): String { + return name + } + } + + override fun getFilterList() = FilterList( + SortByList(getSortByList()), + ) + + private fun getSortByList() = arrayOf( + SortBy("Mới", "updated_at"), + SortBy("Likes", "likes"), + SortBy("Views", "views"), + SortBy("Lưu", "follows"), + SortBy("Tên", "title"), + ) + companion object { + private const val PREFIX_ID_SEARCH = "id:" + } +}