SpyFakku | Fixed SpyFakku (#5314)

* Fixed Spyfakku

* Fixed circles, and others

- Fixed circles ( Original API's currently giving full circle list, not the specific comic circle )
- Added attempts for fetching manga details
- Apply AwkwardPeak's suggestion
This commit is contained in:
KenjieDec 2024-10-04 14:50:39 +07:00 committed by Draff
parent a145f79f35
commit e919ddfe06
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
3 changed files with 220 additions and 110 deletions

View File

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

View File

@ -11,9 +11,17 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@ -26,7 +34,7 @@ class SpyFakku : HttpSource() {
override val baseUrl = "https://hentalk.pw" override val baseUrl = "https://hentalk.pw"
private val baseImageUrl = "https://cdn.fakku.cc/image" private val baseImageUrl = "$baseUrl/image"
private val baseApiUrl = "$baseUrl/api" private val baseApiUrl = "$baseUrl/api"
@ -51,18 +59,13 @@ class SpyFakku : HttpSource() {
override fun popularMangaParse(response: Response): MangasPage { override fun popularMangaParse(response: Response): MangasPage {
val library = response.parseAs<HentaiLib>() val library = response.parseAs<HentaiLib>()
val mangas = library.archives.map(::popularManga) val mangas = library.archives.map { it.toSManga() }
val hasNextPage = library.archives.isNotEmpty() val hasNextPage = library.page * library.limit < library.total
return MangasPage(mangas, hasNextPage) return MangasPage(mangas, hasNextPage)
} }
private fun popularManga(hentai: ShortHentai) = SManga.create().apply {
setUrlWithoutDomain("$baseUrl/g/${hentai.id}")
title = hentai.title
thumbnail_url = "$baseImageUrl/${hentai.hash}/1/c"
}
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 {
@ -95,49 +98,105 @@ class SpyFakku : HttpSource() {
} }
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }.replace("/__data.json", "") manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
return GET(baseApiUrl + manga.url, headers) return GET(baseUrl + manga.url.substringBefore("?") + "/__data.json", headers)
}
override fun pageListRequest(chapter: SChapter): Request {
chapter.url = Regex("^/archive/(\\d+)/.*").replace(chapter.url) { "/g/${it.groupValues[1]}" }.replace("/__data.json", "")
return GET(baseApiUrl + chapter.url, headers)
} }
override fun getFilterList() = getFilters() override fun getFilterList() = getFilters()
// Details
private val dateReformat = 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 { private val releasedAtFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC") timeZone = TimeZone.getTimeZone("UTC")
} }
private val createdAtFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply { private val createdAtFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC") timeZone = TimeZone.getTimeZone("UTC")
} }
private fun getAdditionals(data: List<JsonElement>): ShortHentai {
fun Collection<JsonElement>.getTags(): List<String> = this.map {
data[it.jsonPrimitive.int + 2].jsonPrimitive.content
}
val hentaiIndexes = json.decodeFromJsonElement<HentaiIndexes>(data[1])
val hash = data[hentaiIndexes.hash].jsonPrimitive.content
val thumbnail = data[hentaiIndexes.thumbnail].jsonPrimitive.int
val description = data[hentaiIndexes.description].jsonPrimitive.contentOrNull
val released_at = data[hentaiIndexes.released_at].jsonPrimitive.content
val created_at = data[hentaiIndexes.created_at].jsonPrimitive.content
val size = data[hentaiIndexes.size].jsonPrimitive.long
val pages = data[hentaiIndexes.pages].jsonPrimitive.int
val circles = data[hentaiIndexes.circles].jsonArray.emptyToNull()?.getTags()
val publishers = data[hentaiIndexes.publishers].jsonArray.emptyToNull()?.getTags()
val magazines = data[hentaiIndexes.magazines].jsonArray.emptyToNull()?.getTags()
val events = data[hentaiIndexes.events].jsonArray.emptyToNull()?.getTags()
val parodies = data[hentaiIndexes.parodies].jsonArray.emptyToNull()?.getTags()
return ShortHentai(
hash = hash,
thumbnail = thumbnail,
description = description,
released_at = released_at,
created_at = created_at,
publishers = publishers,
circles = circles,
magazines = magazines,
parodies = parodies,
events = events,
size = size,
pages = pages,
)
}
private fun <T> Collection<T>.emptyToNull(): Collection<T>? {
return this.ifEmpty { null }
}
private fun Hentai.toSManga() = SManga.create().apply { private fun Hentai.toSManga() = SManga.create().apply {
title = this@toSManga.title title = this@toSManga.title
url = "/g/$id" url = "/g/$id?$pages&hash=$hash"
author = (circles?.emptyToNull() ?: artists)?.joinToString { it.name } artist = artists?.joinToString()
artist = artists?.joinToString { it.name } genre = tags?.joinToString()
genre = tags?.joinToString { it.name } thumbnail_url = "$baseImageUrl/$hash/$thumbnail?type=cover"
thumbnail_url = "$baseImageUrl/$hash/1/c" status = SManga.COMPLETED
description = buildString {
this@toSManga.description?.let {
append(this@toSManga.description, "\n\n")
} }
circles?.emptyToNull()?.joinToString { it.name }?.let {
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
var response: Response = client.newCall(mangaDetailsRequest(manga)).execute()
var attempts = 0
while (attempts < 3 && response.code != 200) {
try {
response = client.newCall(mangaDetailsRequest(manga)).execute()
} catch (_: Exception) {
} finally {
attempts++
}
}
val add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
return Observable.just(
manga.apply {
with(add) {
url = "/g/$id?$pages&hash=$hash"
author = (circles ?: listOf(manga.artist)).joinToString()
thumbnail_url = "$baseImageUrl/$hash/$thumbnail?type=cover"
this@apply.description = buildString {
description?.let {
append(it, "\n\n")
}
circles?.emptyToNull()?.joinToString()?.let {
append("Circles: ", it, "\n") append("Circles: ", it, "\n")
} }
publishers?.emptyToNull()?.joinToString { it.name }?.let { publishers?.emptyToNull()?.joinToString()?.let {
append("Publishers: ", it, "\n") append("Publishers: ", it, "\n")
} }
magazines?.emptyToNull()?.joinToString { it.name }?.let { magazines?.emptyToNull()?.joinToString()?.let {
append("Magazines: ", it, "\n") append("Magazines: ", it, "\n")
} }
events?.emptyToNull()?.joinToString { it.name }?.let { events?.emptyToNull()?.joinToString()?.let {
append("Events: ", it, "\n\n") append("Events: ", it, "\n\n")
} }
parodies?.emptyToNull()?.joinToString { it.name }?.let { parodies?.emptyToNull()?.joinToString()?.let {
append("Parodies: ", it, "\n") append("Parodies: ", it, "\n")
} }
append("Pages: ", pages, "\n\n") append("Pages: ", pages, "\n\n")
@ -146,13 +205,16 @@ class SpyFakku : HttpSource() {
releasedAtFormat.parse(released_at)?.let { releasedAtFormat.parse(released_at)?.let {
append("Released: ", dateReformat.format(it.time), "\n") append("Released: ", dateReformat.format(it.time), "\n")
} }
} catch (_: Exception) {} } catch (_: Exception) {
}
try { try {
createdAtFormat.parse(created_at)?.let { createdAtFormat.parse(created_at)?.let {
append("Added: ", dateReformat.format(it.time), "\n") append("Added: ", dateReformat.format(it.time), "\n")
} }
} catch (_: Exception) {} } catch (_: Exception) {
}
append( append(
"Size: ", "Size: ",
when { when {
@ -163,48 +225,75 @@ class SpyFakku : HttpSource() {
}, },
) )
} }
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 {
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 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 = "/g/${hentai.id}"
date_upload = try {
releasedAtFormat.parse(hentai.released_at)!!.time
} catch (e: Exception) {
0L
}
}, },
) )
} }
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBefore("?")
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url // Chapters
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
override fun pageListParse(response: Response): List<Page> { var response: Response = client.newCall(chapterListRequest(manga)).execute()
val hentai = response.parseAs<Hentai>() var attempts = 0
val images = hentai.images while (attempts < 3 && response.code != 200) {
return images.mapIndexed { index, it -> try {
Page(index, imageUrl = "$baseImageUrl/${hentai.hash}/${it.filename}") response = client.newCall(chapterListRequest(manga)).execute()
} catch (_: Exception) {
} finally {
attempts++
} }
} }
val add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
return Observable.just(
listOf(
SChapter.create().apply {
name = "Chapter"
url = manga.url
date_upload = try {
releasedAtFormat.parse(add.released_at)!!.time
} catch (e: Exception) {
0L
}
},
),
)
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBefore("?")
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException()
// Pages
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
if (!chapter.url.contains("&hash=") && !chapter.url.contains("?")) {
val response = client.newCall(pageListRequest(chapter)).execute()
val add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
return Observable.just(
List(add.pages) { index ->
Page(index, imageUrl = "$baseImageUrl/${add.hash}/${index + 1}")
},
)
}
val hash: String = chapter.url.substringAfter("hash=")
val pages: Int = chapter.url.substringAfter("?").substringBefore("&").toInt()
return Observable.just(
List(pages) { index ->
Page(index, imageUrl = "$baseImageUrl/$hash/${index + 1}")
},
)
}
override fun pageListRequest(chapter: SChapter): Request {
chapter.url = Regex("^/archive/(\\d+)/.*").replace(chapter.url) { "/g/${it.groupValues[1]}" }
return GET(baseUrl + chapter.url.substringBefore("?") + "/__data.json", headers)
}
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
// Others
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())
} }

