Add comicfans (#2299)

* add comicfans

* use api calls

* suggestions
This commit is contained in:
Secozzi 2024-04-09 02:29:29 +00:00 committed by Draff
parent da6abfcbc6
commit 158e5ce4e2
9 changed files with 365 additions and 0 deletions

View File

@ -0,0 +1,7 @@
ext {
extName = 'Comic Fans'
extClass = '.ComicFans'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,205 @@
package eu.kanade.tachiyomi.extension.en.comicfans
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class ComicFans : HttpSource() {
override val name = "Comic Fans"
override val baseUrl = "https://comicfans.io"
private val apiUrl = "https://api.comicfans.io/comic-backend/api/v1/content"
private val cdnUrl = "https://static.comicfans.io"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private fun apiHeadersBuilder() = headersBuilder().apply {
add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("site-domain", "www.${baseUrl.toHttpUrl().host}")
}
private val apiHeaders by lazy { apiHeadersBuilder().build() }
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request {
val body = buildJsonObject {
put("conditionJson", "{\"title\":\"You may also like\",\"maxSize\":15}")
put("pageNumber", page)
put("pageSize", 30)
}.let(json::encodeToString).toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
val popularHeaders = apiHeadersBuilder().apply {
set("Accept", "application/json")
}.build()
return POST("$apiUrl/books/custom/MostPopularLocal#$page", popularHeaders, body)
}
override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs<ListDataDto<MangaDto>>().data
val hasNextPage = response.request.url.fragment!!.toInt() < data.totalPages
return MangasPage(data.list.map { it.toSManga(cdnUrl) }, hasNextPage)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl, headers)
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangaList = document.select(
"div:has(>.block-title-bar > .title:contains(New Updates))" +
"> .book-container > .book",
).map { element ->
SManga.create().apply {
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
with(element.selectFirst(".book-name > a")!!) {
title = text()
setUrlWithoutDomain(attr("abs:href"))
}
}
}
return MangasPage(mangaList, false)
}
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/books".toHttpUrl().newBuilder().apply {
addQueryParameter("pageNumber", page.toString())
addQueryParameter("pageSize", "20")
fragment(page.toString())
if (query.isNotBlank()) {
addPathSegment("search")
addQueryParameter("keyWord", query)
} else {
filters.getUriPart<GenreFilter>()?.let {
addQueryParameter("genre", it)
}
filters.getUriPart<LastUpdateFilter>()?.let {
addQueryParameter("withinDay", it)
}
filters.getUriPart<StatusFilter>()?.let {
addQueryParameter("status", it)
}
}
}.build()
return GET(url, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage =
popularMangaParse(response)
// =============================== Filters ==============================
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Text search ignores filters"),
Filter.Separator(),
GenreFilter(),
LastUpdateFilter(),
StatusFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url
override fun mangaDetailsRequest(manga: SManga): Request {
val bookId = manga.url.substringAfter("/comic/")
.substringBefore("-")
return GET("$apiUrl/books/$bookId", apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<DataDto<MangaDto>>().data.toSManga(cdnUrl)
}
// ============================== Chapters ==============================
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
override fun chapterListRequest(manga: SManga): Request {
val bookId = manga.url.substringAfter("/comic/")
.substringBefore("-")
return GET("$apiUrl/chapters/page?sortDirection=ASC&bookId=$bookId&pageNumber=1&pageSize=9999", apiHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<ListDataDto<ChapterDto>>().data.list.mapIndexed { index, chapterDto ->
chapterDto.toSChapter(index + 1)
}.reversed()
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
val chapterId = chapter.url.substringAfter("/episode/")
.substringBefore("-")
return GET("$apiUrl/chapters/$chapterId", apiHeaders)
}
override fun pageListParse(response: Response): List<Page> {
return response.parseAs<DataDto<PageDataDto>>().data.comicImageList.map {
Page(it.sortNum, imageUrl = "$cdnUrl/${it.imageUrl}")
}
}
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder().apply {
add("Accept", "image/avif,image/webp,*/*")
add("Host", page.imageUrl!!.toHttpUrl().host)
}.build()
return GET(page.imageUrl!!, imgHeaders)
}
override fun imageUrlParse(response: Response): String =
throw UnsupportedOperationException()
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.en.comicfans
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
typealias ListDataDto<T> = DataDto<ListDto<T>>
@Serializable
class ListDto<T>(
val totalPages: Int,
val list: List<T>,
)
@Serializable
class DataDto<T>(
val data: T,
)
@Serializable
class MangaDto(
val id: Int,
val title: String,
val coverImgUrl: String,
val status: Int,
val authorPseudonym: String? = null,
val synopsis: String? = null,
) {
fun toSManga(cdnUrl: String): SManga = SManga.create().apply {
title = this@MangaDto.title
thumbnail_url = "$cdnUrl/$coverImgUrl"
author = authorPseudonym
url = buildString {
append("/comic/")
append(slugify(id, title))
}
description = synopsis
status = when (this@MangaDto.status) {
0 -> SManga.ONGOING
1 -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
initialized = true
}
}
@Serializable
class ChapterDto(
val id: Int,
val title: String,
val updateTime: Long? = null,
) {
fun toSChapter(index: Int): SChapter = SChapter.create().apply {
name = "Ch. $index - $title"
chapter_number = index.toFloat()
date_upload = updateTime ?: 0L
url = buildString {
append("/episode/")
append(slugify(id, title))
}
}
}
@Serializable
class PageDataDto(
val comicImageList: List<PageDto>,
) {
@Serializable
class PageDto(
val imageUrl: String,
val sortNum: Int,
)
}
private val symbolsRegex = Regex("\\W")
private val hyphenRegex = Regex("-{2,}")
private fun slugify(id: Int, title: String): String = buildString {
append(id)
append("-")
append(
title.lowercase()
.replace(symbolsRegex, "-")
.replace(hyphenRegex, "-")
.removeSuffix("-")
.removePrefix("-"),
)
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.extension.en.comicfans
import eu.kanade.tachiyomi.source.model.Filter
open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
inline fun <reified R> List<*>.getUriPart(): String? =
(filterIsInstance<R>().first() as UriPartFilter).toUriPart().takeIf { it.isNotEmpty() }
class GenreFilter : UriPartFilter(
"Genre",
arrayOf(
Pair("All", ""),
Pair("BL", "1001"),
Pair("Fantasy", "1002"),
Pair("GL", "1003"),
Pair("CEO", "1004"),
Pair("Romance", "1005"),
Pair("Harem", "1006"),
Pair("Action", "1007"),
Pair("Teen", "1008"),
Pair("Adventure", "1009"),
Pair("Eastern", "1010"),
Pair("Comedy", "1011"),
Pair("Esports", "1012"),
Pair("Historical", "1013"),
Pair("Mystery", "1014"),
Pair("Modern", "1015"),
Pair("Urban", "1016"),
Pair("Wuxia", "1017"),
Pair("Suspense", "1018"),
Pair("Female Lead", "1019"),
Pair("Western Fantasy", "1020"),
Pair("Horror", "1022"),
Pair("Realistic Fiction", "1023"),
Pair("Cute", "1024"),
Pair("Campus", "1025"),
Pair("Sci-fi", "1026"),
Pair("History", "1027"),
),
)
class LastUpdateFilter : UriPartFilter(
"Last Update",
arrayOf(
Pair("All", ""),
Pair("Within 3 Days", "3"),
Pair("Within 7 Days", "7"),
Pair("Within 15 Days", "15"),
Pair("Within 30 Days", "30"),
),
)
class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("All", ""),
Pair("Ongoing", "0"),
Pair("Completed", "1"),
),
)