diff --git a/src/en/koharu/AndroidManifest.xml b/src/all/koharu/AndroidManifest.xml similarity index 54% rename from src/en/koharu/AndroidManifest.xml rename to src/all/koharu/AndroidManifest.xml index 5f565204f..e1aa90b82 100644 --- a/src/en/koharu/AndroidManifest.xml +++ b/src/all/koharu/AndroidManifest.xml @@ -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> diff --git a/src/en/koharu/build.gradle b/src/all/koharu/build.gradle similarity index 59% rename from src/en/koharu/build.gradle rename to src/all/koharu/build.gradle index 8703ea56e..0bd0a7ad2 100644 --- a/src/en/koharu/build.gradle +++ b/src/all/koharu/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'SchaleNetwork' - extClass = '.Koharu' - extVersionCode = 9 + extClass = '.KoharuFactory' + extVersionCode = 10 isNsfw = true } diff --git a/src/en/koharu/res/mipmap-hdpi/ic_launcher.png b/src/all/koharu/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/en/koharu/res/mipmap-hdpi/ic_launcher.png rename to src/all/koharu/res/mipmap-hdpi/ic_launcher.png diff --git a/src/en/koharu/res/mipmap-mdpi/ic_launcher.png b/src/all/koharu/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/en/koharu/res/mipmap-mdpi/ic_launcher.png rename to src/all/koharu/res/mipmap-mdpi/ic_launcher.png diff --git a/src/en/koharu/res/mipmap-xhdpi/ic_launcher.png b/src/all/koharu/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/en/koharu/res/mipmap-xhdpi/ic_launcher.png rename to src/all/koharu/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/en/koharu/res/mipmap-xxhdpi/ic_launcher.png b/src/all/koharu/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/en/koharu/res/mipmap-xxhdpi/ic_launcher.png rename to src/all/koharu/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/en/koharu/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/koharu/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/en/koharu/res/mipmap-xxxhdpi/ic_launcher.png rename to src/all/koharu/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/Koharu.kt similarity index 93% rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/Koharu.kt index 1eb002be0..06cbbe945 100644 --- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt +++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/Koharu.kt @@ -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 + '"' } } } diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuDto.kt similarity index 92% rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuDto.kt index cf54c190f..fb67dc6e9 100644 --- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt +++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuDto.kt @@ -1,4 +1,4 @@ -package eu.kanade.tachiyomi.extension.en.koharu +package eu.kanade.tachiyomi.extension.all.koharu import kotlinx.serialization.Serializable diff --git a/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFactory.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFactory.kt new file mode 100644 index 000000000..4a3edcb52 --- /dev/null +++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFactory.kt @@ -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"), + ) +} diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFilters.kt similarity index 94% rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFilters.kt index b871bc400..96bc1d1d0 100644 --- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt +++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuFilters.kt @@ -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 diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuUrlActivity.kt similarity index 95% rename from src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt rename to src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuUrlActivity.kt index 56799263d..cb008c2eb 100644 --- a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt +++ b/src/all/koharu/src/eu/kanade/tachiyomi/extension/all/koharu/KoharuUrlActivity.kt @@ -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 diff --git a/src/all/pururin/build.gradle b/src/all/pururin/build.gradle new file mode 100644 index 000000000..b52cf0832 --- /dev/null +++ b/src/all/pururin/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Pururin' + extClass = '.PururinFactory' + extVersionCode = 10 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/pururin/res/mipmap-hdpi/ic_launcher.png b/src/all/pururin/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..35093ef9b Binary files /dev/null and b/src/all/pururin/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/pururin/res/mipmap-mdpi/ic_launcher.png b/src/all/pururin/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..31974cf68 Binary files /dev/null and b/src/all/pururin/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/pururin/res/mipmap-xhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..21db9a1cf Binary files /dev/null and b/src/all/pururin/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..cbd8ea8d0 Binary files /dev/null and b/src/all/pururin/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..96424681f Binary files /dev/null and b/src/all/pururin/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt new file mode 100644 index 000000000..8275e55eb --- /dev/null +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/Pururin.kt @@ -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() +} diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt new file mode 100644 index 000000000..95117dcd2 --- /dev/null +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFactory.kt @@ -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", +) diff --git a/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt new file mode 100644 index 000000000..40bc4006a --- /dev/null +++ b/src/all/pururin/src/eu/kanade/tachiyomi/extension/all/pururin/PururinFilters.kt @@ -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"), +) diff --git a/src/en/spyfakku/build.gradle b/src/en/spyfakku/build.gradle index 0d857e931..8622e707e 100644 --- a/src/en/spyfakku/build.gradle +++ b/src/en/spyfakku/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'SpyFakku' extClass = '.SpyFakku' - extVersionCode = 9 + extVersionCode = 10 isNsfw = true } diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt index 6f6de5ee9..d67b03351 100644 --- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt +++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/Filters.kt @@ -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"), diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt index 1f3cb2d05..034c7f355 100644 --- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt +++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakku.kt @@ -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 diff --git a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt index 8cdce1a12..bb2dfb796 100644 --- a/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt +++ b/src/en/spyfakku/src/eu/kanade/tachiyomi/extension/en/spyfakku/SpyFakkuDto.kt @@ -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, )