Komiic: Add manga description and refactor some code (#9445)
* add comic description * fix manga search results missing descriptions * clean unused variables * clean unused class * Add some config options and refactor some code * refactor some code * modify config option summary * apply comments * modify Queries.kt * small modification * Format code * replace parse method * optimize check API limit * modify config summary * add getChapterUrl()
This commit is contained in:
parent
3070ed4967
commit
2d0e57517e
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Komiic'
|
||||
extClass = '.Komiic'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -1,109 +1,65 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import keiyoushi.utils.getPreferences
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.toJsonString
|
||||
import keiyoushi.utils.tryParse
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class Komiic : HttpSource() {
|
||||
// Override variables
|
||||
class Komiic : HttpSource(), ConfigurableSource {
|
||||
override var name = "Komiic"
|
||||
override val baseUrl = "https://komiic.com"
|
||||
override val lang = "zh"
|
||||
override val supportsLatest = true
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
private val apiUrl = "$baseUrl/api/query"
|
||||
private val preferences = getPreferences()
|
||||
|
||||
// Variables
|
||||
private val queryAPIUrl = "$baseUrl/api/query"
|
||||
private val json: Json by injectLazy()
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
preferencesInternal(screen.context).forEach(screen::addPreference)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析漫畫列表
|
||||
* Parse comic list
|
||||
*/
|
||||
private inline fun <reified T : ComicListResult> parseComicList(response: Response): MangasPage {
|
||||
val res = response.parseAs<Data<T>>()
|
||||
val comics = res.data.comics
|
||||
// Customize ===================================================================================
|
||||
|
||||
val entries = comics.map { comic ->
|
||||
comic.toSManga()
|
||||
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")
|
||||
}
|
||||
|
||||
val hasNextPage = comics.size == PAGE_SIZE
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
// Hot Comic
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val payload = Payload(
|
||||
operationName = "hotComics",
|
||||
variables = HotComicsVariables(
|
||||
pagination = MangaListPagination(
|
||||
PAGE_SIZE,
|
||||
(page - 1) * PAGE_SIZE,
|
||||
"MONTH_VIEWS",
|
||||
"",
|
||||
true,
|
||||
),
|
||||
),
|
||||
query = QUERY_HOT_COMICS,
|
||||
).toJsonRequestBody()
|
||||
return POST(queryAPIUrl, headers, payload)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = parseComicList<HotComicsResponse>(response)
|
||||
|
||||
// Recent update
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val payload = Payload(
|
||||
operationName = "recentUpdate",
|
||||
variables = RecentUpdateVariables(
|
||||
pagination = MangaListPagination(
|
||||
PAGE_SIZE,
|
||||
(page - 1) * PAGE_SIZE,
|
||||
"DATE_UPDATED",
|
||||
"",
|
||||
true,
|
||||
),
|
||||
),
|
||||
query = QUERY_RECENT_UPDATE,
|
||||
).toJsonRequestBody()
|
||||
return POST(queryAPIUrl, headers, payload)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = parseComicList<RecentUpdateResponse>(response)
|
||||
private val SManga.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)
|
||||
|
||||
/**
|
||||
* 根據 ID 搜索漫畫
|
||||
* Search the comic based on the ID.
|
||||
*/
|
||||
private fun comicByIDRequest(id: String): Request {
|
||||
val payload = Payload(
|
||||
operationName = "comicById",
|
||||
variables = ComicByIdVariables(id),
|
||||
query = QUERY_COMIC_BY_ID,
|
||||
).toJsonRequestBody()
|
||||
return POST(queryAPIUrl, headers, payload)
|
||||
val variables = Variables().set("comicId", id).build()
|
||||
val payload = Payload("comicById", variables, QUERY_COMIC_BY_ID)
|
||||
return POST(apiUrl, headers, payload.toRequestBody())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -111,32 +67,64 @@ class Komiic : HttpSource() {
|
||||
* Parse the comic based on the ID.
|
||||
*/
|
||||
private fun parseComicByID(response: Response): MangasPage {
|
||||
val res = response.parseAs<Data<ComicByIDResponse>>()
|
||||
val entries = mutableListOf<SManga>()
|
||||
val comic = res.data.comic.toSManga()
|
||||
entries.add(comic)
|
||||
val hasNextPage = entries.size == PAGE_SIZE
|
||||
return MangasPage(entries, hasNextPage)
|
||||
val res = response.parseAs<Data<Comic>>()
|
||||
val entries = listOf(res.data.result.toSManga())
|
||||
return MangasPage(entries, false)
|
||||
}
|
||||
|
||||
// Search
|
||||
/**
|
||||
* 檢查 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 ===============================================================================
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val variables = Variables().set(
|
||||
"pagination",
|
||||
Pagination((page - 1) * PAGE_SIZE, "MONTH_VIEWS"),
|
||||
).build()
|
||||
val payload = Payload("hotComics", variables, QUERY_HOT_COMICS)
|
||||
return POST(apiUrl, headers, payload.toRequestBody())
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val res = response.parseAs<Data<List<Comic>>>()
|
||||
val comics = res.data.result
|
||||
return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// Latest Updates ==============================================================================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val variables = Variables().set(
|
||||
"pagination",
|
||||
Pagination((page - 1) * PAGE_SIZE, "DATE_UPDATED"),
|
||||
).build()
|
||||
val payload = Payload("recentUpdate", variables, QUERY_RECENT_UPDATE)
|
||||
return POST(apiUrl, headers, payload.toRequestBody())
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// Search Manga ================================================================================
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val payload = Payload(
|
||||
operationName = "searchComicAndAuthorQuery",
|
||||
variables = SearchVariables(query),
|
||||
query = QUERY_SEARCH,
|
||||
).toJsonRequestBody()
|
||||
return POST(queryAPIUrl, headers, payload)
|
||||
val variables = Variables().set("keyword", query).build()
|
||||
val payload = Payload("searchComicAndAuthorQuery", variables, QUERY_SEARCH)
|
||||
return POST(apiUrl, headers, payload.toRequestBody())
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> {
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_ID_SEARCH)) {
|
||||
val mangaId = query.substringAfter(PREFIX_ID_SEARCH)
|
||||
client.newCall(comicByIDRequest(mangaId))
|
||||
client.newCall(comicByIDRequest(query.substringAfter(PREFIX_ID_SEARCH)))
|
||||
.asObservableSuccess()
|
||||
.map(::parseComicByID)
|
||||
} else {
|
||||
@ -145,116 +133,68 @@ class Komiic : HttpSource() {
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val res = response.parseAs<Data<SearchResponse>>()
|
||||
val comics = res.data.action.comics
|
||||
|
||||
val entries = comics.map { comic ->
|
||||
comic.toSManga()
|
||||
}
|
||||
|
||||
val hasNextPage = comics.size == PAGE_SIZE
|
||||
return MangasPage(entries, hasNextPage)
|
||||
val res = response.parseAs<Data<Result<List<Comic>>>>()
|
||||
val comics = res.data.result.result
|
||||
return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// Comic details
|
||||
override fun mangaDetailsRequest(manga: SManga) = comicByIDRequest(manga.url.substringAfterLast("/"))
|
||||
// Manga Details ===============================================================================
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = comicByIDRequest(manga.id)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val res = response.parseAs<Data<ComicByIDResponse>>()
|
||||
val comic = res.data.comic.toSManga()
|
||||
return comic
|
||||
val res = response.parseAs<Data<Comic>>()
|
||||
return res.data.result.toSManga()
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}"
|
||||
// Chapter List ================================================================================
|
||||
|
||||
/**
|
||||
* 解析日期
|
||||
* Parse date
|
||||
*/
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return try {
|
||||
DATE_FORMAT.parse(dateStr)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
e.printStackTrace()
|
||||
0L
|
||||
}
|
||||
}
|
||||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + "/images/all"
|
||||
|
||||
// Chapter list
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val payload = Payload(
|
||||
operationName = "chapterByComicId",
|
||||
variables = ChapterByComicIdVariables(manga.url.substringAfterLast("/")),
|
||||
query = QUERY_CHAPTER,
|
||||
).toJsonRequestBody()
|
||||
|
||||
return POST("$queryAPIUrl#${manga.url}", headers, payload)
|
||||
val variables = Variables().set("comicId", manga.id).build()
|
||||
val payload = Payload("chapterByComicId", variables, QUERY_CHAPTER)
|
||||
return POST("$apiUrl#${manga.url}", headers, payload.toRequestBody())
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val res = response.parseAs<Data<ChaptersResponse>>()
|
||||
val comics = res.data.chapters
|
||||
val comicUrl = response.request.url.fragment
|
||||
|
||||
val tChapters = comics.filter { it.type == "chapter" }
|
||||
val tBooks = comics.filter { it.type == "book" }
|
||||
|
||||
val entries = (tChapters + tBooks).map { chapter ->
|
||||
SChapter.create().apply {
|
||||
url = "$comicUrl/chapter/${chapter.id}/page/1"
|
||||
name = when (chapter.type) {
|
||||
"chapter" -> "第 ${chapter.serial} 話"
|
||||
"book" -> "第 ${chapter.serial} 卷"
|
||||
else -> chapter.serial
|
||||
}
|
||||
date_upload = parseDate(chapter.dateCreated)
|
||||
chapter_number = chapter.serial.toFloatOrNull() ?: -1f
|
||||
}
|
||||
}.reversed()
|
||||
|
||||
return entries
|
||||
val res = response.parseAs<Data<List<Chapter>>>()
|
||||
val comics = res.data.result.sortedWith(
|
||||
compareByDescending<Chapter> { it.type }
|
||||
.thenByDescending { it.serial.toFloatOrNull() },
|
||||
)
|
||||
val display = preferences.getString(CHAPTER_FILTER_PREF, "all")
|
||||
val items = when (display) {
|
||||
"chapter" -> comics.filter { it.type == "chapter" }
|
||||
"book" -> comics.filter { it.type == "book" }
|
||||
else -> comics
|
||||
}
|
||||
val comicUrl = response.request.url.fragment!!
|
||||
return items.map { it.toSChapter(comicUrl, DATE_FORMAT::tryParse) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 檢查 API 是否達到上限
|
||||
* Check if the API has reached its limit.
|
||||
*
|
||||
* (Idk how to throw an exception in reading page)
|
||||
*/
|
||||
// private fun fetchAPILimit(): Boolean {
|
||||
// val payload = Payload("getImageLimit", "", QUERY_API_LIMIT).toJsonRequestBody()
|
||||
// val response = client.newCall(POST(queryAPIUrl, headers, payload)).execute()
|
||||
// val limit = response.parseAs<APILimitData>().getImageLimit
|
||||
// return limit.limit <= limit.usage
|
||||
// }
|
||||
// Page List ===================================================================================
|
||||
|
||||
// Page list
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val payload = Payload(
|
||||
operationName = "imagesByChapterId",
|
||||
variables = ImagesByChapterIdVariables(
|
||||
chapter.url.substringAfter("/chapter/").substringBefore("/page/"),
|
||||
),
|
||||
query = QUERY_PAGE_LIST,
|
||||
).toJsonRequestBody()
|
||||
|
||||
return POST("$queryAPIUrl#${chapter.url}", headers, payload)
|
||||
val variables = Variables().set("chapterId", chapter.id).build()
|
||||
val payload = Payload("imagesByChapterId", variables, QUERY_PAGE_LIST)
|
||||
return POST("$apiUrl#${chapter.url}", headers, payload.toRequestBody())
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val res = response.parseAs<Data<ImagesResponse>>()
|
||||
val pages = res.data.images
|
||||
val chapterUrl = response.request.url.toString().split("#")[1]
|
||||
|
||||
return pages.mapIndexed { index, image ->
|
||||
Page(
|
||||
index,
|
||||
"${chapterUrl.substringBeforeLast("/")}/$index",
|
||||
"$baseUrl/api/image/${image.kid}",
|
||||
)
|
||||
val res = response.parseAs<MultiData<Boolean, List<Image>>>()
|
||||
val check = preferences.getBoolean(CHECK_API_LIMIT_PREF, true)
|
||||
require(!check || !res.data.result1) { "今日圖片讀取次數已達上限,請登录或明天再來!" }
|
||||
val chapterUrl = response.request.url.fragment!!
|
||||
return res.data.result2.mapIndexed { index, image ->
|
||||
Page(index, "$chapterUrl/page/$index", "$baseUrl/api/image/${image.kid}")
|
||||
}
|
||||
}
|
||||
|
||||
// Image =======================================================================================
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return super.imageRequest(page).newBuilder()
|
||||
.addHeader("accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8'")
|
||||
@ -263,23 +203,4 @@ class Komiic : HttpSource() {
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
private inline fun <reified T> String.parseAs(): T =
|
||||
json.decodeFromString(this)
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
use { body.string() }.parseAs()
|
||||
|
||||
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =
|
||||
json.encodeToString(this)
|
||||
.toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
companion object {
|
||||
private 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +1,38 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
|
||||
@Serializable
|
||||
class Payload<T>(
|
||||
data class Payload<T>(
|
||||
val operationName: String,
|
||||
val variables: T,
|
||||
val query: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaListPagination(
|
||||
val limit: Int,
|
||||
data class Pagination(
|
||||
val offset: Int,
|
||||
val orderBy: String,
|
||||
val status: String,
|
||||
val asc: Boolean,
|
||||
@EncodeDefault
|
||||
val limit: Int = Komiic.PAGE_SIZE,
|
||||
@EncodeDefault
|
||||
val status: String = "",
|
||||
@EncodeDefault
|
||||
val asc: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HotComicsVariables(
|
||||
val pagination: MangaListPagination,
|
||||
)
|
||||
class Variables {
|
||||
val variableMap = mutableMapOf<String, JsonElement>()
|
||||
|
||||
@Serializable
|
||||
data class RecentUpdateVariables(
|
||||
val pagination: MangaListPagination,
|
||||
)
|
||||
inline fun <reified T> set(key: String, value: T): Variables {
|
||||
variableMap[key] = Json.encodeToJsonElement(value)
|
||||
return this
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SearchVariables(
|
||||
val keyword: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComicByIdVariables(
|
||||
val comicId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterByComicIdVariables(
|
||||
val comicId: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ImagesByChapterIdVariables(
|
||||
val chapterId: String,
|
||||
)
|
||||
fun build() = JsonObject(variableMap)
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
|
||||
const val CHAPTER_FILTER_PREF = "CHAPTER_FILTER"
|
||||
const val CHECK_API_LIMIT_PREF = "CHECK_API_LIMIT"
|
||||
|
||||
fun preferencesInternal(context: Context) = arrayOf(
|
||||
ListPreference(context).apply {
|
||||
key = CHAPTER_FILTER_PREF
|
||||
title = "章節列表顯示"
|
||||
summary = "注:部分漫畫小概率會有章節缺失,僅顯示章節就沒法通過看卷的內容來補充了。建議不要僅顯示卷,會導致無法獲取及時章節更新"
|
||||
entries = arrayOf("同時顯示卷和章節", "僅顯示章節", "僅顯示卷")
|
||||
entryValues = arrayOf("all", "chapter", "book")
|
||||
setDefaultValue("all")
|
||||
},
|
||||
SwitchPreferenceCompat(context).apply {
|
||||
key = CHECK_API_LIMIT_PREF
|
||||
title = "自動檢查API受限"
|
||||
summary = "點擊單個章節請求漫畫圖片時,自動檢查一次圖片API是否達到今日請求上限。若已達上限,則終止後續操作"
|
||||
setDefaultValue(true)
|
||||
},
|
||||
)
|
@ -1,187 +1,90 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||
|
||||
private fun buildQuery(queryAction: () -> String): String {
|
||||
return queryAction()
|
||||
.trimIndent()
|
||||
private fun buildQuery(body: String = "", queryAction: () -> String): String {
|
||||
return queryAction().trimIndent()
|
||||
.replace("#{body}", body.trimIndent())
|
||||
.replace("%", "$")
|
||||
}
|
||||
|
||||
val QUERY_HOT_COMICS: String = buildQuery {
|
||||
const val COMIC_BODY =
|
||||
"""
|
||||
query hotComics(%pagination: Pagination!) {
|
||||
hotComics(pagination: %pagination) {
|
||||
id
|
||||
title
|
||||
status
|
||||
year
|
||||
imageUrl
|
||||
authors {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
dateUpdated
|
||||
monthViews
|
||||
views
|
||||
favoriteCount
|
||||
lastBookUpdate
|
||||
lastChapterUpdate
|
||||
__typename
|
||||
}
|
||||
}
|
||||
{
|
||||
id
|
||||
title
|
||||
description
|
||||
status
|
||||
imageUrl
|
||||
authors {
|
||||
id
|
||||
name
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
val QUERY_HOT_COMICS = buildQuery(COMIC_BODY) {
|
||||
"""
|
||||
query hotComics(%pagination: Pagination!) {
|
||||
result: hotComics(pagination: %pagination) #{body}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val QUERY_RECENT_UPDATE: String = buildQuery {
|
||||
val QUERY_RECENT_UPDATE = buildQuery(COMIC_BODY) {
|
||||
"""
|
||||
query recentUpdate(%pagination: Pagination!) {
|
||||
recentUpdate(pagination: %pagination) {
|
||||
id
|
||||
title
|
||||
status
|
||||
year
|
||||
imageUrl
|
||||
authors {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
dateUpdated
|
||||
monthViews
|
||||
views
|
||||
favoriteCount
|
||||
lastBookUpdate
|
||||
lastChapterUpdate
|
||||
__typename
|
||||
}
|
||||
}
|
||||
query recentUpdate(%pagination: Pagination!) {
|
||||
result: recentUpdate(pagination: %pagination) #{body}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val QUERY_SEARCH: String = buildQuery {
|
||||
val QUERY_SEARCH = buildQuery(COMIC_BODY) {
|
||||
"""
|
||||
query searchComicAndAuthorQuery(%keyword: String!) {
|
||||
searchComicsAndAuthors(keyword: %keyword) {
|
||||
comics {
|
||||
id
|
||||
title
|
||||
status
|
||||
year
|
||||
imageUrl
|
||||
authors {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
dateUpdated
|
||||
monthViews
|
||||
views
|
||||
favoriteCount
|
||||
lastBookUpdate
|
||||
lastChapterUpdate
|
||||
__typename
|
||||
}
|
||||
authors {
|
||||
id
|
||||
name
|
||||
chName
|
||||
enName
|
||||
wikiLink
|
||||
comicCount
|
||||
views
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
query searchComicAndAuthorQuery(%keyword: String!) {
|
||||
result: searchComicsAndAuthors(keyword: %keyword) {
|
||||
result: comics #{body}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val QUERY_CHAPTER: String = buildQuery {
|
||||
val QUERY_COMIC_BY_ID = buildQuery(COMIC_BODY) {
|
||||
"""
|
||||
query chapterByComicId(%comicId: ID!) {
|
||||
chaptersByComicId(comicId: %comicId) {
|
||||
id
|
||||
serial
|
||||
type
|
||||
dateCreated
|
||||
dateUpdated
|
||||
size
|
||||
__typename
|
||||
}
|
||||
}
|
||||
query comicById(%comicId: ID!) {
|
||||
result: comicById(comicId: %comicId) #{body}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val QUERY_COMIC_BY_ID = buildQuery {
|
||||
val QUERY_CHAPTER = buildQuery {
|
||||
"""
|
||||
query comicById(%comicId: ID!) {
|
||||
comicById(comicId: %comicId) {
|
||||
id
|
||||
title
|
||||
status
|
||||
year
|
||||
imageUrl
|
||||
authors {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
categories {
|
||||
id
|
||||
name
|
||||
__typename
|
||||
}
|
||||
dateCreated
|
||||
dateUpdated
|
||||
views
|
||||
favoriteCount
|
||||
lastBookUpdate
|
||||
lastChapterUpdate
|
||||
__typename
|
||||
}
|
||||
}
|
||||
query chapterByComicId(%comicId: ID!) {
|
||||
result: chaptersByComicId(comicId: %comicId) {
|
||||
id
|
||||
serial
|
||||
type
|
||||
size
|
||||
dateCreated
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val QUERY_PAGE_LIST = buildQuery {
|
||||
"""
|
||||
query imagesByChapterId(%chapterId: ID!) {
|
||||
imagesByChapterId(chapterId: %chapterId) {
|
||||
id
|
||||
kid
|
||||
height
|
||||
width
|
||||
__typename
|
||||
}
|
||||
}
|
||||
query imagesByChapterId(%chapterId: ID!) {
|
||||
result1: reachedImageLimit,
|
||||
result2: imagesByChapterId(chapterId: %chapterId) {
|
||||
id
|
||||
kid
|
||||
height
|
||||
width
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val QUERY_API_LIMIT = buildQuery {
|
||||
"""
|
||||
query getImageLimit {
|
||||
getImageLimit {
|
||||
limit
|
||||
usage
|
||||
resetInSeconds
|
||||
__typename
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
// val QUERY_API_LIMIT = buildQuery { "query reachedImageLimit { result: reachedImageLimit }" }
|
||||
|
@ -1,141 +1,70 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Data<T>(val data: T)
|
||||
|
||||
interface ComicListResult {
|
||||
val comics: List<Comic>
|
||||
}
|
||||
class Data<T>(val data: Result<T>)
|
||||
|
||||
@Serializable
|
||||
data class HotComicsResponse(
|
||||
@SerialName("hotComics") override val comics: List<Comic>,
|
||||
) : ComicListResult
|
||||
class MultiData<T, V>(val data: MultiResult<T, V>)
|
||||
|
||||
@Serializable
|
||||
data class RecentUpdateResponse(
|
||||
@SerialName("recentUpdate") override val comics: List<Comic>,
|
||||
) : ComicListResult
|
||||
|
||||
interface SearchResult {
|
||||
val action: ComicsAndAuthors
|
||||
}
|
||||
class Result<T>(val result: T)
|
||||
|
||||
@Serializable
|
||||
data class SearchResponse(
|
||||
@SerialName("searchComicsAndAuthors") override val action: ComicsAndAuthors,
|
||||
) : SearchResult
|
||||
class MultiResult<T, V>(val result1: T, val result2: V)
|
||||
|
||||
@Serializable
|
||||
data class ComicsAndAuthors(
|
||||
val comics: List<Comic>,
|
||||
val authors: List<Author>,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
)
|
||||
|
||||
interface ComicResult {
|
||||
val comic: Comic
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ComicByIDResponse(
|
||||
@SerialName("comicById") override val comic: Comic,
|
||||
) : ComicResult
|
||||
data class ComicItem(val id: String, val name: String)
|
||||
|
||||
@Serializable
|
||||
data class Comic(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val status: String,
|
||||
val year: Int,
|
||||
val imageUrl: String,
|
||||
var authors: List<ComicAuthor>,
|
||||
val categories: List<ComicCategory>,
|
||||
val dateCreated: String = "",
|
||||
val dateUpdated: String,
|
||||
val monthViews: Int = 0,
|
||||
val views: Int,
|
||||
val favoriteCount: Int,
|
||||
val lastBookUpdate: String,
|
||||
val lastChapterUpdate: String,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
var authors: List<ComicItem>,
|
||||
val categories: List<ComicItem>,
|
||||
) {
|
||||
private val parseStatus = when (status) {
|
||||
"ONGOING" -> SManga.ONGOING
|
||||
"END" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = "/comic/$id"
|
||||
title = this@Comic.title
|
||||
thumbnail_url = this@Comic.imageUrl
|
||||
author = this@Comic.authors.joinToString { it.name }
|
||||
genre = this@Comic.categories.joinToString { it.name }
|
||||
description = buildString {
|
||||
append("年份: $year | ")
|
||||
append("點閱: ${simplifyNumber(views)} | ")
|
||||
append("喜愛: ${simplifyNumber(favoriteCount)}\n")
|
||||
description = this@Comic.description
|
||||
status = when (this@Comic.status) {
|
||||
"ONGOING" -> SManga.ONGOING
|
||||
"END" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
status = parseStatus
|
||||
initialized = true
|
||||
initialized = this@Comic.description.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ComicCategory(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComicAuthor(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Author(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val chName: String,
|
||||
val enName: String,
|
||||
val wikiLink: String,
|
||||
val comicCount: Int,
|
||||
val views: Int,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
)
|
||||
|
||||
interface ChaptersResult {
|
||||
val chapters: List<Chapter>
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChaptersResponse(
|
||||
@SerialName("chaptersByComicId") override val chapters: List<Chapter>,
|
||||
) : ChaptersResult
|
||||
|
||||
@Serializable
|
||||
data class Chapter(
|
||||
val id: String,
|
||||
val serial: String,
|
||||
val type: String,
|
||||
val dateCreated: String,
|
||||
val dateUpdated: String,
|
||||
val size: Int,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ImagesResponse(
|
||||
@SerialName("imagesByChapterId") val images: List<Image>,
|
||||
)
|
||||
val dateCreated: String,
|
||||
) {
|
||||
fun toSChapter(comicUrl: String, parseDate: (String) -> Long) = SChapter.create().apply {
|
||||
url = "$comicUrl/chapter/${this@Chapter.id}"
|
||||
name = when (this@Chapter.type) {
|
||||
"chapter" -> "第 ${this@Chapter.serial} 話"
|
||||
"book" -> "第 ${this@Chapter.serial} 卷"
|
||||
else -> this@Chapter.serial
|
||||
}
|
||||
scanlator = "${this@Chapter.size}P"
|
||||
date_upload = parseDate(this@Chapter.dateCreated)
|
||||
chapter_number = if (this@Chapter.type == "book") 0F else this@Chapter.serial.toFloatOrNull() ?: -1f
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Image(
|
||||
@ -143,18 +72,4 @@ data class Image(
|
||||
val kid: String,
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class APILimitData(
|
||||
@SerialName("getImageLimit") val getImageLimit: APILimit,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class APILimit(
|
||||
val limit: Int,
|
||||
val usage: Int,
|
||||
val resetInSeconds: String,
|
||||
@SerialName("__typename") val typeName: String,
|
||||
)
|
||||
|
@ -1,15 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.zh.komiic
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* 簡化數字顯示
|
||||
*/
|
||||
fun simplifyNumber(num: Int): String {
|
||||
return when {
|
||||
abs(num) < 1000 -> "$num"
|
||||
abs(num) < 10000 -> "${num / 1000}千"
|
||||
abs(num) < 100000000 -> "${num / 10000}萬"
|
||||
else -> "${num / 100000000}億"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user