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>
This commit is contained in:
Claudemirovsky 2023-12-28 16:24:35 -03:00 committed by GitHub
parent 7b43d762e0
commit f3316c1cbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 133 additions and 138 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'HoneyManga'
pkgNameSuffix = 'uk.honeymanga'
extClass = '.HoneyManga'
extVersionCode = 1
extVersionCode = 2
isNsfw = true
}

View File

@ -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<CompleteHoneyMangaDto>()
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<SChapter> {
val result = response.asClass<HoneyMangaChapterResponseDto>()
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<SChapter> {
val result = response.asClass<List<HoneyMangaChapterDto>>()
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<Page> {
return response.asClass<HoneyMangaChapterPagesDto>().resourceIds.toList().map {
Page(it.first.toInt(), "", "$IMAGE_STORAGE_URL/${it.second.jsonPrimitive.content}")
val data = response.asClass<HoneyMangaChapterPagesDto>()
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<String> = 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<HoneyMangaResponseDto>().data
return makeMangasPage(mangaList)
@ -153,60 +143,8 @@ class HoneyManga : HttpSource() {
return makeMangasPage(mangaList)
}
private fun makeMangasPage(mangaList: List<HoneyMangaDto>): MangasPage {
return MangasPage(
makeSMangaList(mangaList),
mangaList.size == DEFAULT_PAGE_SIZE,
)
}
private fun makeSMangaList(mangaList: List<HoneyMangaDto>): List<SManga> {
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 <reified R : Any> Response.asClass() = use { json.decodeFromString<R>(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<HoneyMangaDto>): 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 <reified T> Response.asClass(): T = use {
json.decodeFromStream(it.body.byteStream())
}
}
}

View File

@ -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<String>? = null,
val artists: List<String>? = null,
val genresAndTags: List<String>? = 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<String, String>,
)
@Serializable
data class HoneyMangaChapterResponseDto(
val data: List<HoneyMangaChapterDto>,
)
@Serializable