Update Spyfakku (#3712)

* Delete Dto

* Update SpyFakku

* Remove some non-null assert,

* Wrap date parsing around try catch

* test

* Use API

* Remove unused property

* Update

* Update SpyFakku.kt
This commit is contained in:
KenjieDec 2024-06-27 15:21:17 +07:00 committed by Draff
parent 73984b1dcf
commit 592645fa9d
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
4 changed files with 106 additions and 58 deletions

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'SpyFakku' extName = 'SpyFakku'
extClass = '.SpyFakku' extClass = '.SpyFakku'
extVersionCode = 1 extVersionCode = 2
isNsfw = true isNsfw = true
} }

View File

@ -13,9 +13,10 @@ fun getFilters(): FilterList {
TextFilter("Tags", "tag"), TextFilter("Tags", "tag"),
TextFilter("Artists", "artist"), TextFilter("Artists", "artist"),
TextFilter("Magazines", "magazine"), TextFilter("Magazines", "magazine"),
TextFilter("Publishers", "publisher"),
TextFilter("Parodies", "parody"), TextFilter("Parodies", "parody"),
TextFilter("Circles", "circle"), TextFilter("Circles", "circle"),
TextFilter("Pages", "pages"), TextFilter("Events", "event"),
) )
} }
@ -26,9 +27,9 @@ internal open class SortFilter(name: String, selection: Selection, private val v
} }
private val getSortsList: List<Pair<String, String>> = listOf( private val getSortsList: List<Pair<String, String>> = listOf(
Pair("ID", "id"),
Pair("Title", "title"), Pair("Title", "title"),
Pair("Created", "created_at"), Pair("Relevance", "relevance"),
Pair("Published", "published_at"), Pair("Date Added", "created_at"),
Pair("Date Released", "released_at"),
Pair("Pages", "pages"), Pair("Pages", "pages"),
) )

View File

