Split Mangabz multisrc and update Vomic (#16922)

This commit is contained in:
stevenyomi 2023-06-30 00:31:50 +08:00 committed by GitHub
parent fe7559b06f
commit 13f9ab74aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 321 additions and 201 deletions

View File

@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="MangabzGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="tachiyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.mangabz.MangabzGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangabz" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangabz" />
</method>
</configuration>
</component>

View File

@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="WeebreaderGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="tachiyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.weebreader.WeebreaderGenerator" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=weebreader" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=weebreader" />
</method>
</configuration>
</component>

View File

@ -1,3 +0,0 @@
dependencies {
implementation project(':lib-unpacker')
}

View File

@ -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<Application>().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<List<SChapter>> {
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<Page> {
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) }
}
}

View File

@ -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<String>) {
MangabzGenerator().createAll()
}
}
}

View File

@ -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')
}

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

12
src/zh/vomic/build.gradle Normal file
View File

@ -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"

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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<String>? = 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<String, String>
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<MangaDto>,
) {
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<MangaDto>)
@Serializable
class ResponseDto<T>(val data: T)

View File

@ -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("分类")

View File

@ -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<Application>().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<MangaDto>().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<SChapter> {
val chapters: List<ChapterDto> = 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<Page> {
val pageList: List<String> = 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 <reified T> Response.parseAs(): T =
json.decodeFromString<ResponseDto<T>>(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) }
}
}