Kagane: Fix chapters error and search (#10819)
* fix extension * modify url Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com> --------- Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
This commit is contained in:
parent
e7e9bc349d
commit
e533814cc9
@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Kagane'
|
extName = 'Kagane'
|
||||||
extClass = '.Kagane'
|
extClass = '.Kagane'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,77 +9,100 @@ import java.text.SimpleDateFormat
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class BookDto(
|
class SearchDto(
|
||||||
val id: String,
|
val content: List<Book>,
|
||||||
val name: String,
|
val last: Boolean,
|
||||||
val source: String,
|
) {
|
||||||
val metadata: MetadataDto,
|
fun hasNextPage() = !last
|
||||||
val booksMetadata: BooksMetadataDto,
|
|
||||||
|
@Serializable
|
||||||
|
class Book(
|
||||||
|
val name: String,
|
||||||
|
val id: String,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toSManga(domain: String): SManga = SManga.create().apply {
|
||||||
|
title = name
|
||||||
|
url = id
|
||||||
|
thumbnail_url = "$domain/api/v1/series/$id/thumbnail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class DetailsDto(
|
||||||
|
val data: Data,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
class MetadataDto(
|
class Data(
|
||||||
val genres: List<String>,
|
val metadata: Metadata,
|
||||||
val status: String,
|
val source: String,
|
||||||
val summary: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class BooksMetadataDto(
|
|
||||||
val authors: List<AuthorDto>,
|
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
class AuthorDto(
|
class Metadata(
|
||||||
val name: String,
|
val genres: List<String>,
|
||||||
val role: String,
|
val status: String,
|
||||||
)
|
val summary: String,
|
||||||
}
|
val alternateTitles: List<Title>,
|
||||||
|
) {
|
||||||
fun toSManga(domain: String): SManga = SManga.create().apply {
|
@Serializable
|
||||||
title = name
|
class Title(
|
||||||
url = "/series/$id"
|
val title: String,
|
||||||
description = buildString {
|
)
|
||||||
append(metadata.summary)
|
|
||||||
append("\n\n")
|
|
||||||
append("Source: ")
|
|
||||||
append(source)
|
|
||||||
}
|
}
|
||||||
thumbnail_url = "https://api.$domain/api/v1/series/$id/thumbnail"
|
|
||||||
author = getRoles(listOf("writer"))
|
|
||||||
artist = getRoles(listOf("inker", "colorist", "penciller"))
|
|
||||||
genre = metadata.genres.joinToString()
|
|
||||||
status = metadata.status.toStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.toStatus(): Int {
|
fun toSManga(): SManga = SManga.create().apply {
|
||||||
return when (this) {
|
val summary = StringBuilder()
|
||||||
"ONGOING" -> SManga.ONGOING
|
summary.append(metadata.summary)
|
||||||
"ENDED" -> SManga.COMPLETED
|
.append("\n\n")
|
||||||
else -> SManga.COMPLETED
|
.append("Source: ")
|
||||||
|
.append(source)
|
||||||
|
|
||||||
|
if (metadata.alternateTitles.isNotEmpty()) {
|
||||||
|
summary.append("\n\nAssociated Name(s):")
|
||||||
|
metadata.alternateTitles.forEach { summary.append("\n").append("• ${it.title}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
description = summary.toString()
|
||||||
|
genre = metadata.genres.joinToString()
|
||||||
|
status = metadata.status.toStatus()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRoles(roles: List<String>): String {
|
private fun String.toStatus(): Int {
|
||||||
return booksMetadata.authors
|
return when (this) {
|
||||||
.filter { roles.contains(it.role) }
|
"ONGOING" -> SManga.ONGOING
|
||||||
.joinToString { it.name }
|
else -> SManga.COMPLETED
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ChapterDto(
|
class ChapterDto(
|
||||||
val id: String,
|
val data: Data,
|
||||||
val metadata: MetadataDto,
|
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
class MetadataDto(
|
class Data(
|
||||||
val releaseDate: String? = null,
|
val content: List<Book>,
|
||||||
val title: String,
|
) {
|
||||||
)
|
@Serializable
|
||||||
|
class Book(
|
||||||
|
val metadata: Metadata,
|
||||||
|
val id: String,
|
||||||
|
val seriesId: String,
|
||||||
|
val created: String,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class Metadata(
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
fun toSChapter(seriesId: String): SChapter = SChapter.create().apply {
|
fun toSChapter(): SChapter = SChapter.create().apply {
|
||||||
url = "$seriesId;$id"
|
url = "$seriesId;$id"
|
||||||
name = metadata.title
|
name = metadata.title
|
||||||
date_upload = dateFormat.tryParse(metadata.releaseDate)
|
date_upload = dateFormat.tryParse(created)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -93,11 +116,20 @@ class ChapterDto(
|
|||||||
class ChallengeDto(
|
class ChallengeDto(
|
||||||
@SerialName("access_token")
|
@SerialName("access_token")
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
@SerialName("page_count")
|
|
||||||
val pageCount: Int,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class PaginationDto(
|
class PagesCountDto(
|
||||||
val hasNext: Boolean,
|
val data: Data,
|
||||||
)
|
) {
|
||||||
|
@Serializable
|
||||||
|
class Data(
|
||||||
|
val media: PagesCount,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class PagesCount(
|
||||||
|
@SerialName("pagesCount")
|
||||||
|
val pagesCount: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -17,13 +17,13 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
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 eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import keiyoushi.utils.getPreferencesLazy
|
import keiyoushi.utils.getPreferencesLazy
|
||||||
import keiyoushi.utils.parseAs
|
import keiyoushi.utils.parseAs
|
||||||
import keiyoushi.utils.toJsonString
|
import keiyoushi.utils.toJsonString
|
||||||
@ -47,9 +47,6 @@ import java.nio.ByteOrder
|
|||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.collections.forEach
|
|
||||||
import kotlin.getValue
|
|
||||||
import kotlin.text.split
|
|
||||||
|
|
||||||
class Kagane : HttpSource(), ConfigurableSource {
|
class Kagane : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
@ -61,7 +58,7 @@ class Kagane : HttpSource(), ConfigurableSource {
|
|||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
override val supportsLatest = false
|
override val supportsLatest = true
|
||||||
|
|
||||||
private val preferences by getPreferencesLazy()
|
private val preferences by getPreferencesLazy()
|
||||||
|
|
||||||
@ -141,76 +138,70 @@ class Kagane : HttpSource(), ConfigurableSource {
|
|||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList())
|
||||||
return GET("$baseUrl/?page=$page", headers)
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
|
||||||
return pageListParse(response, "initialSeriesData")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pageListParse(response: Response, key: String): MangasPage {
|
|
||||||
val data = response.asJsoup().selectFirst("script:containsData($key)")!!.data()
|
|
||||||
|
|
||||||
val mangaList = data.getNextData(key)
|
|
||||||
.parseAs<List<BookDto>>()
|
|
||||||
.map { it.toSManga(domain) }
|
|
||||||
|
|
||||||
val pagination = data.getNextData("pagination", isList = false, selectFirst = false)
|
|
||||||
.parseAs<PaginationDto>()
|
|
||||||
|
|
||||||
return MangasPage(mangaList, pagination.hasNext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
throw UnsupportedOperationException()
|
searchMangaRequest(page, "", FilterList(SortFilter(0)))
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
val body = buildJsonObject { }
|
||||||
addQueryParameter("name", query)
|
.toJsonString()
|
||||||
addQueryParameter("page", page.toString())
|
.toRequestBody("application/json".toMediaType())
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
val url = "$apiUrl/api/v1/search".toHttpUrl().newBuilder().apply {
|
||||||
|
addQueryParameter("page", (page - 1).toString())
|
||||||
|
addQueryParameter("mature", preferences.showNsfw.toString())
|
||||||
|
addQueryParameter("size", 35.toString()) // Default items per request
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
addQueryParameter("name", query)
|
||||||
|
}
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is SortFilter -> {
|
||||||
|
addQueryParameter("sort", filter.toUriPart())
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return POST(url.toString(), headers, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
return pageListParse(response, "ssrData")
|
val dto = response.parseAs<SearchDto>()
|
||||||
|
val mangas = dto.content.map { it.toSManga(apiUrl) }
|
||||||
|
return MangasPage(mangas, hasNextPage = dto.hasNextPage())
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================== Manga Details ============================
|
// =========================== Manga Details ============================
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
val data = response.asJsoup().selectFirst("script:containsData(initialSeriesData)")!!.data()
|
val dto = response.parseAs<DetailsDto>()
|
||||||
.getNextData("initialSeriesData", isList = false)
|
return dto.data.toSManga()
|
||||||
|
}
|
||||||
|
|
||||||
return data.parseAs<BookDto>().toSManga(domain)
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
return GET("$apiUrl/api/v1/series/${manga.url}", apiHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================== Chapters ==============================
|
// ============================== Chapters ==============================
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val seriesId = response.request.url.pathSegments.last()
|
val dto = response.parseAs<ChapterDto>()
|
||||||
val data = response.asJsoup().selectFirst("script:containsData(initialBooksData)")!!.data()
|
return dto.data.content.map { it.toSChapter() }.reversed()
|
||||||
.getNextData("initialBooksData")
|
|
||||||
.parseAs<List<ChapterDto>>()
|
|
||||||
.reversed()
|
|
||||||
|
|
||||||
return data.map { it.toSChapter(seriesId) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter): String {
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
val (seriesId, chapterId) = chapter.url.split(";")
|
return GET("$apiUrl/api/v1/books/${manga.url}", apiHeaders)
|
||||||
return "$baseUrl/series/$seriesId/reader/$chapterId"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================== Pages ================================
|
// =============================== Pages ================================
|
||||||
@ -231,7 +222,8 @@ class Kagane : HttpSource(), ConfigurableSource {
|
|||||||
|
|
||||||
val challengeResp = getChallengeResponse(seriesId, chapterId)
|
val challengeResp = getChallengeResponse(seriesId, chapterId)
|
||||||
accessToken = challengeResp.accessToken
|
accessToken = challengeResp.accessToken
|
||||||
val pages = (0 until challengeResp.pageCount).map { page ->
|
val pageCount = getPageCountResponse(seriesId, chapterId)
|
||||||
|
val pages = (0 until pageCount).map { page ->
|
||||||
val pageUrl = "$apiUrl/api/v1/books".toHttpUrl().newBuilder().apply {
|
val pageUrl = "$apiUrl/api/v1/books".toHttpUrl().newBuilder().apply {
|
||||||
addPathSegment(seriesId)
|
addPathSegment(seriesId)
|
||||||
addPathSegment("file")
|
addPathSegment("file")
|
||||||
@ -358,6 +350,15 @@ class Kagane : HttpSource(), ConfigurableSource {
|
|||||||
.parseAs<ChallengeDto>()
|
.parseAs<ChallengeDto>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getPageCountResponse(seriesId: String, chapterId: String): Int {
|
||||||
|
val challengeUrl = "$apiUrl/api/v1/books/$seriesId/metadata/$chapterId"
|
||||||
|
|
||||||
|
val dto = client.newCall(GET(challengeUrl, apiHeaders)).execute()
|
||||||
|
.parseAs<PagesCountDto>()
|
||||||
|
|
||||||
|
return dto.data.media.pagesCount
|
||||||
|
}
|
||||||
|
|
||||||
private fun concat(vararg arrays: ByteArray): ByteArray =
|
private fun concat(vararg arrays: ByteArray): ByteArray =
|
||||||
arrays.reduce { acc, bytes -> acc + bytes }
|
arrays.reduce { acc, bytes -> acc + bytes }
|
||||||
|
|
||||||
@ -413,27 +414,36 @@ class Kagane : HttpSource(), ConfigurableSource {
|
|||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
private fun String.getNextData(key: String, isList: Boolean = true, selectFirst: Boolean = true): String {
|
|
||||||
val (startDel, endDel) = if (isList) '[' to ']' else '{' to '}'
|
|
||||||
|
|
||||||
val keyIndex = if (selectFirst) this.indexOf(key) else this.lastIndexOf(key)
|
|
||||||
val start = this.indexOf(startDel, keyIndex)
|
|
||||||
|
|
||||||
var depth = 1
|
|
||||||
var i = start + 1
|
|
||||||
|
|
||||||
while (i < this.length && depth > 0) {
|
|
||||||
when (this[i]) {
|
|
||||||
startDel -> depth++
|
|
||||||
endDel -> depth--
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\"${this.substring(start, i)}\"".parseAs<String>()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SHOW_NSFW_KEY = "pref_show_nsfw"
|
private const val SHOW_NSFW_KEY = "pref_show_nsfw"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================= Filters ==============================
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
SortFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter(state: Int = 0) : UriPartFilter(
|
||||||
|
"Sort By",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Latest", "updated_at"),
|
||||||
|
Pair("Latest Descending", "updated_at,desc"),
|
||||||
|
Pair("By Name", "series_name"),
|
||||||
|
Pair("By Name Descending", "series_name,desc"),
|
||||||
|
Pair("Books count", "books_count"),
|
||||||
|
Pair("Books count Descending", "books_count,desc"),
|
||||||
|
Pair("Created at", "created_at"),
|
||||||
|
Pair("Created at Descending", "created_at,desc"),
|
||||||
|
),
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
|
||||||
|
open class UriPartFilter(
|
||||||
|
displayName: String,
|
||||||
|
private val vals: Array<Pair<String, String>>,
|
||||||
|
state: Int = 0,
|
||||||
|
) : Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
|
||||||
|
fun toUriPart() = vals[state].second
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user