Use TaoSect's API in the extension. (#9141)

This commit is contained in:
Alessandro Jean 2021-09-19 19:42:44 -03:00 committed by GitHub
parent a9a6c9f2c0
commit f267b0f5d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 345 additions and 160 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Tao Sect'
pkgNameSuffix = 'pt.taosect'
extClass = '.TaoSect'
extVersionCode = 7
extVersionCode = 8
}
dependencies {

View File

@ -3,26 +3,31 @@ package eu.kanade.tachiyomi.extension.pt.taosect
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.Jsoup
import org.jsoup.parser.Parser
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.TimeUnit
class TaoSect : ParsedHttpSource() {
class TaoSect : HttpSource() {
override val name = "Tao Sect"
@ -30,179 +35,309 @@ class TaoSect : ParsedHttpSource() {
override val lang = "pt-BR"
override val supportsLatest = false
override val supportsLatest = true
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(RateLimitInterceptor(1, 2, TimeUnit.SECONDS))
.build()
private val json: Json by injectLazy()
private val apiHeaders: Headers by lazy { apiHeadersBuilder().build() }
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", USER_AGENT)
.add("Origin", baseUrl)
.add("Referer", baseUrl)
private fun apiHeadersBuilder(): Headers.Builder = headersBuilder()
.add("Accept", ACCEPT_JSON)
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/situacao/ativos", headers)
val apiUrl = "$baseUrl/$API_BASE_PATH/projetos".toHttpUrl().newBuilder()
.addQueryParameter("order", "desc")
.addQueryParameter("orderby", "views")
.addQueryParameter("page", page.toString())
.addQueryParameter("per_page", PROJECTS_PER_PAGE.toString())
.addQueryParameter("_fields", DEFAULT_FIELDS)
.toString()
return GET(apiUrl, apiHeaders)
}
override fun popularMangaSelector(): String = "div.post-list article.post-projeto"
override fun popularMangaParse(response: Response): MangasPage {
val result = json.decodeFromString<List<TaoSectProjectDto>>(response.body!!.string())
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
val sibling = element.nextElementSibling()!!
val projectList = result.map(::popularMangaFromObject)
title = sibling.select("h3.titulo-popover").text()!!
thumbnail_url = element.select("div.post-projeto-background")!!
.attr("style")
.substringAfter("url(")
.substringBefore(");")
setUrlWithoutDomain(element.select("a[title]").first()!!.attr("href"))
val currentPage = response.request.url.queryParameter("page")!!.toInt()
val lastPage = response.headers["X-Wp-TotalPages"]!!.toInt()
val hasNextPage = currentPage < lastPage
return MangasPage(projectList, hasNextPage)
}
override fun popularMangaNextPageSelector(): String? = null
private fun popularMangaFromObject(obj: TaoSectProjectDto): SManga = SManga.create().apply {
title = Parser.unescapeEntities(obj.title!!.rendered, true)
thumbnail_url = obj.thumbnail
setUrlWithoutDomain(obj.link!!)
}
override fun latestUpdatesRequest(page: Int): Request {
val apiUrl = "$baseUrl/$API_BASE_PATH/capitulos".toHttpUrl().newBuilder()
.addQueryParameter("order", "desc")
.addQueryParameter("orderby", "date")
.addQueryParameter("page", page.toString())
.addQueryParameter("per_page", PROJECTS_PER_PAGE.toString())
.toString()
return GET(apiUrl, apiHeaders)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val result = json.decodeFromString<List<TaoSectChapterDto>>(response.body!!.string())
if (result.isNullOrEmpty()) {
return MangasPage(emptyList(), hasNextPage = false)
}
val projectIds = result
.distinctBy { it.projectId!! }
.joinToString(",") { it.projectId!! }
val projectsApiUrl = "$baseUrl/$API_BASE_PATH/projetos".toHttpUrl().newBuilder()
.addQueryParameter("include", projectIds)
.addQueryParameter("orderby", "include")
.addQueryParameter("_fields", DEFAULT_FIELDS)
.toString()
val projectsRequest = GET(projectsApiUrl, apiHeaders)
val projectsResponse = client.newCall(projectsRequest).execute()
val projectsResult = json.decodeFromString<List<TaoSectProjectDto>>(projectsResponse.body!!.string())
val projectList = projectsResult.map(::popularMangaFromObject)
val currentPage = response.request.url.queryParameter("page")!!.toInt()
val lastPage = response.headers["X-Wp-TotalPages"]!!.toInt()
val hasNextPage = currentPage < lastPage
projectsResponse.close()
return MangasPage(projectList, hasNextPage)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/pesquisar-leitor".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("leitor_titulo_projeto", query)
val apiUrl = "$baseUrl/$API_BASE_PATH/projetos".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("per_page", PROJECTS_PER_PAGE.toString())
.addQueryParameter("search", query)
.addQueryParameter("_fields", DEFAULT_FIELDS)
filters.forEach { filter ->
when (filter) {
is CountryFilter -> {
filter.state
.filter { it.state }
.forEach { url.addQueryParameter("leitor_pais_projeto[]", it.id) }
.groupBy { it.state }
.entries
.forEach { entry ->
val values = entry.value.joinToString(",") { it.id }
if (entry.key == Filter.TriState.STATE_EXCLUDE) {
apiUrl.addQueryParameter("paises_exclude", values)
} else if (entry.key == Filter.TriState.STATE_INCLUDE) {
apiUrl.addQueryParameter("paises", values)
}
}
}
is StatusFilter -> {
filter.state
.filter { it.state }
.forEach { url.addQueryParameter("leitor_status_projeto[]", it.id) }
.groupBy { it.state }
.entries
.forEach { entry ->
val values = entry.value.joinToString(",") { it.id }
if (entry.key == Filter.TriState.STATE_EXCLUDE) {
apiUrl.addQueryParameter("situacao_exclude", values)
} else if (entry.key == Filter.TriState.STATE_INCLUDE) {
apiUrl.addQueryParameter("situacao", values)
}
}
}
is GenreFilter -> {
filter.state.forEach { genre ->
if (genre.isIncluded()) {
url.addQueryParameter("leitor_tem_genero_projeto[]", genre.id)
} else if (genre.isExcluded()) {
url.addQueryParameter("leitor_n_tem_genero_projeto[]", genre.id)
filter.state
.groupBy { it.state }
.entries
.forEach { entry ->
val values = entry.value.joinToString(",") { it.id }
if (entry.key == Filter.TriState.STATE_EXCLUDE) {
apiUrl.addQueryParameter("generos_exclude", values)
} else if (entry.key == Filter.TriState.STATE_INCLUDE) {
apiUrl.addQueryParameter("generos", values)
}
}
}
}
is SortFilter -> {
val sort = when {
filter.state == null -> "a_z"
filter.state!!.ascending -> SORT_LIST[filter.state!!.index].first
else -> SORT_LIST[filter.state!!.index].second
}
val orderBy = if (filter.state == null) SORT_LIST[DEFAULT_ORDERBY].id else
SORT_LIST[filter.state!!.index].id
val order = if (filter.state?.ascending == true) "asc" else "desc"
url.addQueryParameter("leitor_ordem_projeto", sort)
apiUrl.addQueryParameter("order", order)
apiUrl.addQueryParameter("orderby", orderBy)
}
}
}
return GET(url.toString(), headers)
return GET(apiUrl.toString(), apiHeaders)
}
override fun searchMangaSelector() = "article.manga_item"
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
title = element.select("h2.titulo_manga_item a")!!.text()
thumbnail_url = element.select("div.container_imagem")!!
.attr("style")
.substringAfter("url(")
.substringBefore(");")
setUrlWithoutDomain(element.select("h2.titulo_manga_item a")!!.attr("href"))
// Workaround to allow "Open in browser" use the real URL.
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsApiRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
}
}
override fun searchMangaNextPageSelector(): String? = null
private fun mangaDetailsApiRequest(manga: SManga): Request {
val projectSlug = manga.url
.substringAfterLast("projeto/")
.substringBefore("/")
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
val header = document.select("div.cabelho-projeto").first()!!
val apiUrl = "$baseUrl/$API_BASE_PATH/projetos".toHttpUrl().newBuilder()
.addQueryParameter("per_page", "1")
.addQueryParameter("slug", projectSlug)
.addQueryParameter("_fields", "title,informacoes,content,thumbnail")
.toString()
title = header.select("h1.titulo-projeto")!!.text()
author = header.select("table.tabela-projeto tr:eq(1) td:eq(1)")!!.text()
artist = header.select("table.tabela-projeto tr:eq(0) td:eq(1)")!!.text()
genre = header.select("table.tabela-projeto tr:eq(11) a").joinToString { it.text() }
status = header.select("table.tabela-projeto tr:eq(5) td:eq(1)")!!.text().toStatus()
description = header.select("table.tabela-projeto tr:eq(10) p")!!.text()
thumbnail_url = header.select("div.imagens-projeto img[alt]").first()!!.attr("data-src")
return GET(apiUrl, apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga {
val result = json.decodeFromString<List<TaoSectProjectDto>>(response.body!!.string())
if (result.isNullOrEmpty()) {
throw Exception(PROJECT_NOT_FOUND)
}
val project = result[0]
return SManga.create().apply {
title = Parser.unescapeEntities(project.title!!.rendered, true)
author = project.info!!.script
artist = project.info.art
genre = project.info.genres.joinToString { it.name }
status = project.info.status!!.name.toStatus()
description = Jsoup.parse(project.content!!.rendered).text() +
"\n\nTítulo original: " + project.info.originalTitle +
"\nSerialização: " + project.info.serialization
thumbnail_url = project.thumbnail
}
}
override fun chapterListRequest(manga: SManga): Request {
val projectSlug = manga.url
.substringAfterLast("projeto/")
.substringBefore("/")
val apiUrl = "$baseUrl/$API_BASE_PATH/projetos".toHttpUrl().newBuilder()
.addQueryParameter("per_page", "1")
.addQueryParameter("slug", projectSlug)
.addQueryParameter("_fields", "id,slug,capitulos")
.toString()
return GET(apiUrl, apiHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
val result = json.decodeFromString<List<TaoSectProjectDto>>(response.body!!.string())
// Count the project views, requested by the scanlator.
// The website counts the views every time a request is done to the project page,
// so to mimic this behavior, the count view request is sent in the chapterListParse,
// that will then get called every time in the global update.
val projectScript = document.selectFirst("script:containsData(dataAjax.url)").data()
val projectId = PROJECT_ID_REGEX.find(projectScript)?.groupValues?.get(1)
if (projectId.isNullOrBlank().not()) {
val countViewRequest = countViewRequest(document.location(), projectId!!)
runCatching { client.newCall(countViewRequest).execute().close() }
if (result.isNullOrEmpty()) {
throw Exception(PROJECT_NOT_FOUND)
}
return document.select(chapterListSelector())
.map(::chapterFromElement)
val project = result[0]
// Count the project views, requested by the scanlator.
val countViewRequest = countViewRequest(project.id.toString())
runCatching { client.newCall(countViewRequest).execute().close() }
val timeNow = System.currentTimeMillis()
return project.volumes!!
.flatMap { it.chapters }
.reversed()
.map { chapterFromObject(it, project) }
.filter { it.date_upload <= timeNow }
}
override fun chapterListSelector() = "table.tabela-volumes tr"
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
name = element.select("td[align='left'] a")!!.text()
private fun chapterFromObject(obj: TaoSectChapterDto, project: TaoSectProjectDto): SChapter = SChapter.create().apply {
name = obj.name
scanlator = this@TaoSect.name
date_upload = element.select("td[align='right']")!!.text().toDate()
// The page have a template problem and it's printing the end of the PHP echo command.
val fixedUrl = element.select("td[align='left'] a")!!
.attr("href")
.substringBeforeLast(";")
setUrlWithoutDomain(fixedUrl)
date_upload = if (obj.releaseDate.isNullOrEmpty().not()) obj.releaseDate!!.toDate() else obj.date.toDate()
url = "/leitor-online/projeto/${project.slug!!}/${obj.slug}/"
}
override fun pageListRequest(chapter: SChapter): Request {
val projectUrl = "$baseUrl/" + chapter.url
.substringAfter("online/")
val projectSlug = chapter.url
.substringAfter("projeto/")
.substringBefore("/")
val chapterSlug = chapter.url
.removeSuffix("/")
.substringAfterLast("/")
val newHeaders = headersBuilder()
.set("Referer", projectUrl)
.build()
val apiUrl = "$baseUrl/$API_BASE_PATH/projetos".toHttpUrl().newBuilder()
.addQueryParameter("per_page", "1")
.addQueryParameter("slug", projectSlug)
.addQueryParameter("chapter_slug", chapterSlug)
.addQueryParameter("_fields", "id,slug,capitulos")
.toString()
return GET(baseUrl + chapter.url, newHeaders)
return GET(apiUrl, apiHeaders)
}
override fun pageListParse(document: Document): List<Page> {
val readerScript = document.selectFirst("script:containsData(var paginas)")!!.data()
override fun pageListParse(response: Response): List<Page> {
val result = json.decodeFromString<List<TaoSectProjectDto>>(response.body!!.string())
// Count the chapter views, requested by the scanlator.
val projectId = PROJECT_ID_REGEX.find(readerScript)?.groupValues?.get(1)
val chapterId = CHAPTER_ID_REGEX.find(readerScript)?.groupValues?.get(1)
if (projectId.isNullOrBlank().not() && chapterId.isNullOrEmpty().not()) {
val countViewRequest = countViewRequest(document.location(), projectId!!, chapterId!!)
runCatching { client.newCall(countViewRequest).execute().close() }
if (result.isNullOrEmpty()) {
throw Exception(PROJECT_NOT_FOUND)
}
return readerScript
.substringAfter("var paginas = [")
.substringBefore("];")
.split(",")
.mapIndexed { i, url ->
Page(i, document.location(), url.replace("\"", ""))
}
val project = result[0]
val chapterSlug = response.request.url.queryParameter("chapter_slug")!!
val chapter = project.volumes!!
.flatMap { it.chapters }
.firstOrNull { it.slug == chapterSlug }
?: throw Exception(CHAPTER_NOT_FOUND)
val chapterUrl = "$baseUrl/leitor-online/projeto/${project.slug!!}/${chapter.slug}"
// Count the chapter views, requested by the scanlator.
val countViewRequest = countViewRequest(project.id!!.toString(), chapter.id)
runCatching { client.newCall(countViewRequest).execute().close() }
return chapter.pages.mapIndexed { i, pageUrl ->
Page(i, chapterUrl, pageUrl)
}
}
override fun imageUrlParse(document: Document) = ""
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
override fun imageUrlParse(response: Response): String = ""
override fun imageRequest(page: Page): Request {
val newHeaders = headersBuilder()
.add("Accept", ACCEPT_IMAGE)
.set("Referer", page.url)
.build()
return GET(page.imageUrl!!, newHeaders)
}
private fun countViewRequest(chapterUrl: String, projectId: String, chapterId: String? = null): Request {
private fun countViewRequest(projectId: String, chapterId: String? = null): Request {
val formBodyBuilder = FormBody.Builder()
.add("action", "update_views")
.add("projeto", projectId)
@ -216,7 +351,6 @@ class TaoSect : ParsedHttpSource() {
val newHeaders = headersBuilder()
.add("Content-Length", formBody.contentLength().toString())
.add("Content-Type", formBody.contentType().toString())
.set("Referer", chapterUrl)
.build()
return POST("$baseUrl/wp-admin/admin-ajax.php", newHeaders, formBody)
@ -224,35 +358,27 @@ class TaoSect : ParsedHttpSource() {
override fun getFilterList(): FilterList = FilterList(
CountryFilter(getCountryList()),
StatusFilter(getStatusList()),
// Status filter is broken on the API at the moment.
// It will be fixed by the scanlator team.
// StatusFilter(getStatusList()),
GenreFilter(getGenreList()),
SortFilter()
)
private class Tag(val id: String, name: String) : Filter.CheckBox(name)
private class Genre(val id: String, name: String) : Filter.TriState(name)
private class Tag(val id: String, name: String) : Filter.TriState(name)
private class CountryFilter(countries: List<Tag>) : Filter.Group<Tag>("País", countries)
private class StatusFilter(status: List<Tag>) : Filter.Group<Tag>("Status", status)
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Gêneros", genres)
private class GenreFilter(genres: List<Tag>) : Filter.Group<Tag>("Gêneros", genres)
private class SortFilter : Filter.Sort(
"Ordem",
SORT_LIST.map { it.third }.toTypedArray(),
Selection(0, true)
SORT_LIST.map { it.name }.toTypedArray(),
Selection(DEFAULT_ORDERBY, false)
)
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used")
override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used")
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used")
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used")
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(this)?.time }
.getOrNull() ?: 0L
@ -277,54 +403,60 @@ class TaoSect : ParsedHttpSource() {
Tag("6", "One-shot")
)
// [...document.querySelectorAll("#leitor_tem_genero_projeto option")]
// .map(el => `Genre("${el.getAttribute("value")}", "${el.innerText}")`).join(',\n')
private fun getGenreList(): List<Genre> = listOf(
Genre("31", "4Koma"),
Genre("24", "Ação"),
Genre("84", "Adulto"),
Genre("21", "Artes Marciais"),
Genre("25", "Aventura"),
Genre("26", "Comédia"),
Genre("66", "Culinária"),
Genre("78", "Doujinshi"),
Genre("22", "Drama"),
Genre("12", "Ecchi"),
Genre("30", "Escolar"),
Genre("76", "Esporte"),
Genre("23", "Fantasia"),
Genre("29", "Harém"),
Genre("75", "Histórico"),
Genre("83", "Horror"),
Genre("18", "Isekai"),
Genre("20", "Light Novel"),
Genre("61", "Manhua"),
Genre("56", "Psicológico"),
Genre("7", "Romance"),
Genre("27", "Sci-fi"),
Genre("28", "Seinen"),
Genre("55", "Shoujo"),
Genre("54", "Shounen"),
Genre("19", "Slice of life"),
Genre("17", "Sobrenatural"),
Genre("57", "Tragédia"),
Genre("62", "Webtoon")
private fun getGenreList(): List<Tag> = listOf(
Tag("31", "4Koma"),
Tag("24", "Ação"),
Tag("84", "Adulto"),
Tag("21", "Artes Marciais"),
Tag("25", "Aventura"),
Tag("26", "Comédia"),
Tag("66", "Culinária"),
Tag("78", "Doujinshi"),
Tag("22", "Drama"),
Tag("12", "Ecchi"),
Tag("30", "Escolar"),
Tag("76", "Esporte"),
Tag("23", "Fantasia"),
Tag("29", "Harém"),
Tag("75", "Histórico"),
Tag("83", "Horror"),
Tag("18", "Isekai"),
Tag("20", "Light Novel"),
Tag("61", "Manhua"),
Tag("56", "Psicológico"),
Tag("7", "Romance"),
Tag("27", "Sci-fi"),
Tag("28", "Seinen"),
Tag("55", "Shoujo"),
Tag("54", "Shounen"),
Tag("19", "Slice of life"),
Tag("17", "Sobrenatural"),
Tag("57", "Tragédia"),
Tag("62", "Webtoon")
)
companion object {
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36"
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
private const val ACCEPT_JSON = "application/json"
private val USER_AGENT = "Tachiyomi " + System.getProperty("http.agent")
private const val API_BASE_PATH = "wp-json/wp/v2"
private const val PROJECTS_PER_PAGE = 18
private const val DEFAULT_ORDERBY = 4
private const val DEFAULT_FIELDS = "title,thumbnail,link"
private const val PROJECT_NOT_FOUND = "Projeto não encontrado."
private const val CHAPTER_NOT_FOUND = "Capítulo não encontrado."
private val DATE_FORMATTER by lazy {
SimpleDateFormat("(dd/MM/yyyy)", Locale.ENGLISH)
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
}
private val SORT_LIST = listOf(
Triple("a_z", "z_a", "Nome"),
Triple("dt_asc", "date-desc", "Data")
Tag("date", "Data de criação"),
Tag("modified", "Data de modificação"),
Tag("destaque", "Destaque"),
Tag("title", "Título"),
Tag("views", "Visualizações")
)
private val PROJECT_ID_REGEX = "projeto: '(\\d+)',?".toRegex()
private val CHAPTER_ID_REGEX = "capitulo: '(\\d+)',?".toRegex()
}
}

View File

@ -0,0 +1,52 @@
package eu.kanade.tachiyomi.extension.pt.taosect
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TaoSectProjectDto(
val content: TaoSectContentDto? = null,
val id: Int? = -1,
@SerialName("informacoes") val info: TaoSectProjectInfoDto? = null,
val link: String? = "",
val slug: String? = "",
val title: TaoSectContentDto? = null,
val thumbnail: String? = "",
@SerialName("capitulos") val volumes: List<TaoSectVolumeDto>? = emptyList()
)
@Serializable
data class TaoSectContentDto(
val rendered: String = ""
)
@Serializable
data class TaoSectProjectInfoDto(
@SerialName("arte") val art: String = "",
@SerialName("generos") val genres: List<TaoSectTagDto> = emptyList(),
@SerialName("titulo_pais_origem") val originalTitle: String = "",
@SerialName("roteiro") val script: String = "",
@SerialName("serializacao") val serialization: String = "",
@SerialName("status_scan") val status: TaoSectTagDto? = null
)
@Serializable
data class TaoSectTagDto(
@SerialName("nome") val name: String = ""
)
@Serializable
data class TaoSectVolumeDto(
@SerialName("capitulos") val chapters: List<TaoSectChapterDto> = emptyList()
)
@Serializable
data class TaoSectChapterDto(
@SerialName("data_insercao") val date: String = "",
@SerialName("id_capitulo") val id: String = "",
@SerialName("nome_capitulo") val name: String = "",
@SerialName("paginas") val pages: List<String> = emptyList(),
@SerialName("post_id") val projectId: String? = "",
@SerialName("data_hora_agendamento") val releaseDate: String? = "",
val slug: String = ""
)