Koharu: Moved to src/all | Spyfakku: Fixed Errors, Added "Per page" Filter | Pururin: Re-added, Fixed Some Details (#5956)

* Koharu: Moved to src/all | Fixed Spyfakku

- Added 2 language options for Koharu: japanese and english
- Spyfakku: use a cleaner api if available

* Delete src/en/koharu directory

* Fixed #5957

* Added Pururin | Koharu fixed language

- Pururin: Added back, Fixed tags not showing properly
- Koharu: Fixed "Multi" language search not showing anything, added Chinese language as an option

* Fixed Tag Separation in Description

- Fixed: tags were listed with spaces as the separator, instead of commas

* Added Chinese language as an option

- also: applied FourTOne5's suggestion
- I forgor

* Applied suggestion

- Applied FourTOne5's suggestion

* Deeplink support for mirror links
This commit is contained in:
KenjieDec 2024-11-11 14:25:35 +07:00 committed by GitHub
parent a25d228a1c
commit 7f0c68affb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 518 additions and 83 deletions

View File

@ -3,7 +3,7 @@
<application>
<activity
android:name=".en.koharu.KoharuUrlActivity"
android:name=".all.koharu.KoharuUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -13,10 +13,14 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="koharu.to"
android:pathPattern="/g/..*/..*"
android:scheme="https" />
<data android:scheme="https" android:pathPattern="/g/..*/..*"/>
<data android:host="koharu.to" />
<data android:host="schale.network" />
<data android:host="gehenna.jp" />
<data android:host="niyaniya.moe" />
<data android:host="seia.to" />
<data android:host="shupogaki.moe" />
<data android:host="hoshino.one" />
</intent-filter>
</activity>
</application>

View File

@ -1,7 +1,7 @@
ext {
extName = 'SchaleNetwork'
extClass = '.Koharu'
extVersionCode = 9
extClass = '.KoharuFactory'
extVersionCode = 10
isNsfw = true
}

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.koharu
package eu.kanade.tachiyomi.extension.all.koharu
import android.app.Application
import android.content.SharedPreferences
@ -28,19 +28,21 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class Koharu : HttpSource(), ConfigurableSource {
class Koharu(
override val lang: String = "all",
private val searchLang: String = "",
) : HttpSource(), ConfigurableSource {
override val name = "SchaleNetwork"
override val id = 1484902275639232927
override val baseUrl = "https://schale.network"
override val id = if (lang == "en") 1484902275639232927 else super.id
private val apiUrl = baseUrl.replace("://", "://api.")
private val apiBooksUrl = "$apiUrl/books"
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
@ -112,12 +114,12 @@ class Koharu : HttpSource(), ConfigurableSource {
// Latest
override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page", headers)
override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// Popular
override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=8&page=$page", headers)
override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=8&page=$page" + if (searchLang.isNotBlank()) "&s=language!:\"$searchLang\"" else "", headers)
override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs<Books>()
@ -143,6 +145,7 @@ class Koharu : HttpSource(), ConfigurableSource {
val url = apiBooksUrl.toHttpUrl().newBuilder().apply {
val terms: MutableList<String> = mutableListOf()
if (lang != "all") terms += "language!:\"$searchLang\""
filters.forEach { filter ->
when (filter) {
is SortFilter -> addQueryParameter("sort", filter.getValue())
@ -158,7 +161,7 @@ class Koharu : HttpSource(), ConfigurableSource {
if (filter.state.isNotEmpty()) {
val tags = filter.state.split(",").filter(String::isNotBlank).joinToString(",")
if (tags.isNotBlank()) {
terms += "${filter.type}!:" + '"' + tags + '"'
terms += "${filter.type}!:" + if (filter.type == "pages") tags else '"' + tags + '"'
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.koharu
package eu.kanade.tachiyomi.extension.all.koharu
import kotlinx.serialization.Serializable

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.extension.all.koharu
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class KoharuFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Koharu(),
Koharu("en", "english"),
Koharu("ja", "japanese"),
Koharu("zh", "chinese"),
)
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.koharu
package eu.kanade.tachiyomi.extension.all.koharu
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.koharu
package eu.kanade.tachiyomi.extension.all.koharu
import android.app.Activity
import android.content.ActivityNotFoundException

View File

@ -0,0 +1,8 @@
ext {
extName = 'Pururin'
extClass = '.PururinFactory'
extVersionCode = 10
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,271 @@
package eu.kanade.tachiyomi.extension.all.pururin
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
abstract class Pururin(
override val lang: String = "all",
private val searchLang: Pair<String, String>? = null,
private val langPath: String = "",
) : ParsedHttpSource() {
override val name = "Pururin"
final override val baseUrl = "https://pururin.me"
override val supportsLatest = true
override val client = network.cloudflareClient
private val json: Json by injectLazy()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
}
override fun popularMangaSelector(): String = "a.card"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.attr("title")
setUrlWithoutDomain(element.attr("abs:href"))
thumbnail_url = element.select("img").attr("abs:src")
}
}
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse$langPath?page=$page", headers)
}
override fun latestUpdatesSelector(): String = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
// Search
private fun List<Pair<String, String>>.toValue(): String {
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
}
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
if (num < 0) return minPages to maxPages
return when (query.firstOrNull()) {
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
'=' -> when (query[1]) {
'>' -> limitedNum() to maxPages
'<' -> 1 to limitedNum(maxPages)
else -> limitedNum() to limitedNum()
}
else -> limitedNum() to limitedNum()
}
}
@Serializable
class Tag(
val id: Int,
val name: String,
)
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
return tag?.let { Pair(tag.id.toString(), tag.name) }
}
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
val requestBody = FormBody.Builder()
.add("text", tag)
.build()
val request = Request.Builder()
.url("$baseUrl/api/get/tags/search")
.headers(headers)
.post(requestBody)
.build()
val response = client.newCall(request).execute()
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val includeTags = mutableListOf<Pair<String, String>>()
val excludeTags = mutableListOf<Pair<String, String>>()
var pagesMin = 1
var pagesMax = 9999
var sortBy = "newest"
if (searchLang != null) includeTags.add(searchLang)
filters.forEach {
when (it) {
is SelectFilter -> sortBy = it.getValue()
is TypeFilter -> {
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
}
is PageFilter -> {
if (it.state.isNotEmpty()) {
val (min, max) = parsePageRange(it.state)
pagesMin = min
pagesMax = max
}
}
is TextFilter -> {
if (it.state.isNotEmpty()) {
it.state.split(",").filter(String::isNotBlank).map { tag ->
val trimmed = tag.trim()
if (trimmed.startsWith('-')) {
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
excludeTags.add(tagInfo)
}
} else {
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
includeTags.add(tagInfo)
}
}
}
}
}
else -> {}
}
}
// Searching with just one tag usually gives wrong results
if (query.isEmpty()) {
when {
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
includeTags.size == 1 && excludeTags.isEmpty() -> {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("browse")
addPathSegment("tags")
addPathSegment("content")
addPathSegment(includeTags[0].first)
addQueryParameter("sort", sortBy)
addQueryParameter("start_page", pagesMin.toString())
addQueryParameter("last_page", pagesMax.toString())
if (page > 1) addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
}
}
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("search")
addQueryParameter("q", query)
addQueryParameter("sort", sortBy)
addQueryParameter("start_page", pagesMin.toString())
addQueryParameter("last_page", pagesMax.toString())
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
if (page > 1) addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
override fun searchMangaSelector(): String = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
// Details
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
document.select(".box-gallery").let { e ->
initialized = true
title = e.select(".title").text()
author = e.select("a[href*=/circle/]").eachText().joinToString().ifEmpty { e.select("[itemprop=author]").text() }
artist = e.select("[itemprop=author]").eachText().joinToString()
genre = e.select("a[href*=/content/]").eachText().joinToString()
description = e.select(".box-gallery .table-info tr")
.filter { tr ->
tr.select("td").let { td ->
td.isNotEmpty() &&
td.none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
}
}
.joinToString("\n") { tr ->
tr.select("td").let { td ->
var a = td.select("a").toList()
if (a.isEmpty()) a = td.drop(1)
td.first()!!.text() + ": " + a.joinToString { it.text() }
}
}
status = SManga.COMPLETED
thumbnail_url = e.select("img").attr("abs:src")
}
}
}
// Chapters
override fun chapterListSelector(): String = ".table-collection tbody tr a"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
name = element.text()
setUrlWithoutDomain(element.attr("abs:href"))
}
}
override fun chapterListParse(response: Response): List<SChapter> {
return response.asJsoup().select(chapterListSelector())
.map { chapterFromElement(it) }
.reversed()
.let { list ->
list.ifEmpty {
listOf(
SChapter.create().apply {
setUrlWithoutDomain(response.request.url.toString())
name = "Chapter"
},
)
}
}
}
// Pages
override fun pageListParse(document: Document): List<Page> {
return document.select(".gallery-preview a img")
.mapIndexed { i, img ->
Page(i, "", (if (img.hasAttr("abs:src")) img.attr("abs:src") else img.attr("abs:data-src")).replace("t.", "."))
}
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun getFilterList() = getFilters()
}

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.extension.all.pururin
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class PururinFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
PururinAll(),
PururinEN(),
PururinJA(),
)
}
class PururinAll : Pururin()
class PururinEN : Pururin(
"en",
Pair("13010", "english"),
"/tags/language/13010/english",
)
class PururinJA : Pururin(
"ja",
Pair("13011", "japanese"),
"/tags/language/13011/japanese",
)

