From f3316c1cbc8cfd726b131353051ac0885c21ccb4 Mon Sep 17 00:00:00 2001 From: Claudemirovsky <63046606+Claudemirovsky@users.noreply.github.com> Date: Thu, 28 Dec 2023 16:24:35 -0300 Subject: [PATCH] fix(uk/honeymanga): Fix chapter list (#19456) * fix: Fix chapter list * feat: Update search API * refactor: Use URLs from constants * refactor: Minor refactoration * feat: Show more info in manga details page * chore: Bump version * fix: Apply suggestion - Use URL Builder in search Thx alessandrojean! Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> --------- Co-authored-by: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> --- src/uk/honeymanga/build.gradle | 2 +- .../extension/uk/honeymanga/HoneyManga.kt | 250 ++++++++---------- .../uk/honeymanga/dtos/HoneyMangaDto.kt | 19 +- 3 files changed, 133 insertions(+), 138 deletions(-) diff --git a/src/uk/honeymanga/build.gradle b/src/uk/honeymanga/build.gradle index 01d946be1..3671bf50b 100644 --- a/src/uk/honeymanga/build.gradle +++ b/src/uk/honeymanga/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'HoneyManga' pkgNameSuffix = 'uk.honeymanga' extClass = '.HoneyManga' - extVersionCode = 1 + extVersionCode = 2 isNsfw = true } diff --git a/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/HoneyManga.kt b/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/HoneyManga.kt index c3a9a5b4d..21665f264 100644 --- a/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/HoneyManga.kt +++ b/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/HoneyManga.kt @@ -1,7 +1,8 @@ package eu.kanade.tachiyomi.extension.uk.honeymanga -import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.HoneyMangaChapterDto +import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.CompleteHoneyMangaDto import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.HoneyMangaChapterPagesDto +import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.HoneyMangaChapterResponseDto import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.HoneyMangaDto import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.HoneyMangaResponseDto import eu.kanade.tachiyomi.network.GET @@ -13,20 +14,17 @@ 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.json.Json import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject -import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient +import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response -import rx.Observable +import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat import java.util.Locale @@ -37,112 +35,104 @@ class HoneyManga : HttpSource() { override val lang = "uk" override val supportsLatest = true - override fun headersBuilder(): Headers.Builder = Headers.Builder() + override fun headersBuilder() = super.headersBuilder() .add("Origin", baseUrl) .add("Referer", baseUrl) - .add("User-Agent", userAgent) - override val client: OkHttpClient = network.client.newBuilder() + override val client = network.client.newBuilder() .rateLimitHost(API_URL.toHttpUrl(), 10) .build() - // ----- requests ----- + // ============================== Popular =============================== + override fun popularMangaRequest(page: Int) = makeHoneyMangaRequest(page, "likes") - override fun popularMangaRequest(page: Int): Request { - return POST( - "$API_URL/v2/manga/cursor-list", - headers, - makeHoneyMangaRequestBody(page, "likes"), - ) - } + override fun popularMangaParse(response: Response) = parseAsMangaResponseDto(response) - override fun latestUpdatesRequest(page: Int): Request { - return POST( - "$API_URL/v2/manga/cursor-list", - headers, - makeHoneyMangaRequestBody(page, "lastUpdated"), - ) - } + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = makeHoneyMangaRequest(page, "lastUpdated") + override fun latestUpdatesParse(response: Response) = parseAsMangaResponseDto(response) + + // =============================== Search =============================== override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { if (query.length >= 3) { - return GET( - "$SEARCH_API_URL/api/v1/title/search-matching".toHttpUrl() - .newBuilder() - .addQueryParameter("query", query) - .toString(), - headers, - ) + val url = "$SEARCH_API_URL/v2/manga/pattern".toHttpUrl().newBuilder() + .addQueryParameter("query", query) + .build() + return GET(url, headers) } else { throw UnsupportedOperationException("Запит має містити щонайменше 3 символи / The query must contain at least 3 characters") } } - override fun chapterListRequest(manga: SManga): Request { - val url = "$API_URL/chapter".toHttpUrl().newBuilder() - .addQueryParameter("mangaId", manga.url.substringAfterLast('/')) - .addQueryParameter("sortOrder", "DESC") - .addQueryParameter("page", "1") - .addQueryParameter("pageSize", "10000") // most likely there will not be any more pageSize - .build().toString() + override fun searchMangaParse(response: Response) = parseAsMangaResponseArray(response) + + // =========================== Manga Details ============================ + override fun mangaDetailsRequest(manga: SManga): Request { + val mangaId = manga.url.substringAfterLast('/') + val url = "$API_URL/manga/$mangaId" return GET(url, headers) } - override fun mangaDetailsRequest(manga: SManga): Request = mangaDetailsRequest(manga.url) + override fun mangaDetailsParse(response: Response) = SManga.create().apply { + val mangaDto = response.asClass() + title = mangaDto.title + thumbnail_url = "$IMAGE_STORAGE_URL/${mangaDto.posterId}" + url = "$baseUrl/book/${mangaDto.id}" + description = mangaDto.description + genre = mangaDto.genresAndTags?.joinToString() + artist = mangaDto.artists?.joinToString() + author = mangaDto.authors?.joinToString() + status = when (mangaDto.titleStatus.orEmpty()) { + "Онгоінг" -> SManga.ONGOING + "Завершено" -> SManga.COMPLETED + "Покинуто" -> SManga.CANCELLED + "Призупинено" -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + } + // ============================== Chapters ============================== + override fun chapterListRequest(manga: SManga): Request { + val url = "$API_URL/v2/chapter/cursor-list" + val body = buildJsonObject { + put("mangaId", manga.url.substringAfterLast('/')) + put("sortOrder", "DESC") + put("page", "1") + put("pageSize", "10000") // most likely there will not be any more pageSize + }.toString().toRequestBody(JSON_MEDIA_TYPE) + return POST(url, headers, body) + } + + override fun chapterListParse(response: Response): List { + val result = response.asClass() + return result.data.map { + val suffix = if (it.subChapterNum == 0) "" else ".${it.subChapterNum}" + SChapter.create().apply { + url = "$baseUrl/read/${it.id}/${it.mangaId}" + name = "Vol. ${it.volume} Ch. ${it.chapterNum}$suffix" + date_upload = it.lastUpdated.toDate() + } + } + } + + // =============================== Pages ================================ override fun pageListRequest(chapter: SChapter): Request { val chapterId = chapter.url.substringBeforeLast('/').substringAfterLast('/') val url = "$API_URL/chapter/frames/$chapterId" return GET(url, headers) } - // ----- parse ----- - - override fun popularMangaParse(response: Response): MangasPage = parseAsMangaResponseDto(response) - - override fun latestUpdatesParse(response: Response): MangasPage = parseAsMangaResponseDto(response) - - override fun searchMangaParse(response: Response): MangasPage = parseAsMangaResponseArray(response) - - override fun mangaDetailsParse(response: Response): SManga { - return makeSManga(response.asClass()) - } - - override fun chapterListParse(response: Response): List { - val result = response.asClass>() - return result.map { - SChapter.create().apply { - url = "$baseUrl/read/${it.id}/${it.mangaId}" - name = "Vol. ${it.volume} Ch. ${it.chapterNum}" + (if (it.subChapterNum == 0) "" else ".${it.subChapterNum}") - date_upload = parseDate(it.lastUpdated) - } - } - } - override fun pageListParse(response: Response): List { - return response.asClass().resourceIds.toList().map { - Page(it.first.toInt(), "", "$IMAGE_STORAGE_URL/${it.second.jsonPrimitive.content}") + val data = response.asClass() + return data.resourceIds.map { (page, imageId) -> + Page(page.toInt(), "", "$IMAGE_STORAGE_URL/$imageId") } } override fun imageUrlParse(response: Response): String = "" - override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers) - - override fun getMangaUrl(manga: SManga): String = manga.url - - override fun getChapterUrl(chapter: SChapter): String = chapter.url - - override fun fetchImageUrl(page: Page): Observable = Observable.just(page.imageUrl!!) - - // ----- private methods ----- - - private fun mangaDetailsRequest(mangaUrl: String): Request { - val mangaId = mangaUrl.substringAfterLast('/') - val url = "https://data.api.honey-manga.com.ua/manga/$mangaId" - return GET(url, headers) - } - + // ============================= Utilities ============================== private fun parseAsMangaResponseDto(response: Response): MangasPage { val mangaList = response.asClass().data return makeMangasPage(mangaList) @@ -153,60 +143,8 @@ class HoneyManga : HttpSource() { return makeMangasPage(mangaList) } - private fun makeMangasPage(mangaList: List): MangasPage { - return MangasPage( - makeSMangaList(mangaList), - mangaList.size == DEFAULT_PAGE_SIZE, - ) - } - - private fun makeSMangaList(mangaList: List): List { - return mangaList.map(::makeSManga) - } - - private fun makeSManga(mangaDto: HoneyMangaDto): SManga { - return SManga.create().apply { - title = mangaDto.title - thumbnail_url = - "https://manga-storage.fra1.digitaloceanspaces.com/public-resources/${mangaDto.posterId}" - url = "$baseUrl/book/${mangaDto.id}" - description = mangaDto.description - genre = mangaDto.type - } - } - - companion object { - - // utils and constants - - private const val API_URL = "https://data.api.honey-manga.com.ua" - - private const val SEARCH_API_URL = "https://search.api.honey-manga.com.ua" - - private const val IMAGE_STORAGE_URL = "https://manga-storage.fra1.digitaloceanspaces.com/public-resources" - - private val userAgent = System.getProperty("http.agent")!! - - private val DATE_FORMATTER by lazy { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT) - } - - private fun parseDate(dateString: String): Long = runCatching { DATE_FORMATTER.parse(dateString)?.time }.getOrNull() ?: 0L - - private const val DEFAULT_PAGE_SIZE = 30 - - private val json by lazy { - Json { - isLenient = true - ignoreUnknownKeys = true - } - } - - private inline fun Response.asClass() = use { json.decodeFromString(body.string()) } - - private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull() - - private fun makeHoneyMangaRequestBody(page: Int, sortBy: String) = buildJsonObject { + private fun makeHoneyMangaRequest(page: Int, sortBy: String): Request { + val body = buildJsonObject { put("page", page) put("pageSize", DEFAULT_PAGE_SIZE) putJsonObject("sort") { @@ -214,5 +152,47 @@ class HoneyManga : HttpSource() { put("sortOrder", "DESC") } }.toString().toRequestBody(JSON_MEDIA_TYPE) + + return POST("$API_URL/v2/manga/cursor-list", headers, body) + } + + private fun makeMangasPage(mangaList: List): MangasPage { + return MangasPage( + mangaList.map(::makeSManga), + mangaList.size == DEFAULT_PAGE_SIZE, + ) + } + + private fun makeSManga(mangaDto: HoneyMangaDto) = SManga.create().apply { + title = mangaDto.title + thumbnail_url = "$IMAGE_STORAGE_URL/${mangaDto.posterId}" + url = "$baseUrl/book/${mangaDto.id}" + } + + companion object { + private const val API_URL = "https://data.api.honey-manga.com.ua" + + private const val SEARCH_API_URL = "https://search.api.honey-manga.com.ua" + + private const val IMAGE_STORAGE_URL = "https://manga-storage.fra1.digitaloceanspaces.com/public-resources" + + private const val DEFAULT_PAGE_SIZE = 30 + + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT) + } + + private fun String.toDate(): Long { + return runCatching { DATE_FORMATTER.parse(this)?.time } + .getOrNull() ?: 0L + } + + private val json: Json by injectLazy() + + private inline fun Response.asClass(): T = use { + json.decodeFromStream(it.body.byteStream()) + } } } diff --git a/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/dtos/HoneyMangaDto.kt b/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/dtos/HoneyMangaDto.kt index c2ab1ab4b..06ce9c87b 100644 --- a/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/dtos/HoneyMangaDto.kt +++ b/src/uk/honeymanga/src/eu/kanade/tachiyomi/extension/uk/honeymanga/dtos/HoneyMangaDto.kt @@ -1,15 +1,25 @@ package eu.kanade.tachiyomi.extension.uk.honeymanga.dtos import kotlinx.serialization.Serializable -import kotlinx.serialization.json.JsonObject @Serializable data class HoneyMangaDto( val id: String, val posterId: String, val title: String, +) + +@Serializable +data class CompleteHoneyMangaDto( + val id: String, + val posterId: String, + val title: String, val description: String?, val type: String, + val authors: List? = null, + val artists: List? = null, + val genresAndTags: List? = null, + val titleStatus: String? = null, ) @Serializable @@ -20,7 +30,12 @@ data class HoneyMangaResponseDto( @Serializable data class HoneyMangaChapterPagesDto( val id: String, - val resourceIds: JsonObject, + val resourceIds: Map, +) + +@Serializable +data class HoneyMangaChapterResponseDto( + val data: List, ) @Serializable