diff --git a/src/all/misskon/build.gradle b/src/all/misskon/build.gradle new file mode 100644 index 000000000..a9d790cb0 --- /dev/null +++ b/src/all/misskon/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'MissKon' + extClass = '.MissKon' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/misskon/res/mipmap-hdpi/ic_launcher.png b/src/all/misskon/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..13cbf5995 Binary files /dev/null and b/src/all/misskon/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/misskon/res/mipmap-mdpi/ic_launcher.png b/src/all/misskon/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..6a27a77e3 Binary files /dev/null and b/src/all/misskon/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/misskon/res/mipmap-xhdpi/ic_launcher.png b/src/all/misskon/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..9288b88bb Binary files /dev/null and b/src/all/misskon/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/misskon/res/mipmap-xxhdpi/ic_launcher.png b/src/all/misskon/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..ec94f822c Binary files /dev/null and b/src/all/misskon/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/misskon/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/misskon/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..2bdeeda35 Binary files /dev/null and b/src/all/misskon/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/MissKon.kt b/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/MissKon.kt new file mode 100644 index 000000000..f3c80cb56 --- /dev/null +++ b/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/MissKon.kt @@ -0,0 +1,135 @@ +package eu.kanade.tachiyomi.extension.all.misskon + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimitHost +import eu.kanade.tachiyomi.source.model.Filter +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.model.UpdateStrategy +import eu.kanade.tachiyomi.util.asJsoup +import keiyoushi.utils.firstInstance +import keiyoushi.utils.tryParse +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.concurrent.TimeUnit + +class MissKon() : SimpleParsedHttpSource() { + + override val baseUrl = "https://misskon.com" + override val lang = "all" + override val name = "MissKon" + override val supportsLatest = true + + override val client = network.cloudflareClient.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 10, 1, TimeUnit.SECONDS) + .build() + + override fun simpleMangaSelector() = "article.item-list" + + override fun simpleMangaFromElement(element: Element): SManga { + val titleEL = element.selectFirst(".post-box-title")!! + return SManga.create().apply { + title = titleEL.text() + thumbnail_url = element.selectFirst(".post-thumbnail img")?.absUrl("data-src") + setUrlWithoutDomain(titleEL.selectFirst("a")!!.absUrl("href")) + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + } + } + + override fun simpleNextPageSelector(): String? = null + + // region popular + override fun popularMangaRequest(page: Int) = GET("$baseUrl/top3/", headers) + // endregion + + // region latest + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/$page", headers) + + override fun latestUpdatesNextPageSelector() = ".current + a.page" + // endregion + + // region Search + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val filter = filters.firstInstance() + return filter.selectedCategory?.let { + GET("$baseUrl${it.url}", headers) + } ?: run { + "$baseUrl/page/$page/".toHttpUrl().newBuilder() + .addEncodedQueryParameter("s", query) + .build() + .let { GET(it, headers) } + } + } + + override fun searchMangaNextPageSelector() = "div.content > div.pagination > span.current + a" + // endregion + + // region Details + override fun mangaDetailsParse(document: Document): SManga { + val postInnerEl = document.selectFirst("article > .post-inner")!! + return SManga.create().apply { + title = postInnerEl.select(".post-title").text() + genre = postInnerEl.select(".post-tag > a").joinToString { it.text() } + } + } + + override fun chapterListSelector() = "html" + + override fun chapterFromElement(element: Element): SChapter { + val dateStr = element.selectFirst(".entry img")?.absUrl("data-src") + ?.let { url -> + FULL_DATE_REGEX.find(url)?.groupValues?.get(1) + ?: YEAR_MONTH_REGEX.find(url)?.groupValues?.get(1)?.let { "$it/01" } + } + + return SChapter.create().apply { + chapter_number = 0F + setUrlWithoutDomain(element.selectFirst("link[rel=canonical]")!!.absUrl("href")) + name = "Gallery" + date_upload = FULL_DATE_FORMAT.tryParse(dateStr) + } + } + // endregion + + // region Pages + override fun pageListParse(document: Document): List { + val basePageUrl = document.selectFirst("link[rel=canonical]")!!.absUrl("href") + + val pages = mutableListOf() + document.select("div.post-inner div.page-link:nth-child(1) .post-page-numbers") + .forEachIndexed { index, pageEl -> + val doc = when (index) { + 0 -> document + else -> { + val url = "$basePageUrl${pageEl.text()}/" + client.newCall(GET(url, headers)).execute().asJsoup() + } + } + doc.select("div.post-inner > div.entry > p > img") + .map { it.absUrl("data-src") } + .forEach { pages.add(Page(pages.size, imageUrl = it)) } + } + return pages + } + // endregion + + override fun getFilterList(): FilterList = FilterList( + Filter.Header("NOTE: Unable to further search in the category!"), + Filter.Separator(), + SourceCategorySelector.create(), + ) + + companion object { + + private val FULL_DATE_REGEX = Regex("""/(\d{4}/\d{2}/\d{2})/""") + private val YEAR_MONTH_REGEX = Regex("""/(\d{4}/\d{2})/""") + + private val FULL_DATE_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.US) + } +} diff --git a/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/SimpleParsedHttpSource.kt b/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/SimpleParsedHttpSource.kt new file mode 100644 index 000000000..29ff34487 --- /dev/null +++ b/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/SimpleParsedHttpSource.kt @@ -0,0 +1,39 @@ +package eu.kanade.tachiyomi.extension.all.misskon + +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +abstract class SimpleParsedHttpSource : ParsedHttpSource() { + + abstract fun simpleMangaSelector(): String + + abstract fun simpleMangaFromElement(element: Element): SManga + + abstract fun simpleNextPageSelector(): String? + + // region popular + override fun popularMangaSelector() = simpleMangaSelector() + override fun popularMangaNextPageSelector() = simpleNextPageSelector() + override fun popularMangaFromElement(element: Element) = simpleMangaFromElement(element) + // endregion + + // region last + override fun latestUpdatesSelector() = simpleMangaSelector() + override fun latestUpdatesFromElement(element: Element) = simpleMangaFromElement(element) + override fun latestUpdatesNextPageSelector() = simpleNextPageSelector() + // endregion + + // region search + override fun searchMangaSelector() = simpleMangaSelector() + override fun searchMangaFromElement(element: Element) = simpleMangaFromElement(element) + override fun searchMangaNextPageSelector() = simpleNextPageSelector() + // endregion + + override fun chapterListSelector() = simpleMangaSelector() + override fun imageUrlParse(document: Document): String { + throw UnsupportedOperationException() + } + // endregion +} diff --git a/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/SourceCategorySelector.kt b/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/SourceCategorySelector.kt new file mode 100644 index 000000000..5e7a18ed7 --- /dev/null +++ b/src/all/misskon/src/eu/kanade/tachiyomi/extension/all/misskon/SourceCategorySelector.kt @@ -0,0 +1,128 @@ +package eu.kanade.tachiyomi.extension.all.misskon + +import eu.kanade.tachiyomi.extension.all.misskon.SourceCategorySelector.Companion.CategoryPresets.CHINESE +import eu.kanade.tachiyomi.extension.all.misskon.SourceCategorySelector.Companion.CategoryPresets.KOREAN +import eu.kanade.tachiyomi.extension.all.misskon.SourceCategorySelector.Companion.CategoryPresets.OTHER +import eu.kanade.tachiyomi.extension.all.misskon.SourceCategorySelector.Companion.CategoryPresets.TOP +import eu.kanade.tachiyomi.source.model.Filter + +data class SourceCategory(private val name: String, val url: String) { + override fun toString() = this.name +} + +class SourceCategorySelector( + name: String, + categories: List, +) : Filter.Select(name, categories.toTypedArray()) { + + val selectedCategory: SourceCategory? + get() = if (state > 0) values[state] else null + + companion object { + + private object CategoryPresets { + + val TOP = listOf( + SourceCategory("Top 3 days", "/top3/"), + SourceCategory("Top 7 days", "/top7/"), + SourceCategory("Top 30 days", "/top30/"), + SourceCategory("Top 60 days", "/top60/"), + ) + + val CHINESE = listOf( + SourceCategory("Chinese:[MTCos] 喵糖映画", "/tag/mtcos/"), + SourceCategory("Chinese:BoLoli", "/tag/bololi/"), + SourceCategory("Chinese:CANDY", "/tag/candy/"), + SourceCategory("Chinese:FEILIN", "/tag/feilin/"), + SourceCategory("Chinese:FToow", "/tag/ftoow/"), + SourceCategory("Chinese:GIRLT", "/tag/girlt/"), + SourceCategory("Chinese:HuaYan", "/tag/huayan/"), + SourceCategory("Chinese:HuaYang", "/tag/huayang/"), + SourceCategory("Chinese:IMISS", "/tag/imiss/"), + SourceCategory("Chinese:ISHOW", "/tag/ishow/"), + SourceCategory("Chinese:JVID", "/tag/jvid/"), + SourceCategory("Chinese:KelaGirls", "/tag/kelagirls/"), + SourceCategory("Chinese:Kimoe", "/tag/kimoe/"), + SourceCategory("Chinese:LegBaby", "/tag/legbaby/"), + SourceCategory("Chinese:MF", "/tag/mf/"), + SourceCategory("Chinese:MFStar", "/tag/mfstar/"), + SourceCategory("Chinese:MiiTao", "/tag/miitao/"), + SourceCategory("Chinese:MintYe", "/tag/mintye/"), + SourceCategory("Chinese:MISSLEG", "/tag/missleg/"), + SourceCategory("Chinese:MiStar", "/tag/mistar/"), + SourceCategory("Chinese:MTMeng", "/tag/mtmeng/"), + SourceCategory("Chinese:MyGirl", "/tag/mygirl/"), + SourceCategory("Chinese:PartyCat", "/tag/partycat/"), + SourceCategory("Chinese:QingDouKe", "/tag/qingdouke/"), + SourceCategory("Chinese:RuiSG", "/tag/ruisg/"), + SourceCategory("Chinese:SLADY", "/tag/slady/"), + SourceCategory("Chinese:TASTE", "/tag/taste/"), + SourceCategory("Chinese:TGOD", "/tag/tgod/"), + SourceCategory("Chinese:TouTiao", "/tag/toutiao/"), + SourceCategory("Chinese:TuiGirl", "/tag/tuigirl/"), + SourceCategory("Chinese:Tukmo", "/tag/tukmo/"), + SourceCategory("Chinese:UGIRLS", "/tag/ugirls/"), + SourceCategory("Chinese:UGIRLS - Ai You Wu App", "/tag/ugirls-ai-you-wu-app/"), + SourceCategory("Chinese:UXING", "/tag/uxing/"), + SourceCategory("Chinese:WingS", "/tag/wings/"), + SourceCategory("Chinese:XiaoYu", "/tag/xiaoyu/"), + SourceCategory("Chinese:XingYan", "/tag/xingyan/"), + SourceCategory("Chinese:XIUREN", "/tag/xiuren/"), + SourceCategory("Chinese:XR Uncensored", "/tag/xr-uncensored/"), + SourceCategory("Chinese:YouMei", "/tag/youmei/"), + SourceCategory("Chinese:YouMi", "/tag/youmi/"), + SourceCategory("Chinese:YouMi尤蜜", "/tag/youmiapp/"), + SourceCategory("Chinese:YouWu", "/tag/youwu/"), + ) + + val KOREAN = listOf( + SourceCategory("Korean:AG", "/tag/ag/"), + SourceCategory("Korean:Bimilstory", "/tag/bimilstory/"), + SourceCategory("Korean:BLUECAKE", "/tag/bluecake/"), + SourceCategory("Korean:CreamSoda", "/tag/creamsoda/"), + SourceCategory("Korean:DJAWA", "/tag/djawa/"), + SourceCategory("Korean:Espacia Korea", "/tag/espacia-korea/"), + SourceCategory("Korean:Fantasy Factory", "/tag/fantasy-factory/"), + SourceCategory("Korean:Fantasy Story", "/tag/fantasy-story/"), + SourceCategory("Korean:Glamarchive", "/tag/glamarchive/"), + SourceCategory("Korean:HIGH FANTASY", "/tag/high-fantasy/"), + SourceCategory("Korean:KIMLEMON", "/tag/kimlemon/"), + SourceCategory("Korean:KIREI", "/tag/kirei/"), + SourceCategory("Korean:KiSiA", "/tag/kisia/"), + SourceCategory("Korean:Korean Realgraphic", "/tag/korean-realgraphic/"), + SourceCategory("Korean:Lilynah", "/tag/lilynah/"), + SourceCategory("Korean:Lookas", "/tag/lookas/"), + SourceCategory("Korean:Loozy", "/tag/loozy/"), + SourceCategory("Korean:Moon Night Snap", "/tag/moon-night-snap/"), + SourceCategory("Korean:Paranhosu", "/tag/paranhosu/"), + SourceCategory("Korean:PhotoChips", "/tag/photochips/"), + SourceCategory("Korean:Pure Media", "/tag/pure-media/"), + SourceCategory("Korean:PUSSYLET", "/tag/pussylet/"), + SourceCategory("Korean:SAINT Photolife", "/tag/saint-photolife/"), + SourceCategory("Korean:SWEETBOX", "/tag/sweetbox/"), + SourceCategory("Korean:UHHUNG MAGAZINE", "/tag/uhhung-magazine/"), + SourceCategory("Korean:UMIZINE", "/tag/umizine/"), + SourceCategory("Korean:WXY ENT", "/tag/wxy-ent/"), + SourceCategory("Korean:Yo-U", "/tag/yo-u/"), + ) + + val OTHER = listOf( + SourceCategory("Other:AI Generated", "/tag/ai-generated/"), + SourceCategory("Other:Cosplay", "/tag/cosplay/"), + SourceCategory("Other:JP", "/tag/jp/"), + SourceCategory("Other:JVID", "/tag/jvid/"), + SourceCategory("Other:Patreon", "/tag/patreon/"), + ) + } + + fun create(): SourceCategorySelector { + val options = mutableListOf(SourceCategory("unselected", "")).apply { + addAll(TOP) + addAll(CHINESE) + addAll(KOREAN) + addAll(OTHER) + } + return SourceCategorySelector("Category", options) + } + } +}