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:
parent
a145f79f35
commit
e919ddfe06
|
@ -1,7 +1,7 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'SpyFakku'
|
extName = 'SpyFakku'
|
||||||
extClass = '.SpyFakku'
|
extClass = '.SpyFakku'
|
||||||
extVersionCode = 7
|
extVersionCode = 8
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,116 +98,202 @@ 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 Hentai.toSManga() = SManga.create().apply {
|
private fun getAdditionals(data: List<JsonElement>): ShortHentai {
|
||||||
title = this@toSManga.title
|
fun Collection<JsonElement>.getTags(): List<String> = this.map {
|
||||||
url = "/g/$id"
|
data[it.jsonPrimitive.int + 2].jsonPrimitive.content
|
||||||
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"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
status = SManga.COMPLETED
|
val hentaiIndexes = json.decodeFromJsonElement<HentaiIndexes>(data[1])
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
val hash = data[hentaiIndexes.hash].jsonPrimitive.content
|
||||||
return response.parseAs<Hentai>().toSManga()
|
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>? {
|
private fun <T> Collection<T>.emptyToNull(): Collection<T>? {
|
||||||
return this.ifEmpty { null }
|
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> {
|
circles?.emptyToNull()?.joinToString()?.let {
|
||||||
val hentai = response.parseAs<Hentai>()
|
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(
|
try {
|
||||||
SChapter.create().apply {
|
releasedAtFormat.parse(released_at)?.let {
|
||||||
name = "Chapter"
|
append("Released: ", dateReformat.format(it.time), "\n")
|
||||||
url = "/g/${hentai.id}"
|
}
|
||||||
date_upload = try {
|
} catch (_: Exception) {
|
||||||
releasedAtFormat.parse(hentai.released_at)!!.time
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
0L
|
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
|
// 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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue