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:
Chopper 2024-11-11 05:34:52 -03:00 committed by Draff
parent a038b718b1
commit 0fc23e3cb0
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
4 changed files with 80 additions and 50 deletions

View File

@ -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>

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'Taiyō' extName = 'Taiyō'
extClass = '.Taiyo' extClass = '.Taiyo'
extVersionCode = 1 extVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -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)
} }
} }
} }

View File

@ -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)