Taiyo: Fix popularManga, search and chapterList (#5963)
* Fix popularManga, search and chapterList * Typo * Change limit per page * Remove useless code * Cleanup * Fix chapter date_upload
This commit is contained in:
parent
a038b718b1
commit
0fc23e3cb0
|
@ -13,7 +13,7 @@
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data
|
||||||
android:host="www.taiyo.moe"
|
android:host="taiyo.moe"
|
||||||
android:pathPattern="/media/..*"
|
android:pathPattern="/media/..*"
|
||||||
android:scheme="https" />
|
android:scheme="https" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Taiyō'
|
extName = 'Taiyō'
|
||||||
extClass = '.Taiyo'
|
extClass = '.Taiyo'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension.pt.taiyo
|
||||||
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.AdditionalInfoDto
|
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.AdditionalInfoDto
|
||||||
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.ChapterListDto
|
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.ChapterListDto
|
||||||
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.MediaChapterDto
|
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.MediaChapterDto
|
||||||
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.ResponseDto
|
|
||||||
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.SearchResultDto
|
import eu.kanade.tachiyomi.extension.pt.taiyo.dto.SearchResultDto
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
@ -19,6 +18,8 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
|
@ -39,11 +40,12 @@ class Taiyo : ParsedHttpSource() {
|
||||||
|
|
||||||
override val name = "Taiyō"
|
override val name = "Taiyō"
|
||||||
|
|
||||||
override val baseUrl = "https://www.taiyo.moe"
|
override val baseUrl = "https://taiyo.moe"
|
||||||
|
|
||||||
override val lang = "pt-BR"
|
override val lang = "pt-BR"
|
||||||
|
|
||||||
override val supportsLatest = true
|
// The source doesn't show the title on the home page
|
||||||
|
override val supportsLatest = false
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.client.newBuilder()
|
||||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||||
|
@ -53,34 +55,41 @@ class Taiyo : ParsedHttpSource() {
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
|
var bearerToken = ""
|
||||||
|
|
||||||
override fun popularMangaSelector() = "main > div.flex > div.overflow-hidden div.flex > a"
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
if (bearerToken.isBlank()) {
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
getBearerToken()
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
}
|
||||||
thumbnail_url = element.selectFirst("div.overflow-hidden > img")?.getImageUrl()
|
return searchMangaRequest(page, "", FilterList())
|
||||||
title = element.selectFirst("p")!!.text()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
override fun popularMangaSelector() = throw UnsupportedOperationException()
|
||||||
|
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
override fun popularMangaNextPageSelector() = null
|
override fun popularMangaNextPageSelector() = null
|
||||||
|
|
||||||
|
private fun getBearerToken() {
|
||||||
|
val scriptUrl = client.newCall(GET(baseUrl, headers))
|
||||||
|
.execute().asJsoup()
|
||||||
|
.selectFirst("script[src*=ee07d8437723d9f5]")
|
||||||
|
?.attr("src") ?: throw Exception("Não foi possivel localizar o token")
|
||||||
|
|
||||||
|
val script = client.newCall(GET("$baseUrl$scriptUrl", headers))
|
||||||
|
.execute().body.string()
|
||||||
|
|
||||||
|
bearerToken = TOKEN_REGEX.find(script)?.groups?.get("token")?.value
|
||||||
|
?: throw Exception("Não foi possivel extrair o token")
|
||||||
|
}
|
||||||
|
|
||||||
// =============================== Latest ===============================
|
// =============================== Latest ===============================
|
||||||
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
|
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||||
|
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
|
||||||
override fun latestUpdatesSelector() = "main div.grow div.flex:has(div.grow)"
|
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
|
|
||||||
with(element.selectFirst("a.line-clamp-1")!!) {
|
|
||||||
setUrlWithoutDomain(attr("href"))
|
|
||||||
title = text()
|
|
||||||
}
|
|
||||||
thumbnail_url = element.selectFirst("img")?.getImageUrl()?.replace("&w=128", "&w=256")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = null
|
override fun latestUpdatesNextPageSelector() = null
|
||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||||
val id = query.removePrefix(PREFIX_SEARCH)
|
val id = query.removePrefix(PREFIX_SEARCH)
|
||||||
|
@ -98,31 +107,48 @@ class Taiyo : ParsedHttpSource() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val limit = 21
|
||||||
|
|
||||||
val jsonObj = buildJsonObject {
|
val jsonObj = buildJsonObject {
|
||||||
putJsonObject("0") {
|
put(
|
||||||
putJsonObject("json") {
|
"queries",
|
||||||
put("title", query)
|
buildJsonArray {
|
||||||
}
|
add(
|
||||||
}
|
buildJsonObject {
|
||||||
|
put("indexUid", "medias")
|
||||||
|
put("q", query)
|
||||||
|
put("filter", buildJsonArray { add("deletedAt IS NULL") })
|
||||||
|
put("limit", limit)
|
||||||
|
put("offset", limit * (page - 1))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val requestBody = json.encodeToString(jsonObj).toRequestBody(MEDIA_TYPE)
|
val requestBody = json.encodeToString(jsonObj).toRequestBody(MEDIA_TYPE)
|
||||||
|
|
||||||
return POST("$baseUrl/api/trpc/medias.search?batch=1", headers, requestBody)
|
val apiHeaders = headers.newBuilder()
|
||||||
|
.set("Authorization", "Bearer $bearerToken")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("https://meilisearch.${baseUrl.substringAfterLast("/")}/multi-search", apiHeaders, requestBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val obj = response.parseAs<List<ResponseDto<List<SearchResultDto>>>>().first()
|
val obj = response.parseAs<SearchResultDto>()
|
||||||
val mangas = obj.data.map { item ->
|
val mangas = obj.mangas.map { item ->
|
||||||
SManga.create().apply {
|
SManga.create().apply {
|
||||||
url = "/media/${item.id}"
|
url = "/media/${item.id}"
|
||||||
title = item.title
|
title = item.titles.firstOrNull { it.language.contains("en") }?.title
|
||||||
|
?: item.titles.maxByOrNull { it.priority }!!.title
|
||||||
|
|
||||||
thumbnail_url = item.coverId?.let {
|
thumbnail_url = item.coverId?.let {
|
||||||
"$baseUrl/_next/image?url=$IMG_CDN/${item.id}/covers/$it.jpg&w=256&q=75"
|
"$baseUrl/_next/image?url=$IMG_CDN/${item.id}/covers/$it.jpg&w=256&q=75"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MangasPage(mangas, false)
|
return MangasPage(mangas, mangas.isNotEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector(): String {
|
override fun searchMangaSelector(): String {
|
||||||
|
@ -173,7 +199,7 @@ class Taiyo : ParsedHttpSource() {
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
val id = manga.url.substringAfter("/media/").trimEnd('/')
|
val id = manga.url.substringAfter("/media/").trimEnd('/')
|
||||||
var page = 1
|
var page = 1
|
||||||
val apiUrl = "$baseUrl/api/trpc/mediaChapters.getByMediaId?batch=1".toHttpUrl()
|
val apiUrl = "$baseUrl/api/trpc/chapters.getByMediaId?batch=1".toHttpUrl()
|
||||||
val chapters = buildList {
|
val chapters = buildList {
|
||||||
do {
|
do {
|
||||||
val input = buildJsonObject {
|
val input = buildJsonObject {
|
||||||
|
@ -192,13 +218,17 @@ class Taiyo : ParsedHttpSource() {
|
||||||
.addQueryParameter("input", json.encodeToString(input))
|
.addQueryParameter("input", json.encodeToString(input))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val res = client.newCall(GET(pageUrl, headers)).execute()
|
val chapters = client.newCall(GET(pageUrl, headers)).execute().let {
|
||||||
val parsed = res.parseAs<List<ResponseDto<ChapterListDto>>>().first().data
|
CHAPTER_REGEX.find(it.body.string())?.groups?.get("chapters")?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
val parsed = json.decodeFromString<ChapterListDto>(chapters!!)
|
||||||
|
|
||||||
addAll(
|
addAll(
|
||||||
parsed.chapters.map {
|
parsed.chapters.map {
|
||||||
SChapter.create().apply {
|
SChapter.create().apply {
|
||||||
chapter_number = it.number
|
chapter_number = it.number
|
||||||
name = it.title ?: "Capítulo ${it.number}".replace(".0", "")
|
name = it.title?.takeIf(String::isNotBlank) ?: "Capítulo ${it.number}".replace(".0", "")
|
||||||
url = "/chapter/${it.id}/1"
|
url = "/chapter/${it.id}/1"
|
||||||
date_upload = it.createdAt.orEmpty().toDate()
|
date_upload = it.createdAt.orEmpty().toDate()
|
||||||
}
|
}
|
||||||
|
@ -262,13 +292,15 @@ class Taiyo : ParsedHttpSource() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PREFIX_SEARCH = "id:"
|
const val PREFIX_SEARCH = "id:"
|
||||||
|
val CHAPTER_REGEX = """(?<chapters>\{"chapters".+"totalPages":\d+\})""".toRegex()
|
||||||
|
val TOKEN_REGEX = """NEXT_PUBLIC_MEILISEARCH_PUBLIC_KEY:(\s+)?"(?<token>[^"]+)""".toRegex()
|
||||||
|
|
||||||
private const val IMG_CDN = "https://cdn.taiyo.moe/medias"
|
private const val IMG_CDN = "https://cdn.taiyo.moe/medias"
|
||||||
|
|
||||||
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
|
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaType()
|
||||||
|
|
||||||
private val DATE_FORMATTER by lazy {
|
private val DATE_FORMATTER by lazy {
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.ENGLISH)
|
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,29 @@
|
||||||
package eu.kanade.tachiyomi.extension.pt.taiyo.dto
|
package eu.kanade.tachiyomi.extension.pt.taiyo.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ResponseDto<T>(val result: ResultDto<T>) {
|
data class SearchResultDto(
|
||||||
val data: T = result.data.json
|
val results: List<SearchWrapper>,
|
||||||
|
) {
|
||||||
|
val mangas: List<AdditionalInfoDto> get() = results.firstOrNull()?.hits ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ResultDto<T>(val data: DataDto<T>)
|
data class SearchWrapper(
|
||||||
|
val hits: List<AdditionalInfoDto>,
|
||||||
@Serializable
|
|
||||||
data class DataDto<T>(val json: T)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchResultDto(
|
|
||||||
val id: String,
|
|
||||||
val title: String,
|
|
||||||
val coverId: String? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class AdditionalInfoDto(
|
data class AdditionalInfoDto(
|
||||||
|
val id: String,
|
||||||
val synopsis: String? = null,
|
val synopsis: String? = null,
|
||||||
val status: String? = null,
|
val status: String? = null,
|
||||||
val genres: List<Genre>? = null,
|
val genres: List<Genre>? = null,
|
||||||
val titles: List<TitleDto>? = null,
|
@SerialName("mainCoverId")
|
||||||
|
val coverId: String,
|
||||||
|
val titles: List<TitleDto>,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Genre(val portugueseName: String) {
|
enum class Genre(val portugueseName: String) {
|
||||||
|
@ -51,7 +49,7 @@ enum class Genre(val portugueseName: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TitleDto(val title: String, val language: String)
|
data class TitleDto(val title: String, val language: String, val priority: Int)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ChapterListDto(val chapters: List<ChapterDto>, val totalPages: Int)
|
data class ChapterListDto(val chapters: List<ChapterDto>, val totalPages: Int)
|
||||||
|
|
Loading…
Reference in New Issue