View File

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.extension.all.pururin
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
TypeFilter("Types"),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Tags", "[Content]"),
TextFilter("Artists", "[Artist]"),
TextFilter("Circles", "[Circle]"),
TextFilter("Parodies", "[Parody]"),
TextFilter("Languages", "[Language]"),
TextFilter("Scanlators", "[Scanlator]"),
TextFilter("Conventions", "[Convention]"),
TextFilter("Collections", "[Collections]"),
TextFilter("Categories", "[Category]"),
TextFilter("Uploaders", "[Uploader]"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
PageFilter("Pages"),
)
}
internal class TypeFilter(name: String) :
Filter.Group<CheckBoxFilter>(
name,
listOf(
Pair("Artbook", "17783"),
Pair("Artist CG", "13004"),
Pair("Doujinshi", "13003"),
Pair("Game CG", "13008"),
Pair("Manga", "13004"),
Pair("Webtoon", "27939"),
).map { CheckBoxFilter(it.first, it.second, true) },
)
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
internal open class PageFilter(name: String) : Filter.Text(name)
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SelectFilter(name: String, val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second
}
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Newest", "newest"),
Pair("Most Popular", "most-popular"),
Pair("Highest Rated", "highest-rated"),
Pair("Most Viewed", "most-viewed"),
Pair("Title", "title"),
)

