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 Draff
parent 158ddc6c91
commit 85d977e407
No known key found for this signature in database
GPG Key ID: E8A89F3211677653
25 changed files with 518 additions and 83 deletions

View File

@ -3,7 +3,7 @@
<application> <application>
<activity <activity
android:name=".en.koharu.KoharuUrlActivity" android:name=".all.koharu.KoharuUrlActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:theme="@android:style/Theme.NoDisplay"> android:theme="@android:style/Theme.NoDisplay">
@ -13,10 +13,14 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data android:scheme="https" android:pathPattern="/g/..*/..*"/>
android:host="koharu.to" <data android:host="koharu.to" />
android:pathPattern="/g/..*/..*" <data android:host="schale.network" />
android:scheme="https" /> <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> </intent-filter>
</activity> </activity>
</application> </application>

View File

@ -1,7 +1,7 @@
ext { ext {
extName = 'SchaleNetwork' extName = 'SchaleNetwork'
extClass = '.Koharu' extClass = '.KoharuFactory'
extVersionCode = 9 extVersionCode = 10
isNsfw = true 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.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
@ -28,19 +28,21 @@ import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale 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 name = "SchaleNetwork"
override val id = 1484902275639232927
override val baseUrl = "https://schale.network" override val baseUrl = "https://schale.network"
override val id = if (lang == "en") 1484902275639232927 else super.id
private val apiUrl = baseUrl.replace("://", "://api.") private val apiUrl = baseUrl.replace("://", "://api.")
private val apiBooksUrl = "$apiUrl/books" private val apiBooksUrl = "$apiUrl/books"
override val lang = "en"
override val supportsLatest = true override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder() override val client: OkHttpClient = network.cloudflareClient.newBuilder()
@ -112,12 +114,12 @@ class Koharu : HttpSource(), ConfigurableSource {
// Latest // 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) override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
// Popular // 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 { override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs<Books>() val data = response.parseAs<Books>()
@ -143,6 +145,7 @@ class Koharu : HttpSource(), ConfigurableSource {
val url = apiBooksUrl.toHttpUrl().newBuilder().apply { val url = apiBooksUrl.toHttpUrl().newBuilder().apply {
val terms: MutableList<String> = mutableListOf() val terms: MutableList<String> = mutableListOf()
if (lang != "all") terms += "language!:\"$searchLang\""
filters.forEach { filter -> filters.forEach { filter ->
when (filter) { when (filter) {
is SortFilter -> addQueryParameter("sort", filter.getValue()) is SortFilter -> addQueryParameter("sort", filter.getValue())
@ -158,7 +161,7 @@ class Koharu : HttpSource(), ConfigurableSource {
if (filter.state.isNotEmpty()) { if (filter.state.isNotEmpty()) {
val tags = filter.state.split(",").filter(String::isNotBlank).joinToString(",") val tags = filter.state.split(",").filter(String::isNotBlank).joinToString(",")
if (tags.isNotBlank()) { 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 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.Filter
import eu.kanade.tachiyomi.source.model.FilterList 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.app.Activity
import android.content.ActivityNotFoundException 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 { ext {
extName = 'SpyFakku' extName = 'SpyFakku'
extClass = '.SpyFakku' extClass = '.SpyFakku'
extVersionCode = 9 extVersionCode = 10
isNsfw = true isNsfw = true
} }

View File

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

View File

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

View File

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