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