View File

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

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SortFilter("Sort by", Selection(0, false), getSortsList),
SelectFilter("Per page", getLimits),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
@ -25,7 +26,16 @@ internal open class SortFilter(name: String, selection: Selection, private val v
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
fun getValue() = vals[state!!.index].second
}
internal open class SelectFilter(name: String, val vals: List<String>, state: Int = 2) :
Filter.Select<String>(name, vals.map { it }.toTypedArray(), state)
private val getLimits = listOf(
"6",
"12",
"24",
"36",
"48",
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Title", "title"),
Pair("Relevance", "relevance"),

View File

@ -83,6 +83,10 @@ class SpyFakku : HttpSource() {
addQueryParameter("order", if (filter.state!!.ascending) "asc" else "desc")
}
is SelectFilter -> {
addQueryParameter("limit", filter.vals[filter.state])
}
is TextFilter -> {
if (filter.state.isNotEmpty()) {
terms += filter.state.split(",").filter { it.isNotBlank() }.map { tag ->
@ -101,11 +105,6 @@ class SpyFakku : HttpSource() {
return GET(url, headers)
}
override fun mangaDetailsRequest(manga: SManga): Request {
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
@ -118,8 +117,8 @@ class SpyFakku : HttpSource() {
}
private fun getAdditionals(data: List<JsonElement>): ShortHentai {
fun Collection<JsonElement>.getTags(): List<String> = this.map {
data[it.jsonPrimitive.int + 2].jsonPrimitive.content
fun Collection<JsonElement>.getTags(): List<Name> = this.map {
Name(data[it.jsonPrimitive.int + 2].jsonPrimitive.content, data[it.jsonPrimitive.int + 3].jsonPrimitive.content)
}
val hentaiIndexes = json.decodeFromJsonElement<HentaiIndexes>(data[1])
@ -132,22 +131,14 @@ class SpyFakku : HttpSource() {
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()
val tags = data[hentaiIndexes.tags].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,
tags = tags,
size = size,
pages = pages,
)
@ -159,62 +150,79 @@ class SpyFakku : HttpSource() {
private fun Hentai.toSManga() = SManga.create().apply {
title = this@toSManga.title
url = "/g/$id?$pages&hash=$hash"
artist = artists?.joinToString()
genre = tags?.joinToString()
author = tags?.filter { it.namespace == "circle" }?.joinToString { it.name }
artist = tags?.filter { it.namespace == "artist" }?.joinToString { it.name }
genre = tags?.filter { it.namespace == "tag" }?.joinToString { it.name }
thumbnail_url = "$baseImageUrl/$hash/$thumbnail?type=cover"
status = SManga.COMPLETED
}
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 response1: Response = client.newCall(mangaDetailsRequest(manga)).execute()
val add: ShortHentai
if (response1.isSuccessful) {
add = response1.parseAs<ShortHentai>()
} else {
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++
}
}
add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
}
val add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
return Observable.just(
manga.apply {
with(add) {
val tags = tags?.groupBy { it.namespace }
url = "/g/$id?$pages&hash=$hash"
author = (circles ?: listOf(manga.artist)).joinToString()
author = (tags?.get("circle") ?: tags?.get("artist"))?.joinToString { it.name }
artist = tags?.get("artist")?.joinToString { it.name }
thumbnail_url = "$baseImageUrl/$hash/$thumbnail?type=cover"
genre = tags?.get("tag")?.joinToString { it.name }
this@apply.description = buildString {
description?.let {
append(it, "\n\n")
}
circles?.emptyToNull()?.joinToString()?.let {
tags?.get("circle")?.emptyToNull()?.joinToString { it.name }?.let {
append("Circles: ", it, "\n")
}
publishers?.emptyToNull()?.joinToString()?.let {
tags?.get("publisher")?.emptyToNull()?.joinToString { it.name }?.let {
append("Publishers: ", it, "\n")
}
magazines?.emptyToNull()?.joinToString()?.let {
tags?.get("magazine")?.emptyToNull()?.joinToString { it.name }?.let {
append("Magazines: ", it, "\n")
}
events?.emptyToNull()?.joinToString()?.let {
tags?.get("event")?.emptyToNull()?.joinToString { it.name }?.let {
append("Events: ", it, "\n\n")
}
parodies?.emptyToNull()?.joinToString()?.let {
tags?.get("parody")?.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")
releasedAt?.let {
releasedAtFormat.parse(it)?.let {
append("Released: ", dateReformat.format(it.time), "\n")
}
}
} catch (_: Exception) {
}
try {
createdAtFormat.parse(created_at)?.let {
append("Added: ", dateReformat.format(it.time), "\n")
createdAt?.let {
createdAtFormat.parse(it)?.let {
append("Added: ", dateReformat.format(it.time), "\n")
}
}
} catch (_: Exception) {
}
@ -235,22 +243,39 @@ class SpyFakku : HttpSource() {
},
)
}
override fun mangaDetailsRequest(manga: SManga): Request {
manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
return GET(baseApiUrl + manga.url.substringBefore("?"), headers)
}
private fun mangaDetailsRequest2(manga: SManga): Request {
manga.url = Regex("^/archive/(\\d+)/.*").replace(manga.url) { "/g/${it.groupValues[1]}" }
return GET(baseUrl + manga.url.substringBefore("?") + "/__data.json", headers)
}
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.substringBefore("?")
// 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 response1: Response = client.newCall(chapterListRequest(manga)).execute()
val add: ShortHentai
if (response1.isSuccessful) {
add = response1.parseAs<ShortHentai>()
} else {
var response: Response = client.newCall(chapterListRequest2(manga)).execute()
var attempts = 0
while (attempts < 3 && response.code != 200) {
try {
response = client.newCall(mangaDetailsRequest(manga)).execute()
} catch (_: Exception) {
} finally {
attempts++
}
}
add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
}
val add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
return Observable.just(
listOf(
SChapter.create().apply {
@ -267,13 +292,25 @@ class SpyFakku : HttpSource() {
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBefore("?")
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
private fun chapterListRequest2(manga: SManga) = mangaDetailsRequest2(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 response1 = client.newCall(pageListRequest(chapter)).execute()
if (response1.isSuccessful) {
val hentai = response1.parseAs<Hentai>()
return Observable.just(
List(hentai.pages) { index ->
Page(index, imageUrl = "$baseImageUrl/${hentai.hash}/${index + 1}")
},
)
}
val response = client.newCall(pageListRequest2(chapter)).execute()
val add = getAdditionals(response.parseAs<Nodes>().nodes.last().data)
return Observable.just(
List(add.pages) { index ->
@ -292,9 +329,14 @@ class SpyFakku : HttpSource() {
}
override fun pageListRequest(chapter: SChapter): Request {
chapter.url = Regex("^/archive/(\\d+)/.*").replace(chapter.url) { "/g/${it.groupValues[1]}" }
return GET(baseApiUrl + chapter.url.substringBefore("?"), headers)
}
private fun pageListRequest2(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

View File

@ -18,9 +18,7 @@ class Hentai(
val title: String,
val thumbnail: Int,
val pages: Int,
val artists: List<String>?,
val circles: List<String>?,
val tags: List<String>?,
val tags: List<Name>?,
)
@Serializable
@ -28,15 +26,24 @@ class ShortHentai(
val hash: 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 released_at: String? = null,
val created_at: String? = null,
var releasedAt: String? = null,
var createdAt: String? = null,
val tags: List<Name>?,
val size: Long,
val pages: Int,
) {
init {
releasedAt = released_at ?: releasedAt
createdAt = created_at ?: createdAt
}
}
@Serializable
class Name(
val namespace: String,
val name: String,
)
@Serializable
@ -56,11 +63,7 @@ class HentaiIndexes(
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 tags: Int,
val size: Int,
val pages: Int,
)