Mediocretoons: update for new site (#11452)

* feat: nova lib para mediocretoons

* Movendo código do multisrc orangeshit para o pacote mediocretoons

* Removendo multisrc orangeshit

* Atualizando .gitignore

* Adicionando filtros completos

* fix: correção das review
- add novas url api e imagens temporarias, até retornar as originais
- add name.toSlug para wevbiew

* Atualizar o .gitignore

* Update .gitignore

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
Felipe Ávila 2025-11-12 02:37:12 -03:00 committed by Draff
parent 91115ac93f
commit 151718b605
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 410 additions and 13 deletions

View File

@ -1,9 +1,8 @@
ext {
extName = 'Mediocre Toons'
extClass = '.MediocreToons'
themePkg = 'greenshit'
baseUrl = 'https://mediocretoons.com'
overrideVersionCode = 0
baseUrl = 'https://mediocretoons.site'
extVersionCode = 7
isNsfw = true
}

View File

@ -1,19 +1,276 @@
package eu.kanade.tachiyomi.extension.pt.mediocretoons
import eu.kanade.tachiyomi.multisrc.greenshit.GreenShit
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.HttpSource
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import java.text.Normalizer
class MediocreToons : GreenShit(
"Mediocre Toons",
"https://mediocretoons.com",
"pt-BR",
scanId = 2,
) {
override val targetAudience = TargetAudience.Shoujo
class MediocreToons : HttpSource() {
override val contentOrigin = ContentOrigin.Mobile
override val name = "Mediocre Toons"
override val client = super.client.newBuilder()
override val baseUrl = "https://mediocretoons.site"
override val lang = "pt-BR"
override val supportsLatest = true
private val apiUrl = "https://api.mediocretoons.site"
private val scanId: Long = 2
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.set("scan-id", scanId.toString())
.set("x-app-key", "toons-mediocre-app")
// ============================== Popular ================================
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/obras".toHttpUrl().newBuilder()
.addQueryParameter("ordenarPor", "views_hoje")
.addQueryParameter("limite", "20")
.addQueryParameter("pagina", page.toString())
.build()
return GET(url, headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<MediocreListDto<List<MediocreMangaDto>>>()
val mangas = dto.data.map { it.toSManga() }
val hasNext = dto.pagination?.hasNextPage ?: false
return MangasPage(mangas, hasNextPage = hasNext)
}
// ============================= Latest Updates ==========================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/obras/novos".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "24")
.addQueryParameter("formato", "4")
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<MediocreListDto<List<MediocreMangaDto>>>()
val mangas = dto.data.map { it.toSManga() }
val hasNext = dto.pagination?.hasNextPage ?: false
return MangasPage(mangas, hasNextPage = hasNext)
}
// =============================== Search ================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/obras".toHttpUrl().newBuilder()
.addQueryParameter("limite", "20")
.addQueryParameter("pagina", page.toString())
.addQueryParameter("temCapitulo", "true")
if (query.isNotEmpty()) {
url.addQueryParameter("string", query)
}
filters.forEach { filter ->
when (filter) {
is FormatoFilter -> {
if (filter.selected.isNotEmpty()) {
url.addQueryParameter("formato", filter.selected)
}
}
is StatusFilter -> {
if (filter.selected.isNotEmpty()) {
url.addQueryParameter("status", filter.selected)
}
}
is TagsFilter -> {
filter.state
.filter { it.state }
.forEach { url.addQueryParameter("tags[]", it.value) }
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<MediocreListDto<List<MediocreMangaDto>>>()
val mangas = dto.data.map { it.toSManga() }
val hasNext = dto.pagination?.hasNextPage ?: false
return MangasPage(mangas, hasNextPage = hasNext)
}
// ============================== Filters ================================
override fun getFilterList() = FilterList(
FormatoFilter(),
StatusFilter(),
TagsFilter(),
)
private class FormatoFilter : UriSelectFilter(
"Formato",
arrayOf(
Pair("Todos", ""),
Pair("Novel", "3"),
Pair("Shoujo", "4"),
Pair("Comic", "5"),
Pair("Yaoi", "8"),
Pair("Yuri", "9"),
Pair("Hentai", "10"),
),
)
private class StatusFilter : UriSelectFilter(
"Status",
arrayOf(
Pair("Todos", ""),
Pair("Ativo", "1"),
Pair("Em Andamento", "2"),
Pair("Cancelada", "3"),
Pair("Concluído", "4"),
Pair("Hiato", "6"),
),
)
private class TagsFilter : Filter.Group<TagCheckBox>(
"Tags",
listOf(
TagCheckBox("Ação", "2"),
TagCheckBox("Aventura", "3"),
TagCheckBox("Fantasia", "4"),
TagCheckBox("Romance", "5"),
TagCheckBox("Comédia", "6"),
TagCheckBox("Drama", "7"),
TagCheckBox("Terror", "8"),
TagCheckBox("Horror", "9"),
TagCheckBox("Suspense", "10"),
TagCheckBox("Histórico", "11"),
TagCheckBox("Vida escolar", "12"),
TagCheckBox("Sobrenatural", "13"),
TagCheckBox("Militar", "14"),
TagCheckBox("Shounen", "15"),
TagCheckBox("Shoujo", "16"),
TagCheckBox("Josei", "17"),
TagCheckBox("One-shot", "18"),
TagCheckBox("Isekai", "19"),
TagCheckBox("Retorno", "20"),
TagCheckBox("Reencarnação", "21"),
TagCheckBox("Sistema", "22"),
TagCheckBox("Cultivo", "23"),
TagCheckBox("Artes Marciais", "24"),
TagCheckBox("Dungeon", "25"),
TagCheckBox("Tragédia", "26"),
TagCheckBox("Psicológico", "27"),
TagCheckBox("Culinaria", "28"),
TagCheckBox("Magia", "29"),
TagCheckBox("SuperPoder", "30"),
TagCheckBox("Murim", "31"),
TagCheckBox("Necromante", "32"),
TagCheckBox("Apocalipse", "33"),
TagCheckBox("Seinen", "34"),
TagCheckBox("Luta", "35"),
TagCheckBox("máfia", "36"),
TagCheckBox("Monstros", "37"),
TagCheckBox("Esportes", "38"),
TagCheckBox("Demônios", "39"),
TagCheckBox("Ficção Científica", "40"),
TagCheckBox("Fatia da Vida/Slice of Life", "41"),
TagCheckBox("Ecchi", "42"),
TagCheckBox("Mistério", "43"),
TagCheckBox("Harém", "44"),
TagCheckBox("manhua", "45"),
TagCheckBox("Jogo", "46"),
TagCheckBox("Regressão", "47"),
TagCheckBox("+18", "48"),
TagCheckBox("Oneshot", "49"),
TagCheckBox("Yuri", "50"),
TagCheckBox("Crime", "51"),
TagCheckBox("Policial", "52"),
TagCheckBox("Viagem no Tempo", "53"),
TagCheckBox("Moderno", "54"),
),
)
private class TagCheckBox(name: String, val value: String) : Filter.CheckBox(name)
private open class UriSelectFilter(
displayName: String,
private val options: Array<Pair<String, String>>,
defaultValue: Int = 0,
) : Filter.Select<String>(
displayName,
options.map { it.first }.toTypedArray(),
defaultValue,
) {
val selected get() = options[state].second
}
// ============================ Manga Details ============================
override fun getMangaUrl(manga: SManga): String {
// manga.url is "/obra/{id}" for API/internal use. Build webview URL with slug from title.
val id = manga.url.substringAfter("/obra/").substringBefore('/')
val slug = manga.title.toSlug()
return "$baseUrl/obra/$id/$slug"
}
override fun mangaDetailsRequest(manga: SManga): Request {
val pathSegment = manga.url.replace("/obra/", "/obras/")
return GET("$apiUrl$pathSegment", headers)
}
override fun mangaDetailsParse(response: Response) =
response.parseAs<MediocreMangaDto>().toSManga()
// ============================== Chapters ===============================
override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> =
response.parseAs<MediocreMangaDto>().chapters.map { it.toSChapter() }
.distinctBy(SChapter::url)
// =============================== Pages =================================
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfterLast("/")
return GET("$apiUrl/capitulos/$chapterId", headers)
}
override fun pageListParse(response: Response): List<Page> =
response.parseAs<MediocreChapterDetailDto>().toPageList()
override fun imageUrlParse(response: Response): String = ""
override fun imageUrlRequest(page: Page): Request {
val imageHeaders = headers.newBuilder()
.add("Referer", "$baseUrl/")
.build()
return GET(page.url, imageHeaders)
}
companion object {
const val CDN_URL = "https://cdn2.fufutebol.com.br"
}
}
private fun String.toSlug(): String {
val noDiacritics = Normalizer.normalize(this, Normalizer.Form.NFD)
.replace(Regex("\\p{InCombiningDiacriticalMarks}+"), "")
val slug = noDiacritics.lowercase()
.replace(Regex("[^a-z0-9]+"), "-")
.trim('-')
return if (slug.isEmpty()) this.hashCode().toString() else slug
}

View File

@ -0,0 +1,141 @@
package eu.kanade.tachiyomi.extension.pt.mediocretoons
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Serializable
data class MediocrePaginationDto(
val currentPage: Int? = null,
val totalPages: Int = 0,
val totalItems: Int = 0,
val itemsPerPage: Int = 0,
val hasNextPage: Boolean = false,
val hasPreviousPage: Boolean = false,
)
@Serializable
data class MediocreListDto<T>(
val data: T,
val pagination: MediocrePaginationDto? = null,
)
@Serializable
data class MediocreTagDto(
val id: Int = 0,
@SerialName("nome") val name: String = "",
)
@Serializable
data class MediocreFormatDto(
val id: Int = 0,
@SerialName("nome") val name: String = "",
)
@Serializable
data class MediocreStatusDto(
val id: Int = 0,
@SerialName("nome") val name: String = "",
)
@Serializable
data class MediocreChapterSimpleDto(
val id: Int = 0,
@SerialName("nome") val name: String = "",
@SerialName("numero") val number: Float? = null,
@SerialName("imagem") val image: String? = null,
@SerialName("lancado_em") val releasedAt: String? = null,
@SerialName("criado_em") val createdAt: String? = null,
@SerialName("descricao") val description: String? = null,
@SerialName("tem_paginas") val hasPages: Boolean = false,
val totallinks: Int = 0,
@SerialName("lido") val read: Boolean = false,
val views: Int = 0,
)
@Serializable
data class MediocreMangaDto(
val id: Int = 0,
val slug: String = "",
@SerialName("nome") val name: String = "",
@SerialName("descricao") val description: String? = null,
@SerialName("imagem") val image: String? = null,
@SerialName("formato") val format: MediocreFormatDto? = null,
val tags: List<MediocreTagDto> = emptyList(),
val status: MediocreStatusDto? = null,
@SerialName("total_capitulos") val totalChapters: Int = 0,
@SerialName("capitulos") val chapters: List<MediocreChapterSimpleDto> = emptyList(),
)
@Serializable
data class MediocrePageSrcDto(
val src: String = "",
)
@Serializable
data class MediocreChapterDetailDto(
val id: Int = 0,
@SerialName("nome") val name: String = "",
@SerialName("numero") val number: Float? = null,
@SerialName("imagem") val image: String? = null,
@SerialName("paginas") val pages: List<MediocrePageSrcDto> = emptyList(),
@SerialName("lancado_em") val releasedAt: String? = null,
@SerialName("criado_em") val createdAt: String? = null,
@SerialName("obra") val manga: MediocreMangaDto? = null,
)
fun MediocreMangaDto.toSManga(): SManga {
val sManga = SManga.create().apply {
title = name
thumbnail_url = image?.let {
when {
it.startsWith("http") -> it
else -> "${MediocreToons.CDN_URL}/obras/${this@toSManga.id}/$it?v=3"
}
}
initialized = true
url = "/obra/$id"
genre = tags.joinToString { it.name }
}
description?.let { Jsoup.parseBodyFragment(it).let { sManga.description = it.text() } }
status?.let {
sManga.status = when (it.name.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
return sManga
}
fun MediocreChapterSimpleDto.toSChapter(): SChapter {
return SChapter.create().apply {
name = this@toSChapter.name
chapter_number = number ?: 0f
url = "/capitulo/$id"
date_upload = dateFormat.tryParse(createdAt)
}
}
fun MediocreChapterDetailDto.toPageList(): List<Page> {
val obraId = manga?.id ?: 0
val capituloNome = name
return pages.mapIndexed { idx, p ->
val imageUrl = "${MediocreToons.CDN_URL}/obras/$obraId/capitulos/$capituloNome/${p.src}"
Page(idx, imageUrl = imageUrl)
}
}
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}