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 GitHub
parent 8248520df4
commit fd809747ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 220 additions and 110 deletions

View File

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

View File

@ -11,9 +11,17 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString
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.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
@ -26,7 +34,7 @@ class SpyFakku : HttpSource() {
override val baseUrl = "https://hentalk.pw"
private val baseImageUrl = "https://cdn.fakku.cc/image"
private val baseImageUrl = "$baseUrl/image"
private val baseApiUrl = "$baseUrl/api"
@ -51,18 +59,13 @@ class SpyFakku : HttpSource() {
override fun popularMangaParse(response: Response): MangasPage {
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)
}
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 searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
@ -95,116 +98,202 @@ class SpyFakku : HttpSource() {
}
override fun mangaDetailsRequest(manga: SManga): Request {
manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }.replace("/__data.json", "")
return GET(baseApiUrl + manga.url, 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)
manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
return GET(baseUrl + manga.url.substringBefore("?") + "/__data.json", headers)
}
override fun getFilterList() = getFilters()
// Details
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 {
private val createdAtFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private fun Hentai.toSManga() = SManga.create().apply {
title = this@toSManga.title
url = "/g/$id"
author = (circles?.emptyToNull() ?: artists)?.joinToString { it.name }
artist = artists?.joinToString { it.name }
genre = tags?.joinToString { it.name }
thumbnail_url = "$baseImageUrl/$hash/1/c"
description = buildString {
this@toSManga.description?.let {
append(this@toSManga.description, "\n\n")
}
circles?.emptyToNull()?.joinToString { it.name }?.let {
append("Circles: ", it, "\n")
}
publishers?.emptyToNull()?.joinToString { it.name }?.let {
append("Publishers: ", it, "\n")
}
magazines?.emptyToNull()?.joinToString { it.name }?.let {
append("Magazines: ", it, "\n")
}
events?.emptyToNull()?.joinToString { it.name }?.let {
append("Events: ", it, "\n\n")
}
parodies?.emptyToNull()?.joinToString { it.name }?.let {
append("Parodies: ", it, "\n")
}
append("Pages: ", pages, "\n\n")
try {
releasedAtFormat.parse(released_at)?.let {
append("Released: ", dateReformat.format(it.time), "\n")
}
} catch (_: Exception) {}
try {
createdAtFormat.parse(created_at)?.let {
append("Added: ", dateReformat.format(it.time), "\n")
}
} catch (_: Exception) {}
append(
"Size: ",
when {
size >= 300 * 1000 * 1000 -> "${"%.2f".format(size / (1000.0 * 1000.0 * 1000.0))} GB"
size >= 100 * 1000 -> "${"%.2f".format(size / (1000.0 * 1000.0))} MB"
size >= 1000 -> "${"%.2f".format(size / (1000.0))} kB"
else -> "$size B"
},
)
private fun getAdditionals(data: List<JsonElement>): ShortHentai {
fun Collection<JsonElement>.getTags(): List<String> = this.map {
data[it.jsonPrimitive.int + 2].jsonPrimitive.content
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
initialized = true
}
val hentaiIndexes = json.decodeFromJsonElement<HentaiIndexes>(data[1])
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<Hentai>().toSManga()
}
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 }
}
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
private fun Hentai.toSManga() = SManga.create().apply {
title = this@toSManga.title
url = "/g/$id?$pages&hash=$hash"
artist = artists?.joinToString()
genre = tags?.joinToString()
thumbnail_url = "$baseImageUrl/$hash/$thumbnail?type=cover"
status = SManga.COMPLETED
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
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")
}
override fun chapterListParse(response: Response): List<SChapter> {
val hentai = response.parseAs<Hentai>()
circles?.emptyToNull()?.joinToString()?.let {
append("Circles: ", it, "\n")
}
publishers?.emptyToNull()?.joinToString()?.let {
append("Publishers: ", it, "\n")
}
magazines?.emptyToNull()?.joinToString()?.let {
append("Magazines: ", it, "\n")
}
events?.emptyToNull()?.joinToString()?.let {
append("Events: ", it, "\n\n")
}
parodies?.emptyToNull()?.joinToString()?.let {
append("Parodies: ", it, "\n")
}
append("Pages: ", pages, "\n\n")
return listOf(
SChapter.create().apply {
name = "Chapter"
url = "/g/${hentai.id}"
date_upload = try {
releasedAtFormat.parse(hentai.released_at)!!.time
} catch (e: Exception) {
0L
try {
releasedAtFormat.parse(released_at)?.let {
append("Released: ", dateReformat.format(it.time), "\n")
}
} catch (_: Exception) {
}
try {
createdAtFormat.parse(created_at)?.let {
append("Added: ", dateReformat.format(it.time), "\n")
}
} catch (_: Exception) {
}
append(
"Size: ",
when {
size >= 300 * 1000 * 1000 -> "${"%.2f".format(size / (1000.0 * 1000.0 * 1000.0))} GB"
size >= 100 * 1000 -> "${"%.2f".format(size / (1000.0 * 1000.0))} MB"
size >= 1000 -> "${"%.2f".format(size / (1000.0))} kB"
else -> "$size B"
},
)
}
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
initialized = true
}
},
)
}
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
override fun pageListParse(response: Response): List<Page> {
val hentai = response.parseAs<Hentai>()
val images = hentai.images
return images.mapIndexed { index, it ->
Page(index, imageUrl = "$baseImageUrl/${hentai.hash}/${it.filename}")
// Chapters
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
var response: Response = client.newCall(chapterListRequest(manga)).execute()
var attempts = 0
while (attempts < 3 && response.code != 200) {
try {
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 {
return json.decodeFromString(body.string())
}

View File

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