From f267b0f5d75fac464ee73d08ce113e6ddeba337d Mon Sep 17 00:00:00 2001
From: Alessandro Jean <alessandrojean@gmail.com>
Date: Sun, 19 Sep 2021 19:42:44 -0300
Subject: [PATCH] Use TaoSect's API in the extension. (#9141)

---
 src/pt/taosect/build.gradle                   |   3 +-
 .../tachiyomi/extension/pt/taosect/TaoSect.kt | 450 +++++++++++-------
 .../extension/pt/taosect/TaoSectDto.kt        |  52 ++
 3 files changed, 345 insertions(+), 160 deletions(-)
 create mode 100644 src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSectDto.kt

diff --git a/src/pt/taosect/build.gradle b/src/pt/taosect/build.gradle
index 86a72d4f5..dad2d0a1f 100644
--- a/src/pt/taosect/build.gradle
+++ b/src/pt/taosect/build.gradle
@@ -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 {
diff --git a/src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSect.kt b/src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSect.kt
index 98f78a3de..e36be76ae 100644
--- a/src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSect.kt
+++ b/src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSect.kt
@@ -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()
     }
 }
diff --git a/src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSectDto.kt b/src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSectDto.kt
new file mode 100644
index 000000000..576fe2985
--- /dev/null
+++ b/src/pt/taosect/src/eu/kanade/tachiyomi/extension/pt/taosect/TaoSectDto.kt
@@ -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 = ""
+)