From fd809747ff5a3998d47e84c857fa9b238d485e0f Mon Sep 17 00:00:00 2001 From: KenjieDec <65448230+KenjieDec@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:50:39 +0700 Subject: [PATCH] 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 --- src/en/spyfakku/build.gradle | 2 +- .../extension/en/spyfakku/SpyFakku.kt | 269 ++++++++++++------ .../extension/en/spyfakku/SpyFakkuDto.kt | 59 ++-- 3 files changed, 220 insertions(+), 110 deletions(-) diff --git a/src/en/spyfakku/build.gradle b/src/en/spyfakku/build.gradle index f1964a366..e5d7245a6 100644 --- a/src/en/spyfakku/build.gradle +++ b/src/en/spyfakku/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'SpyFakku' extClass = '.SpyFakku' - extVersionCode = 7 + extVersionCode = 8 isNsfw = true } diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt index ec2710484..bb95f5479 100644 --- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt +++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt @@ -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() - 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): ShortHentai { + fun Collection.getTags(): List = this.map { + data[it.jsonPrimitive.int + 2].jsonPrimitive.content } - status = SManga.COMPLETED - update_strategy = UpdateStrategy.ONLY_FETCH_ONCE - initialized = true - } + val hentaiIndexes = json.decodeFromJsonElement(data[1]) - override fun mangaDetailsParse(response: Response): SManga { - return response.parseAs().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 Collection.emptyToNull(): Collection? { 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 { + 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.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 { - val hentai = response.parseAs() + 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 { - val hentai = response.parseAs() - val images = hentai.images - return images.mapIndexed { index, it -> - Page(index, imageUrl = "$baseImageUrl/${hentai.hash}/${it.filename}") + // Chapters + override fun fetchChapterList(manga: SManga): Observable> { + 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.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 = throw UnsupportedOperationException() + + // Pages + override fun fetchPageList(chapter: SChapter): Observable> { + if (!chapter.url.contains("&hash=") && !chapter.url.contains("?")) { + val response = client.newCall(pageListRequest(chapter)).execute() + val add = getAdditionals(response.parseAs().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 = throw UnsupportedOperationException() + + // Others private inline fun Response.parseAs(): T { return json.decodeFromString(body.string()) } diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt index 731b4fecc..8cdce1a12 100644 --- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt +++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt @@ -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, + val archives: List, + 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?, - val artists: List?, - val circles: List?, - val magazines: List?, - val parodies: List?, - val events: List?, - val tags: List?, - val images: List, + val artists: List?, + val circles: List?, + val tags: List?, ) @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?, + val circles: List?, + val magazines: List?, + val parodies: List?, + val events: List?, + val size: Long, + val pages: Int, ) @Serializable -class Image( - val filename: String, +class Nodes( + val nodes: List, ) @Serializable -class Name( - val name: String, +class Data( + val data: List, +) + +@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, )