feat(misskon): add source misskon. (#8225)
* feat(misskon): add source misskon. refs: #1855 * feat(misskon): Improve network handling and code structure • Network enhancements: - Add headers parameter for `GET` methods - Implement standardized query with `HttpUrl` - Replace `attr()` with `asbUrl()` for safe path composition • Structural improvements: - Move date regex to companion object - Extract SimpleDateFormat as global constant - Refactor Filter logic into standalone class • Robustness upgrades: - Remove unsafe non-null assertion(!!) on date_upload - Adopt SManga.create().apply { } chaining pattern refs: #1855 * feat(misskon): improve code structure and null safety - Replace `attr()` with `absUrl()` for more reliable URL extraction - Remove non-null assertion (`!!`) from `thumbnail_url` as it's not critical - Fix usage of `select()` and `selectFirst()` to properly handle nullable cases refs: #1855 * chore(MissKon): remove needless blank line - Fix lint violation (no-consecutive-blank-lines) Closes: #1855 * refactor(MissKon): optimize URL handling and reuse utils * Remove baseUrl from category URLs (build dynamically when used) * Reuse existing utils for: - Date parsing - Filter queries * Standardize pagination URL construction Closes: #1855 * fix(MissKon): Correct URL template, set default gallery name, and unify selectors Closes: #1855 * refactor(MissKon): fix URL construction and client configuration - Fix string interpolation in search URL ("$it.url" → "${it.url}") - Remove MOBILE user agent restriction - Remove unused dependency - Add ONLY_FETCH_ONCE update strategy - Clean up selector syntax
This commit is contained in:
parent
6341d5ff73
commit
40a9d2ec6a
8
src/all/misskon/build.gradle
Normal file
8
src/all/misskon/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'MissKon'
|
||||
extClass = '.MissKon'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/misskon/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/misskon/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
src/all/misskon/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/misskon/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
src/all/misskon/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/misskon/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
BIN
src/all/misskon/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/misskon/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/all/misskon/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/misskon/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@ -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<SourceCategorySelector>()
|
||||
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<Page> {
|
||||
val basePageUrl = document.selectFirst("link[rel=canonical]")!!.absUrl("href")
|
||||
|
||||
val pages = mutableListOf<Page>()
|
||||
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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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<SourceCategory>,
|
||||
) : Filter.Select<SourceCategory>(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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user