diff --git a/src/main/kotlin/org/dokiteam/doki/parsers/site/mangareader/id/KiryuuParser.kt b/src/main/kotlin/org/dokiteam/doki/parsers/site/mangareader/id/KiryuuParser.kt deleted file mode 100644 index 601d7ac..0000000 --- a/src/main/kotlin/org/dokiteam/doki/parsers/site/mangareader/id/KiryuuParser.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.dokiteam.doki.parsers.site.mangareader.id - -import org.dokiteam.doki.parsers.MangaLoaderContext -import org.dokiteam.doki.parsers.MangaSourceParser -import org.dokiteam.doki.parsers.model.MangaListFilterCapabilities -import org.dokiteam.doki.parsers.model.MangaParserSource -import org.dokiteam.doki.parsers.site.mangareader.MangaReaderParser - -@MangaSourceParser("KIRYUU", "Kiryuu", "id") -internal class KiryuuParser(context: MangaLoaderContext) : - MangaReaderParser(context, MangaParserSource.KIRYUU, "kiryuu02.com", pageSize = 50, searchPageSize = 10) { - - override val listUrl = "/manga/" - - override val filterCapabilities: MangaListFilterCapabilities - get() = super.filterCapabilities.copy( - isTagsExclusionSupported = false, - ) -} diff --git a/src/main/kotlin/org/dokiteam/doki/parsers/site/id/Ikiru.kt b/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/NatsuParser.kt similarity index 74% rename from src/main/kotlin/org/dokiteam/doki/parsers/site/id/Ikiru.kt rename to src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/NatsuParser.kt index 1591e3d..1ee898c 100644 --- a/src/main/kotlin/org/dokiteam/doki/parsers/site/id/Ikiru.kt +++ b/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/NatsuParser.kt @@ -1,4 +1,4 @@ -package org.dokiteam.doki.parsers.site.id +package org.dokiteam.doki.parsers.site.natsu import okhttp3.Headers import okhttp3.MultipartBody @@ -7,8 +7,6 @@ import okhttp3.Request import org.json.JSONArray import org.jsoup.nodes.Document import org.dokiteam.doki.parsers.MangaLoaderContext -import org.dokiteam.doki.parsers.MangaSourceParser -import org.dokiteam.doki.parsers.config.ConfigKey import org.dokiteam.doki.parsers.core.PagedMangaParser import org.dokiteam.doki.parsers.model.ContentRating import org.dokiteam.doki.parsers.model.ContentType @@ -39,22 +37,23 @@ import java.util.Calendar import java.util.EnumSet import java.util.Locale -@MangaSourceParser("IKIRU", "Ikiru", "id") -internal class Ikiru(context: MangaLoaderContext) : - PagedMangaParser(context, MangaParserSource.IKIRU, 24, 24) { +/** + * Base parser for NatsuId WordPress theme + * Theme: https://themesinfo.com/natsu_id-theme-wordpress-c8x1c + * Author: Dzul Qurnain + */ +internal abstract class NatsuParser( + context: MangaLoaderContext, + source: MangaParserSource, + pageSize: Int = 24, +) : PagedMangaParser(context, source, pageSize, pageSize) { - override val configKeyDomain = ConfigKey.Domain("02.ikiru.wtf") override val sourceLocale: Locale = Locale.ENGLISH - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } - - override fun getRequestHeaders() = super.getRequestHeaders().newBuilder() - .add("Referer", "https://$domain/") - .add("Origin", "https://$domain") - .build() + override fun getRequestHeaders() = super.getRequestHeaders().newBuilder() + .add("Referer", "https://$domain/") + .add("Origin", "https://$domain") + .build() override val availableSortOrders: Set = EnumSet.of( SortOrder.UPDATED, @@ -100,29 +99,29 @@ internal class Ikiru(context: MangaLoaderContext) : val url = "https://${domain}/wp-admin/admin-ajax.php?action=advanced_search" val formParts = mutableMapOf() - formParts["nonce"] = getNonce() + formParts["nonce"] = getNonce() - formParts["inclusion"] = "AND" - if (filter.tags.isNotEmpty()) { - val genreArray = JSONArray(filter.tags.map { it.key }) - formParts["genre"] = genreArray.toString() - } else formParts["genre"] = "[]" + formParts["inclusion"] = "OR" + if (filter.tags.isNotEmpty()) { + val genreArray = JSONArray(filter.tags.map { it.key }) + formParts["genre"] = genreArray.toString() + } else formParts["genre"] = "[]" - formParts["exclusion"] = "AND" - if (filter.tagsExclude.isNotEmpty()) { - val exGenreArray = JSONArray(filter.tags.map { it.key }) - formParts["genre_exclude"] = exGenreArray.toString() - } else formParts["genre_exclude"] = "[]" + formParts["exclusion"] = "OR" + if (filter.tagsExclude.isNotEmpty()) { + val exGenreArray = JSONArray(filter.tagsExclude.map { it.key }) + formParts["genre_exclude"] = exGenreArray.toString() + } else formParts["genre_exclude"] = "[]" - formParts["page"] = page.toString() + formParts["page"] = page.toString() - if (!filter.author.isNullOrEmpty()) { - val authorArray = JSONArray(filter.author) - formParts["author"] = authorArray.toString() - } else formParts["author"] = "[]" + if (!filter.author.isNullOrEmpty()) { + val authorArray = JSONArray(filter.author) + formParts["author"] = authorArray.toString() + } else formParts["author"] = "[]" - formParts["artist"] = "[]" - formParts["project"] = "0" + formParts["artist"] = "[]" + formParts["project"] = "0" if (filter.types.isNotEmpty()) { val typeArray = JSONArray() @@ -136,10 +135,10 @@ internal class Ikiru(context: MangaLoaderContext) : else -> {} } } - formParts["type"] = typeArray.toString() + formParts["type"] = typeArray.toString() } else { - formParts["type"] = "[]" - } + formParts["type"] = "[]" + } if (filter.states.isNotEmpty()) { val statusArray = JSONArray() @@ -151,14 +150,12 @@ internal class Ikiru(context: MangaLoaderContext) : else -> {} } } - if (statusArray.length() > 0) { - formParts["status"] = statusArray.toString() - } + formParts["status"] = statusArray.toString() } else { - formParts["status"] = "[]" - } + formParts["status"] = "[]" + } - formParts["order"] = "desc" + formParts["order"] = "desc" formParts["orderby"] = when (order) { SortOrder.UPDATED -> "updated" SortOrder.POPULARITY -> "popular" @@ -167,17 +164,15 @@ internal class Ikiru(context: MangaLoaderContext) : else -> "popular" } - if (!filter.query.isNullOrEmpty()) { - filter.query.let { formParts["query"] = it } - } else { - formParts["query"] = "[]" - } + if (!filter.query.isNullOrEmpty()) { + formParts["query"] = filter.query + } - val html = httpPost(url, formParts) + val html = httpPost(url, formParts) return parseMangaList(html) } - private fun parseMangaList(doc: Document): List { + protected open fun parseMangaList(doc: Document): List { val mangaList = mutableListOf() doc.select("body > div").forEach { divElement -> @@ -298,7 +293,7 @@ internal class Ikiru(context: MangaLoaderContext) : ) } - private suspend fun loadChapters( + protected open suspend fun loadChapters( mangaId: String, mangaAbsoluteUrl: String, ): List { @@ -361,39 +356,65 @@ internal class Ikiru(context: MangaLoaderContext) : } } - private suspend fun fetchAvailableTags(): Set { - val doc = webClient.httpGet("https://${domain}/advanced-search/").parseHtml() - val scriptContent = doc.select("script") - .firstOrNull { it.data().contains("var searchTerms") } - ?.data() - ?: return emptySet() - - val jsonString = scriptContent - .substringAfter("var searchTerms =") - .substringBeforeLast(";") - .trim() - - val json = org.json.JSONObject(jsonString) - val genreObject = json.optJSONObject("genre") ?: return emptySet() - val tags = mutableSetOf() - - for (key in genreObject.keys()) { - val item = genreObject.optJSONObject(key) ?: continue - val taxonomy = item.optString("taxonomy") - if (taxonomy != "genre") continue - val slug = item.optString("slug").takeIf { it.isNotBlank() } ?: continue - val name = item.optString("name").takeIf { it.isNotBlank() } ?: continue - - tags += MangaTag( - title = name.toTitleCase(), - key = slug, - source = source - ) - } - return tags - } - - private fun parseDate(dateStr: String?): Long { + protected open suspend fun fetchAvailableTags(): Set { + return try { + // Try to fetch from WP JSON API first (more reliable) + val response = webClient.httpGet("https://${domain}/wp-json/wp/v2/genre?per_page=100&page=1&orderby=count&order=desc") + val jsonText = response.body.use { it?.string() } ?: return emptySet() + val jsonArray = org.json.JSONArray(jsonText) + val tags = mutableSetOf() + + for (i in 0 until jsonArray.length()) { + val item = jsonArray.getJSONObject(i) + val slug = item.optString("slug").takeIf { it.isNotBlank() } ?: continue + val name = item.optString("name").takeIf { it.isNotBlank() } ?: continue + + tags += MangaTag( + title = name.toTitleCase(), + key = slug, + source = source + ) + } + tags + } catch (e: Exception) { + // Fallback to advanced-search page method + try { + val doc = webClient.httpGet("https://${domain}/advanced-search/").parseHtml() + val scriptContent = doc.select("script") + .firstOrNull { it.data().contains("var searchTerms") } + ?.data() + ?: return emptySet() + + val jsonString = scriptContent + .substringAfter("var searchTerms =") + .substringBeforeLast(";") + .trim() + + val json = org.json.JSONObject(jsonString) + val genreObject = json.optJSONObject("genre") ?: return emptySet() + val tags = mutableSetOf() + + for (key in genreObject.keys()) { + val item = genreObject.optJSONObject(key) ?: continue + val taxonomy = item.optString("taxonomy") + if (taxonomy != "genre") continue + val slug = item.optString("slug").takeIf { it.isNotBlank() } ?: continue + val name = item.optString("name").takeIf { it.isNotBlank() } ?: continue + + tags += MangaTag( + title = name.toTitleCase(), + key = slug, + source = source + ) + } + tags + } catch (e2: Exception) { + emptySet() + } + } + } + + protected open fun parseDate(dateStr: String?): Long { if (dateStr.isNullOrEmpty()) return 0 return try { @@ -427,13 +448,13 @@ internal class Ikiru(context: MangaLoaderContext) : } } - // Utils - private val multipartHttpClient by lazy { - OkHttpClient.Builder() - .build() - } + // Utils + private val multipartHttpClient by lazy { + OkHttpClient.Builder() + .build() + } - private suspend fun httpPost(url: String, form: Map, extraHeaders: Headers? = null): Document { + protected open suspend fun httpPost(url: String, form: Map, extraHeaders: Headers? = null): Document { val body = MultipartBody.Builder().setType(MultipartBody.FORM) form.forEach { (k, v) -> body.addFormDataPart(k, v) } diff --git a/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/id/Ikiru.kt b/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/id/Ikiru.kt new file mode 100644 index 0000000..71cfd4d --- /dev/null +++ b/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/id/Ikiru.kt @@ -0,0 +1,19 @@ +package org.dokiteam.doki.parsers.site.natsu.id + +import org.dokiteam.doki.parsers.MangaLoaderContext +import org.dokiteam.doki.parsers.MangaSourceParser +import org.dokiteam.doki.parsers.config.ConfigKey +import org.dokiteam.doki.parsers.model.MangaParserSource +import org.dokiteam.doki.parsers.site.natsu.NatsuParser + +@MangaSourceParser("IKIRU", "Ikiru", "id") +internal class Ikiru(context: MangaLoaderContext) : + NatsuParser(context, MangaParserSource.IKIRU, pageSize = 24) { + + override val configKeyDomain = ConfigKey.Domain("02.ikiru.wtf") + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } +} diff --git a/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/id/Kiryuu.kt b/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/id/Kiryuu.kt new file mode 100644 index 0000000..20b3314 --- /dev/null +++ b/src/main/kotlin/org/dokiteam/doki/parsers/site/natsu/id/Kiryuu.kt @@ -0,0 +1,19 @@ +package org.dokiteam.doki.parsers.site.natsu.id + +import org.dokiteam.doki.parsers.MangaLoaderContext +import org.dokiteam.doki.parsers.MangaSourceParser +import org.dokiteam.doki.parsers.config.ConfigKey +import org.dokiteam.doki.parsers.model.MangaParserSource +import org.dokiteam.doki.parsers.site.natsu.NatsuParser + +@MangaSourceParser("KIRYUU", "Kiryuu", "id") +internal class Kiryuu(context: MangaLoaderContext) : + NatsuParser(context, MangaParserSource.KIRYUU, pageSize = 24) { + + override val configKeyDomain = ConfigKey.Domain("kiryuu03.com") + + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } +}