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
@ -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>
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'SchaleNetwork'
|
||||
extClass = '.Koharu'
|
||||
extVersionCode = 9
|
||||
extClass = '.KoharuFactory'
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
@ -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 + '"'
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.extension.en.koharu
|
||||
package eu.kanade.tachiyomi.extension.all.koharu
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@ -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"),
|
||||
)
|
||||
}
|
@ -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
|
@ -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
|
8
src/all/pururin/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'Pururin'
|
||||
extClass = '.PururinFactory'
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/pururin/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/all/pururin/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/all/pururin/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
@ -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()
|
||||
}
|
@ -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",
|
||||
)
|
@ -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"),
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'SpyFakku'
|
||||
extClass = '.SpyFakku'
|
||||
extVersionCode = 9
|
||||
extVersionCode = 10
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|