ZinChanManga: new extension (#9414)
This commit is contained in:
parent
6fd52b09de
commit
e323909165
2
src/en/zinchanmanga/AndroidManifest.xml
Normal file
2
src/en/zinchanmanga/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
17
src/en/zinchanmanga/build.gradle
Normal file
17
src/en/zinchanmanga/build.gradle
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'ZinChanManga'
|
||||||
|
pkgNameSuffix = 'en.zinchanmanga'
|
||||||
|
extClass = '.ZinChanManga'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':lib-ratelimit')
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/zinchanmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/zinchanmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
src/en/zinchanmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/zinchanmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
src/en/zinchanmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/zinchanmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
src/en/zinchanmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/zinchanmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
src/en/zinchanmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/zinchanmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
BIN
src/en/zinchanmanga/res/web_hi_res_512.png
Normal file
BIN
src/en/zinchanmanga/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1,102 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.zinchanmanga
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlinx.serialization.SerialName as N
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Data<T>(
|
||||||
|
private val data: List<T>,
|
||||||
|
private val err_message: String
|
||||||
|
) : List<T> by data {
|
||||||
|
init { require(err_message == "Success") { err_message } }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeriesList(
|
||||||
|
private val data: Pagination,
|
||||||
|
private val err_message: String
|
||||||
|
) : List<Series> by data {
|
||||||
|
init { require(err_message == "Success") { err_message } }
|
||||||
|
|
||||||
|
val pages by lazy { data.pages }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Pagination(
|
||||||
|
@N("total_page") val pages: Int,
|
||||||
|
private val data: List<Series>
|
||||||
|
) : List<Series> by data
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Series(
|
||||||
|
private val id_story: Int,
|
||||||
|
@N("name_story") val title: String,
|
||||||
|
private val slug_story: String,
|
||||||
|
@N("status_story") val status: String? = null,
|
||||||
|
private val name_genre: String,
|
||||||
|
private val name_author: String,
|
||||||
|
@N("thumbnail_story") val cover: String,
|
||||||
|
private val content_story: String
|
||||||
|
) {
|
||||||
|
val url by lazy {
|
||||||
|
"$slug_story?id=$id_story"
|
||||||
|
}
|
||||||
|
|
||||||
|
val description by lazy {
|
||||||
|
Jsoup.parse(content_story).text().takeIf { "Updating" !in it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val genres by lazy {
|
||||||
|
name_genre.trim('|').replace("|", ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
val authors by lazy {
|
||||||
|
name_author.replace(",|", "|").trim('|').takeIf {
|
||||||
|
it != "Updating" && it != "Đang Cập Nhật"
|
||||||
|
}?.replace("Author: ", "")?.replace(" ", ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Chapter(
|
||||||
|
private val id_chapter: Int,
|
||||||
|
private val name_chapter: String,
|
||||||
|
private val latest_update_chapter: String,
|
||||||
|
private val name_extend: String
|
||||||
|
) {
|
||||||
|
val params by lazy {
|
||||||
|
"?id_chapter=$id_chapter&type_story=manga"
|
||||||
|
}
|
||||||
|
|
||||||
|
val title by lazy {
|
||||||
|
buildString {
|
||||||
|
append(name_chapter)
|
||||||
|
if (name_extend != "") {
|
||||||
|
append(" - ")
|
||||||
|
append(name_extend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val number by lazy {
|
||||||
|
name_chapter.substringAfter(' ').toFloatOrNull() ?: -1f
|
||||||
|
}
|
||||||
|
|
||||||
|
val timestamp by lazy {
|
||||||
|
dateFormat.parse(latest_update_chapter)?.time ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val dateFormat by lazy {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PageList(
|
||||||
|
private val data_chapter: List<String>
|
||||||
|
) : List<String> by data_chapter
|
@ -0,0 +1,78 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.zinchanmanga
|
||||||
|
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.TrustManagerFactory
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
object ZinChanCert {
|
||||||
|
private val keystore by lazy {
|
||||||
|
KeyStore.getInstance(KeyStore.getDefaultType()).apply {
|
||||||
|
load(null, null)
|
||||||
|
setCertificateEntry("zin-chan-cert", certificate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val certificate by lazy {
|
||||||
|
CertificateFactory.getInstance("X.509").run {
|
||||||
|
PEM.byteInputStream().use(::generateCertificate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val managers by lazy {
|
||||||
|
TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm()
|
||||||
|
).apply { init(keystore) }.trustManagers
|
||||||
|
}
|
||||||
|
|
||||||
|
val factory by lazy {
|
||||||
|
SSLContext.getInstance("TLS").apply {
|
||||||
|
init(null, managers, SecureRandom())
|
||||||
|
}.socketFactory!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val manager: X509TrustManager
|
||||||
|
get() = managers[0] as X509TrustManager
|
||||||
|
|
||||||
|
private const val PEM = """-----BEGIN CERTIFICATE-----
|
||||||
|
MIIG1TCCBL2gAwIBAgIQbFWr29AHksedBwzYEZ7WvzANBgkqhkiG9w0BAQwFADCB
|
||||||
|
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
|
||||||
|
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
|
||||||
|
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAw
|
||||||
|
MTMwMDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UE
|
||||||
|
ChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBSU0EgRG9tYWluIFNlY3VyZSBT
|
||||||
|
aXRlIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhmlzfqO1Mdgj
|
||||||
|
4W3dpBPTVBX1AuvcAyG1fl0dUnw/MeueCWzRWTheZ35LVo91kLI3DDVaZKW+TBAs
|
||||||
|
JBjEbYmMwcWSTWYCg5334SF0+ctDAsFxsX+rTDh9kSrG/4mp6OShubLaEIUJiZo4
|
||||||
|
t873TuSd0Wj5DWt3DtpAG8T35l/v+xrN8ub8PSSoX5Vkgw+jWf4KQtNvUFLDq8mF
|
||||||
|
WhUnPL6jHAADXpvs4lTNYwOtx9yQtbpxwSt7QJY1+ICrmRJB6BuKRt/jfDJF9Jsc
|
||||||
|
RQVlHIxQdKAJl7oaVnXgDkqtk2qddd3kCDXd74gv813G91z7CjsGyJ93oJIlNS3U
|
||||||
|
gFbD6V54JMgZ3rSmotYbz98oZxX7MKbtCm1aJ/q+hTv2YK1yMxrnfcieKmOYBbFD
|
||||||
|
hnW5O6RMA703dBK92j6XRN2EttLkQuujZgy+jXRKtaWMIlkNkWJmOiHmErQngHvt
|
||||||
|
iNkIcjJumq1ddFX4iaTI40a6zgvIBtxFeDs2RfcaH73er7ctNUUqgQT5rFgJhMmF
|
||||||
|
x76rQgB5OZUkodb5k2ex7P+Gu4J86bS15094UuYcV09hVeknmTh5Ex9CBKipLS2W
|
||||||
|
2wKBakf+aVYnNCU6S0nASqt2xrZpGC1v7v6DhuepyyJtn3qSV2PoBiU5Sql+aARp
|
||||||
|
wUibQMGm44gjyNDqDlVp+ShLQlUH9x8CAwEAAaOCAXUwggFxMB8GA1UdIwQYMBaA
|
||||||
|
FFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBTI2XhootkZaNU9ct5fCj7c
|
||||||
|
tYaGpjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUE
|
||||||
|
FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIgYDVR0gBBswGTANBgsrBgEEAbIxAQIC
|
||||||
|
TjAIBgZngQwBAgEwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1
|
||||||
|
c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYG
|
||||||
|
CCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3Qu
|
||||||
|
Y29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRw
|
||||||
|
Oi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAVDwoIzQDV
|
||||||
|
ercT0eYqZjBNJ8VNWwVFlQOtZERqn5iWnEVaLZZdzxlbvz2Fx0ExUNuUEgYkIVM4
|
||||||
|
YocKkCQ7hO5noicoq/DrEYH5IuNcuW1I8JJZ9DLuB1fYvIHlZ2JG46iNbVKA3ygA
|
||||||
|
Ez86RvDQlt2C494qqPVItRjrz9YlJEGT0DrttyApq0YLFDzf+Z1pkMhh7c+7fXeJ
|
||||||
|
qmIhfJpduKc8HEQkYQQShen426S3H0JrIAbKcBCiyYFuOhfyvuwVCFDfFvrjADjd
|
||||||
|
4jX1uQXd161IyFRbm89s2Oj5oU1wDYz5sx+hoCuh6lSs+/uPuWomIq3y1GDFNafW
|
||||||
|
+LsHBU16lQo5Q2yh25laQsKRgyPmMpHJ98edm6y2sHUabASmRHxvGiuwwE25aDU0
|
||||||
|
2SAeepyImJ2CzB80YG7WxlynHqNhpE7xfC7PzQlLgmfEHdU+tHFeQazRQnrFkW2W
|
||||||
|
kqRGIq7cKRnyypvjPMkjeiV9lRdAM9fSJvsB3svUuu1coIG1xxI1yegoGM4r5QP4
|
||||||
|
RGIVvYaiI76C0djoSbQ/dkIUUXQuB8AL5jyH34g3BZaaXyvpmnV4ilppMXVAnAYG
|
||||||
|
ON51WhJ6W0xNdNJwzYASZYH+tmCWI+N60Gv2NNMGHwMZ7e9bXgzUCZH5FaBFDGR5
|
||||||
|
S9VWqHB73Q+OyIVvIbKYcSc2w/aSuFKGSA==
|
||||||
|
-----END CERTIFICATE-----"""
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.zinchanmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
class ZinChanGenre(values: Array<String>) : Filter.Select<String>("Genres", values) {
|
||||||
|
override fun toString() = (state + 27).toString()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
object Note : Filter.Header("NOTE: can't combine with text search!")
|
||||||
|
|
||||||
|
val genres: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"<select>",
|
||||||
|
"BL",
|
||||||
|
"Manhwa",
|
||||||
|
"Smut",
|
||||||
|
"Comedy",
|
||||||
|
"Romance",
|
||||||
|
"Cooking",
|
||||||
|
"Korean",
|
||||||
|
"Japanese",
|
||||||
|
"Manga",
|
||||||
|
"Manhua",
|
||||||
|
"Webtoon",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,148 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.zinchanmanga
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
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.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
@ExperimentalSerializationApi
|
||||||
|
class ZinChanManga : HttpSource() {
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val name = "ZinChanManga"
|
||||||
|
|
||||||
|
override val baseUrl = "https://zinchanmanga.net"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val json by injectLazy<Json>()
|
||||||
|
|
||||||
|
private val apiClient by lazy {
|
||||||
|
network.client.newBuilder()
|
||||||
|
.sslSocketFactory(ZinChanCert.factory, ZinChanCert.manager)
|
||||||
|
.addInterceptor(RateLimitInterceptor(3)).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val apiHeaders by lazy {
|
||||||
|
headers.newBuilder().add("Origin", baseUrl).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
GET("$API_URL/latest-manga-updates?page=$page&total=10", apiHeaders)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) =
|
||||||
|
GET("$API_URL/all?page=$page&total=10", apiHeaders)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
filters.find { it is ZinChanGenre }?.takeIf { it.state != 0 }?.let {
|
||||||
|
GET("$API_URL/category?id_cate=$it&page=$page&total=10", apiHeaders)
|
||||||
|
} ?: GET("$API_URL/search?keyword=$query&page=$page&total=10", apiHeaders)
|
||||||
|
|
||||||
|
// Request the frontend URL for the webview
|
||||||
|
override fun mangaDetailsRequest(manga: SManga) =
|
||||||
|
GET("$baseUrl/manga/${manga.url}", headers)
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga) =
|
||||||
|
GET("$API_URL/list-chapters?id_story=${manga.id}&id_user=-1", apiHeaders)
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter) =
|
||||||
|
GET("$API_URL/reading${chapter.url}", apiHeaders)
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int) =
|
||||||
|
fetchManga(latestUpdatesRequest(page), page)
|
||||||
|
|
||||||
|
override fun fetchPopularManga(page: Int) =
|
||||||
|
fetchManga(popularMangaRequest(page), page)
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList) =
|
||||||
|
fetchManga(searchMangaRequest(page, query, filters), page)
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga) =
|
||||||
|
rx.Observable.just(manga.apply { initialized = true })!!
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga) =
|
||||||
|
apiClient.newCall(chapterListRequest(manga)).asObservableSuccess().map { res ->
|
||||||
|
res.parse<Data<Chapter>>().map {
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = it.title
|
||||||
|
chapter_number = it.number
|
||||||
|
date_upload = it.timestamp
|
||||||
|
url = "${it.params}&id_story=${manga.id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}!!
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter) =
|
||||||
|
apiClient.newCall(pageListRequest(chapter)).asObservableSuccess().map { res ->
|
||||||
|
res.parse<Data<PageList>>().single()
|
||||||
|
.mapIndexed { idx, img -> Page(idx, "", img) }
|
||||||
|
}!!
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
ZinChanGenre.Companion.Note,
|
||||||
|
ZinChanGenre(ZinChanGenre.genres)
|
||||||
|
)
|
||||||
|
|
||||||
|
private inline val SManga.id: String
|
||||||
|
get() = url.substringAfter("?id=")
|
||||||
|
|
||||||
|
private fun fetchManga(request: okhttp3.Request, page: Int) =
|
||||||
|
apiClient.newCall(request).asObservableSuccess().map { res ->
|
||||||
|
res.parse<SeriesList>().run {
|
||||||
|
val manga = map {
|
||||||
|
SManga.create().apply {
|
||||||
|
url = it.url
|
||||||
|
title = it.title
|
||||||
|
thumbnail_url = it.cover
|
||||||
|
description = it.description
|
||||||
|
genre = it.genres
|
||||||
|
author = it.authors
|
||||||
|
artist = author
|
||||||
|
status = when (it.status) {
|
||||||
|
"on-going" -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MangasPage(manga, pages != page)
|
||||||
|
}
|
||||||
|
}!!
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parse() =
|
||||||
|
json.decodeFromString<T>(body!!.string())
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val API_URL = "https://api.zinchanmanga.net:5555/api/web/manga"
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user