diff --git a/.run/MangabzGenerator.run.xml b/.run/MangabzGenerator.run.xml
deleted file mode 100644
index f7e671c76..000000000
--- a/.run/MangabzGenerator.run.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/.run/WeebreaderGenerator.run.xml b/.run/WeebreaderGenerator.run.xml
deleted file mode 100644
index 3aa96b3a8..000000000
--- a/.run/WeebreaderGenerator.run.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/multisrc/overrides/mangabz/mangabz/additional.gradle b/multisrc/overrides/mangabz/mangabz/additional.gradle
deleted file mode 100644
index 027379881..000000000
--- a/multisrc/overrides/mangabz/mangabz/additional.gradle
+++ /dev/null
@@ -1,3 +0,0 @@
-dependencies {
- implementation project(':lib-unpacker')
-}
diff --git a/multisrc/overrides/mangabz/vomic/src/Vomic.kt b/multisrc/overrides/mangabz/vomic/src/Vomic.kt
deleted file mode 100644
index ae4d2c48b..000000000
--- a/multisrc/overrides/mangabz/vomic/src/Vomic.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package eu.kanade.tachiyomi.extension.zh.vomic
-
-import android.app.Application
-import androidx.preference.ListPreference
-import androidx.preference.PreferenceScreen
-import eu.kanade.tachiyomi.multisrc.mangabz.MangabzTheme
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.source.ConfigurableSource
-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.util.asJsoup
-import okhttp3.HttpUrl.Companion.toHttpUrl
-import okhttp3.Request
-import okhttp3.Response
-import org.jsoup.Jsoup
-import org.jsoup.nodes.Document
-import org.jsoup.nodes.Element
-import org.jsoup.select.Elements
-import org.jsoup.select.Evaluator
-import rx.Observable
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.text.SimpleDateFormat
-import java.util.Locale
-
-class Vomic : MangabzTheme("vomic", ""), ConfigurableSource {
-
- override val supportsLatest = false
-
- override val baseUrl: String
-
- init {
- val mirrors = MIRRORS
- val mirrorIndex = Injekt.get().getSharedPreferences("source_$id", 0x0000)
- .getString(MIRROR_PREF, "0")!!.toInt().coerceAtMost(mirrors.size - 1)
- baseUrl = "http://" + mirrors[mirrorIndex]
- }
-
- override fun headersBuilder() = super.headersBuilder().removeAll("Referer")
-
- override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
-
- // original credit: https://github.com/tachiyomiorg/tachiyomi-extensions/pull/5628
- override fun popularMangaParse(response: Response): MangasPage {
- val document = response.asJsoup()
-
- val link = Evaluator.Tag("a")
- val image = Evaluator.Tag("img")
- val paragraph = Evaluator.Tag("p")
-
- /* top banner - no thumbnail
- document.selectFirst(Evaluator.Class("banner-con")).select(link).mapTo(mangas) { element ->
- SManga.create().apply {
- title = element.attr("title")
- url = element.attr("href")
- thumbnail_url = element.selectFirst(image).attr("src")
- .takeIf { !it.endsWith("/static/images/bg/banner_info_a.jpg") }
- }
- } */
-
- val mangas = buildList {
- // ranking sidebar
- addAll(document.selectFirst(Evaluator.Class("rank-list"))!!.children())
- // carousel list
- addAll(document.selectFirst(Evaluator.Class("carousel-right-list"))!!.children())
- // recommend list
- addAll(document.select(Evaluator.Class("index-manga-item"))!!)
- }.map { element ->
- SManga.create().apply {
- title = element.selectFirst(paragraph)!!.text()
- url = element.selectFirst(link)!!.attr("href")
- thumbnail_url = element.selectFirst(image)!!.attr("src")
- }
- }
-
- return MangasPage(mangas.distinctBy { it.url }, false)
- }
-
- override fun parseDescription(element: Element, title: String, details: Elements): String {
- val text = element.ownText()
- val collapsed = element.selectFirst(Evaluator.Tag("span"))?.ownText() ?: ""
- val source = details[3].text()
- return "$source\n\n$text$collapsed"
- }
-
- override fun fetchChapterList(manga: SManga): Observable> {
- val chapterId = manga.url.removePrefix("/").removeSuffix("_c/")
- return super.fetchChapterList(manga).doOnNext {
- for (chapter in it) chapter.url = chapter.url + "chapterimage.ashx?mid=" + chapterId
- }
- }
-
- override fun getChapterElements(document: Document): Elements {
- val chapterId = document.location().removeSuffix("_c/").substringAfterLast('/')
- val response = client.newCall(GET("$baseUrl/chapter-$chapterId-s2/", headers)).execute()
- return Jsoup.parseBodyFragment(response.body.string()).body().children()
- }
-
- override val needPageCount = false
-
- override fun parseDate(listTitle: String): Long {
- val date = listTitle.split("|")[2].trim()
- return dateFormat.parse(date)!!.time
- }
-
- override fun pageListParse(response: Response): List {
- val urls = response.body.string().run {
- val left = indexOf('[')
- val right = lastIndexOf(']')
- if (left + 1 == right) return emptyList()
- substring(left + 1, right).split(", ")
- }
- return urls.mapIndexed { index, rawUrl ->
- val url = rawUrl.trim('"')
- val imageUrl = when {
- url.startsWith("http://127.0.0.1") -> url.toHttpUrl().queryParameter("url")
- else -> url
- }
- Page(index, imageUrl = imageUrl)
- }
- }
-
- override fun imageRequest(page: Page): Request {
- val url = page.imageUrl!!
- val host = url.toHttpUrl().host
- val headers = headersBuilder().set("Referer", "https://$host/").build()
- return GET(url, headers)
- }
-
- override fun setupPreferenceScreen(screen: PreferenceScreen) {
- ListPreference(screen.context).apply {
- val mirrors = MIRRORS
- key = MIRROR_PREF
- title = "镜像网址"
- summary = "%s\n重启生效"
- entries = mirrors
- entryValues = Array(mirrors.size) { it.toString() }
- setDefaultValue("0")
- }.let(screen::addPreference)
- }
-
- companion object {
- private const val MIRROR_PREF = "MIRROR"
- private val MIRRORS get() = arrayOf("www.vomicmh.com", "www.iewoai.com")
-
- private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.ENGLISH) }
- }
-}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangabz/MangabzGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangabz/MangabzGenerator.kt
deleted file mode 100644
index f78c07fc2..000000000
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangabz/MangabzGenerator.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package eu.kanade.tachiyomi.multisrc.mangabz
-
-import generator.ThemeSourceData.SingleLang
-import generator.ThemeSourceGenerator
-
-class MangabzGenerator : ThemeSourceGenerator {
- override val themeClass = "MangabzTheme"
- override val themePkg = "mangabz"
- override val baseVersionCode = 1
- override val sources = listOf(
- SingleLang("Mangabz", "https://mangabz.com", "zh", overrideVersionCode = 6),
- SingleLang("vomic", "http://www.vomicmh.com", "zh", className = "Vomic"),
- )
-
- companion object {
- @JvmStatic
- fun main(args: Array) {
- MangabzGenerator().createAll()
- }
- }
-}
diff --git a/multisrc/overrides/mangabz/mangabz/AndroidManifest.xml b/src/zh/mangabz/AndroidManifest.xml
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/AndroidManifest.xml
rename to src/zh/mangabz/AndroidManifest.xml
diff --git a/src/zh/mangabz/build.gradle b/src/zh/mangabz/build.gradle
new file mode 100644
index 000000000..4c3fc1758
--- /dev/null
+++ b/src/zh/mangabz/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Mangabz'
+ pkgNameSuffix = 'zh.mangabz'
+ extClass = '.Mangabz'
+ extVersionCode = 7
+}
+
+apply from: "$rootDir/common.gradle"
+
+dependencies {
+ implementation project(':lib-unpacker')
+}
diff --git a/multisrc/overrides/mangabz/mangabz/res/mipmap-hdpi/ic_launcher.png b/src/zh/mangabz/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/res/mipmap-hdpi/ic_launcher.png
rename to src/zh/mangabz/res/mipmap-hdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/mangabz/res/mipmap-mdpi/ic_launcher.png b/src/zh/mangabz/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/res/mipmap-mdpi/ic_launcher.png
rename to src/zh/mangabz/res/mipmap-mdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/mangabz/res/mipmap-xhdpi/ic_launcher.png b/src/zh/mangabz/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/res/mipmap-xhdpi/ic_launcher.png
rename to src/zh/mangabz/res/mipmap-xhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/mangabz/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/mangabz/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/res/mipmap-xxhdpi/ic_launcher.png
rename to src/zh/mangabz/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/mangabz/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/mangabz/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/res/mipmap-xxxhdpi/ic_launcher.png
rename to src/zh/mangabz/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/mangabz/res/web_hi_res_512.png b/src/zh/mangabz/res/web_hi_res_512.png
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/res/web_hi_res_512.png
rename to src/zh/mangabz/res/web_hi_res_512.png
diff --git a/multisrc/overrides/mangabz/mangabz/src/CookieInterceptor.kt b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/CookieInterceptor.kt
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/src/CookieInterceptor.kt
rename to src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/CookieInterceptor.kt
diff --git a/multisrc/overrides/mangabz/mangabz/src/Date.kt b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Date.kt
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/src/Date.kt
rename to src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Date.kt
diff --git a/multisrc/overrides/mangabz/mangabz/src/Filters.kt b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Filters.kt
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/src/Filters.kt
rename to src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Filters.kt
diff --git a/multisrc/overrides/mangabz/mangabz/src/Mangabz.kt b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Mangabz.kt
similarity index 98%
rename from multisrc/overrides/mangabz/mangabz/src/Mangabz.kt
rename to src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Mangabz.kt
index d28bef540..25fa4148f 100644
--- a/multisrc/overrides/mangabz/mangabz/src/Mangabz.kt
+++ b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Mangabz.kt
@@ -4,7 +4,6 @@ import android.app.Application
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.lib.unpacker.SubstringExtractor
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
-import eu.kanade.tachiyomi.multisrc.mangabz.MangabzTheme
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@@ -25,7 +24,7 @@ import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-class Mangabz : MangabzTheme("Mangabz", ""), ConfigurableSource {
+class Mangabz : MangabzTheme("Mangabz"), ConfigurableSource {
override val baseUrl: String
override val client: OkHttpClient
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangabz/MangabzTheme.kt b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/MangabzTheme.kt
similarity index 97%
rename from multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangabz/MangabzTheme.kt
rename to src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/MangabzTheme.kt
index e6dc71392..21d1f30cd 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/mangabz/MangabzTheme.kt
+++ b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/MangabzTheme.kt
@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.multisrc.mangabz
+package eu.kanade.tachiyomi.extension.zh.mangabz
import android.util.Log
import eu.kanade.tachiyomi.network.GET
@@ -18,10 +18,10 @@ import org.jsoup.select.Evaluator
abstract class MangabzTheme(
override val name: String,
- override val baseUrl: String,
- override val lang: String = "zh",
) : HttpSource() {
+ override val lang = "zh"
+
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().add("Referer", baseUrl)
diff --git a/multisrc/overrides/mangabz/mangabz/src/MangabzUrlActivity.kt b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/MangabzUrlActivity.kt
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/src/MangabzUrlActivity.kt
rename to src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/MangabzUrlActivity.kt
diff --git a/multisrc/overrides/mangabz/mangabz/src/Preferences.kt b/src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Preferences.kt
similarity index 100%
rename from multisrc/overrides/mangabz/mangabz/src/Preferences.kt
rename to src/zh/mangabz/src/eu/kanade/tachiyomi/extension/zh/mangabz/Preferences.kt
diff --git a/src/zh/vomic/AndroidManifest.xml b/src/zh/vomic/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/zh/vomic/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/zh/vomic/build.gradle b/src/zh/vomic/build.gradle
new file mode 100644
index 000000000..e52e592d3
--- /dev/null
+++ b/src/zh/vomic/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'vomic'
+ pkgNameSuffix = 'zh.vomic'
+ extClass = '.Vomic'
+ extVersionCode = 2
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/multisrc/overrides/mangabz/vomic/res/mipmap-hdpi/ic_launcher.png b/src/zh/vomic/res/mipmap-hdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/vomic/res/mipmap-hdpi/ic_launcher.png
rename to src/zh/vomic/res/mipmap-hdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/vomic/res/mipmap-mdpi/ic_launcher.png b/src/zh/vomic/res/mipmap-mdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/vomic/res/mipmap-mdpi/ic_launcher.png
rename to src/zh/vomic/res/mipmap-mdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/vomic/res/mipmap-xhdpi/ic_launcher.png b/src/zh/vomic/res/mipmap-xhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/vomic/res/mipmap-xhdpi/ic_launcher.png
rename to src/zh/vomic/res/mipmap-xhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/vomic/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/vomic/res/mipmap-xxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/vomic/res/mipmap-xxhdpi/ic_launcher.png
rename to src/zh/vomic/res/mipmap-xxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/vomic/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/vomic/res/mipmap-xxxhdpi/ic_launcher.png
similarity index 100%
rename from multisrc/overrides/mangabz/vomic/res/mipmap-xxxhdpi/ic_launcher.png
rename to src/zh/vomic/res/mipmap-xxxhdpi/ic_launcher.png
diff --git a/multisrc/overrides/mangabz/vomic/res/web_hi_res_512.png b/src/zh/vomic/res/web_hi_res_512.png
similarity index 100%
rename from multisrc/overrides/mangabz/vomic/res/web_hi_res_512.png
rename to src/zh/vomic/res/web_hi_res_512.png
diff --git a/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Dto.kt b/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Dto.kt
new file mode 100644
index 000000000..f522a2865
--- /dev/null
+++ b/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Dto.kt
@@ -0,0 +1,89 @@
+package eu.kanade.tachiyomi.extension.zh.vomic
+
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonPrimitive
+import java.text.SimpleDateFormat
+
+val SManga.id get() = url.substring(1, 1 + 32)
+
+@Serializable
+class MangaDto(
+ private val mid: String,
+ private val title: String,
+ private val site: SiteDto? = null,
+ private val cover_img_url: String?,
+ private val authors_name: List? = null,
+ private val status: String? = null,
+ private val categories: JsonElement? = null,
+ private val description: String? = null,
+) {
+ fun toSMangaOrNull() = if (title.isEmpty()) null else toSManga()
+
+ private fun toSManga() = SManga.create().apply {
+ url = "/${mid}_c/"
+ title = this@MangaDto.title
+ thumbnail_url = cover_img_url
+ }
+
+ fun toSMangaDetails() = toSManga().apply {
+ author = authors_name!!.joinToString()
+ description = "站点:" + site + "\n\n" + this@MangaDto.description
+ genre = categories!!.jsonArray.joinToString { it.jsonPrimitive.content }
+ status = when (this@MangaDto.status!!) {
+ "连载中" -> SManga.ONGOING
+ "已完结" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ initialized = true
+ }
+}
+
+@Serializable
+class SiteDto(
+ private val site_en: String,
+ private val site_cn: String? = null,
+) {
+ override fun toString() = "$site_cn ($site_en)"
+}
+
+val SChapter.id: Pair
+ get() {
+ val url = url
+ val length = url.length
+ val mangaId = url.substring(length - 32, length)
+ val chapterId = url.substring(3, 3 + 32)
+ return Pair(mangaId, chapterId)
+ }
+
+@Serializable
+class ChapterDto(
+ private val title: String,
+ private val cid: String,
+ private val update_time: String,
+) {
+ fun toSChapter(mangaId: String, dateFormat: SimpleDateFormat) = SChapter.create().apply {
+ url = "/m_$cid/chapterimage.ashx?mid=$mangaId"
+ name = title
+ date_upload = dateFormat.parse(update_time)!!.time
+ }
+}
+
+@Serializable
+class MangaListDto(
+ private val page: Int,
+ private val result_count: Int,
+ private val result: List,
+) {
+ val entries get() = if (result_count != 0) result else emptyList()
+ val hasNextPage get() = page < 100 && page * 12 < result_count
+}
+
+@Serializable
+class RankingDto(val result: List)
+
+@Serializable
+class ResponseDto(val data: T)
diff --git a/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Filters.kt b/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Filters.kt
new file mode 100644
index 000000000..ddc251027
--- /dev/null
+++ b/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Filters.kt
@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.extension.zh.vomic
+
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+
+class SearchQuery(val title: String, val category: String)
+
+fun parseSearchQuery(query: String, filters: FilterList): SearchQuery {
+ for (filter in filters) {
+ if (filter is SearchCategoryToggle) {
+ if (filter.state) return SearchQuery("", query)
+ } else if (filter is CategoryFilter) {
+ return SearchQuery(query, filter.state.trim())
+ }
+ }
+ return SearchQuery(query, "")
+}
+
+fun getFilterListInternal() = FilterList(SearchCategoryToggle(), CategoryFilter())
+
+private class SearchCategoryToggle : Filter.CheckBox("将搜索词视为分类,勾选后下面的文本框无效")
+
+private class CategoryFilter : Filter.Text("分类")
diff --git a/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Vomic.kt b/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Vomic.kt
new file mode 100644
index 000000000..240760b36
--- /dev/null
+++ b/src/zh/vomic/src/eu/kanade/tachiyomi/extension/zh/vomic/Vomic.kt
@@ -0,0 +1,175 @@
+package eu.kanade.tachiyomi.extension.zh.vomic
+
+import android.app.Application
+import android.util.Base64
+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.HttpSource
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+class Vomic : HttpSource(), ConfigurableSource {
+
+ override val name = "vomic"
+
+ override val lang = "zh"
+
+ override val supportsLatest = false
+
+ override val baseUrl: String
+
+ private val apiUrl: String
+
+ init {
+ val mirrors = MIRRORS
+ val mirrorIndex = Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ .getString(MIRROR_PREF, "0")!!.toInt().coerceAtMost(mirrors.size - 1)
+ baseUrl = "http://" + mirrors[mirrorIndex]
+ apiUrl = "http://" + mirrors[mirrorIndex].replace("www.", "api.")
+ }
+
+ override fun headersBuilder() = Headers.Builder().add("User-Agent", System.getProperty("http.agent")!!)
+
+ override fun popularMangaRequest(page: Int) = GET("$apiUrl/api/v1/rank/rank-data?rank_id=1&page=$page", headers)
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val mangaList: RankingDto = response.parseAs()
+ val entries = mangaList.result.mapNotNull { it.toSMangaOrNull() }
+ val hasNextPage = response.request.url.queryParameter("page") != "4"
+ return MangasPage(entries, hasNextPage)
+ }
+
+ override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
+
+ override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
+
+ override fun getFilterList() = getFilterListInternal()
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val searchQuery = parseSearchQuery(query.trim(), filters)
+ if (searchQuery.title.isEmpty() && searchQuery.category.isEmpty()) throw Exception("请输入搜索词或分类")
+
+ val url = "$apiUrl/api/v1/search/search-comic-data".toHttpUrl().newBuilder()
+ .addQueryParameter("title", searchQuery.title)
+ .addQueryParameter("category", searchQuery.category)
+ .addEncodedQueryParameter("page", page.toString())
+ .build()
+ return GET(url, headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val mangaList: MangaListDto = response.parseAs()
+ val entries = mangaList.entries.mapNotNull { it.toSMangaOrNull() }
+ return MangasPage(entries, mangaList.hasNextPage)
+ }
+
+ override fun getMangaUrl(manga: SManga) = "$baseUrl/#/detail?id=${manga.id}"
+
+ override fun mangaDetailsRequest(manga: SManga) =
+ GET("$apiUrl/api/v1/detail/get-comic-detail-data?mid=${manga.id}", headers)
+
+ override fun mangaDetailsParse(response: Response) =
+ response.parseAs().toSMangaDetails()
+
+ override fun chapterListRequest(manga: SManga) =
+ GET("$apiUrl/api/v1/detail/get-comic-detail-chapter-data?mid=${manga.id}", headers)
+
+ override fun chapterListParse(response: Response): List {
+ val chapters: List = response.parseAs()
+ val mangaId = response.request.url.queryParameter("mid")!!
+ val dateFormat = dateFormat
+ return chapters.map { it.toSChapter(mangaId, dateFormat) }
+ }
+
+ override fun getChapterUrl(chapter: SChapter): String {
+ val (mangaId, chapterId) = chapter.id
+ return "$baseUrl/#/page/$mangaId/$chapterId"
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val (mangaId, chapterId) = chapter.id
+ val key = run {
+ val alphanumeric = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ val chars = CharArray(24) { alphanumeric.random() }
+ String(chars)
+ }
+ val time = System.currentTimeMillis().toString()
+ val encrypted = run {
+ val keySpec = SecretKeySpec(key.toByteArray(), "DESede")
+ val iv = "k8tUyS\$m"
+ val ivSpec = IvParameterSpec(iv.toByteArray())
+ val payload = key + iv + "cid=" + chapterId + "&mid=" + mangaId + time
+ val bytes = Cipher.getInstance("DESede/CBC/PKCS5Padding").run {
+ init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
+ doFinal(payload.toByteArray())
+ }
+ Base64.encodeToString(bytes, Base64.DEFAULT)
+ }
+ val url = "$apiUrl/api/v2/page/get-comic-page-img-data".toHttpUrl().newBuilder()
+ .addEncodedQueryParameter("k", key)
+ .addEncodedQueryParameter("t", time)
+ .addQueryParameter("e", encrypted)
+ .build()
+ return GET(url, headers)
+ }
+
+ override fun pageListParse(response: Response): List {
+ val pageList: List = response.parseAs()
+ if (pageList.size == 1 && pageList[0] == "https://cdn.vomicer.com/qiniu/vomic/otherImg/info2.webp") {
+ throw Exception("无法阅读此章节")
+ }
+ return pageList.mapIndexed { index, imageUrl -> Page(index, imageUrl = imageUrl) }
+ }
+
+ override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
+
+ override fun imageRequest(page: Page): Request {
+ val url = page.imageUrl!!
+ val host = url.toHttpUrl().host
+ val headers = headersBuilder().set("Referer", "https://$host/").build()
+ return GET(url, headers)
+ }
+
+ override fun setupPreferenceScreen(screen: PreferenceScreen) {
+ ListPreference(screen.context).apply {
+ val mirrors = MIRRORS
+ key = MIRROR_PREF
+ title = "镜像网址"
+ summary = "%s\n重启生效"
+ entries = mirrors
+ entryValues = Array(mirrors.size) { it.toString() }
+ setDefaultValue("0")
+ }.let(screen::addPreference)
+ }
+
+ private val json: Json by injectLazy()
+
+ private inline fun Response.parseAs(): T =
+ json.decodeFromString>(body.string()).data
+
+ companion object {
+ private const val MIRROR_PREF = "MIRROR"
+ private val MIRRORS get() = arrayOf("www.vomicmh.com", "www.iewoai.com")
+
+ private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.ENGLISH) }
+ }
+}