diff --git a/src/zh/komiic/build.gradle b/src/zh/komiic/build.gradle index eb0dee562..ca5840c2f 100644 --- a/src/zh/komiic/build.gradle +++ b/src/zh/komiic/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Komiic' extClass = '.Komiic' - extVersionCode = 3 + extVersionCode = 4 isNsfw = true } diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Entity.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Entity.kt index b881f291e..82c5b1597 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Entity.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Entity.kt @@ -3,73 +3,86 @@ package eu.kanade.tachiyomi.extension.zh.komiic import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat @Serializable -class Data(val data: Result) +class ResponseDto(private val data: DataDto?, private val errors: List?) { + fun getData() = data ?: throw Exception(errors!!.joinToString("\n") { it.message }) +} @Serializable -class MultiData(val data: MultiResult) +class ErrorDto(val message: String) @Serializable -class Result(val result: T) - -@Serializable -class MultiResult(val result1: T, val result2: V) - -@Serializable -data class Item(val id: String, val name: String) - -@Serializable -data class Comic( - val id: String, - val title: String, - val description: String, - val status: String, - val imageUrl: String, - var authors: List, - val categories: List, +class DataDto( + private val comics: List?, + val allCategory: List?, + private val searchComicsAndAuthors: DataDto?, + val comicById: MangaDto?, + val chaptersByComicId: List?, + val reachedImageLimit: Boolean?, + val imagesByChapterId: List?, ) { + fun getListing(): List = 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, + private val categories: List, +) { + val url get() = "/comic/$id" + 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 = this@Comic.description - status = when (this@Comic.status) { + url = this@MangaDto.url + title = this@MangaDto.title + thumbnail_url = this@MangaDto.imageUrl + author = this@MangaDto.authors.joinToString { it.name } + genre = this@MangaDto.categories.joinToString { it.name } + description = this@MangaDto.description + status = when (this@MangaDto.status) { "ONGOING" -> SManga.ONGOING "END" -> SManga.COMPLETED else -> SManga.UNKNOWN } - initialized = this@Comic.description.isNotEmpty() + initialized = this@MangaDto.description.isNotEmpty() } } @Serializable -data class Chapter( - val id: String, +class ChapterDto( + private val id: String, val serial: String, val type: String, - val size: Int, - val dateCreated: String, + private val size: Int, + private 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 + fun toSChapter(mangaUrl: String, dateFormat: SimpleDateFormat) = SChapter.create().apply { + val (suffix, typeName) = when (val type = this@ChapterDto.type) { + "chapter" -> Pair("話", "連載") + "book" -> Pair("卷", "單行本") + else -> throw Exception("未知章節類型:$type") } - 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 + url = "$mangaUrl/chapter/${this@ChapterDto.id}" + name = "${this@ChapterDto.serial}$suffix(${this@ChapterDto.size}P)" + scanlator = typeName + date_upload = dateFormat.parse(this@ChapterDto.dateCreated)!!.time + chapter_number = this@ChapterDto.serial.toFloatOrNull() ?: -1f } } @Serializable -data class Image( - val id: String, +class PageDto( val kid: String, - val height: Int, - val width: Int, ) diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt index 13970c07b..4afca9a32 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Filters.kt @@ -3,43 +3,50 @@ package eu.kanade.tachiyomi.extension.zh.komiic import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList +var categories: List = emptyList() + fun buildFilterList(): FilterList { - val categories = mapOf( - "1" to "愛情", "3" to "神鬼", "4" to "校園", "5" to "搞笑", "6" to "生活", - "7" to "懸疑", "8" to "冒險", "10" to "職場", "11" to "魔幻", "2" to "後宮", - "12" to "魔法", "13" to "格鬥", "14" to "宅男", "15" to "勵志", "16" to "耽美", - "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 "同人", - ) + val categoryFilter = if (categories.isNotEmpty()) { + CategoryFilter() + } else { + Filter.Header("點擊“重設”載入類型") + } return FilterList( - Filter.Header("過濾條件(搜索關鍵字時無效)"), - CategoryFilter(categories), - StatusFilter(), + Filter.Header("篩選條件(搜索關鍵字時無效)"), + categoryFilter, SortFilter(), + StatusFilter(), + RatingFilter(), ) } +interface KomiicFilter { + fun apply(variables: ListingVariables) +} + class Category(val id: String, name: String) : Filter.CheckBox(name) -class CategoryFilter(categories: Map) : - Filter.Group("類型(篩選同時包含全部所選標簽的漫畫)", categories.map { Category(it.key, it.value) }) { - val selected get() = state.filter(Category::state).map(Category::id) +class CategoryFilter : + Filter.Group("類型(篩選同時包含全部所選標簽的漫畫)", categories.map { Category(it.id, it.name) }), KomiicFilter { + override fun apply(variables: ListingVariables) { + variables.categoryId = state.mapNotNull { if (it.state) it.id else null } + } } -class StatusFilter : Filter.Select("狀態", arrayOf("全部", "連載", "完結")) { - val value get() = arrayOf("", "ONGOING", "END")[state] +class StatusFilter : Filter.Select("狀態", arrayOf("全部", "連載", "完結")), KomiicFilter { + override fun apply(variables: ListingVariables) { + variables.pagination.status = arrayOf("", "ONGOING", "END")[state] + } } -class SortFilter : Filter.Select("排序", arrayOf("更新", "觀看數", "喜愛數")) { - val value get() = arrayOf("DATE_UPDATED", "VIEWS", "FAVORITE_COUNT")[state] +class SortFilter : Filter.Select("排序", arrayOf("更新", "本月觀看數(不能篩選類型)", "觀看數", "喜愛數")), KomiicFilter { + 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("色氣程度", arrayOf("全部", "無", "1", "2", "3", "≥4", "5")), KomiicFilter { + override fun apply(variables: ListingVariables) { + variables.pagination.sexyLevel = arrayOf(null, 0, 1, 2, 3, 4, 5)[state] + } } diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt index ad158cd8c..3a065c552 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Komiic.kt @@ -1,35 +1,48 @@ package eu.kanade.tachiyomi.extension.zh.komiic +import android.util.Base64 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 keiyoushi.utils.firstInstance import keiyoushi.utils.getPreferencesLazy import keiyoushi.utils.parseAs -import keiyoushi.utils.toJsonString -import keiyoushi.utils.tryParse -import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Interceptor import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.RequestBody import okhttp3.Response -import rx.Observable +import java.io.IOException import java.text.SimpleDateFormat import java.util.Locale import java.util.TimeZone class Komiic : HttpSource(), ConfigurableSource { - override var name = "Komiic" + override val name = "Komiic" override val baseUrl = "https://komiic.com" override val lang = "zh" 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().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 preferences by getPreferencesLazy() @@ -40,168 +53,98 @@ class Komiic : HttpSource(), ConfigurableSource { // 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 SChapter.id get() = url.substringAfterLast("/") - private inline fun Payload.toRequestBody() = this.toJsonString().toRequestBody(JSON_MEDIA_TYPE) - /** - * 根據 ID 搜索漫畫 - * 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>() - 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 { - // 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.result } - // return limit - // } + private fun RequestBody.request() = POST(apiUrl, headers, this) + private fun Response.parse() = parseAs().getData() // Popular Manga =============================================================================== override fun popularMangaRequest(page: Int): Request { - val pagination = Pagination((page - 1) * PAGE_SIZE, "MONTH_VIEWS") - val variables = Variables().field("pagination", pagination).build() - val payload = Payload(Query.HOT_COMICS, variables) - return POST(apiUrl, headers, payload.toRequestBody()) + val pagination = Pagination((page - 1) * PAGE_SIZE, OrderBy.MONTH_VIEWS) + return listingQuery(ListingVariables(pagination)).request() } - override fun popularMangaParse(response: Response): MangasPage { - val res = response.parseAs>>() - val comics = res.data.result - return MangasPage(comics.map(Comic::toSManga), comics.size == PAGE_SIZE) - } + override fun popularMangaParse(response: Response) = parseListing(response.parse()) // Latest Updates ============================================================================== override fun latestUpdatesRequest(page: Int): Request { - val pagination = Pagination((page - 1) * PAGE_SIZE, "DATE_UPDATED") - val variables = Variables().field("pagination", pagination).build() - val payload = Payload(Query.RECENT_UPDATE, variables) - return POST(apiUrl, headers, payload.toRequestBody()) + val pagination = Pagination((page - 1) * PAGE_SIZE, OrderBy.DATE_UPDATED) + return listingQuery(ListingVariables(pagination)).request() } - override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + override fun latestUpdatesParse(response: Response) = parseListing(response.parse()) // Search Manga ================================================================================ override fun getFilterList() = buildFilterList() override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - if (query.isNotBlank()) { - val variables = Variables().field("keyword", query).build() - val payload = Payload(Query.SEARCH, variables) - return POST(apiUrl, headers, payload.toRequestBody()) - } else { - val categories = filters.firstInstance() - val status = filters.firstInstance() - val sort = filters.firstInstance() - 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>>>() - 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 { - if (query.startsWith(PREFIX_ID_SEARCH)) { - return client.newCall(comicByIDRequest(query.substringAfter(PREFIX_ID_SEARCH))) - .asObservableSuccess().map(::parseComicByID) + return if (query.startsWith(PREFIX_ID_SEARCH)) { + idsQuery(query.removePrefix(PREFIX_ID_SEARCH)).request() } 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 =============================================================================== 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 { - val res = response.parseAs>() - return res.data.result.toSManga() - } + override fun mangaDetailsParse(response: Response) = response.parse().comicById!!.toSManga() // Chapter List ================================================================================ override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url + "/images/all" - override fun chapterListRequest(manga: SManga): 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 chapterListRequest(manga: SManga) = mangaQuery(manga.id).request() override fun chapterListParse(response: Response): List { - val res = response.parseAs>>() - val comics = res.data.result.sortedWith( - compareByDescending { it.type } + val data = response.parse() + val chapters = data.chaptersByComicId!!.toMutableList() + when (preferences.getString(CHAPTER_FILTER_PREF, "all")!!) { + "chapter" -> chapters.retainAll { it.type == "chapter" } + "book" -> chapters.retainAll { it.type == "book" } + else -> {} + } + chapters.sortWith( + compareByDescending { 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 mangaUrl = data.comicById!!.url + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("UTC") } - val comicUrl = response.request.url.fragment!! - return items.map { it.toSChapter(comicUrl, DATE_FORMAT::tryParse) } + return chapters.map { it.toSChapter(mangaUrl, dateFormat) } } // Page List =================================================================================== override fun pageListRequest(chapter: SChapter): Request { - val variables = Variables().field("chapterId", chapter.id).build() - val payload = Payload(Query.IMAGES_BY_CHAPTER_ID, variables) - return POST("$apiUrl#${chapter.url}", headers, payload.toRequestBody()) + return pageListQuery(chapter.id).request().newBuilder() + .tag(String::class.java, chapter.url) + .build() } override fun pageListParse(response: Response): List { - val res = response.parseAs>>() + val data = response.parse() val check = preferences.getBoolean(CHECK_API_LIMIT_PREF, true) - check(!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}") + if (check && data.reachedImageLimit!!) { + throw Exception("今日圖片讀取次數已達上限,請登录或明天再來!") + } + 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 { 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) .build() } diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt index 0628f6b54..f086b8ecb 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Payload.kt @@ -3,38 +3,48 @@ 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 +const val PAGE_SIZE = 30 // using 20 causes weird behavior in the filter endpoint + @Serializable -data class Payload( - val operationName: String, - val variables: T, - val query: String, +class ListingVariables( + val pagination: Pagination, ) { - constructor(query: Query, variables: T) : this(query.operation, variables, query.body) + @EncodeDefault + var categoryId: List = emptyList() + + fun encode() = Json.encodeToJsonElement(this) as JsonObject } +@Suppress("unused") @Serializable -data class Pagination( - val offset: Int, - val orderBy: String, +class Pagination( + private val offset: Int, @EncodeDefault - val status: String = "", + var orderBy: OrderBy = OrderBy.DATE_UPDATED, +) { @EncodeDefault - val asc: Boolean = true, + var status: String = "" + @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 { - val variableMap = mutableMapOf() + @EncodeDefault + private val limit: Int = PAGE_SIZE - inline fun field(key: String, value: T): Variables { - variableMap[key] = Json.encodeToJsonElement(value) - return this - } - - fun build() = JsonObject(variableMap) + @EncodeDefault + var sexyLevel: Int? = null +} + +enum class OrderBy { + DATE_UPDATED, + DATE_CREATED, + VIEWS, + MONTH_VIEWS, + ID, + COMIC_DATE_UPDATED, + FAVORITE_ADDED, + FAVORITE_COUNT, } diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Queries.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Queries.kt new file mode 100644 index 000000000..378db14b8 --- /dev/null +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Queries.kt @@ -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) +} diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Query.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Query.kt deleted file mode 100644 index 7f2beb88a..000000000 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/Query.kt +++ /dev/null @@ -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("%", "$") - } -} diff --git a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/UrlActivity.kt b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/UrlActivity.kt index 54be52071..3e2239b9f 100644 --- a/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/UrlActivity.kt +++ b/src/zh/komiic/src/eu/kanade/tachiyomi/extension/zh/komiic/UrlActivity.kt @@ -7,6 +7,8 @@ import android.os.Bundle import android.util.Log import kotlin.system.exitProcess +const val PREFIX_ID_SEARCH = "id:" + class UrlActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -15,7 +17,7 @@ class UrlActivity : Activity() { val id = pathSegments[1] val mainIntent = Intent().apply { action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${Komiic.PREFIX_ID_SEARCH}$id") + putExtra("query", "$PREFIX_ID_SEARCH$id") putExtra("filter", packageName) }