Added SpyFakku (#3538)

* Added SpyFakku

* Changes

- Combined SortFilter and OrderFilter into One Filter.Sort 
- Remove useless regex
- Filter out empty tags
- Rate limit added 
- etc
This commit is contained in:
KenjieDec 2024-06-14 14:27:27 +07:00 committed by GitHub
parent 7787b0ce73
commit c7659f0562
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 246 additions and 0 deletions

View File

@ -0,0 +1,8 @@
ext {
extName = 'SpyFakku'
extClass = '.SpyFakku'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.extension.en.spyfakku
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.Filter.Sort.Selection
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SortFilter("Sort by", Selection(0, false), getSortsList),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Tags", "tag"),
TextFilter("Artists", "artist"),
TextFilter("Magazines", "magazine"),
TextFilter("Parodies", "parody"),
TextFilter("Circles", "circle"),
TextFilter("Pages", "pages"),
)
}
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
fun getValue() = vals[state!!.index].second
}
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("ID", "id"),
Pair("Title", "title"),
Pair("Created", "created_at"),
Pair("Published", "published_at"),
Pair("Pages", "pages"),
)

View File

@ -0,0 +1,181 @@
package eu.kanade.tachiyomi.extension.en.spyfakku
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.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
class SpyFakku : HttpSource() {
override val name = "SpyFakku"
override val baseUrl = "https://spy.fakku.cc"
private val baseImageUrl = "https://cdn.fakku.cc/data"
override val lang = "en"
override val supportsLatest = false
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2, 1, TimeUnit.SECONDS)
.build()
override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/")
.set("origin", baseUrl)
override fun popularMangaRequest(page: Int): Request {
return GET(baseUrl, headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select("article.entry").map(::popularMangaFromElement)
val hasNextPage = document.selectFirst(".next") != null
return MangasPage(mangas, hasNextPage)
}
private fun popularMangaFromElement(element: Element) = SManga.create().apply {
with(element.selectFirst("a")!!) {
setUrlWithoutDomain(absUrl("href"))
title = attr("title")
}
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
val terms = mutableListOf(query.trim())
filters.forEach { filter ->
when (filter) {
is SortFilter -> {
addQueryParameter("sort", filter.getValue())
addQueryParameter("order", if (filter.state!!.ascending) "asc" else "desc")
}
is TextFilter -> {
if (filter.state.isNotEmpty()) {
terms += filter.state.split(",").filter { it.isNotBlank() }.map { tag ->
val trimmed = tag.trim().replace(" ", "_")
(if (trimmed.startsWith("-")) "-" else "") + filter.type + "&:" + trimmed.removePrefix("-")
}
}
}
else -> {}
}
}
addPathSegment("search")
addQueryParameter("q", terms.joinToString(" "))
addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(baseUrl + manga.url + ".json", headers)
}
override fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + chapter.url + ".json", headers)
}
override fun getFilterList() = getFilters()
private val dateFormat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
private fun Hentai.toSManga() = SManga.create().apply {
title = this@toSManga.title
url = "/archive/$id/$slug"
author = artists?.joinToString { it.value }
artist = artists?.joinToString { it.value }
genre = tags?.joinToString { it.value }
thumbnail_url = "$baseImageUrl/$id/1/288.webp"
description = buildString {
circle?.joinToString { it.value }?.let {
append("Circles: ", it, "\n")
}
magazines?.joinToString { it.value }?.let {
append("Magazines: ", it, "\n")
}
parodies?.joinToString { it.value }?.let {
append("Parodies: ", it, "\n")
}
append(
"Created At: ",
dateFormat.format(
Date(createdAt * 1000),
),
"\n",
)
append("Pages: ", pages, "\n")
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
initialized = true
}
override fun mangaDetailsParse(response: Response): SManga = runBlocking {
response.parseAs<Hentai>().toSManga()
}
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val hentai = response.parseAs<Hentai>()
return listOf(
SChapter.create().apply {
name = "Chapter"
url = "/archive/${hentai.id}/${hentai.slug}"
date_upload = hentai.createdAt * 1000
},
)
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url
override fun pageListParse(response: Response): List<Page> {
val hentai = response.parseAs<Hentai>()
val range = 1..hentai.pages
val baseImageUrl = "$baseImageUrl/${hentai.id}/"
return range.map {
val imageUrl = baseImageUrl + it
Page(it - 1, imageUrl = imageUrl)
}
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
}

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.extension.en.spyfakku
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class Hentai(
val id: Int,
val slug: String,
val title: String,
val createdAt: Long,
val pages: Int,
val artists: List<Name>?,
val circle: List<Name>?,
val magazines: List<Name>?,
val parodies: List<Name>?,
val tags: List<Name>?,
)
@Serializable
class Name(
@SerialName("name") val value: String,
)