Update Komiic (#10376)
- Refresh token automatically - Refactor requests - Fetch genres from website - Tweak chapter list
This commit is contained in:
parent
3e65d19929
commit
df9da07535
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Komiic'
|
extName = 'Komiic'
|
||||||
extClass = '.Komiic'
|
extClass = '.Komiic'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,73 +3,86 @@ package eu.kanade.tachiyomi.extension.zh.komiic
|
|||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Data<T>(val data: Result<T>)
|
class ResponseDto(private val data: DataDto?, private val errors: List<ErrorDto>?) {
|
||||||
|
fun getData() = data ?: throw Exception(errors!!.joinToString("\n") { it.message })
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class MultiData<T, V>(val data: MultiResult<T, V>)
|
class ErrorDto(val message: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Result<T>(val result: T)
|
class DataDto(
|
||||||
|
private val comics: List<MangaDto>?,
|
||||||
@Serializable
|
val allCategory: List<ItemDto>?,
|
||||||
class MultiResult<T, V>(val result1: T, val result2: V)
|
private val searchComicsAndAuthors: DataDto?,
|
||||||
|
val comicById: MangaDto?,
|
||||||
@Serializable
|
val chaptersByComicId: List<ChapterDto>?,
|
||||||
data class Item(val id: String, val name: String)
|
val reachedImageLimit: Boolean?,
|
||||||
|
val imagesByChapterId: List<PageDto>?,
|
||||||
@Serializable
|
|
||||||
data class Comic(
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val description: String,
|
|
||||||
val status: String,
|
|
||||||
val imageUrl: String,
|
|
||||||
var authors: List<Item>,
|
|
||||||
val categories: List<Item>,
|
|
||||||
) {
|
) {
|
||||||
|
fun getListing(): List<MangaDto> = comics ?: searchComicsAndAuthors!!.comics!!
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class JwtPayload(val exp: Long)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ItemDto(val id: String, val name: String)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaDto(
|
||||||
|
private val id: String,
|
||||||
|
private val title: String,
|
||||||
|
private val description: String,
|
||||||
|
private val status: String,
|
||||||
|
private val imageUrl: String,
|
||||||
|
private val authors: List<ItemDto>,
|
||||||
|
private val categories: List<ItemDto>,
|
||||||
|
) {
|
||||||
|
val url get() = "/comic/$id"
|
||||||
|
|
||||||
fun toSManga() = SManga.create().apply {
|
fun toSManga() = SManga.create().apply {
|
||||||
url = "/comic/$id"
|
url = this@MangaDto.url
|
||||||
title = this@Comic.title
|
title = this@MangaDto.title
|
||||||
thumbnail_url = this@Comic.imageUrl
|
thumbnail_url = this@MangaDto.imageUrl
|
||||||
author = this@Comic.authors.joinToString { it.name }
|
author = this@MangaDto.authors.joinToString { it.name }
|
||||||
genre = this@Comic.categories.joinToString { it.name }
|
genre = this@MangaDto.categories.joinToString { it.name }
|
||||||
description = this@Comic.description
|
description = this@MangaDto.description
|
||||||
status = when (this@Comic.status) {
|
status = when (this@MangaDto.status) {
|
||||||
"ONGOING" -> SManga.ONGOING
|
"ONGOING" -> SManga.ONGOING
|
||||||
"END" -> SManga.COMPLETED
|
"END" -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
initialized = this@Comic.description.isNotEmpty()
|
initialized = this@MangaDto.description.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Chapter(
|
class ChapterDto(
|
||||||
val id: String,
|
private val id: String,
|
||||||
val serial: String,
|
val serial: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val size: Int,
|
private val size: Int,
|
||||||
val dateCreated: String,
|
private val dateCreated: String,
|
||||||
) {
|
) {
|
||||||
fun toSChapter(comicUrl: String, parseDate: (String) -> Long) = SChapter.create().apply {
|
fun toSChapter(mangaUrl: String, dateFormat: SimpleDateFormat) = SChapter.create().apply {
|
||||||
url = "$comicUrl/chapter/${this@Chapter.id}"
|
val (suffix, typeName) = when (val type = this@ChapterDto.type) {
|
||||||
name = when (this@Chapter.type) {
|
"chapter" -> Pair("話", "連載")
|
||||||
"chapter" -> "第 ${this@Chapter.serial} 話"
|
"book" -> Pair("卷", "單行本")
|
||||||
"book" -> "第 ${this@Chapter.serial} 卷"
|
else -> throw Exception("未知章節類型:$type")
|
||||||
else -> this@Chapter.serial
|
|
||||||
}
|
}
|
||||||
scanlator = "${this@Chapter.size}P"
|
url = "$mangaUrl/chapter/${this@ChapterDto.id}"
|
||||||
date_upload = parseDate(this@Chapter.dateCreated)
|
name = "${this@ChapterDto.serial}$suffix(${this@ChapterDto.size}P)"
|
||||||
chapter_number = if (this@Chapter.type == "book") 0F else this@Chapter.serial.toFloatOrNull() ?: -1f
|
scanlator = typeName
|
||||||
|
date_upload = dateFormat.parse(this@ChapterDto.dateCreated)!!.time
|
||||||
|
chapter_number = this@ChapterDto.serial.toFloatOrNull() ?: -1f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Image(
|
class PageDto(
|
||||||
val id: String,
|
|
||||||
val kid: String,
|
val kid: String,
|
||||||
val height: Int,
|
|
||||||
val width: Int,
|
|
||||||
)
|
)
|
||||||
|
@ -3,43 +3,50 @@ package eu.kanade.tachiyomi.extension.zh.komiic
|
|||||||
import eu.kanade.tachiyomi.source.model.Filter
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
|
||||||
|
var categories: List<ItemDto> = emptyList()
|
||||||
|
|
||||||
fun buildFilterList(): FilterList {
|
fun buildFilterList(): FilterList {
|
||||||
val categories = mapOf(
|
val categoryFilter = if (categories.isNotEmpty()) {
|
||||||
"1" to "愛情", "3" to "神鬼", "4" to "校園", "5" to "搞笑", "6" to "生活",
|
CategoryFilter()
|
||||||
"7" to "懸疑", "8" to "冒險", "10" to "職場", "11" to "魔幻", "2" to "後宮",
|
} else {
|
||||||
"12" to "魔法", "13" to "格鬥", "14" to "宅男", "15" to "勵志", "16" to "耽美",
|
Filter.Header("點擊“重設”載入類型")
|
||||||
"17" to "科幻", "18" to "百合", "19" to "治癒", "20" to "萌系", "21" to "熱血",
|
}
|
||||||
"22" to "競技", "23" to "推理", "24" to "雜誌", "25" to "偵探", "26" to "偽娘",
|
|
||||||
"27" to "美食", "9" to "恐怖", "28" to "四格", "31" to "社會", "32" to "歷史",
|
|
||||||
"33" to "戰爭", "34" to "舞蹈", "35" to "武俠", "36" to "機戰", "37" to "音樂",
|
|
||||||
"40" to "體育", "42" to "黑道", "46" to "腐女", "47" to "異世界", "48" to "驚悚",
|
|
||||||
"51" to "成人", "54" to "戰鬥", "55" to "復仇", "56" to "轉生", "57" to "黑暗奇幻",
|
|
||||||
"58" to "戲劇", "59" to "生存", "60" to "策略", "61" to "政治", "62" to "黑暗",
|
|
||||||
"64" to "動作", "70" to "性轉換", "73" to "色情", "181" to "校园", "78" to "日常",
|
|
||||||
"81" to "青春", "83" to "料理", "85" to "醫療", "86" to "致鬱", "87" to "心理",
|
|
||||||
"88" to "穿越", "92" to "友情", "93" to "犯罪", "97" to "劇情",
|
|
||||||
"110" to "運動", "113" to "少女", "114" to "賭博", "119" to "情色", "123" to "女性向",
|
|
||||||
"128" to "性轉", "129" to "溫馨", "164" to "同人",
|
|
||||||
)
|
|
||||||
return FilterList(
|
return FilterList(
|
||||||
Filter.Header("過濾條件(搜索關鍵字時無效)"),
|
Filter.Header("篩選條件(搜索關鍵字時無效)"),
|
||||||
CategoryFilter(categories),
|
categoryFilter,
|
||||||
StatusFilter(),
|
|
||||||
SortFilter(),
|
SortFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
RatingFilter(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface KomiicFilter {
|
||||||
|
fun apply(variables: ListingVariables)
|
||||||
|
}
|
||||||
|
|
||||||
class Category(val id: String, name: String) : Filter.CheckBox(name)
|
class Category(val id: String, name: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
class CategoryFilter(categories: Map<String, String>) :
|
class CategoryFilter :
|
||||||
Filter.Group<Category>("類型(篩選同時包含全部所選標簽的漫畫)", categories.map { Category(it.key, it.value) }) {
|
Filter.Group<Category>("類型(篩選同時包含全部所選標簽的漫畫)", categories.map { Category(it.id, it.name) }), KomiicFilter {
|
||||||
val selected get() = state.filter(Category::state).map(Category::id)
|
override fun apply(variables: ListingVariables) {
|
||||||
|
variables.categoryId = state.mapNotNull { if (it.state) it.id else null }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusFilter : Filter.Select<String>("狀態", arrayOf("全部", "連載", "完結")) {
|
class StatusFilter : Filter.Select<String>("狀態", arrayOf("全部", "連載", "完結")), KomiicFilter {
|
||||||
val value get() = arrayOf("", "ONGOING", "END")[state]
|
override fun apply(variables: ListingVariables) {
|
||||||
|
variables.pagination.status = arrayOf("", "ONGOING", "END")[state]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SortFilter : Filter.Select<String>("排序", arrayOf("更新", "觀看數", "喜愛數")) {
|
class SortFilter : Filter.Select<String>("排序", arrayOf("更新", "本月觀看數(不能篩選類型)", "觀看數", "喜愛數")), KomiicFilter {
|
||||||
val value get() = arrayOf("DATE_UPDATED", "VIEWS", "FAVORITE_COUNT")[state]
|
override fun apply(variables: ListingVariables) {
|
||||||
|
variables.pagination.orderBy = arrayOf(OrderBy.DATE_UPDATED, OrderBy.MONTH_VIEWS, OrderBy.VIEWS, OrderBy.FAVORITE_COUNT)[state]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RatingFilter : Filter.Select<String>("色氣程度", arrayOf("全部", "無", "1", "2", "3", "≥4", "5")), KomiicFilter {
|
||||||
|
override fun apply(variables: ListingVariables) {
|
||||||
|
variables.pagination.sexyLevel = arrayOf(null, 0, 1, 2, 3, 4, 5)[state]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,48 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import keiyoushi.utils.firstInstance
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import keiyoushi.utils.parseAs
|
import keiyoushi.utils.parseAs
|
||||||
import keiyoushi.utils.toJsonString
|
import okhttp3.Interceptor
|
||||||
import keiyoushi.utils.tryParse
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.TimeZone
|
import java.util.TimeZone
|
||||||
|
|
||||||
class Komiic : HttpSource(), ConfigurableSource {
|
class Komiic : HttpSource(), ConfigurableSource {
|
||||||
override var name = "Komiic"
|
override val name = "Komiic"
|
||||||
override val baseUrl = "https://komiic.com"
|
override val baseUrl = "https://komiic.com"
|
||||||
override val lang = "zh"
|
override val lang = "zh"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
override val client = network.cloudflareClient
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
refreshToken(chain)
|
||||||
|
chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun refreshToken(chain: Interceptor.Chain) {
|
||||||
|
val url = chain.request().url
|
||||||
|
if (url.pathSegments[0] != "api") return
|
||||||
|
val cookie = client.cookieJar.loadForRequest(url).find { it.name == "komiic-access-token" } ?: return
|
||||||
|
val parts = cookie.value.split(".")
|
||||||
|
if (parts.size != 3) throw IOException("Token 格式無效")
|
||||||
|
val payload = Base64.decode(parts[1], Base64.DEFAULT).decodeToString()
|
||||||
|
if (System.currentTimeMillis() + 3600_000 < payload.parseAs<JwtPayload>().exp * 1000) return
|
||||||
|
val response = chain.proceed(POST("$baseUrl/auth/refresh", headers)).apply { close() }
|
||||||
|
if (!response.isSuccessful) throw IOException("刷新 Token 失敗:HTTP ${response.code}")
|
||||||
|
}
|
||||||
|
|
||||||
private val apiUrl = "$baseUrl/api/query"
|
private val apiUrl = "$baseUrl/api/query"
|
||||||
private val preferences by getPreferencesLazy()
|
private val preferences by getPreferencesLazy()
|
||||||
@ -40,168 +53,98 @@ class Komiic : HttpSource(), ConfigurableSource {
|
|||||||
|
|
||||||
// Customize ===================================================================================
|
// Customize ===================================================================================
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val PAGE_SIZE = 20
|
|
||||||
const val PREFIX_ID_SEARCH = "id:"
|
|
||||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
|
||||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
|
||||||
timeZone = TimeZone.getTimeZone("UTC")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SManga.id get() = url.substringAfterLast("/")
|
private val SManga.id get() = url.substringAfterLast("/")
|
||||||
private val SChapter.id get() = url.substringAfterLast("/")
|
private val SChapter.id get() = url.substringAfterLast("/")
|
||||||
private inline fun <reified T> Payload<T>.toRequestBody() = this.toJsonString().toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
/**
|
private fun RequestBody.request() = POST(apiUrl, headers, this)
|
||||||
* 根據 ID 搜索漫畫
|
private fun Response.parse() = parseAs<ResponseDto>().getData()
|
||||||
* Search the comic based on the ID.
|
|
||||||
*/
|
|
||||||
private fun comicByIDRequest(id: String): Request {
|
|
||||||
val variables = Variables().field("comicId", id).build()
|
|
||||||
val payload = Payload(Query.COMIC_BY_ID, variables)
|
|
||||||
return POST(apiUrl, headers, payload.toRequestBody())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根據 ID 解析搜索來的漫畫
|
|
||||||
* Parse the comic based on the ID.
|
|
||||||
*/
|
|
||||||
private fun parseComicByID(response: Response): MangasPage {
|
|
||||||
val res = response.parseAs<Data<Comic>>()
|
|
||||||
val entries = listOf(res.data.result.toSManga())
|
|
||||||
return MangasPage(entries, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 檢查 API 是否達到上限
|
|
||||||
* Check if the API has reached its limit.
|
|
||||||
* But how to throw an exception message to notify user in reading page?
|
|
||||||
*/
|
|
||||||
// private fun checkAPILimit(): Observable<Boolean> {
|
|
||||||
// val payload = Payload("reachedImageLimit", Variables().build(), QUERY_API_LIMIT)
|
|
||||||
// val response = client.newCall(POST(queryAPIUrl, headers, payload.toRequestBody()))
|
|
||||||
// val limit = response.asObservableSuccess().map { it.parseAs<Data<Boolean>>().data.result }
|
|
||||||
// return limit
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Popular Manga ===============================================================================
|
// Popular Manga ===============================================================================
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
val pagination = Pagination((page - 1) * PAGE_SIZE, "MONTH_VIEWS")
|
val pagination = Pagination((page - 1) * PAGE_SIZE, OrderBy.MONTH_VIEWS)
|
||||||
val variables = Variables().field("pagination", pagination).build()
|
return listingQuery(ListingVariables(pagination)).request()
|
||||||
val payload = Payload(Query.HOT_COMICS, variables)
|
|
||||||
return POST(apiUrl, headers, payload.toRequestBody())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response) = parseListing(response.parse())
|
||||||
val res = response.parseAs<Data<List<Comic>>>()
|
|
||||||
val comics = res.data.result
|
|
||||||
return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latest Updates ==============================================================================
|
// Latest Updates ==============================================================================
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
val pagination = Pagination((page - 1) * PAGE_SIZE, "DATE_UPDATED")
|
val pagination = Pagination((page - 1) * PAGE_SIZE, OrderBy.DATE_UPDATED)
|
||||||
val variables = Variables().field("pagination", pagination).build()
|
return listingQuery(ListingVariables(pagination)).request()
|
||||||
val payload = Payload(Query.RECENT_UPDATE, variables)
|
|
||||||
return POST(apiUrl, headers, payload.toRequestBody())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
override fun latestUpdatesParse(response: Response) = parseListing(response.parse())
|
||||||
|
|
||||||
// Search Manga ================================================================================
|
// Search Manga ================================================================================
|
||||||
|
|
||||||
override fun getFilterList() = buildFilterList()
|
override fun getFilterList() = buildFilterList()
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
if (query.isNotBlank()) {
|
return if (query.startsWith(PREFIX_ID_SEARCH)) {
|
||||||
val variables = Variables().field("keyword", query).build()
|
idsQuery(query.removePrefix(PREFIX_ID_SEARCH)).request()
|
||||||
val payload = Payload(Query.SEARCH, variables)
|
|
||||||
return POST(apiUrl, headers, payload.toRequestBody())
|
|
||||||
} else {
|
|
||||||
val categories = filters.firstInstance<CategoryFilter>()
|
|
||||||
val status = filters.firstInstance<StatusFilter>()
|
|
||||||
val sort = filters.firstInstance<SortFilter>()
|
|
||||||
val variables = Variables().field(
|
|
||||||
"pagination",
|
|
||||||
Pagination((page - 1) * PAGE_SIZE, sort.value, status.value, false),
|
|
||||||
).field("categoryId", categories.selected).build()
|
|
||||||
val payload = Payload(Query.COMIC_BY_CATEGORIES, variables)
|
|
||||||
return POST(apiUrl, headers, payload.toRequestBody())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
val res = response.parseAs<Data<Result<List<Comic>>>>()
|
|
||||||
val comics = res.data.result.result
|
|
||||||
return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith(PREFIX_ID_SEARCH)) {
|
|
||||||
return client.newCall(comicByIDRequest(query.substringAfter(PREFIX_ID_SEARCH)))
|
|
||||||
.asObservableSuccess().map(::parseComicByID)
|
|
||||||
} else if (query.isNotBlank()) {
|
} else if (query.isNotBlank()) {
|
||||||
return super.fetchSearchManga(page, query, filters)
|
searchQuery(query).request()
|
||||||
|
} else {
|
||||||
|
val variables = ListingVariables(Pagination((page - 1) * PAGE_SIZE))
|
||||||
|
for (filter in filters) if (filter is KomiicFilter) filter.apply(variables)
|
||||||
|
listingQuery(variables).request()
|
||||||
}
|
}
|
||||||
return client.newCall(searchMangaRequest(page, query, filters))
|
|
||||||
.asObservableSuccess().map(::popularMangaParse)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) = parseListing(response.parse())
|
||||||
|
|
||||||
// Manga Details ===============================================================================
|
// Manga Details ===============================================================================
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga) = comicByIDRequest(manga.id)
|
override fun mangaDetailsRequest(manga: SManga) = mangaQuery(manga.id).request()
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response) = response.parse().comicById!!.toSManga()
|
||||||
val res = response.parseAs<Data<Comic>>()
|
|
||||||
return res.data.result.toSManga()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapter List ================================================================================
|
// Chapter List ================================================================================
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + "/images/all"
|
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + "/images/all"
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun chapterListRequest(manga: SManga) = mangaQuery(manga.id).request()
|
||||||
val variables = Variables().field("comicId", manga.id).build()
|
|
||||||
val payload = Payload(Query.CHAPTERS_BY_COMIC_ID, variables)
|
|
||||||
return POST("$apiUrl#${manga.url}", headers, payload.toRequestBody())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val res = response.parseAs<Data<List<Chapter>>>()
|
val data = response.parse()
|
||||||
val comics = res.data.result.sortedWith(
|
val chapters = data.chaptersByComicId!!.toMutableList()
|
||||||
compareByDescending<Chapter> { it.type }
|
when (preferences.getString(CHAPTER_FILTER_PREF, "all")!!) {
|
||||||
|
"chapter" -> chapters.retainAll { it.type == "chapter" }
|
||||||
|
"book" -> chapters.retainAll { it.type == "book" }
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
chapters.sortWith(
|
||||||
|
compareByDescending<ChapterDto> { it.type }
|
||||||
.thenByDescending { it.serial.toFloatOrNull() },
|
.thenByDescending { it.serial.toFloatOrNull() },
|
||||||
)
|
)
|
||||||
val display = preferences.getString(CHAPTER_FILTER_PREF, "all")
|
val mangaUrl = data.comicById!!.url
|
||||||
val items = when (display) {
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||||
"chapter" -> comics.filter { it.type == "chapter" }
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
"book" -> comics.filter { it.type == "book" }
|
|
||||||
else -> comics
|
|
||||||
}
|
}
|
||||||
val comicUrl = response.request.url.fragment!!
|
return chapters.map { it.toSChapter(mangaUrl, dateFormat) }
|
||||||
return items.map { it.toSChapter(comicUrl, DATE_FORMAT::tryParse) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page List ===================================================================================
|
// Page List ===================================================================================
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
val variables = Variables().field("chapterId", chapter.id).build()
|
return pageListQuery(chapter.id).request().newBuilder()
|
||||||
val payload = Payload(Query.IMAGES_BY_CHAPTER_ID, variables)
|
.tag(String::class.java, chapter.url)
|
||||||
return POST("$apiUrl#${chapter.url}", headers, payload.toRequestBody())
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val res = response.parseAs<MultiData<Boolean, List<Image>>>()
|
val data = response.parse()
|
||||||
val check = preferences.getBoolean(CHECK_API_LIMIT_PREF, true)
|
val check = preferences.getBoolean(CHECK_API_LIMIT_PREF, true)
|
||||||
check(!check || !res.data.result1) { "今日圖片讀取次數已達上限,請登录或明天再來!" }
|
if (check && data.reachedImageLimit!!) {
|
||||||
val chapterUrl = response.request.url.fragment!!
|
throw Exception("今日圖片讀取次數已達上限,請登录或明天再來!")
|
||||||
return res.data.result2.mapIndexed { index, image ->
|
}
|
||||||
Page(index, "$chapterUrl/page/$index", "$baseUrl/api/image/${image.kid}")
|
val chapterUrl = response.request.tag(String::class.java)!!
|
||||||
|
return data.imagesByChapterId!!.mapIndexed { index, image ->
|
||||||
|
Page(index, "$chapterUrl/page/${index + 1}", "$baseUrl/api/image/${image.kid}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,7 +152,7 @@ class Komiic : HttpSource(), ConfigurableSource {
|
|||||||
|
|
||||||
override fun imageRequest(page: Page): Request {
|
override fun imageRequest(page: Page): Request {
|
||||||
return super.imageRequest(page).newBuilder()
|
return super.imageRequest(page).newBuilder()
|
||||||
.addHeader("accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8'")
|
.addHeader("accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||||
.addHeader("referer", page.url)
|
.addHeader("referer", page.url)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -3,38 +3,48 @@ package eu.kanade.tachiyomi.extension.zh.komiic
|
|||||||
import kotlinx.serialization.EncodeDefault
|
import kotlinx.serialization.EncodeDefault
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
|
||||||
|
const val PAGE_SIZE = 30 // using 20 causes weird behavior in the filter endpoint
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Payload<T>(
|
class ListingVariables(
|
||||||
val operationName: String,
|
val pagination: Pagination,
|
||||||
val variables: T,
|
|
||||||
val query: String,
|
|
||||||
) {
|
) {
|
||||||
constructor(query: Query, variables: T) : this(query.operation, variables, query.body)
|
@EncodeDefault
|
||||||
|
var categoryId: List<String> = emptyList()
|
||||||
|
|
||||||
|
fun encode() = Json.encodeToJsonElement(this) as JsonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Pagination(
|
class Pagination(
|
||||||
val offset: Int,
|
private val offset: Int,
|
||||||
val orderBy: String,
|
|
||||||
@EncodeDefault
|
@EncodeDefault
|
||||||
val status: String = "",
|
var orderBy: OrderBy = OrderBy.DATE_UPDATED,
|
||||||
|
) {
|
||||||
@EncodeDefault
|
@EncodeDefault
|
||||||
val asc: Boolean = true,
|
var status: String = ""
|
||||||
|
|
||||||
@EncodeDefault
|
@EncodeDefault
|
||||||
val limit: Int = Komiic.PAGE_SIZE,
|
private val asc: Boolean = false // this should be true in popular but doesn't take effect in any case
|
||||||
)
|
|
||||||
|
|
||||||
class Variables {
|
@EncodeDefault
|
||||||
val variableMap = mutableMapOf<String, JsonElement>()
|
private val limit: Int = PAGE_SIZE
|
||||||
|
|
||||||
inline fun <reified T> field(key: String, value: T): Variables {
|
@EncodeDefault
|
||||||
variableMap[key] = Json.encodeToJsonElement(value)
|
var sexyLevel: Int? = null
|
||||||
return this
|
}
|
||||||
}
|
|
||||||
|
enum class OrderBy {
|
||||||
fun build() = JsonObject(variableMap)
|
DATE_UPDATED,
|
||||||
|
DATE_CREATED,
|
||||||
|
VIEWS,
|
||||||
|
MONTH_VIEWS,
|
||||||
|
ID,
|
||||||
|
COMIC_DATE_UPDATED,
|
||||||
|
FAVORITE_ADDED,
|
||||||
|
FAVORITE_COUNT,
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonArray
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
|
private fun buildQuery(query: String): String {
|
||||||
|
return query.trimIndent()
|
||||||
|
.replace("#{body}", COMIC_BODY.trimIndent())
|
||||||
|
.replace("%", "$")
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val COMIC_BODY =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
status
|
||||||
|
imageUrl
|
||||||
|
authors {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
private fun buildRequestBody(query: String, variables: JsonObject): RequestBody {
|
||||||
|
val body = buildJsonObject {
|
||||||
|
put("query", query)
|
||||||
|
put("variables", variables)
|
||||||
|
}
|
||||||
|
val contentType = "application/json".toMediaType()
|
||||||
|
return Json.encodeToString(body).toByteArray().toRequestBody(contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseListing(data: DataDto): MangasPage {
|
||||||
|
data.allCategory?.let { categories = it }
|
||||||
|
val listing = data.getListing()
|
||||||
|
val entries = listing.map { it.toSManga() }
|
||||||
|
val hasNextPage = listing.size == PAGE_SIZE
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listingQuery(variables: ListingVariables): RequestBody {
|
||||||
|
if (variables.pagination.orderBy == OrderBy.MONTH_VIEWS) return popularQuery(variables)
|
||||||
|
val query = buildQuery(
|
||||||
|
"""
|
||||||
|
query comicByCategories(%categoryId: [ID!]!, %pagination: Pagination!) {
|
||||||
|
comics: comicByCategories(categoryId: %categoryId, pagination: %pagination) #{body}
|
||||||
|
allCategory { id name }
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return buildRequestBody(query, variables.encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun popularQuery(variables: ListingVariables): RequestBody {
|
||||||
|
if (variables.categoryId.isNotEmpty()) throw Exception("“本月最夯”不能篩選類型")
|
||||||
|
val query = buildQuery(
|
||||||
|
"""
|
||||||
|
query hotComics(%pagination: Pagination!) {
|
||||||
|
comics: hotComics(pagination: %pagination) #{body}
|
||||||
|
allCategory { id name }
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return buildRequestBody(query, variables.encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchQuery(keyword: String): RequestBody {
|
||||||
|
val query = buildQuery(
|
||||||
|
"""
|
||||||
|
query searchComicAndAuthorQuery(%keyword: String!) {
|
||||||
|
searchComicsAndAuthors(keyword: %keyword) {
|
||||||
|
comics #{body}
|
||||||
|
}
|
||||||
|
allCategory { id name }
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
val variables = buildJsonObject {
|
||||||
|
put("keyword", keyword)
|
||||||
|
}
|
||||||
|
return buildRequestBody(query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun idsQuery(id: String): RequestBody {
|
||||||
|
val query = buildQuery(
|
||||||
|
"""
|
||||||
|
query comicByIds(%comicIds: [ID]!) {
|
||||||
|
comics: comicByIds(comicIds: %comicIds) #{body}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
val variables = buildJsonObject {
|
||||||
|
putJsonArray("comicIds") {
|
||||||
|
add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildRequestBody(query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mangaQuery(id: String): RequestBody {
|
||||||
|
val query = buildQuery(
|
||||||
|
"""
|
||||||
|
query chapterByComicId(%comicId: ID!) {
|
||||||
|
comicById(comicId: %comicId) #{body}
|
||||||
|
chaptersByComicId(comicId: %comicId) {
|
||||||
|
id
|
||||||
|
serial
|
||||||
|
type
|
||||||
|
size
|
||||||
|
dateCreated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
val variables = buildJsonObject {
|
||||||
|
put("comicId", id)
|
||||||
|
}
|
||||||
|
return buildRequestBody(query, variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pageListQuery(chapterId: String): RequestBody {
|
||||||
|
val query = buildQuery(
|
||||||
|
"""
|
||||||
|
query imagesByChapterId(%chapterId: ID!) {
|
||||||
|
reachedImageLimit
|
||||||
|
imagesByChapterId(chapterId: %chapterId) {
|
||||||
|
kid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
val variables = buildJsonObject {
|
||||||
|
put("chapterId", chapterId)
|
||||||
|
}
|
||||||
|
return buildRequestBody(query, variables)
|
||||||
|
}
|
@ -1,115 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
|
||||||
|
|
||||||
enum class Query {
|
|
||||||
HOT_COMICS {
|
|
||||||
override val operation = "hotComics"
|
|
||||||
override val body = buildQuery(comicBody) {
|
|
||||||
"""
|
|
||||||
query hotComics(%pagination: Pagination!) {
|
|
||||||
result: hotComics(pagination: %pagination) #{body}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RECENT_UPDATE {
|
|
||||||
override val operation = "recentUpdate"
|
|
||||||
override val body = buildQuery(comicBody) {
|
|
||||||
"""
|
|
||||||
query recentUpdate(%pagination: Pagination!) {
|
|
||||||
result: recentUpdate(pagination: %pagination) #{body}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SEARCH {
|
|
||||||
override val operation = "searchComicAndAuthorQuery"
|
|
||||||
override val body = buildQuery(comicBody) {
|
|
||||||
"""
|
|
||||||
query searchComicAndAuthorQuery(%keyword: String!) {
|
|
||||||
result: searchComicsAndAuthors(keyword: %keyword) {
|
|
||||||
result: comics #{body}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
COMIC_BY_CATEGORIES {
|
|
||||||
override val operation = "comicByCategories"
|
|
||||||
override val body = buildQuery(comicBody) {
|
|
||||||
"""
|
|
||||||
query comicByCategories(%categoryId: [ID!]!, %pagination: Pagination!) {
|
|
||||||
result: comicByCategories(categoryId: %categoryId, pagination: %pagination) #{body}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
COMIC_BY_ID {
|
|
||||||
override val operation = "comicById"
|
|
||||||
override val body = buildQuery(comicBody) {
|
|
||||||
"""
|
|
||||||
query comicById(%comicId: ID!) {
|
|
||||||
result: comicById(comicId: %comicId) #{body}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
CHAPTERS_BY_COMIC_ID {
|
|
||||||
override val operation = "chapterByComicId"
|
|
||||||
override val body = buildQuery {
|
|
||||||
"""
|
|
||||||
query chapterByComicId(%comicId: ID!) {
|
|
||||||
result: chaptersByComicId(comicId: %comicId) {
|
|
||||||
id
|
|
||||||
serial
|
|
||||||
type
|
|
||||||
size
|
|
||||||
dateCreated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
IMAGES_BY_CHAPTER_ID {
|
|
||||||
override val operation = "imagesByChapterId"
|
|
||||||
override val body = buildQuery {
|
|
||||||
"""
|
|
||||||
query imagesByChapterId(%chapterId: ID!) {
|
|
||||||
result1: reachedImageLimit,
|
|
||||||
result2: imagesByChapterId(chapterId: %chapterId) {
|
|
||||||
id
|
|
||||||
kid
|
|
||||||
height
|
|
||||||
width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
}, ;
|
|
||||||
|
|
||||||
abstract val body: String
|
|
||||||
abstract val operation: String
|
|
||||||
val comicBody =
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
id
|
|
||||||
title
|
|
||||||
description
|
|
||||||
status
|
|
||||||
imageUrl
|
|
||||||
authors {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
categories {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
fun buildQuery(body: String = "", queryAction: () -> String): String {
|
|
||||||
return queryAction().trimIndent()
|
|
||||||
.replace("#{body}", body.trimIndent())
|
|
||||||
.replace("%", "$")
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,8 @@ import android.os.Bundle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
const val PREFIX_ID_SEARCH = "id:"
|
||||||
|
|
||||||
class UrlActivity : Activity() {
|
class UrlActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -15,7 +17,7 @@ class UrlActivity : Activity() {
|
|||||||
val id = pathSegments[1]
|
val id = pathSegments[1]
|
||||||
val mainIntent = Intent().apply {
|
val mainIntent = Intent().apply {
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
putExtra("query", "${Komiic.PREFIX_ID_SEARCH}$id")
|
putExtra("query", "$PREFIX_ID_SEARCH$id")
|
||||||
putExtra("filter", packageName)
|
putExtra("filter", packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user