View File

@ -1,10 +1,14 @@
package eu.kanade.tachiyomi.extension.en.spyfakku package eu.kanade.tachiyomi.extension.en.spyfakku
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@Serializable @Serializable
class HentaiLib( class HentaiLib(
val archives: List<ShortHentai>, val archives: List<Hentai>,
val page: Int,
val limit: Int,
val total: Int,
) )
@Serializable @Serializable
@ -12,34 +16,51 @@ class Hentai(
val id: Int, val id: Int,
val hash: String, val hash: String,
val title: String, val title: String,
val description: String?, val thumbnail: Int,
val released_at: String,
val created_at: String,
val pages: Int, val pages: Int,
val size: Int = 0, val artists: List<String>?,
val publishers: List<Name>?, val circles: List<String>?,
val artists: List<Name>?, val tags: List<String>?,
val circles: List<Name>?,
val magazines: List<Name>?,
val parodies: List<Name>?,
val events: List<Name>?,
val tags: List<Name>?,
val images: List<Image>,
) )
@Serializable @Serializable
class ShortHentai( class ShortHentai(
val id: Int,
val hash: String, val hash: String,
val title: String, val thumbnail: Int,
val description: String?,
val released_at: String,
val created_at: String,
val publishers: List<String>?,
val circles: List<String>?,
val magazines: List<String>?,
val parodies: List<String>?,
val events: List<String>?,
val size: Long,
val pages: Int,
) )
@Serializable @Serializable
class Image( class Nodes(
val filename: String, val nodes: List<Data>,
) )
@Serializable @Serializable
class Name( class Data(
val name: String, val data: List<JsonElement>,
)
@Serializable
class HentaiIndexes(
val hash: Int,
val thumbnail: Int,
val description: Int,
val released_at: Int,
val created_at: Int,
val publishers: Int,
val circles: Int,
val magazines: Int,
val parodies: Int,
val events: Int,
val size: Int,
val pages: Int,
) )