diff --git a/src/zh/manwa/AndroidManifest.xml b/src/zh/manwa/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/zh/manwa/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/zh/manwa/build.gradle b/src/zh/manwa/build.gradle new file mode 100644 index 000000000..2a087ca72 --- /dev/null +++ b/src/zh/manwa/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Manwa' + pkgNameSuffix = 'zh.manwa' + extClass = '.Manwa' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/manwa/res/mipmap-hdpi/ic_launcher.png b/src/zh/manwa/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..f0cf3eed0 Binary files /dev/null and b/src/zh/manwa/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/manwa/res/mipmap-mdpi/ic_launcher.png b/src/zh/manwa/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..e15a1529e Binary files /dev/null and b/src/zh/manwa/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/manwa/res/mipmap-xhdpi/ic_launcher.png b/src/zh/manwa/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..96160d179 Binary files /dev/null and b/src/zh/manwa/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/manwa/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/manwa/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..33ae56273 Binary files /dev/null and b/src/zh/manwa/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/manwa/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/manwa/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..809a885f5 Binary files /dev/null and b/src/zh/manwa/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/manwa/res/web_hi_res_512.png b/src/zh/manwa/res/web_hi_res_512.png new file mode 100644 index 000000000..1e9821c03 Binary files /dev/null and b/src/zh/manwa/res/web_hi_res_512.png differ diff --git a/src/zh/manwa/src/eu/kanade/tachiyomi/extension/zh/manwa/Manwa.kt b/src/zh/manwa/src/eu/kanade/tachiyomi/extension/zh/manwa/Manwa.kt new file mode 100644 index 000000000..e6f9905c3 --- /dev/null +++ b/src/zh/manwa/src/eu/kanade/tachiyomi/extension/zh/manwa/Manwa.kt @@ -0,0 +1,172 @@ +package eu.kanade.tachiyomi.extension.zh.manwa + +import android.app.Application +import android.content.SharedPreferences +import android.net.Uri +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.ConfigurableSource +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.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +class Manwa : ParsedHttpSource(), ConfigurableSource { + override val name: String = "漫蛙" + override val lang: String = "zh" + override val supportsLatest: Boolean = true + override val baseUrl = "https://manwa.me" + private val json: Json by injectLazy() + private val preferences: SharedPreferences = + Injekt.get().getSharedPreferences("source_$id", 0x0000) + + private val rewriteOctetStream: Interceptor = Interceptor { chain -> + val originalResponse: Response = chain.proceed(chain.request()) + if (originalResponse.request.url.toString().endsWith("?v=20220724")) { + // Decrypt images in mangas + val orgBody = originalResponse.body!!.bytes() + val key = "my2ecret782ecret".toByteArray() + val aesKey = SecretKeySpec(key, "AES") + val cipher = Cipher.getInstance("AES/CBC/NOPADDING") + cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(key)) + val result = cipher.doFinal(orgBody) + val newBody = result.toResponseBody("image/webp".toMediaTypeOrNull()) + originalResponse.newBuilder() + .body(newBody) + .build() + } else originalResponse + } + override val client: OkHttpClient = network.client.newBuilder() + .addNetworkInterceptor(rewriteOctetStream) + .build() + + // Popular + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/rank", headers) + override fun popularMangaNextPageSelector(): String? = null + override fun popularMangaSelector(): String = "#rankList_2 > a" + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.attr("title") + url = element.attr("href") + thumbnail_url = element.select("img").attr("data-original") + } + + // Latest + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/getUpdate?page=${page * 15 - 15}&date=", headers) + override fun latestUpdatesParse(response: Response): MangasPage { + // Get image host + val resp = client.newCall(GET("$baseUrl/update?img_host=${preferences.getString(IMAGE_HOST_KEY, IMAGE_HOST_ENTRY_VALUES[0])}")).execute() + val document = resp.asJsoup() + val imgHost = document.selectFirst(".manga-list-2-cover-img").attr(":src").drop(1).substringBefore("'") + + val jsonObject = json.parseToJsonElement(response.body!!.string()).jsonObject + val mangas = jsonObject["books"]!!.jsonArray.map { + SManga.create().apply { + val obj = it.jsonObject + title = obj["book_name"]!!.jsonPrimitive.content + url = "/book/${obj["id"]!!.jsonPrimitive.content}" + thumbnail_url = imgHost + obj["cover_url"]!!.jsonPrimitive.content + } + } + + val currentPage = response.request.url.toString().substringAfter("page=").substringBefore("&").toInt() + val totalPage = jsonObject["total"]!!.jsonPrimitive.int + return MangasPage(mangas, totalPage > currentPage + 15) + } + override fun latestUpdatesNextPageSelector() = throw Exception("Not used") + override fun latestUpdatesSelector() = throw Exception("Not used") + override fun latestUpdatesFromElement(element: Element) = throw Exception("Not used") + + // Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val uri = Uri.parse(baseUrl).buildUpon() + uri.appendPath("search") + .appendQueryParameter("keyword", query) + return GET(uri.toString(), headers) + } + + override fun searchMangaNextPageSelector(): String? = null + override fun searchMangaSelector(): String = "ul.book-list > li" + override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { + title = element.selectFirst("p.book-list-info-title").text() + setUrlWithoutDomain(element.selectFirst("a").attr("abs:href")) + thumbnail_url = element.selectFirst("img").attr("data-original") + } + + // Details + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + title = document.selectFirst("p.detail-main-info-title").text() + thumbnail_url = document.selectFirst("div.detail-main-cover > img").attr("data-original") + author = document.select("p.detail-main-info-author > span.detail-main-info-value > a").text() + artist = author + genre = document.select("div.detail-main-info-class > a.info-tag").eachText().joinToString(", ") + description = document.selectFirst("#detail > p.detail-desc").text() + } + + // Chapters + + override fun chapterListSelector(): String = "ul#detail-list-select > li > a" + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + url = element.attr("href") + name = element.text() + } + override fun chapterListParse(response: Response): List { + return super.chapterListParse(response).reversed() + } + + // Pages + + override fun pageListRequest(chapter: SChapter): Request { + return GET("$baseUrl${chapter.url}?img_host=${preferences.getString(IMAGE_HOST_KEY, IMAGE_HOST_ENTRY_VALUES[0])}", headers) + } + + override fun pageListParse(document: Document): List = mutableListOf().apply { + document.select("#cp_img > img[data-r-src]").forEachIndexed { index, it -> + add(Page(index, "", it.attr("data-r-src"))) + } + } + + override fun imageUrlParse(document: Document): String = throw Exception("Not Used") + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = IMAGE_HOST_KEY + title = "图源" + entries = IMAGE_HOST_ENTRIES + entryValues = IMAGE_HOST_ENTRY_VALUES + setDefaultValue(IMAGE_HOST_ENTRY_VALUES[0]) + }.let { screen.addPreference(it) } + } + + companion object { + private const val IMAGE_HOST_KEY = "IMG_HOST" + private val IMAGE_HOST_ENTRIES = arrayOf("图源1", "图源2", "图源3") + private val IMAGE_HOST_ENTRY_VALUES = arrayOf("1", "2", "3") + } +}