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:
parent
7b43d762e0
commit
f3316c1cbc
|
@ -6,7 +6,7 @@ ext {
|
|||
extName = 'HoneyManga'
|
||||
pkgNameSuffix = 'uk.honeymanga'
|
||||
extClass = '.HoneyManga'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
val url = "$SEARCH_API_URL/v2/manga/pattern".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("query", query)
|
||||
.toString(),
|
||||
headers,
|
||||
)
|
||||
.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue