From 41812dd97bbe7c2335907733da1fa557d12c5d3a Mon Sep 17 00:00:00 2001 From: KenjieDec <65448230+KenjieDec@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:27:24 +0700 Subject: [PATCH] Add Koharu (#3981) * Add Koharu.to * Remove some useless things * Fixed Filters, and More - Fixed Filters/Search not working when query is empty - Changed image resolution default value to match website's - Use `when` instead of kotlin's reflect - Added search by "id", and url intent filter? * Apply Suggestions - Apply vetleledaal's suggestions - Fix UrlActivity? * Apply suggestions * Apply Suggestions * Add files via upload --- src/en/koharu/AndroidManifest.xml | 23 ++ src/en/koharu/build.gradle | 8 + src/en/koharu/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2715 bytes src/en/koharu/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1480 bytes .../koharu/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3732 bytes .../koharu/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6902 bytes .../koharu/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9835 bytes .../tachiyomi/extension/en/koharu/Koharu.kt | 305 ++++++++++++++++++ .../extension/en/koharu/KoharuDto.kt | 75 +++++ .../extension/en/koharu/KoharuFilters.kt | 51 +++ .../extension/en/koharu/KoharuUrlActivity.kt | 34 ++ 11 files changed, 496 insertions(+) create mode 100644 src/en/koharu/AndroidManifest.xml create mode 100644 src/en/koharu/build.gradle create mode 100644 src/en/koharu/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/en/koharu/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/en/koharu/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/en/koharu/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/en/koharu/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt create mode 100644 src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt create mode 100644 src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt create mode 100644 src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt diff --git a/src/en/koharu/AndroidManifest.xml b/src/en/koharu/AndroidManifest.xml new file mode 100644 index 000000000..5f565204f --- /dev/null +++ b/src/en/koharu/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/src/en/koharu/build.gradle b/src/en/koharu/build.gradle new file mode 100644 index 000000000..edf2bff13 --- /dev/null +++ b/src/en/koharu/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Koharu' + extClass = '.Koharu' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/koharu/res/mipmap-hdpi/ic_launcher.png b/src/en/koharu/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..03649687f6129ec8b2d46a89a7e33bd846039527 GIT binary patch literal 2715 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 Vl;2J+6XgH^002ovPDHLkV1l`836cN+ literal 0 HcmV?d00001 diff --git a/src/en/koharu/res/mipmap-mdpi/ic_launcher.png b/src/en/koharu/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5728c35f89c0bfaab7f4ee1592f90d1c5314d32e GIT binary patch literal 1480 zcmV;(1vmPMP)Px)f=NU{RA@u(T3bt7Sr}c3iK0=eGZF7Ic&qn#LA;=%f)64^L@Aw7gn}>9zLXIJ zQ44~jw4%>NynrI3Pd zzO41FZ+&~`Y<0Nhm+qG1$4>$`t)P+u^qn5Kah1LRI0HcMK9>{$T~$?;Nw3$BoSdBe z0U+$lm+yVOpT+MqvX<|p#u1J6*=RJ*9v>h7wYIi)AZ8#%z))UZ{=(^Wwh0H40;mL9 z4M85_!NI}ftE;Q+06anxVgmHHsj#r{-?Ou`P_>|W2=TO$BCpr$4>mS7VgMWp(@Baz zL_tBp?q#B{2Bey{R;%?_0DJxj{F0xa|DRAMB@gfbxT-c*tJMs^!A(F2oHv~=Yi6bX zstCw||4~GwROcp;mzVb~h195^HTt|Mrka3O6|RfO+qZA|Jdm54D{Epc)rmLl{H-0T z2`DLWRYcUR^4tV07Rv<%!`LqqMXXjg5_PyWOCO5-^9ufxf;zyn6Kt zWo2b}`SK+yDk?BNJ&nY~MAn`__xJa+w*EXt7&bRI`Kk~pAORnUS5XHW`o&mMqyzgs;a8c+uMtTgakA@Xs+v9XAXio(*;62ij5*nN-3 z^X(@luL`6f1w~v^y+pYQWM*b6mQgZbWo0D;sMqT;J3Gr#$LVxpcXyYOqjxH&`T6!0&*JEO0P+woqYCV~gE`!0q3`^@n zTU#5}*4B`jnTeX38bg*VNP$BiGv6in+NtR`WMDHW*+6+0f8%;R!}@aWU%Z z>R4G7`{LUTH69QW5Gz2XFE1|-qobo(Sy^Ev@!h+37ozs?;X`C)W#Rq%_pB|g3Dl;k zsfn3=Y;25qr=z0-RM9AXsN5bLe0^#n=RIx@q^GA#DnNtNDoS7|r^(EFdwV_;(!xbX z7JnZZ8Oc(F`cl3V8S;v^^eTZ@RfWU^NXey*CJzt+ zf06OG-Bv?{2fQi;Qd3j?{vuP#`|AL&iNJNMz#DmQJ8l!Fq@>82nAZ#b9tNlduSx+S z0VNf*lFdjkvI% z$K(0OZnx7B(`Tjg0qPSJ6cqHk*=+vXU@+9_bh;2R04-^C2@+1X+r8v)IJ#XfmkoeR zIF{roB|RAd5J4xD0F0NI{3^P-&!bL9OHT1|tK2y;-8Y=S=&blAFM8%kTV8tkoE884 iSA(0BhUUKY3j7aEJn?+05W6k_0000cEYFt literal 0 HcmV?d00001 diff --git a/src/en/koharu/res/mipmap-xhdpi/ic_launcher.png b/src/en/koharu/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4237ae4c414ac21fa8fda1ca6a90920d13087aa2 GIT binary patch literal 3732 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 literal 0 HcmV?d00001 diff --git a/src/en/koharu/res/mipmap-xxhdpi/ic_launcher.png b/src/en/koharu/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..fd67a6cc3939ba29a0652671344cbf999ef8bd37 GIT binary patch literal 6902 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 literal 0 HcmV?d00001 diff --git a/src/en/koharu/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/koharu/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a22749b24cff66630740221a8342140d27427c86 GIT binary patch literal 9835 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 O068gT$#RJ4m;V7zFBrQ3 literal 0 HcmV?d00001 diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt new file mode 100644 index 000000000..1698cd469 --- /dev/null +++ b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/Koharu.kt @@ -0,0 +1,305 @@ +package eu.kanade.tachiyomi.extension.en.koharu + +import android.app.Application +import android.content.SharedPreferences +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +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.model.UpdateStrategy +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class Koharu : HttpSource(), ConfigurableSource { + override val name = "Koharu" + + override val baseUrl = "https://koharu.to" + + 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() + .rateLimit(1) + .build() + + private val json: Json by injectLazy() + + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + private fun quality() = preferences.getString(PREF_IMAGERES, "1280")!! + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + .add("Origin", baseUrl) + + private fun getManga(book: Entry) = SManga.create().apply { + setUrlWithoutDomain("${book.id}/${book.public_key}") + title = book.title + thumbnail_url = book.thumbnail.path + } + + private fun getImagesByMangaEntry(entry: MangaEntry): ImagesInfo { + val data = entry.data + val dataKey = when (quality()) { + "1600" -> data.`1600` ?: data.`1280` ?: data.`0` + "1280" -> data.`1280` ?: data.`1600` ?: data.`0` + "980" -> data.`980` ?: data.`1280` ?: data.`0` + "780" -> data.`780` ?: data.`980` ?: data.`0` + else -> data.`0` + } + + val imagesResponse = client.newCall(POST("$apiBooksUrl/data/${entry.id}/${entry.public_key}/${dataKey.id}/${dataKey.public_key}", headers)).execute() + val images = imagesResponse.parseAs() + return images + } + + // Latest + + override fun latestUpdatesRequest(page: Int) = GET("$apiBooksUrl?page=$page", headers) + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + + // Popular + + override fun popularMangaRequest(page: Int) = GET("$apiBooksUrl?sort=6&page=$page", headers) + override fun popularMangaParse(response: Response): MangasPage { + val data = response.parseAs() + + return MangasPage(data.entries.map(::getManga), data.page * data.limit < data.total) + } + + // Search + + override fun getFilterList(): FilterList = getFilters() + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return when { + query.startsWith(PREFIX_ID_KEY_SEARCH) -> { + val ipk = query.removePrefix(PREFIX_ID_KEY_SEARCH) + val response = client.newCall(GET("$apiBooksUrl/detail/$ipk", headers)).execute() + Observable.just(searchMangaParse2(response)) + } + else -> super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = apiBooksUrl.toHttpUrl().newBuilder().apply { + val terms = mutableListOf(query.trim()) + + filters.forEach { filter -> + when (filter) { + is SortFilter -> addQueryParameter("sort", filter.getValue()) + + is CategoryFilter -> { + val activeFilter = filter.state.filter { it.state } + if (activeFilter.isNotEmpty()) { + addQueryParameter("cat", activeFilter.sumOf { it.value }.toString()) + } + } + + is TextFilter -> { + if (filter.state.isNotEmpty()) { + terms += filter.state.split(",").filter(String::isNotBlank).map { tag -> + val trimmed = tag.trim() + buildString { + if (trimmed.startsWith('-')) { + append("-") + } + append(filter.type) + append("!:") + append("\"") + append(trimmed.lowercase().removePrefix("-")) + append("\"") + } + } + } + } + else -> {} + } + } + if (query.isNotEmpty()) terms.add("title:\"$query\"") + if (terms.isNotEmpty()) addQueryParameter("s", terms.joinToString(" ")) + addQueryParameter("page", page.toString()) + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + + private fun searchMangaParse2(response: Response): MangasPage { + val entry = response.parseAs() + + return MangasPage( + listOf( + SManga.create().apply { + setUrlWithoutDomain("${entry.id}/${entry.public_key}") + title = entry.title + thumbnail_url = entry.thumbnails.base + entry.thumbnails.main.path + }, + ), + false, + ) + } + // Details + + override fun mangaDetailsRequest(manga: SManga): Request { + return GET("$apiBooksUrl/detail/${manga.url}", headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + return response.parseAs().toSManga() + } + + private val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH) + private fun MangaEntry.toSManga() = SManga.create().apply { + val artists = mutableListOf() + val circles = mutableListOf() + val parodies = mutableListOf() + val magazines = mutableListOf() + val characters = mutableListOf() + val cosplayers = mutableListOf() + val females = mutableListOf() + val males = mutableListOf() + val mixed = mutableListOf() + val other = mutableListOf() + val uploaders = mutableListOf() + val tags = mutableListOf() + for (tag in this@toSManga.tags) { + when (tag.namespace) { + 1 -> artists.add(tag.name) + 2 -> circles.add(tag.name) + 3 -> parodies.add(tag.name) + 4 -> magazines.add(tag.name) + 5 -> characters.add(tag.name) + 6 -> cosplayers.add(tag.name) + 7 -> uploaders.add(tag.name) + 8 -> males.add(tag.name + " ♂") + 9 -> females.add(tag.name + " ♀") + 10 -> mixed.add(tag.name) + 12 -> other.add(tag.name) + else -> tags.add(tag.name) + } + } + author = (circles.emptyToNull() ?: artists).joinToString() + artist = artists.joinToString() + genre = (tags + males + females + mixed).joinToString() + description = buildString { + circles.emptyToNull()?.joinToString()?.let { + append("Circles: ", it, "\n") + } + uploaders.emptyToNull()?.joinToString()?.let { + append("Uploaders: ", it, "\n") + } + magazines.emptyToNull()?.joinToString()?.let { + append("Magazines: ", it, "\n") + } + cosplayers.emptyToNull()?.joinToString()?.let { + append("Cosplayers: ", it, "\n") + } + parodies.emptyToNull()?.joinToString()?.let { + append("Parodies: ", it, "\n") + } + characters.emptyToNull()?.joinToString()?.let { + append("Characters: ", it, "\n") + } + append("Pages: ", thumbnails.entries.size, "\n\n") + + try { + append("Added: ", dateReformat.format(((updated_at ?: created_at))), "\n") + } catch (_: Exception) {} + } + status = SManga.COMPLETED + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + initialized = true + } + + private fun Collection.emptyToNull(): Collection? { + return this.ifEmpty { null } + } + + override fun getMangaUrl(manga: SManga) = "$baseUrl/g/${manga.url}" + + // Chapter + + override fun chapterListRequest(manga: SManga): Request { + return GET("$apiBooksUrl/detail/${manga.url}", headers) + } + + override fun chapterListParse(response: Response): List { + val manga = response.parseAs() + return listOf( + SChapter.create().apply { + name = "Chapter" + url = "${manga.id}/${manga.public_key}" + date_upload = (manga.updated_at ?: manga.created_at) + }, + ) + } + + override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${chapter.url}" + + // Page List + + override fun pageListRequest(chapter: SChapter): Request { + return GET("$apiBooksUrl/detail/${chapter.url}", headers) + } + + override fun pageListParse(response: Response): List { + val mangaEntry = response.parseAs() + val imagesInfo = getImagesByMangaEntry(mangaEntry) + + return imagesInfo.entries.mapIndexed { index, image -> + Page(index, imageUrl = "${imagesInfo.base}/${image.path}") + } + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + // Settings + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = PREF_IMAGERES + title = "Image Resolution" + entries = arrayOf("780x", "980x", "1280x", "1600x", "Original") + entryValues = arrayOf("780", "980", "1280", "1600", "0") + summary = "%s" + setDefaultValue("1280") + }.also(screen::addPreference) + } + + private inline fun Response.parseAs(): T { + return json.decodeFromString(body.string()) + } + + companion object { + const val PREFIX_ID_KEY_SEARCH = "id:" + private const val PREF_IMAGERES = "pref_image_quality" + } +} diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt new file mode 100644 index 000000000..b3cd70062 --- /dev/null +++ b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuDto.kt @@ -0,0 +1,75 @@ +package eu.kanade.tachiyomi.extension.en.koharu + +import kotlinx.serialization.Serializable + +@Serializable +class Tag( + var name: String, + var namespace: Int = 0, +) + +@Serializable +class Books( + val entries: List = emptyList(), + val total: Int = 0, + val limit: Int = 0, + val page: Int, +) + +@Serializable +class Entry( + val id: Int, + val public_key: String, + val title: String, + val thumbnail: Thumbnail, +) + +@Serializable +class MangaEntry( + val id: Int, + val title: String, + val public_key: String, + val created_at: Long = 0L, + val updated_at: Long?, + val thumbnails: Thumbnails, + val tags: List = emptyList(), + val data: Data, +) + +@Serializable +class Thumbnails( + val base: String, + val main: Thumbnail, + val entries: List, +) + +@Serializable +class Thumbnail( + val path: String, +) + +@Serializable +class Data( + val `0`: DataKey, + val `780`: DataKey? = null, + val `980`: DataKey? = null, + val `1280`: DataKey? = null, + val `1600`: DataKey? = null, +) + +@Serializable +class DataKey( + val id: Int, + val public_key: String, +) + +@Serializable +class ImagesInfo( + val base: String, + val entries: List, +) + +@Serializable +class ImagePath( + val path: String, +) diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt new file mode 100644 index 000000000..603de00fc --- /dev/null +++ b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuFilters.kt @@ -0,0 +1,51 @@ +package eu.kanade.tachiyomi.extension.en.koharu + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +fun getFilters(): FilterList { + return FilterList( + SortFilter("Sort by", getSortsList), + CategoryFilter("Category"), + Filter.Separator(), + Filter.Header("Separate tags with commas (,)"), + Filter.Header("Prepend with dash (-) to exclude"), + TextFilter("Artists", "artist"), + TextFilter("Magazines", "magazine"), + TextFilter("Publishers", "publisher"), + TextFilter("Characters", "character"), + TextFilter("Cosplayers", "cosplayer"), + TextFilter("Parodies", "parody"), + TextFilter("Circles", "circle"), + TextFilter("Male Tags", "male"), + TextFilter("Female Tags", "female"), + TextFilter("Tags ( Universal )", "tag"), + Filter.Header("Filter by pages, for example: (>20)"), + TextFilter("Pages", "pages"), + ) +} + +internal open class TextFilter(name: String, val type: String) : Filter.Text(name) +internal open class SortFilter(name: String, private val vals: List>, state: Int = 0) : + Filter.Select(name, vals.map { it.first }.toTypedArray(), state) { + fun getValue() = vals[state].second +} + +internal class CategoryFilter(name: String) : + Filter.Group( + name, + listOf( + Pair("Manga", 2), + Pair("Doujinshi", 4), + Pair("Illustration", 8), + ).map { CheckBoxFilter(it.first, it.second, true) }, + ) +internal open class CheckBoxFilter(name: String, val value: Int, state: Boolean) : Filter.CheckBox(name, state) + +private val getSortsList: List> = listOf( + Pair("Title", "1"), + Pair("Pages", "2"), + Pair("Recently Posted", ""), + Pair("Most Viewed", "6"), + Pair("Most Favorited", "8"), +) diff --git a/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt new file mode 100644 index 000000000..56799263d --- /dev/null +++ b/src/en/koharu/src/eu/kanade/tachiyomi/extension/en/koharu/KoharuUrlActivity.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.extension.en.koharu + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class KoharuUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size > 2) { + val id = "${pathSegments[1]}/${pathSegments[2]}" + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${Koharu.PREFIX_ID_KEY_SEARCH}$id") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("KoharuUrlActivity", "Could not start activity", e) + } + } else { + Log.e("KoharuUrlActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +}