parent
9d05af7e2d
commit
00046adbdc
@ -1,7 +0,0 @@
|
|||||||
ext {
|
|
||||||
extName = "MiMiHentai"
|
|
||||||
extClass = ".MiMiHentai"
|
|
||||||
extVersionCode = 3
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@ -1,76 +0,0 @@
|
|||||||
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<MangaDto>,
|
|
||||||
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<Author>,
|
|
||||||
private val genres: List<Genre>,
|
|
||||||
) {
|
|
||||||
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<ListPages>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class ListPages(
|
|
||||||
val imageUrl: String,
|
|
||||||
val drm: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Genres(
|
|
||||||
val id: Long,
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.vi.mimihentai
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
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 kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
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 - 1).toString())
|
|
||||||
addQueryParameter("sort", "updated_at")
|
|
||||||
addQueryParameter("ex", "196")
|
|
||||||
}.build(),
|
|
||||||
headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
|
||||||
val res = response.parseAs<ListingDto>()
|
|
||||||
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<SChapter> {
|
|
||||||
val segments = response.request.url.pathSegments
|
|
||||||
val mangaId = segments.last()
|
|
||||||
val res = response.parseAs<List<ChapterDto>>()
|
|
||||||
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<MangaDto>()
|
|
||||||
return res.toSManga()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
return GET("$apiUrl/chapter?id=${chapter.url.substringAfterLast("/")}", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val res = response.parseAs<PageListDto>()
|
|
||||||
return res.pages.mapIndexed { index, url ->
|
|
||||||
Page(index, imageUrl = url.imageUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response)
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = GET(
|
|
||||||
apiUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegments("tatcatruyen")
|
|
||||||
addQueryParameter("page", (page - 1).toString())
|
|
||||||
addQueryParameter("sort", "views")
|
|
||||||
addQueryParameter("ex", "196")
|
|
||||||
}.build(),
|
|
||||||
headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
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("advance-search")
|
|
||||||
addQueryParameter("author", "")
|
|
||||||
addQueryParameter("character", "")
|
|
||||||
addQueryParameter("parody", "")
|
|
||||||
addQueryParameter("page", (page - 1).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)
|
|
||||||
}
|
|
||||||
is GenreList -> filters.state.forEach {
|
|
||||||
when (it.state) {
|
|
||||||
Filter.TriState.STATE_INCLUDE -> addQueryParameter("genre", it.id)
|
|
||||||
Filter.TriState.STATE_EXCLUDE -> addQueryParameter("ex", it.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TextField -> setQueryParameter(filters.key, filters.state)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun genresRequest(): Request = GET("$apiUrl/genres", headers)
|
|
||||||
|
|
||||||
private fun parseGenres(response: Response): List<Pair<Long, String>> {
|
|
||||||
return response.parseAs<List<Genres>>().map { Pair(it.id, it.name) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var fetchGenresAttempts: Int = 0
|
|
||||||
private fun fetchGenres() {
|
|
||||||
if (fetchGenresAttempts >= 3 || genreList.isEmpty()) {
|
|
||||||
launchIO {
|
|
||||||
try {
|
|
||||||
client.newCall(genresRequest()).await()
|
|
||||||
.use { parseGenres(it) }
|
|
||||||
.takeIf { it.isNotEmpty() }
|
|
||||||
?.also { genreList = it }
|
|
||||||
} catch (_: Exception) {
|
|
||||||
} finally {
|
|
||||||
fetchGenresAttempts++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchIO(block: suspend () -> Unit) = GlobalScope.launch(Dispatchers.IO) { block() }
|
|
||||||
|
|
||||||
private var genreList: List<Pair<Long, String>> = emptyList()
|
|
||||||
|
|
||||||
private class GenreList(name: String, pairs: List<Pair<Long, String>>) : GenresFilter(name, pairs)
|
|
||||||
private open class GenresFilter(title: String, pairs: List<Pair<Long, String>>) :
|
|
||||||
Filter.Group<GenreCheckBox>(title, pairs.map { GenreCheckBox(it.second, it.first.toString()) })
|
|
||||||
class GenreCheckBox(name: String, val id: String = name) : Filter.TriState(name)
|
|
||||||
|
|
||||||
private class SortByList(sort: Array<SortBy>) : Filter.Select<SortBy>("Sắp xếp", sort)
|
|
||||||
private class SortBy(name: String, val id: String) : Filter.CheckBox(name) {
|
|
||||||
override fun toString(): String {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
|
||||||
override fun getFilterList(): FilterList {
|
|
||||||
fetchGenres()
|
|
||||||
return FilterList(
|
|
||||||
SortByList(getSortByList()),
|
|
||||||
TextField("Tác giả", "author"),
|
|
||||||
TextField("Parody", "parody"),
|
|
||||||
TextField("Nhân vật", "character"),
|
|
||||||
if (genreList.isEmpty()) {
|
|
||||||
Filter.Header("Nhấn 'Làm mới' để thử tải thể loại")
|
|
||||||
} else {
|
|
||||||
GenreList("Thể loại", genreList)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user