diff --git a/src/en/mangahot/build.gradle b/src/en/mangahot/build.gradle new file mode 100644 index 000000000..99ad6430b --- /dev/null +++ b/src/en/mangahot/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'MangaHot' + extClass = '.MangaHot' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/mangahot/res/mipmap-hdpi/ic_launcher.png b/src/en/mangahot/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..65100a538 Binary files /dev/null and b/src/en/mangahot/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/mangahot/res/mipmap-mdpi/ic_launcher.png b/src/en/mangahot/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..347b9478b Binary files /dev/null and b/src/en/mangahot/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/mangahot/res/mipmap-xhdpi/ic_launcher.png b/src/en/mangahot/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..1fa12ea43 Binary files /dev/null and b/src/en/mangahot/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/mangahot/res/mipmap-xxhdpi/ic_launcher.png b/src/en/mangahot/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..0bfb5cb82 Binary files /dev/null and b/src/en/mangahot/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/mangahot/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/mangahot/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..8d7a409fe Binary files /dev/null and b/src/en/mangahot/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/mangahot/src/eu/kanade/tachiyomi/extension/en/mangahot/MangaHot.kt b/src/en/mangahot/src/eu/kanade/tachiyomi/extension/en/mangahot/MangaHot.kt new file mode 100644 index 000000000..e32380531 --- /dev/null +++ b/src/en/mangahot/src/eu/kanade/tachiyomi/extension/en/mangahot/MangaHot.kt @@ -0,0 +1,303 @@ +package eu.kanade.tachiyomi.extension.en.mangahot + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +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 eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.put +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import org.jsoup.nodes.Element +import uy.kohesive.injekt.injectLazy +import java.net.URLEncoder +import kotlin.math.ceil + +class MangaHot : HttpSource() { + + override val name = "MangaHot" + + override val baseUrl = "https://mangahot.to" + + override val lang = "en" + + override val supportsLatest = false + + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .rateLimit(2) + .build() + + override fun headersBuilder() = super.headersBuilder().apply { + add("Referer", "$baseUrl/") + } + + private val apiHeaders by lazy { apiHeadersBuilder().build() } + + private fun apiHeadersBuilder() = headersBuilder().apply { + add("Accept", "*/*") + set("Referer", "$baseUrl/list") + add("Sec-Fetch-Dest", "empty") + add("Sec-Fetch-Mode", "cors") + add("Sec-Fetch-Site", "same-origin") + } + + private val json: Json by injectLazy() + + private var currentTotalNumberOfPages = 1 + + // ============================== Popular =============================== + + override fun popularMangaRequest(page: Int): Request = + GET("$baseUrl/api/list/latest?page=$page#$page", apiHeaders) + + override fun popularMangaParse(response: Response): MangasPage = + searchMangaParse(response) + + // =============================== Latest =============================== + + override fun latestUpdatesRequest(page: Int): Request = + throw UnsupportedOperationException() + + override fun latestUpdatesParse(response: Response): MangasPage = + throw UnsupportedOperationException() + + // =============================== Search =============================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/api".toHttpUrl().newBuilder() + val filterList = if (filters.isEmpty()) getFilterList() else filters + val tag = (filterList.find { it is TagFilter } as TagFilter).toUriPart() + + if (page == 1) { + setTotalNumberPages(query, tag) + } + + return when { + query.isNotBlank() -> { + url.addPathSegments("search") + url.fragment(page.toString()) + val body = buildJsonObject { + put("keyword", query) + put("page", page) + put("size", PAGE_LIMIT) + }.toRequestBody() + val headers = apiHeadersBuilder().apply { + set("Referer", "$baseUrl/search?q=${URLEncoder.encode(query, "UTF-8")}") + }.build() + POST(url.build().toString(), headers, body) + } + tag?.isNotBlank() == true -> { + url.addPathSegments("tags") + url.fragment(page.toString()) + val body = buildJsonObject { + put("keyword", tag) + put("page", page) + put("size", PAGE_LIMIT) + }.toRequestBody() + val headers = apiHeadersBuilder().apply { + add("Origin", baseUrl) + set("Referer", "$baseUrl/tags/${tag.replace(" ", "-")}") + }.build() + POST(url.build().toString(), headers, body) + } + else -> popularMangaRequest(page) + } + } + + override fun searchMangaParse(response: Response): MangasPage { + val data = response.parseAs() + data.data.total?.also { + currentTotalNumberOfPages = ceil(it.toDouble() / PAGE_LIMIT).toInt() + } + + val mangaList = data.data.listManga.map { it.toSManga(baseUrl) } + + val currentPage = response.request.url.encodedFragment!!.toInt() + return MangasPage(mangaList, currentPage < currentTotalNumberOfPages) + } + + private fun setTotalNumberPages(query: String, tag: String?) { + val request = if (query.isNotBlank()) { + GET("$baseUrl/search?q=${URLEncoder.encode(query, "UTF-8")}", headers) + } else if (tag?.isNotBlank() == true) { + GET("$baseUrl/tags/${tag.replace(" ", "-")}", headers) + } else { + currentTotalNumberOfPages = 1 + return + } + + val document = client.newCall(request).execute().asJsoup() + document.selectFirst("ul.ant-pagination > li:nth-last-child(2)")?.run { + currentTotalNumberOfPages = text().toIntOrNull() ?: 1 + } ?: run { + currentTotalNumberOfPages = 1 + } + } + + // =============================== Filters ============================== + + override fun getFilterList(): FilterList = FilterList( + Filter.Header("Note: Ignored if using text search!"), + Filter.Separator(), + TagFilter(), + ) + + private class TagFilter : UriPartFilter( + "Tag", + arrayOf( + Pair("