Add new multi-src: zerotheme (#9544)

This commit is contained in:
Chopper 2025-07-08 14:28:27 -03:00 committed by Draff
parent 1980853506
commit 4050e42337
Signed by: Draff
GPG Key ID: E8A89F3211677653
12 changed files with 260 additions and 7 deletions

View File

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -0,0 +1,83 @@
package eu.kanade.tachiyomi.multisrc.zerotheme
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
abstract class ZeroTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
open val cdnUrl: String = "https://cdn.${baseUrl.substringAfterLast("/")}"
open val imageLocation: String = "images"
private val sourceLocation: String get() = "$cdnUrl/$imageLocation"
// =========================== Popular ================================
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList())
override fun popularMangaParse(response: Response) = searchMangaParse(response)
// =========================== Latest ===================================
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesParse(response: Response): MangasPage =
MangasPage(response.toDto<LatestDto>().toSMangaList(sourceLocation), hasNextPage = false)
// =========================== Search =================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
val mangas = dto.mangas.map { it.toSManga(sourceLocation) }
return MangasPage(mangas, hasNextPage = dto.hasNextPage())
}
// =========================== Details =================================
override fun mangaDetailsParse(response: Response) = response.toDto<MangaDetailsDto>().toSManga(sourceLocation)
// =========================== Chapter =================================
override fun chapterListParse(response: Response) = response.toDto<MangaDetailsDto>().toSChapterList()
// =========================== Pages ===================================
override fun pageListParse(response: Response): List<Page> =
response.toDto<PageDto>().toPageList(sourceLocation)
override fun imageUrlParse(response: Response) = ""
// =========================== Utilities ===============================
inline fun <reified T> Response.toDto(): T {
val jsonString = asJsoup().selectFirst("[data-page]")!!.attr("data-page")
return jsonString.parseAs<T>()
}
}

View File

@ -0,0 +1,139 @@
package eu.kanade.tachiyomi.multisrc.zerotheme
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 kotlinx.serialization.json.JsonNames
import org.jsoup.Jsoup
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class Props<T>(
@JsonNames("comic_infos", "chapter", "new_chapters")
val content: T,
)
@Serializable
class LatestDto(
private val props: Props<List<Comic>>,
) {
fun toSMangaList(srcPath: String) = props.content.map { it.comic.toSManga(srcPath) }
@Serializable
class Comic(
val comic: MangaDto,
)
}
@Serializable
class MangaDetailsDto(
private val props: Props<MangaDto>,
) {
fun toSManga(srcPath: String) = props.content.toSManga(srcPath)
fun toSChapterList() = props.content.chapters!!.map { it.toSChapter() }
}
@Serializable
class PageDto(
val props: Props<ChapterWrapper>,
) {
fun toPageList(srcPath: String): List<Page> {
return props.content.chapter.pages
.filter { it.pathSegment.contains("xml").not() }
.mapIndexed { index, path ->
Page(index, imageUrl = "$srcPath/${path.pathSegment}")
}
}
@Serializable
class ChapterWrapper(
val chapter: Chapter,
)
@Serializable
class Chapter(
val pages: List<Image>,
)
@Serializable
class Image(
@SerialName("page_path")
val pathSegment: String,
)
}
@Serializable
class SearchDto(
@SerialName("comics")
private val page: PageDto,
) {
val mangas: List<MangaDto> get() = page.data
fun hasNextPage() = page.currentPage < page.lastPage
@Serializable
class PageDto(
val `data`: List<MangaDto>,
@SerialName("last_page")
val lastPage: Int = 0,
@SerialName("current_page")
val currentPage: Int = 0,
)
}
@Serializable
class MangaDto(
val title: String,
val description: String?,
@SerialName("cover")
val thumbnailUrl: String?,
val slug: String,
val status: List<ValueDto>? = emptyList(),
val genres: List<ValueDto>? = emptyList(),
val chapters: List<ChapterDto>? = emptyList(),
) {
fun toSManga(srcPath: String) = SManga.create().apply {
title = this@MangaDto.title
description = this@MangaDto.description?.let { Jsoup.parseBodyFragment(it).text() }
this.thumbnail_url = thumbnailUrl?.let { "$srcPath/$it" }
status = when (this@MangaDto.status?.firstOrNull()?.name?.lowercase()) {
"em andamento" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
genre = genres?.joinToString { it.name }
url = "/comic/$slug"
}
@Serializable
class ValueDto(
val name: String,
)
}
@Serializable
class ChapterDto(
@SerialName("chapter_number")
val number: Float,
@SerialName("chapter_path")
val path: String,
@SerialName("created_at")
val createdAt: String,
) {
fun toSChapter() = SChapter.create().apply {
name = number.toString()
chapter_number = number
date_upload = dateFormat.tryParse(createdAt)
url = "/chapter/$path"
}
companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -1,9 +1,9 @@
ext {
extName = 'Ego Toons'
extClass = '.EgoToons'
themePkg = 'yuyu'
themePkg = 'zerotheme'
baseUrl = 'https://egotoons.com'
overrideVersionCode = 1
overrideVersionCode = 2
isNsfw = true
}

View File

@ -1,18 +1,18 @@
package eu.kanade.tachiyomi.extension.pt.egotoons
import eu.kanade.tachiyomi.multisrc.yuyu.YuYu
import eu.kanade.tachiyomi.multisrc.zerotheme.ZeroTheme
import eu.kanade.tachiyomi.network.interceptor.rateLimit
class EgoToons : YuYu(
class EgoToons : ZeroTheme(
"Ego Toons",
"https://egotoons.com",
"pt-BR",
) {
override fun headersBuilder() = super.headersBuilder()
.set("Accept-Encoding", "")
override val versionId = 2
override val client = super.client.newBuilder()
.rateLimit(2)
.build()
override val imageLocation = "image-db"
}

View File

@ -0,0 +1,10 @@
ext {
extName = 'Ler Toons'
extClass = '.LerToons'
themePkg = 'zerotheme'
baseUrl = 'https://lertoons.com'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.pt.lertoons
import eu.kanade.tachiyomi.multisrc.zerotheme.ZeroTheme
import eu.kanade.tachiyomi.network.interceptor.rateLimit
class LerToons : ZeroTheme(
"Ler Toons",
"https://lertoons.com",
"pt-BR",
) {
override val versionId = 3
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
}