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:
parent
91115ac93f
commit
151718b605
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user