-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a2b512c
commit 55ec007
Showing
9 changed files
with
406 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
ext { | ||
extName = 'Zaimanhua' | ||
extClass = '.Zaimanhua' | ||
extVersionCode = 1 | ||
} | ||
|
||
apply from: "$rootDir/common.gradle" |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions
30
src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Common.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package eu.kanade.tachiyomi.extension.zh.zaimanhua | ||
|
||
import eu.kanade.tachiyomi.source.model.SManga | ||
import kotlinx.serialization.decodeFromString | ||
import kotlinx.serialization.json.Json | ||
import okhttp3.Response | ||
import uy.kohesive.injekt.injectLazy | ||
|
||
val json: Json by injectLazy() | ||
|
||
inline fun <reified T> Response.parseAs(): T { | ||
return json.decodeFromString(body.string()) | ||
} | ||
|
||
fun parseStatus(status: String): Int = when (status) { | ||
"连载中" -> SManga.ONGOING | ||
"已完结" -> SManga.COMPLETED | ||
else -> SManga.UNKNOWN | ||
} | ||
|
||
private val chapterNameRegex = Regex("""(?:连载版?)?(\d[.\d]*)([话卷])?""") | ||
|
||
fun String.formatChapterName(): String { | ||
val match = chapterNameRegex.matchEntire(this) ?: return this | ||
val (number, optionalType) = match.destructured | ||
val type = optionalType.ifEmpty { "话" } | ||
return "第$number$type" | ||
} | ||
|
||
fun String.formatList() = replace("/", ", ") |
225 changes: 225 additions & 0 deletions
225
src/zh/zaimanhua/src/eu/kanade/tachiyomi/extension/zh/zaimanhua/Zaimanhua.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
package eu.kanade.tachiyomi.extension.zh.zaimanhua | ||
|
||
import android.app.Application | ||
import android.content.SharedPreferences | ||
import androidx.preference.EditTextPreference | ||
import androidx.preference.ListPreference | ||
import androidx.preference.PreferenceScreen | ||
import eu.kanade.tachiyomi.network.GET | ||
import eu.kanade.tachiyomi.network.POST | ||
import eu.kanade.tachiyomi.network.interceptor.rateLimit | ||
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 okhttp3.FormBody | ||
import okhttp3.Headers | ||
import okhttp3.HttpUrl | ||
import okhttp3.HttpUrl.Companion.toHttpUrl | ||
import okhttp3.Interceptor | ||
import okhttp3.OkHttpClient | ||
import okhttp3.Request | ||
import okhttp3.RequestBody | ||
import okhttp3.Response | ||
import uy.kohesive.injekt.Injekt | ||
import uy.kohesive.injekt.api.get | ||
import java.security.MessageDigest | ||
|
||
class Zaimanhua : HttpSource(), ConfigurableSource { | ||
override val lang = "zh" | ||
override val supportsLatest = true | ||
|
||
override val name = "再漫画" | ||
override val baseUrl = "https://manhua.zaimanhua.com" | ||
private val apiUrl = "https://v4api.zaimanhua.com/app/v1" | ||
private val accountApiUrl = "https://account-api.zaimanhua.com/v1" | ||
|
||
private val preferences: SharedPreferences = | ||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) | ||
|
||
override val client: OkHttpClient = | ||
network.client.newBuilder().rateLimit(5).addInterceptor(::authIntercept).build() | ||
|
||
private fun authIntercept(chain: Interceptor.Chain): Response { | ||
val request = chain.request() | ||
if (request.url.host != "v4api.zaimanhua.com" || !request.headers["authorization"].isNullOrBlank()) { | ||
return chain.proceed(request) | ||
} | ||
|
||
var token: String = preferences.getString("TOKEN", "")!! | ||
if (token.isBlank() || !isValid(token)) { | ||
val username = preferences.getString("USERNAME", "")!! | ||
val password = preferences.getString("PASSWORD", "")!! | ||
token = getToken(username, password) | ||
if (token.isBlank()) { | ||
preferences.edit().putString("TOKEN", "").apply() | ||
preferences.edit().putString("USERNAME", "").apply() | ||
preferences.edit().putString("PASSWORD", "").apply() | ||
return chain.proceed(request) | ||
} else { | ||
preferences.edit().putString("TOKEN", token).apply() | ||
apiHeaders = apiHeaders.newBuilder().setToken(token).build() | ||
} | ||
} | ||
val authRequest = request.newBuilder().apply { | ||
header("authorization", "Bearer $token") | ||
}.build() | ||
return chain.proceed(authRequest) | ||
} | ||
|
||
private fun Headers.Builder.setToken(token: String): Headers.Builder = apply { | ||
if (token.isNotBlank()) set("authorization", "Bearer $token") | ||
} | ||
|
||
private var apiHeaders = headersBuilder().setToken(preferences.getString("TOKEN", "")!!).build() | ||
|
||
private fun isValid(token: String): Boolean { | ||
val response = client.newCall( | ||
GET( | ||
"$accountApiUrl/userInfo/get", | ||
headersBuilder().setToken(token).build(), | ||
), | ||
).execute().parseAs<ResponseDto<UserDto>>() | ||
return response.errno == 0 | ||
} | ||
|
||
private fun getToken(username: String, password: String): String { | ||
if (username.isBlank() || password.isBlank()) return "" | ||
val passwordEncoded = | ||
MessageDigest.getInstance("MD5").digest(password.toByteArray(Charsets.UTF_8)) | ||
.joinToString("") { "%02x".format(it) } | ||
val formBody: RequestBody = FormBody.Builder().addEncoded("username", username) | ||
.addEncoded("passwd", passwordEncoded).build() | ||
val response = client.newCall( | ||
POST( | ||
"$accountApiUrl/login/passwd", | ||
headers, | ||
formBody, | ||
), | ||
).execute().parseAs<ResponseDto<UserDto>>() | ||
return response.data.user?.token ?: "" | ||
} | ||
|
||
// Detail | ||
// path: "/comic/detail/mangaId" | ||
override fun mangaDetailsRequest(manga: SManga): Request = | ||
GET("$apiUrl/comic/detail/${manga.url}", apiHeaders) | ||
|
||
override fun mangaDetailsParse(response: Response): SManga { | ||
val result = response.parseAs<ResponseDto<DataWrapperDto<MangaDto>>>() | ||
if (result.errmsg.isNotBlank()) { | ||
throw Exception(result.errmsg) | ||
} else { | ||
return result.data.data!!.toSManga() | ||
} | ||
} | ||
|
||
// Chapter | ||
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) | ||
|
||
override fun chapterListParse(response: Response): List<SChapter> { | ||
val result = response.parseAs<ResponseDto<DataWrapperDto<MangaDto>>>() | ||
if (result.errmsg.isNotBlank()) { | ||
throw Exception(result.errmsg) | ||
} else { | ||
return result.data.data!!.parseChapterList() | ||
} | ||
} | ||
|
||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() | ||
|
||
// PageList | ||
// path: "/comic/chapter/mangaId/chapterId" | ||
override fun pageListRequest(chapter: SChapter) = | ||
GET("$apiUrl/comic/chapter/${chapter.url}", apiHeaders) | ||
|
||
override fun pageListParse(response: Response): List<Page> { | ||
val result = response.parseAs<ResponseDto<DataWrapperDto<ChapterImagesDto>>>() | ||
if (result.errmsg.isNotBlank()) { | ||
throw Exception(result.errmsg) | ||
} else { | ||
return result.data.data!!.images.mapIndexed { index, it -> | ||
Page(index, imageUrl = it) | ||
} | ||
} | ||
} | ||
|
||
// Popular | ||
private fun rankApiUrl(): HttpUrl.Builder = | ||
"$apiUrl/comic/rank/list".toHttpUrl().newBuilder().addQueryParameter("by_time", "3") | ||
.addQueryParameter("tag_id", "0").addQueryParameter("rank_type", "0") | ||
|
||
override fun popularMangaRequest(page: Int): Request = GET( | ||
rankApiUrl().apply { | ||
addQueryParameter("page", page.toString()) | ||
}.build(), | ||
apiHeaders, | ||
) | ||
|
||
override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response) | ||
|
||
// Search | ||
private fun searchApiUrl(): HttpUrl.Builder = | ||
"$apiUrl/search/index".toHttpUrl().newBuilder().addQueryParameter("source", "0") | ||
.addQueryParameter("size", "20") | ||
|
||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET( | ||
searchApiUrl().apply { | ||
addQueryParameter("keyword", query) | ||
addQueryParameter("page", page.toString()) | ||
}.build(), | ||
apiHeaders, | ||
) | ||
|
||
override fun searchMangaParse(response: Response): MangasPage = | ||
response.parseAs<ResponseDto<PageDto>>().data.toMangasPage() | ||
|
||
// Latest | ||
// "$apiUrl/comic/update/list/1/$page" is same content | ||
override fun latestUpdatesRequest(page: Int): Request = | ||
GET("$apiUrl/comic/update/list/0/$page", apiHeaders) | ||
|
||
override fun latestUpdatesParse(response: Response): MangasPage { | ||
val mangas = response.parseAs<ResponseDto<List<PageItemDto>>>().data | ||
return MangasPage(mangas.map { it.toSManga() }, true) | ||
} | ||
|
||
override fun setupPreferenceScreen(screen: PreferenceScreen) { | ||
ListPreference(screen.context).apply { | ||
EditTextPreference(screen.context).apply { | ||
key = "USERNAME" | ||
title = "用户名" | ||
summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置" | ||
setOnPreferenceChangeListener { _, _ -> | ||
// clean token after username/password changed | ||
preferences.edit().putString("TOKEN", "").apply() | ||
true | ||
} | ||
}.let(screen::addPreference) | ||
|
||
EditTextPreference(screen.context).apply { | ||
key = "PASSWORD" | ||
title = "密码" | ||
summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置" | ||
setOnPreferenceChangeListener { _, _ -> | ||
// clean token after username/password changed | ||
preferences.edit().putString("TOKEN", "").apply() | ||
true | ||
} | ||
}.let(screen::addPreference) | ||
|
||
EditTextPreference(screen.context).apply { | ||
key = "TOKEN" | ||
title = "令牌(Token)" | ||
summary = "当前登录状态:${ | ||
if (preferences.getString("TOKEN", "").isNullOrEmpty()) "未登录" else "已登录" | ||
}\n填写用户名和密码后,不会立刻尝试登录,会在下次请求时自动尝试" | ||
|
||
setEnabled(false) | ||
}.let(screen::addPreference) | ||
} | ||
} | ||
} |
Oops, something went wrong.