@ -9,27 +9,26 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class SpyFakku : HttpSource() { class SpyFakku : HttpSource() {
override val name = "SpyFakku" override val name = "SpyFakku"
override val baseUrl = "https://spy.fakku.cc" override val baseUrl = "https://fakku.cc"
private val baseImageUrl = "https://cdn.fakku.cc/data" private val baseImageUrl = "https://cdn.fakku.cc/image"
private val baseApiUrl = "$baseUrl/api"
override val lang = "en" override val lang = "en"
@ -42,34 +41,32 @@ class SpyFakku : HttpSource() {
.build() .build()
override fun headersBuilder() = super.headersBuilder() override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/") .set("Referer", "$baseUrl/")
.set("origin", baseUrl) .set("Origin", baseUrl)
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET(baseUrl, headers) return GET("$baseApiUrl/library?sort=released_at&page=$page", headers)
} }
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val library = response.parseAs<HentaiLib>()
val mangas = document.select("article.entry").map(::popularMangaFromElement) val mangas = library.archives.map(::popularManga)
val hasNextPage = document.selectFirst(".next") != null val hasNextPage = library.archives.isNotEmpty()
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }
private fun popularMangaFromElement(element: Element) = SManga.create().apply { private fun popularManga(hentai: ShortHentai) = SManga.create().apply {
with(element.selectFirst("a")!!) { setUrlWithoutDomain("$baseUrl/g/${hentai.id}")
setUrlWithoutDomain(absUrl("href")) title = hentai.title
title = attr("title") thumbnail_url = "$baseImageUrl/${hentai.hash}/cover"
}
thumbnail_url = element.selectFirst("img")?.absUrl("src")
} }
override fun searchMangaParse(response: Response) = popularMangaParse(response) override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply { val url = "$baseApiUrl/library".toHttpUrl().newBuilder().apply {
val terms = mutableListOf(query.trim()) val terms = mutableListOf(query.trim())
filters.forEach { filter -> filters.forEach { filter ->
@ -83,7 +80,7 @@ class SpyFakku : HttpSource() {
if (filter.state.isNotEmpty()) { if (filter.state.isNotEmpty()) {
terms += filter.state.split(",").filter { it.isNotBlank() }.map { tag -> terms += filter.state.split(",").filter { it.isNotBlank() }.map { tag ->
val trimmed = tag.trim().replace(" ", "_") val trimmed = tag.trim().replace(" ", "_")
(if (trimmed.startsWith("-")) "-" else "") + filter.type + "&:" + trimmed.removePrefix("-") (if (trimmed.startsWith("-")) "-" else "") + filter.type + ":" + trimmed.removePrefix("-")
} }
} }
} }
@ -91,7 +88,6 @@ class SpyFakku : HttpSource() {
else -> {} else -> {}
} }
} }
addPathSegment("search")
addQueryParameter("q", terms.joinToString(" ")) addQueryParameter("q", terms.joinToString(" "))
addQueryParameter("page", page.toString()) addQueryParameter("page", page.toString())
}.build() }.build()
@ -99,62 +95,94 @@ class SpyFakku : HttpSource() {
} }
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
return GET(baseUrl + manga.url + ".json", headers) manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
return GET(baseApiUrl + manga.url, headers)
} }
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + chapter.url + ".json", headers) chapter.url = Regex("^/archive/(\\d+)/.*").replace(chapter.url) { "/g/${it.groupValues[1]}" }
return GET(baseApiUrl + chapter.url, headers)
} }
override fun getFilterList() = getFilters() override fun getFilterList() = getFilters()
private val dateFormat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH) private val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
private val releasedAtFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private val createdAtFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private fun Hentai.toSManga() = SManga.create().apply { private fun Hentai.toSManga() = SManga.create().apply {
title = this@toSManga.title title = this@toSManga.title
url = "/archive/$id/$slug" url = "/g/$id"
author = artists?.joinToString { it.value } author = (circles?.emptyToNull() ?: artists)?.joinToString { it.value }
artist = artists?.joinToString { it.value } artist = artists?.joinToString { it.value }
genre = tags?.joinToString { it.value } genre = tags?.joinToString { it.value }
thumbnail_url = "$baseImageUrl/$id/1/288.webp" thumbnail_url = "$baseImageUrl/$hash/cover"
description = buildString { description = buildString {
circle?.joinToString { it.value }?.let { this@toSManga.description?.let {
append(this@toSManga.description, "\n\n")
}
circles?.emptyToNull()?.joinToString { it.value }?.let {
append("Circles: ", it, "\n") append("Circles: ", it, "\n")
} }
magazines?.joinToString { it.value }?.let { publishers?.emptyToNull()?.joinToString { it.value }?.let {
append("Publishers: ", it, "\n")
}
magazines?.emptyToNull()?.joinToString { it.value }?.let {
append("Magazines: ", it, "\n") append("Magazines: ", it, "\n")
} }
parodies?.joinToString { it.value }?.let { events?.emptyToNull()?.joinToString { it.value }?.let {
append("Events: ", it, "\n\n")
}
parodies?.emptyToNull()?.joinToString { it.value }?.let {
append("Parodies: ", it, "\n") append("Parodies: ", it, "\n")
} }
append( append("Pages: ", pages, "\n\n")
"Created At: ",
dateFormat.format( try {
Date(createdAt * 1000), releasedAtFormat.parse(released_at)?.let {
), append("Released: ", dateReformat.format(it.time), "\n")
"\n", }
) } catch (_: Exception) {}
append("Pages: ", pages, "\n")
try {
createdAtFormat.parse(created_at)?.let {
append("Added: ", dateReformat.format(it.time), "\n")
}
} catch (_: Exception) {}
} }
status = SManga.COMPLETED status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
initialized = true initialized = true
} }
override fun mangaDetailsParse(response: Response): SManga = runBlocking { override fun mangaDetailsParse(response: Response): SManga {
response.parseAs<Hentai>().toSManga() return response.parseAs<Hentai>().toSManga()
}
private fun <T> Collection<T>.emptyToNull(): Collection<T>? {
return this.ifEmpty { null }
} }
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga) override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val hentai = response.parseAs<Hentai>() val hentai = response.parseAs<Hentai>()
return listOf( return listOf(
SChapter.create().apply { SChapter.create().apply {
name = "Chapter" name = "Chapter"
url = "/archive/${hentai.id}/${hentai.slug}" url = "/g/${hentai.id}"
date_upload = hentai.createdAt * 1000 date_upload = try {
releasedAtFormat.parse(hentai.released_at)!!.time
} catch (e: Exception) {
0L
}
}, },
) )
} }
@ -163,19 +191,16 @@ class SpyFakku : HttpSource() {
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val hentai = response.parseAs<Hentai>() val hentai = response.parseAs<Hentai>()
val range = 1..hentai.pages val images = hentai.images
val baseImageUrl = "$baseImageUrl/${hentai.id}/" return images.mapIndexed { index, it ->
return range.map { Page(index, imageUrl = "$baseImageUrl/${hentai.hash}/${it.filename}")
val imageUrl = baseImageUrl + it
Page(it - 1, imageUrl = imageUrl)
} }
} }
private inline fun <reified T> Response.parseAs(): T { private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string()) return json.decodeFromString(body.string())
} }
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException() override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
} }

View File

@ -3,18 +3,40 @@ package eu.kanade.tachiyomi.extension.en.spyfakku
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
class HentaiLib(
val archives: List<ShortHentai>,
)
@Serializable @Serializable
class Hentai( class Hentai(
val id: Int, val id: Int,
val slug: String, val hash: String,
val title: String, val title: String,
val createdAt: Long, val description: String?,
val released_at: String,
val created_at: String,
val pages: Int, val pages: Int,
val publishers: List<Name>?,
val artists: List<Name>?, val artists: List<Name>?,
val circle: List<Name>?, val circles: List<Name>?,
val magazines: List<Name>?, val magazines: List<Name>?,
val parodies: List<Name>?, val parodies: List<Name>?,
val events: List<Name>?,
val tags: List<Name>?, val tags: List<Name>?,
val images: List<Image>,
)
@Serializable
class ShortHentai(
val id: Int,
val hash: String,
val title: String,
)
@Serializable
class Image(
val filename: String,
) )
@Serializable @Serializable