Remove DMZJ (#10248)
This commit is contained in:
parent
70df8cbfa9
commit
8acd1707ae
@ -1,140 +0,0 @@
|
||||
# API v4
|
||||
|
||||
## Manga details
|
||||
|
||||
Mostly taken from [here](https://github.com/xiaoyaocz/dmzj_flutter/blob/aac6ba3/lib/protobuf/comic/detail_response.proto).
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
|
||||
package dmzj.comic;
|
||||
|
||||
|
||||
message ComicDetailResponse {
|
||||
int32 Errno = 1;
|
||||
string Errmsg = 2;
|
||||
ComicDetailInfoResponse Data= 3;
|
||||
}
|
||||
|
||||
message ComicDetailInfoResponse {
|
||||
int32 Id = 1;
|
||||
string Title = 2;
|
||||
int32 Direction=3;
|
||||
int32 Islong=4;
|
||||
int32 IsDmzj=5;
|
||||
string Cover=6;
|
||||
string Description=7;
|
||||
int64 LastUpdatetime=8;
|
||||
string LastUpdateChapterName=9;
|
||||
int32 Copyright=10;
|
||||
string FirstLetter=11;
|
||||
string ComicPy=12;
|
||||
int32 Hidden=13;
|
||||
int32 HotNum=14;
|
||||
int32 HitNum=15;
|
||||
int32 Uid=16;
|
||||
int32 IsLock=17;
|
||||
int32 LastUpdateChapterId=18;
|
||||
repeated ComicDetailTypeItemResponse Types=19;
|
||||
repeated ComicDetailTypeItemResponse Status=20;
|
||||
repeated ComicDetailTypeItemResponse Authors=21;
|
||||
int32 SubscribeNum=22;
|
||||
repeated ComicDetailChapterResponse Chapters=23;
|
||||
int32 IsNeedLogin=24;
|
||||
//object UrlLinks=25; { string name = 1; repeated object links = 2; }
|
||||
// link { int32 = 1; string name = 2; string uriOrApk = 3; string icon = 4; string packageName = 5; string apk = 6; int32 = 7; }
|
||||
int32 IsHideChapter=26;
|
||||
//repeated object DhUrlLinks=27; { string name = 1; }
|
||||
|
||||
}
|
||||
|
||||
message ComicDetailTypeItemResponse {
|
||||
int32 TagId = 1;
|
||||
string TagName = 2;
|
||||
}
|
||||
|
||||
message ComicDetailChapterResponse {
|
||||
string Title = 1;
|
||||
repeated ComicDetailChapterInfoResponse Data=2;
|
||||
}
|
||||
message ComicDetailChapterInfoResponse {
|
||||
int32 ChapterId = 1;
|
||||
string ChapterTitle = 2;
|
||||
int64 Updatetime=3;
|
||||
int32 Filesize=4;
|
||||
int32 ChapterOrder=5;
|
||||
}
|
||||
```
|
||||
|
||||
## Ranking
|
||||
|
||||
Taken from [here](https://github.com/xiaoyaocz/dmzj_flutter/blob/e7f1b1e/lib/protobuf/comic/rank_list_response.proto).
|
||||
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
|
||||
package dmzj.comic;
|
||||
|
||||
|
||||
message ComicRankListResponse {
|
||||
int32 Errno = 1;
|
||||
string Errmsg = 2;
|
||||
repeated ComicRankListItemResponse Data= 3;
|
||||
}
|
||||
|
||||
message ComicRankListItemResponse {
|
||||
int32 ComicId = 1;
|
||||
string Title = 2;
|
||||
string Authors=3;
|
||||
string Status=4;
|
||||
string Cover=5;
|
||||
string Types=6;
|
||||
int64 LastUpdatetime=7;
|
||||
string LastUpdateChapterName=8;
|
||||
string ComicPy=9;
|
||||
int32 Num=10;
|
||||
int32 TagId=11;
|
||||
string ChapterName=12;
|
||||
int32 ChapterId=13;
|
||||
}
|
||||
```
|
||||
|
||||
## Chapter images
|
||||
|
||||
```kotlin
|
||||
@Serializable
|
||||
class ResponseDto<T>(
|
||||
@ProtoNumber(1) val code: Int?,
|
||||
@ProtoNumber(2) val message: String?,
|
||||
@ProtoNumber(3) val data: T?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ChapterImagesDto(
|
||||
@ProtoNumber(1) val id: Int,
|
||||
@ProtoNumber(2) val mangaId: Int,
|
||||
@ProtoNumber(3) val name: String,
|
||||
@ProtoNumber(4) val order: Int,
|
||||
@ProtoNumber(5) val direction: Int,
|
||||
// initial letter is sometimes different from that in original URLs, see manga ID 56649
|
||||
@ProtoNumber(6) val lowResImages: List<String>,
|
||||
// page count of low-res images
|
||||
@ProtoNumber(7) val pageCount: Int?,
|
||||
@ProtoNumber(8) val images: List<String>,
|
||||
@ProtoNumber(9) val commentCount: Int,
|
||||
)
|
||||
```
|
||||
|
||||
# Unused legacy API
|
||||
|
||||
## Chapter images
|
||||
|
||||
```kotlin
|
||||
val webviewPageListApiUrl = "https://m.dmzj.com/chapinfo"
|
||||
GET("$webviewPageListApiUrl/${chapter.url}.html")
|
||||
```
|
||||
|
||||
```kotlin
|
||||
val oldPageListApiUrl = "http://api.m.dmzj.com" // this domain has an expired certificate
|
||||
GET("$oldPageListApiUrl/comic/chapter/${chapter.url}.html")
|
||||
```
|
@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".zh.dmzj.DmzjUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="m.dmzj.com"
|
||||
android:pathPattern="/info/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="www.dmzj.com"
|
||||
android:pathPattern="/info/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="manhua.dmzj.com"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="m.muwai.com"
|
||||
android:pathPattern="/info/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="www.muwai.com"
|
||||
android:pathPattern="/info/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="manhua.muwai.com"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@ -1,7 +0,0 @@
|
||||
ext {
|
||||
extName = 'DMZJ'
|
||||
extClass = '.Dmzj'
|
||||
extVersionCode = 46
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
Before Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
@ -1,64 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Response
|
||||
|
||||
object ApiSearch {
|
||||
|
||||
fun searchUrlV1(page: Int, query: String) =
|
||||
"https://manhua.idmzj.com/api/v1/comic2/search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("page", page.toString())
|
||||
.addQueryParameter("size", "30")
|
||||
.addQueryParameter("keyword", query)
|
||||
.toString()
|
||||
|
||||
fun parsePageV1(response: Response): MangasPage {
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
return MangasPage(emptyList(), false)
|
||||
}
|
||||
val result = response.parseAs<ResponseDto<SearchResultDto?>>()
|
||||
if (result.errmsg.isNotBlank()) {
|
||||
throw Exception(result.errmsg)
|
||||
} else {
|
||||
val url = response.request.url
|
||||
val page = url.queryParameter("page")?.toInt()
|
||||
val size = url.queryParameter("size")?.toInt()
|
||||
return if (result.data != null) {
|
||||
MangasPage(result.data.comicList.map { it.toSManga() }, page!! * size!! < result.data.totalNum)
|
||||
} else {
|
||||
MangasPage(emptyList(), false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaDtoV1(
|
||||
private val id: Int,
|
||||
private val name: String,
|
||||
private val authors: String,
|
||||
private val cover: String,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = getMangaUrl(id.toString())
|
||||
title = name
|
||||
author = authors
|
||||
thumbnail_url = cover
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class SearchResultDto(
|
||||
val comicList: List<MangaDtoV1>,
|
||||
val totalNum: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ResponseDto<T>(
|
||||
val errmsg: String = "",
|
||||
val data: T,
|
||||
)
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import okhttp3.Response
|
||||
import org.jsoup.parser.Parser
|
||||
|
||||
object ApiV3 {
|
||||
lateinit var preferences: SharedPreferences
|
||||
private val v3apiUrl: String
|
||||
get() = if (preferences.isOlderV3API == true) {
|
||||
"https://v3api.idmzj.com"
|
||||
} else {
|
||||
"https://nnv3api.dmzj.com"
|
||||
}
|
||||
|
||||
private const val apiUrl = "https://api.dmzj.com"
|
||||
|
||||
fun popularMangaUrl(page: Int) = "$v3apiUrl/classify/0/0/${page - 1}.json"
|
||||
|
||||
fun latestUpdatesUrl(page: Int) = "$v3apiUrl/classify/0/1/${page - 1}.json"
|
||||
|
||||
fun pageUrl(page: Int, filters: FilterList) = "$v3apiUrl/classify/${parseFilters(filters)}/${page - 1}.json"
|
||||
|
||||
fun parsePage(response: Response): MangasPage {
|
||||
val data: List<MangaDto> = response.parseAs()
|
||||
return MangasPage(data.map { it.toSManga() }, data.isNotEmpty())
|
||||
}
|
||||
|
||||
fun mangaInfoUrlV1(id: String) = "$apiUrl/dynamic/comicinfo/$id.json"
|
||||
|
||||
private fun parseMangaInfoV1(response: Response): ResponseDto = try {
|
||||
response.parseAs()
|
||||
} catch (_: Throwable) {
|
||||
throw Exception("获取漫画信息失败")
|
||||
}
|
||||
|
||||
fun parseMangaDetailsV1(response: Response): SManga {
|
||||
return parseMangaInfoV1(response).data.info.toSManga()
|
||||
}
|
||||
|
||||
fun parseChapterListV1(response: Response): List<SChapter> {
|
||||
val data = parseMangaInfoV1(response).data
|
||||
return buildList(data.list.size + data.alone.size) {
|
||||
data.list.mapTo(this) {
|
||||
it.toSChapter()
|
||||
}
|
||||
data.alone.mapTo(this) {
|
||||
it.toSChapter().apply { name = "单行本: $name" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun chapterImagesUrlV1(path: String) = "https://m.idmzj.com/chapinfo/$path.html"
|
||||
|
||||
fun parseChapterImagesV1(response: Response) =
|
||||
response.parseAs<ChapterImagesDto>().toPageList()
|
||||
|
||||
fun chapterCommentsUrl(path: String) = "$v3apiUrl/viewPoint/0/$path.json"
|
||||
|
||||
fun parseChapterComments(response: Response, count: Int): List<String> {
|
||||
val result: List<ChapterCommentDto> = response.parseAs()
|
||||
if (result.isEmpty()) return listOf("没有吐槽")
|
||||
val aggregated = result.groupBy({ it.content }, { it.num }).map { (content, likes) ->
|
||||
ChapterCommentDto(Parser.unescapeEntities(content, false), likes.sum())
|
||||
} as ArrayList
|
||||
aggregated.sort()
|
||||
return aggregated.take(count).map { it.toString() }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
private val id: JsonPrimitive, // can be int or string
|
||||
private val title: String,
|
||||
private val authors: String?,
|
||||
private val status: String,
|
||||
private val cover: String,
|
||||
private val types: String,
|
||||
private val description: String? = null,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = getMangaUrl(id.content)
|
||||
title = this@MangaDto.title
|
||||
author = authors?.formatList()
|
||||
genre = types.formatList()
|
||||
status = parseStatus(this@MangaDto.status)
|
||||
thumbnail_url = cover
|
||||
|
||||
val desc = this@MangaDto.description ?: return@apply
|
||||
description = "$desc\n\n漫画 ID (2): ${id.content}" // hidden
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
private val id: String,
|
||||
private val comic_id: String,
|
||||
private val chapter_name: String,
|
||||
private val updatetime: String,
|
||||
) {
|
||||
fun toSChapter() = SChapter.create().apply {
|
||||
url = "$comic_id/$id"
|
||||
name = chapter_name.formatChapterName()
|
||||
date_upload = updatetime.toLong() * 1000
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterImagesDto(
|
||||
private val page_url: List<String>,
|
||||
) {
|
||||
fun toPageList() = parsePageList(page_url)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterCommentDto(
|
||||
val content: String,
|
||||
val num: Int,
|
||||
) : Comparable<ChapterCommentDto> {
|
||||
override fun toString() = if (num > 0) "$content [+$num]" else content
|
||||
override fun compareTo(other: ChapterCommentDto) = other.num.compareTo(num) // descending
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class DataDto(val info: MangaDto, val list: List<ChapterDto>, val alone: List<ChapterDto>)
|
||||
|
||||
@Serializable
|
||||
class ResponseDto(val data: DataDto)
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import eu.kanade.tachiyomi.extension.zh.dmzj.utils.RSA
|
||||
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 kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import kotlinx.serialization.serializer
|
||||
import okhttp3.Response
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
object ApiV4 {
|
||||
|
||||
private const val v4apiUrl = "https://nnv4api.dmzj.com"
|
||||
|
||||
fun mangaInfoUrl(id: String) = "$v4apiUrl/comic/detail/$id?uid=2665531"
|
||||
|
||||
fun parseMangaInfo(response: Response): MangaDto? {
|
||||
val result: ResponseDto<MangaDto> = response.decrypt()
|
||||
return result.data
|
||||
}
|
||||
|
||||
// path = "mangaId/chapterId"
|
||||
fun chapterImagesUrl(path: String) = "$v4apiUrl/comic/chapter/$path"
|
||||
|
||||
fun parseChapterImages(response: Response, isLowRes: Boolean): ArrayList<Page> {
|
||||
val result: ResponseDto<ChapterImagesDto> = response.decrypt()
|
||||
return result.data!!.toPageList(isLowRes)
|
||||
}
|
||||
|
||||
fun rankingUrl(page: Int, filters: RankingGroup) =
|
||||
"$v4apiUrl/comic/rank/list?${filters.parse()}&uid=2665531&page=$page"
|
||||
|
||||
fun parseRanking(response: Response): MangasPage {
|
||||
val result: ResponseDto<List<RankingItemDto>> = response.decrypt()
|
||||
val data = result.data ?: return MangasPage(emptyList(), false)
|
||||
return MangasPage(data.map { it.toSManga() }, data.isNotEmpty())
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.decrypt(): T = decrypt(typeOf<T>())
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> Response.decrypt(type: KType): T {
|
||||
val bytes = RSA.decrypt(body.string(), cipher)
|
||||
val deserializer = serializer(type) as KSerializer<T>
|
||||
return ProtoBuf.decodeFromByteArray(deserializer, bytes)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
@ProtoNumber(1) private val id: Int,
|
||||
@ProtoNumber(2) private val title: String,
|
||||
@ProtoNumber(6) private val cover: String,
|
||||
@ProtoNumber(7) private val description: String,
|
||||
@ProtoNumber(19) private val genres: List<TagDto>,
|
||||
@ProtoNumber(20) private val status: List<TagDto>,
|
||||
@ProtoNumber(21) private val authors: List<TagDto>,
|
||||
@ProtoNumber(23) private val chapterGroups: List<ChapterGroupDto>,
|
||||
) {
|
||||
val isLicensed get() = chapterGroups.isEmpty()
|
||||
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = getMangaUrl(id.toString())
|
||||
title = this@MangaDto.title
|
||||
author = authors.joinToString { it.name }
|
||||
description = if (isLicensed) {
|
||||
"${this@MangaDto.description}\n\n漫画 ID (1): $id"
|
||||
} else {
|
||||
this@MangaDto.description
|
||||
}
|
||||
genre = genres.joinToString { it.name }
|
||||
status = parseStatus(this@MangaDto.status[0].name)
|
||||
thumbnail_url = cover
|
||||
initialized = true
|
||||
}
|
||||
|
||||
fun parseChapterList(): List<SChapter> {
|
||||
val mangaId = id.toString()
|
||||
val size = chapterGroups.sumOf { it.size }
|
||||
return chapterGroups.flatMapTo(ArrayList(size)) {
|
||||
it.toSChapterList(mangaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class TagDto(@ProtoNumber(2) val name: String)
|
||||
|
||||
@Serializable
|
||||
class ChapterGroupDto(
|
||||
@ProtoNumber(1) private val name: String,
|
||||
@ProtoNumber(2) private val chapters: List<ChapterDto>,
|
||||
) {
|
||||
fun toSChapterList(mangaId: String): List<SChapter> {
|
||||
val groupName = name
|
||||
val isDefaultGroup = groupName == "连载"
|
||||
return chapters.map {
|
||||
it.toSChapterInternal().apply {
|
||||
url = "$mangaId/$url"
|
||||
if (!isDefaultGroup) name = "$groupName: $name"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val size get() = chapters.size
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
@ProtoNumber(1) private val id: Int,
|
||||
@ProtoNumber(2) private val name: String,
|
||||
@ProtoNumber(3) private val updateTime: Long,
|
||||
) {
|
||||
fun toSChapterInternal() = SChapter.create().apply {
|
||||
url = id.toString()
|
||||
name = this@ChapterDto.name.formatChapterName()
|
||||
date_upload = updateTime * 1000
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterImagesDto(
|
||||
@ProtoNumber(6) private val lowResImages: List<String>,
|
||||
@ProtoNumber(8) private val images: List<String>,
|
||||
) {
|
||||
fun toPageList(isLowRes: Boolean) =
|
||||
// page count can be messy, see manga ID 55847 chapters 107-109
|
||||
if (images.size == lowResImages.size) {
|
||||
parsePageList(images, lowResImages)
|
||||
} else if (isLowRes) {
|
||||
parsePageList(lowResImages, lowResImages)
|
||||
} else {
|
||||
parsePageList(images)
|
||||
}
|
||||
}
|
||||
|
||||
// same as ApiV3.MangaDto
|
||||
@Serializable
|
||||
class RankingItemDto(
|
||||
@ProtoNumber(1) private val id: Int?,
|
||||
@ProtoNumber(2) private val title: String,
|
||||
@ProtoNumber(3) private val authors: String,
|
||||
@ProtoNumber(4) private val status: String,
|
||||
@ProtoNumber(5) private val cover: String,
|
||||
@ProtoNumber(6) private val genres: String,
|
||||
@ProtoNumber(9) private val slug: String?,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = when {
|
||||
id != null -> getMangaUrl(id.toString())
|
||||
slug != null -> PREFIX_ID_SEARCH + slug
|
||||
else -> throw Exception("无法解析")
|
||||
}
|
||||
title = this@RankingItemDto.title
|
||||
author = authors.formatList()
|
||||
genre = genres.formatList()
|
||||
status = parseStatus(this@RankingItemDto.status)
|
||||
thumbnail_url = cover
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ResponseDto<T>(
|
||||
@ProtoNumber(2) val message: String?,
|
||||
@ProtoNumber(3) val data: T?,
|
||||
)
|
||||
|
||||
private val cipher by lazy { RSA.getPrivateKey("MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAK8nNR1lTnIfIes6oRWJNj3mB6OssDGx0uGMpgpbVCpf6+VwnuI2stmhZNoQcM417Iz7WqlPzbUmu9R4dEKmLGEEqOhOdVaeh9Xk2IPPjqIu5TbkLZRxkY3dJM1htbz57d/roesJLkZXqssfG5EJauNc+RcABTfLb4IiFjSMlTsnAgMBAAECgYEAiz/pi2hKOJKlvcTL4jpHJGjn8+lL3wZX+LeAHkXDoTjHa47g0knYYQteCbv+YwMeAGupBWiLy5RyyhXFoGNKbbnvftMYK56hH+iqxjtDLnjSDKWnhcB7089sNKaEM9Ilil6uxWMrMMBH9v2PLdYsqMBHqPutKu/SigeGPeiB7VECQQDizVlNv67go99QAIv2n/ga4e0wLizVuaNBXE88AdOnaZ0LOTeniVEqvPtgUk63zbjl0P/pzQzyjitwe6HoCAIpAkEAxbOtnCm1uKEp5HsNaXEJTwE7WQf7PrLD4+BpGtNKkgja6f6F4ld4QZ2TQ6qvsCizSGJrjOpNdjVGJ7bgYMcczwJBALvJWPLmDi7ToFfGTB0EsNHZVKE66kZ/8Stx+ezueke4S556XplqOflQBjbnj2PigwBN/0afT+QZUOBOjWzoDJkCQClzo+oDQMvGVs9GEajS/32mJ3hiWQZrWvEzgzYRqSf3XVcEe7PaXSd8z3y3lACeeACsShqQoc8wGlaHXIJOHTcCQQCZw5127ZGs8ZDTSrogrH73Kw/HvX55wGAeirKYcv28eauveCG7iyFR0PFB/P/EDZnyb+ifvyEFlucPUI0+Y87F") }
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
object CommentsInterceptor : Interceptor {
|
||||
|
||||
class Tag
|
||||
|
||||
private const val MAX_HEIGHT = 1920
|
||||
private const val WIDTH = 1080
|
||||
private const val UNIT = 32
|
||||
private const val UNIT_F = UNIT.toFloat()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
if (request.tag(Tag::class) == null) return response
|
||||
|
||||
val comments = ApiV3.parseChapterComments(response, MAX_HEIGHT / (UNIT * 2))
|
||||
|
||||
val paint = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = UNIT_F
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
var height = UNIT
|
||||
val layouts = comments.map {
|
||||
@Suppress("DEPRECATION")
|
||||
StaticLayout(it, paint, WIDTH - 2 * UNIT, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
|
||||
}.takeWhile {
|
||||
val lineHeight = it.height + UNIT
|
||||
if (height + lineHeight <= MAX_HEIGHT) {
|
||||
height += lineHeight
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
val bitmap = Bitmap.createBitmap(WIDTH, height, Bitmap.Config.ARGB_8888)
|
||||
bitmap.eraseColor(Color.WHITE)
|
||||
val canvas = Canvas(bitmap)
|
||||
|
||||
var y = UNIT
|
||||
for (layout in layouts) {
|
||||
canvas.save()
|
||||
canvas.translate(UNIT_F, y.toFloat())
|
||||
layout.draw(canvas)
|
||||
canvas.restore()
|
||||
y += layout.height + UNIT
|
||||
}
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, output)
|
||||
val body = output.toByteArray().toResponseBody("image/png".toMediaType())
|
||||
return response.newBuilder().body(body).build()
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLDecoder
|
||||
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
|
||||
val json: Json by injectLazy()
|
||||
|
||||
inline fun <reified T> Response.parseAs(): T {
|
||||
return json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
fun getMangaUrl(id: String) = "/comic/comic_$id.json?version=2.7.019"
|
||||
|
||||
fun String.extractMangaId(): String {
|
||||
val start = 13 // length of "/comic/comic_"
|
||||
return substring(start, indexOf('.', start))
|
||||
}
|
||||
|
||||
fun String.formatList() = replace("/", ", ")
|
||||
|
||||
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 parsePageList(
|
||||
images: List<String>,
|
||||
lowResImages: List<String> = List(images.size) { "" },
|
||||
): ArrayList<Page> {
|
||||
val pageCount = images.size
|
||||
val list = ArrayList<Page>(pageCount + 1) // for comments page
|
||||
for (i in 0 until pageCount) {
|
||||
list.add(Page(i, lowResImages[i], images[i]))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun String.decodePath(): String = URLDecoder.decode(this, "UTF-8")
|
||||
|
||||
const val COMMENTS_FLAG = "COMMENTS"
|
@ -1,224 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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 keiyoushi.utils.getPreferences
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
* Dmzj source
|
||||
*/
|
||||
|
||||
class Dmzj : ConfigurableSource, HttpSource() {
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = true
|
||||
override val name = "动漫之家"
|
||||
override val baseUrl = "https://m.idmzj.com"
|
||||
|
||||
private val preferences: SharedPreferences = getPreferences()
|
||||
init {
|
||||
ApiV3.preferences = preferences
|
||||
}
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(ImageUrlInterceptor)
|
||||
.addInterceptor(CommentsInterceptor)
|
||||
.rateLimit(4)
|
||||
.apply {
|
||||
val interceptors = interceptors()
|
||||
val index = interceptors.indexOfFirst { "Brotli" in it.javaClass.simpleName }
|
||||
if (index >= 0) {
|
||||
interceptors.add(interceptors.removeAt(index))
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
// API v4 randomly fails
|
||||
private val retryClient = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(RetryInterceptor)
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
private fun fetchIdBySlug(slug: String): String {
|
||||
val request = GET("https://manhua.dmzj.com/$slug/", headers)
|
||||
val html = client.newCall(request).execute().body.string()
|
||||
val start = "g_comic_id = \""
|
||||
val startIndex = html.indexOf(start) + start.length
|
||||
val endIndex = html.indexOf('"', startIndex)
|
||||
return html.substring(startIndex, endIndex)
|
||||
}
|
||||
|
||||
private fun fetchMangaInfoV4(id: String): ApiV4.MangaDto? {
|
||||
val response = retryClient.newCall(GET(ApiV4.mangaInfoUrl(id), headers)).execute()
|
||||
return ApiV4.parseMangaInfo(response)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET(ApiV3.popularMangaUrl(page), headers)
|
||||
|
||||
override fun popularMangaParse(response: Response) = ApiV3.parsePage(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET(ApiV3.latestUpdatesUrl(page), headers)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = ApiV3.parsePage(response)
|
||||
|
||||
private fun searchMangaById(id: String): MangasPage {
|
||||
val idNumber = if (id.all { it.isDigit() }) {
|
||||
id
|
||||
} else {
|
||||
// Chinese Pinyin ID
|
||||
fetchIdBySlug(id)
|
||||
}
|
||||
|
||||
val sManga = fetchMangaDetails(idNumber)
|
||||
|
||||
return MangasPage(listOf(sManga), false)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.isEmpty()) {
|
||||
val ranking = filters.filterIsInstance<RankingGroup>().firstOrNull()
|
||||
if (ranking != null && ranking.isEnabled) {
|
||||
val call = retryClient.newCall(GET(ApiV4.rankingUrl(page, ranking), headers))
|
||||
return Observable.fromCallable {
|
||||
val result = ApiV4.parseRanking(call.execute())
|
||||
// result has no manga ID if filtered by certain genres; this can be slow
|
||||
for (manga in result.mangas) if (manga.url.startsWith(PREFIX_ID_SEARCH)) {
|
||||
manga.url = getMangaUrl(fetchIdBySlug(manga.url.removePrefix(PREFIX_ID_SEARCH)))
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
val call = client.newCall(GET(ApiV3.pageUrl(page, filters), headers))
|
||||
Observable.fromCallable { ApiV3.parsePage(call.execute()) }
|
||||
} else if (query.startsWith(PREFIX_ID_SEARCH)) {
|
||||
// ID may be numbers or Chinese pinyin
|
||||
val id = query.removePrefix(PREFIX_ID_SEARCH).removeSuffix(".html")
|
||||
Observable.fromCallable { searchMangaById(id) }
|
||||
} else {
|
||||
val request = GET(ApiSearch.searchUrlV1(page, query), headers)
|
||||
Observable.fromCallable {
|
||||
// this API fails randomly, and might return empty list
|
||||
repeat(5) {
|
||||
val result = ApiSearch.parsePageV1(client.newCall(request).execute())
|
||||
if (result.mangas.isNotEmpty()) return@fromCallable result
|
||||
}
|
||||
throw Exception("搜索出错或无结果")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
val id = manga.url.extractMangaId()
|
||||
return Observable.fromCallable { fetchMangaDetails(id) }
|
||||
}
|
||||
|
||||
private fun fetchMangaDetails(id: String): SManga {
|
||||
fetchMangaInfoV4(id)?.run { return toSManga() }
|
||||
val response = client.newCall(GET(ApiV3.mangaInfoUrlV1(id), headers)).execute()
|
||||
return ApiV3.parseMangaDetailsV1(response)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
val cid = manga.url.extractMangaId()
|
||||
return "$baseUrl/info/$cid.html"
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.fromCallable {
|
||||
val id = manga.url.extractMangaId()
|
||||
val result = fetchMangaInfoV4(id)
|
||||
if (result != null && !result.isLicensed) {
|
||||
return@fromCallable result.parseChapterList()
|
||||
}
|
||||
val response = client.newCall(GET(ApiV3.mangaInfoUrlV1(id), headers)).execute()
|
||||
ApiV3.parseChapterListV1(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/view/${chapter.url}.html"
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
val path = chapter.url
|
||||
return Observable.fromCallable {
|
||||
val response = retryClient.newCall(GET(ApiV4.chapterImagesUrl(path), headers)).execute()
|
||||
val result = try {
|
||||
ApiV4.parseChapterImages(response, preferences.imageQuality == LOW_RES)
|
||||
} catch (_: Throwable) {
|
||||
client.newCall(GET(ApiV3.chapterImagesUrlV1(path), headers)).execute()
|
||||
.let(ApiV3::parseChapterImagesV1)
|
||||
}
|
||||
if (preferences.showChapterComments) {
|
||||
result.add(Page(result.size, COMMENTS_FLAG, ApiV3.chapterCommentsUrl(path)))
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// see https://github.com/tachiyomiorg/tachiyomi-extensions/issues/10475
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val url = page.url.takeIf { it.isNotEmpty() }
|
||||
val imageUrl = page.imageUrl!!
|
||||
if (url == COMMENTS_FLAG) {
|
||||
return GET(imageUrl, headers).newBuilder()
|
||||
.tag(CommentsInterceptor.Tag::class, CommentsInterceptor.Tag())
|
||||
.build()
|
||||
}
|
||||
val fallbackUrl = when (preferences.imageQuality) {
|
||||
AUTO_RES -> url
|
||||
ORIGINAL_RES -> null
|
||||
LOW_RES -> if (url == null) null else return GET(url, headers)
|
||||
else -> url
|
||||
}
|
||||
return GET(imageUrl, headers).newBuilder()
|
||||
.tag(ImageUrlInterceptor.Tag::class, ImageUrlInterceptor.Tag(fallbackUrl))
|
||||
.build()
|
||||
}
|
||||
|
||||
// Unused, we can get image urls directly from the chapter page
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList() = getFilterListInternal(preferences.isMultiGenreFilter)
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
getPreferencesInternal(screen.context).forEach(screen::addPreference)
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://www.dmzj.com/info/xxx intents and redirects them to
|
||||
* the main tachiyomi process. The idea is to not install the intent filter unless
|
||||
* you have this extension installed, but still let the main tachiyomi app control
|
||||
* things.
|
||||
*/
|
||||
class DmzjUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 0) {
|
||||
val titleId = if (pathSegments.size > 1) {
|
||||
pathSegments[1] // [m,www].dmzj.com/info/{titleId}
|
||||
} else {
|
||||
pathSegments[0] // manhua.dmzj.com/{titleId}
|
||||
}
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "$PREFIX_ID_SEARCH$titleId")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("DmzjUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("DmzjUrlActivity", "Could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
fun getFilterListInternal(isMultiGenre: Boolean) = FilterList(
|
||||
RankingGroup(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("分类筛选(查看排行榜、搜索文本时无效)"),
|
||||
if (isMultiGenre) GenreGroup() else GenreSelectFilter(),
|
||||
StatusFilter(),
|
||||
ReaderFilter(),
|
||||
RegionFilter(),
|
||||
SortFilter(),
|
||||
)
|
||||
|
||||
// region Ranking filters
|
||||
|
||||
class RankingGroup : Filter.Group<Filter<*>>(
|
||||
"排行榜(搜索文本时无效)",
|
||||
listOf<Filter<*>>(
|
||||
EnabledFilter(),
|
||||
TimeFilter(),
|
||||
SortFilter(),
|
||||
GenreFilter(),
|
||||
),
|
||||
) {
|
||||
val isEnabled get() = (state[0] as EnabledFilter).state
|
||||
|
||||
fun parse() = state.filterIsInstance<QueryFilter>().joinToString("&") { it.uriPart }
|
||||
|
||||
private class EnabledFilter : CheckBox("查看排行榜")
|
||||
|
||||
private class TimeFilter : QueryFilter(
|
||||
"榜单",
|
||||
"by_time",
|
||||
arrayOf(
|
||||
Pair("日排行", "0"),
|
||||
Pair("周排行", "1"),
|
||||
Pair("月排行", "2"),
|
||||
Pair("总排行", "3"),
|
||||
),
|
||||
)
|
||||
|
||||
private class SortFilter : QueryFilter(
|
||||
"排序",
|
||||
"rank_type",
|
||||
arrayOf(
|
||||
Pair("人气", "0"),
|
||||
Pair("吐槽", "1"),
|
||||
Pair("订阅", "2"),
|
||||
),
|
||||
)
|
||||
|
||||
private class GenreFilter : QueryFilter("题材(慎用/易出错)", "tag_id", genres)
|
||||
|
||||
private open class QueryFilter(
|
||||
name: String,
|
||||
private val query: String,
|
||||
values: Array<Pair<String, String>>,
|
||||
) : SelectFilter(name, values) {
|
||||
override val uriPart get() = query + '=' + super.uriPart
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Normal filters
|
||||
|
||||
fun parseFilters(filters: FilterList): String {
|
||||
val tags = filters.filterIsInstance<TagFilter>().mapNotNull {
|
||||
it.uriPart.takeUnless(String::isEmpty)
|
||||
}.joinToString("-").ifEmpty { "0" }
|
||||
val sort = filters.filterIsInstance<SortFilter>().firstOrNull()?.uriPart ?: "0"
|
||||
return "$tags/$sort"
|
||||
}
|
||||
|
||||
private interface TagFilter : UriPartFilter
|
||||
|
||||
private class GenreSelectFilter : TagFilter, SelectFilter("题材", genres)
|
||||
|
||||
private class GenreGroup : TagFilter, Filter.Group<GenreFilter>(
|
||||
"题材(作品需包含勾选的所有项目)",
|
||||
genres.drop(1).map { GenreFilter(it.first, it.second) },
|
||||
) {
|
||||
override val uriPart get() = state.filter { it.state }.joinToString("-") { it.value }
|
||||
}
|
||||
|
||||
private class GenreFilter(name: String, val value: String) : Filter.CheckBox(name)
|
||||
|
||||
private class StatusFilter : TagFilter, SelectFilter(
|
||||
"状态",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("连载中", "2309"),
|
||||
Pair("已完结", "2310"),
|
||||
),
|
||||
)
|
||||
|
||||
private class ReaderFilter : TagFilter, SelectFilter(
|
||||
"受众",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("少年漫画", "3262"),
|
||||
Pair("少女漫画", "3263"),
|
||||
Pair("青年漫画", "3264"),
|
||||
Pair("女青漫画", "13626"),
|
||||
),
|
||||
)
|
||||
|
||||
private class RegionFilter : TagFilter, SelectFilter(
|
||||
"地域",
|
||||
arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("日本", "2304"),
|
||||
Pair("韩国", "2305"),
|
||||
Pair("欧美", "2306"),
|
||||
Pair("港台", "2307"),
|
||||
Pair("内地", "2308"),
|
||||
Pair("其他", "8453"),
|
||||
),
|
||||
)
|
||||
|
||||
private class SortFilter : SelectFilter(
|
||||
"排序",
|
||||
arrayOf(
|
||||
Pair("人气", "0"),
|
||||
Pair("更新", "1"),
|
||||
),
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
private val genres
|
||||
get() = arrayOf(
|
||||
Pair("全部", ""),
|
||||
Pair("冒险", "4"),
|
||||
Pair("欢乐向", "5"),
|
||||
Pair("格斗", "6"),
|
||||
Pair("科幻", "7"),
|
||||
Pair("爱情", "8"),
|
||||
Pair("侦探", "9"),
|
||||
Pair("竞技", "10"),
|
||||
Pair("魔法", "11"),
|
||||
Pair("神鬼", "12"),
|
||||
Pair("校园", "13"),
|
||||
Pair("惊悚", "14"),
|
||||
Pair("其他", "16"),
|
||||
Pair("四格", "17"),
|
||||
Pair("生活", "3242"),
|
||||
Pair("ゆり", "3243"),
|
||||
Pair("秀吉", "3244"),
|
||||
Pair("悬疑", "3245"),
|
||||
Pair("纯爱", "3246"),
|
||||
Pair("热血", "3248"),
|
||||
Pair("泛爱", "3249"),
|
||||
Pair("历史", "3250"),
|
||||
Pair("战争", "3251"),
|
||||
Pair("萌系", "3252"),
|
||||
Pair("宅系", "3253"),
|
||||
Pair("治愈", "3254"),
|
||||
Pair("励志", "3255"),
|
||||
Pair("武侠", "3324"),
|
||||
Pair("机战", "3325"),
|
||||
Pair("音乐舞蹈", "3326"),
|
||||
Pair("美食", "3327"),
|
||||
Pair("职场", "3328"),
|
||||
Pair("西方魔幻", "3365"),
|
||||
Pair("高清单行", "4459"),
|
||||
Pair("TS", "4518"),
|
||||
Pair("东方", "5077"),
|
||||
Pair("魔幻", "5806"),
|
||||
Pair("奇幻", "5848"),
|
||||
Pair("节操", "6219"),
|
||||
Pair("轻小说", "6316"),
|
||||
Pair("颜艺", "6437"),
|
||||
Pair("搞笑", "7568"),
|
||||
Pair("仙侠", "7900"),
|
||||
Pair("舰娘", "13627"),
|
||||
Pair("动画", "17192"),
|
||||
Pair("AA", "18522"),
|
||||
Pair("福瑞", "23323"),
|
||||
Pair("生存", "23388"),
|
||||
Pair("2021大赛", "23399"),
|
||||
Pair("未来漫画家", "25011"),
|
||||
)
|
||||
|
||||
interface UriPartFilter {
|
||||
val uriPart: String
|
||||
}
|
||||
|
||||
private open class SelectFilter(
|
||||
name: String,
|
||||
values: Array<Pair<String, String>>,
|
||||
) : UriPartFilter, Filter.Select<String>(
|
||||
name = name,
|
||||
values = Array(values.size) { values[it].first },
|
||||
) {
|
||||
private val uriParts = Array(values.size) { values[it].second }
|
||||
override val uriPart get() = uriParts[state]
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import android.util.Log
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
|
||||
object ImageUrlInterceptor : Interceptor {
|
||||
|
||||
class Tag(val url: String?)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val tag = request.tag(Tag::class) ?: return chain.proceed(request)
|
||||
|
||||
try {
|
||||
val response = chain.proceed(request)
|
||||
if (response.isSuccessful) return response
|
||||
response.close()
|
||||
Log.e("DMZJ", "failed to fetch '${request.url}': HTTP ${response.code}")
|
||||
} catch (e: IOException) {
|
||||
Log.e("DMZJ", "failed to fetch '${request.url}'", e)
|
||||
}
|
||||
|
||||
// this can sometimes bypass encoding issues by decoding '+' to ' '
|
||||
val decodedUrl = request.url.toString().decodePath()
|
||||
val newRequest = request.newBuilder().url(decodedUrl).build()
|
||||
try {
|
||||
val response = chain.proceed(newRequest)
|
||||
if (response.isSuccessful) return response
|
||||
response.close()
|
||||
Log.e("DMZJ", "failed to fetch '$decodedUrl': HTTP ${response.code}")
|
||||
} catch (e: IOException) {
|
||||
Log.e("DMZJ", "failed to fetch '$decodedUrl'", e)
|
||||
}
|
||||
|
||||
val url = tag.url ?: throw IOException()
|
||||
val fallbackRequest = request.newBuilder().url(url).build()
|
||||
return chain.proceed(fallbackRequest)
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
|
||||
// Legacy preferences:
|
||||
// "apiRatelimitPreference" -> 1..10 default "5"
|
||||
// "imgCDNRatelimitPreference" -> 1..10 default "5"
|
||||
// "licensedList" -> StringSet of manga ID
|
||||
// "hiddenList" -> StringSet of manga ID
|
||||
|
||||
fun getPreferencesInternal(context: Context) = arrayOf(
|
||||
|
||||
ListPreference(context).apply {
|
||||
key = IMAGE_QUALITY_PREF
|
||||
title = "图片质量"
|
||||
summary = "%s\n修改后,已加载的章节需要清除章节缓存才能生效。"
|
||||
entries = arrayOf("优先原图", "只用原图 (加载出错概率更高)", "优先低清")
|
||||
entryValues = arrayOf(AUTO_RES, ORIGINAL_RES, LOW_RES)
|
||||
setDefaultValue(AUTO_RES)
|
||||
},
|
||||
|
||||
SwitchPreferenceCompat(context).apply {
|
||||
key = CHAPTER_COMMENTS_PREF
|
||||
title = "章末吐槽页"
|
||||
summary = "修改后,已加载的章节需要清除章节缓存才能生效。"
|
||||
setDefaultValue(false)
|
||||
},
|
||||
|
||||
SwitchPreferenceCompat(context).apply {
|
||||
key = MULTI_GENRE_FILTER_PREF
|
||||
title = "分类筛选时允许勾选多个题材"
|
||||
summary = "可以更精细地筛选出同时符合多个题材的作品。"
|
||||
setDefaultValue(false)
|
||||
},
|
||||
|
||||
SwitchPreferenceCompat(context).apply {
|
||||
key = DMZJ_V3API_PREF
|
||||
title = "V3API选择"
|
||||
summary = "是否使用旧版v3API(默认nnv3api)"
|
||||
setDefaultValue(false)
|
||||
},
|
||||
)
|
||||
|
||||
val SharedPreferences.imageQuality get() = getString(IMAGE_QUALITY_PREF, AUTO_RES)!!
|
||||
|
||||
val SharedPreferences.showChapterComments get() = getBoolean(CHAPTER_COMMENTS_PREF, false)
|
||||
|
||||
val SharedPreferences.isMultiGenreFilter get() = getBoolean(MULTI_GENRE_FILTER_PREF, false)
|
||||
|
||||
val SharedPreferences.isOlderV3API get() = getBoolean(DMZJ_V3API_PREF, false)
|
||||
|
||||
private const val IMAGE_QUALITY_PREF = "imageSourcePreference"
|
||||
const val AUTO_RES = "PREFER_ORIG_RES"
|
||||
const val ORIGINAL_RES = "ORIG_RES_ONLY"
|
||||
const val LOW_RES = "LOW_RES_ONLY"
|
||||
|
||||
private const val CHAPTER_COMMENTS_PREF = "chapterComments"
|
||||
private const val MULTI_GENRE_FILTER_PREF = "multiGenreFilter"
|
||||
|
||||
private const val DMZJ_V3API_PREF = "v3APIVersion"
|
@ -1,18 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj
|
||||
|
||||
import android.util.Log
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
object RetryInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
repeat(2) {
|
||||
val response = chain.proceed(request)
|
||||
if (response.isSuccessful) return response
|
||||
response.close()
|
||||
Log.e("DMZJ", "failed to fetch '${request.url}': HTTP ${response.code}")
|
||||
}
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.dmzj.utils
|
||||
|
||||
import android.util.Base64
|
||||
import java.security.KeyFactory
|
||||
import java.security.PrivateKey
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import javax.crypto.Cipher
|
||||
import kotlin.math.min
|
||||
|
||||
object RSA {
|
||||
private val cipher by lazy(LazyThreadSafetyMode.NONE) {
|
||||
Cipher.getInstance("RSA/ECB/PKCS1Padding")
|
||||
}
|
||||
|
||||
private const val MAX_DECRYPT_BLOCK = 128
|
||||
|
||||
fun getPrivateKey(privateKey: String): PrivateKey {
|
||||
val keyBytes = Base64.decode(privateKey, Base64.DEFAULT)
|
||||
val pkcs8KeySpec = PKCS8EncodedKeySpec(keyBytes)
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
val privateK = keyFactory.generatePrivate(pkcs8KeySpec)
|
||||
return privateK
|
||||
}
|
||||
|
||||
@Synchronized // because Cipher is not thread-safe
|
||||
fun decrypt(encrypted: String, key: PrivateKey): ByteArray {
|
||||
val cipher = this.cipher
|
||||
cipher.init(Cipher.DECRYPT_MODE, key) // always reset in case of illegal state
|
||||
val encryptedData = Base64.decode(encrypted, Base64.DEFAULT)
|
||||
val inputLen = encryptedData.size
|
||||
|
||||
val result = ByteArray(inputLen)
|
||||
var resultSize = 0
|
||||
|
||||
for (offset in 0 until inputLen step MAX_DECRYPT_BLOCK) {
|
||||
val blockLen = min(MAX_DECRYPT_BLOCK, inputLen - offset)
|
||||
resultSize += cipher.doFinal(encryptedData, offset, blockLen, result, resultSize)
|
||||
}
|
||||
|
||||
return result.copyOf(resultSize)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user