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'
|
extName = 'HoneyManga'
|
||||||
pkgNameSuffix = 'uk.honeymanga'
|
pkgNameSuffix = 'uk.honeymanga'
|
||||||
extClass = '.HoneyManga'
|
extClass = '.HoneyManga'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package eu.kanade.tachiyomi.extension.uk.honeymanga
|
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.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.HoneyMangaDto
|
||||||
import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.HoneyMangaResponseDto
|
import eu.kanade.tachiyomi.extension.uk.honeymanga.dtos.HoneyMangaResponseDto
|
||||||
import eu.kanade.tachiyomi.network.GET
|
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.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@ -37,112 +35,104 @@ class HoneyManga : HttpSource() {
|
||||||
override val lang = "uk"
|
override val lang = "uk"
|
||||||
override val supportsLatest = true
|
override val supportsLatest = true
|
||||||
|
|
||||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
.add("Origin", baseUrl)
|
.add("Origin", baseUrl)
|
||||||
.add("Referer", 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)
|
.rateLimitHost(API_URL.toHttpUrl(), 10)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// ----- requests -----
|
// ============================== Popular ===============================
|
||||||
|
override fun popularMangaRequest(page: Int) = makeHoneyMangaRequest(page, "likes")
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaParse(response: Response) = parseAsMangaResponseDto(response)
|
||||||
return POST(
|
|
||||||
"$API_URL/v2/manga/cursor-list",
|
|
||||||
headers,
|
|
||||||
makeHoneyMangaRequestBody(page, "likes"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
// =============================== Latest ===============================
|
||||||
return POST(
|
override fun latestUpdatesRequest(page: Int) = makeHoneyMangaRequest(page, "lastUpdated")
|
||||||
"$API_URL/v2/manga/cursor-list",
|
|
||||||
headers,
|
|
||||||
makeHoneyMangaRequestBody(page, "lastUpdated"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) = parseAsMangaResponseDto(response)
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
if (query.length >= 3) {
|
if (query.length >= 3) {
|
||||||
return GET(
|
val url = "$SEARCH_API_URL/v2/manga/pattern".toHttpUrl().newBuilder()
|
||||||
"$SEARCH_API_URL/api/v1/title/search-matching".toHttpUrl()
|
.addQueryParameter("query", query)
|
||||||
.newBuilder()
|
.build()
|
||||||
.addQueryParameter("query", query)
|
return GET(url, headers)
|
||||||
.toString(),
|
|
||||||
headers,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedOperationException("Запит має містити щонайменше 3 символи / The query must contain at least 3 characters")
|
throw UnsupportedOperationException("Запит має містити щонайменше 3 символи / The query must contain at least 3 characters")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override fun searchMangaParse(response: Response) = parseAsMangaResponseArray(response)
|
||||||
val url = "$API_URL/chapter".toHttpUrl().newBuilder()
|
|
||||||
.addQueryParameter("mangaId", manga.url.substringAfterLast('/'))
|
// =========================== Manga Details ============================
|
||||||
.addQueryParameter("sortOrder", "DESC")
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
.addQueryParameter("page", "1")
|
val mangaId = manga.url.substringAfterLast('/')
|
||||||
.addQueryParameter("pageSize", "10000") // most likely there will not be any more pageSize
|
val url = "$API_URL/manga/$mangaId"
|
||||||
.build().toString()
|
|
||||||
return GET(url, headers)
|
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 {
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
val chapterId = chapter.url.substringBeforeLast('/').substringAfterLast('/')
|
val chapterId = chapter.url.substringBeforeLast('/').substringAfterLast('/')
|
||||||
val url = "$API_URL/chapter/frames/$chapterId"
|
val url = "$API_URL/chapter/frames/$chapterId"
|
||||||
return GET(url, headers)
|
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> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
return response.asClass<HoneyMangaChapterPagesDto>().resourceIds.toList().map {
|
val data = response.asClass<HoneyMangaChapterPagesDto>()
|
||||||
Page(it.first.toInt(), "", "$IMAGE_STORAGE_URL/${it.second.jsonPrimitive.content}")
|
return data.resourceIds.map { (page, imageId) ->
|
||||||
|
Page(page.toInt(), "", "$IMAGE_STORAGE_URL/$imageId")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = ""
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
|
|
||||||
override fun imageRequest(page: Page): Request = GET(page.imageUrl!!, headers)
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseAsMangaResponseDto(response: Response): MangasPage {
|
private fun parseAsMangaResponseDto(response: Response): MangasPage {
|
||||||
val mangaList = response.asClass<HoneyMangaResponseDto>().data
|
val mangaList = response.asClass<HoneyMangaResponseDto>().data
|
||||||
return makeMangasPage(mangaList)
|
return makeMangasPage(mangaList)
|
||||||
|
@ -153,60 +143,8 @@ class HoneyManga : HttpSource() {
|
||||||
return makeMangasPage(mangaList)
|
return makeMangasPage(mangaList)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun makeMangasPage(mangaList: List<HoneyMangaDto>): MangasPage {
|
private fun makeHoneyMangaRequest(page: Int, sortBy: String): Request {
|
||||||
return MangasPage(
|
val body = buildJsonObject {
|
||||||
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 {
|
|
||||||
put("page", page)
|
put("page", page)
|
||||||
put("pageSize", DEFAULT_PAGE_SIZE)
|
put("pageSize", DEFAULT_PAGE_SIZE)
|
||||||
putJsonObject("sort") {
|
putJsonObject("sort") {
|
||||||
|
@ -214,5 +152,47 @@ class HoneyManga : HttpSource() {
|
||||||
put("sortOrder", "DESC")
|
put("sortOrder", "DESC")
|
||||||
}
|
}
|
||||||
}.toString().toRequestBody(JSON_MEDIA_TYPE)
|
}.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
|
package eu.kanade.tachiyomi.extension.uk.honeymanga.dtos
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HoneyMangaDto(
|
data class HoneyMangaDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val posterId: String,
|
val posterId: String,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CompleteHoneyMangaDto(
|
||||||
|
val id: String,
|
||||||
|
val posterId: String,
|
||||||
|
val title: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
val authors: List<String>? = null,
|
||||||
|
val artists: List<String>? = null,
|
||||||
|
val genresAndTags: List<String>? = null,
|
||||||
|
val titleStatus: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -20,7 +30,12 @@ data class HoneyMangaResponseDto(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HoneyMangaChapterPagesDto(
|
data class HoneyMangaChapterPagesDto(
|
||||||
val id: String,
|
val id: String,
|
||||||
val resourceIds: JsonObject,
|
val resourceIds: Map<String, String>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HoneyMangaChapterResponseDto(
|
||||||
|
val data: List<HoneyMangaChapterDto>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
Loading…
Reference in New Issue