Kemono: add mirror setting and Coomer new design (#17311)

This commit is contained in:
stevenyomi 2023-07-30 05:31:38 +08:00 committed by GitHub
parent bbcae007e8
commit 66cd665b92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 42 additions and 49 deletions

View File

@ -20,7 +20,6 @@ import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okio.blackholeSink import okio.blackholeSink
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator import org.jsoup.select.Evaluator
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -32,12 +31,12 @@ import kotlin.math.min
open class Kemono( open class Kemono(
override val name: String, override val name: String,
override val baseUrl: String, private val defaultUrl: String,
override val lang: String = "all", override val lang: String = "all",
) : HttpSource(), ConfigurableSource { ) : HttpSource(), ConfigurableSource {
override val supportsLatest = true override val supportsLatest = true
private val isNewDesign get() = name == "Kemono" private val mirrorUrls get() = arrayOf(defaultUrl, defaultUrl.removeSuffix(".party") + ".su")
override val client = network.client.newBuilder().rateLimit(2).build() override val client = network.client.newBuilder().rateLimit(2).build()
@ -46,47 +45,33 @@ open class Kemono(
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val preferences by lazy { private val preferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularMangaRequest(page: Int): Request = override val baseUrl = preferences.getString(BASE_URL_PREF, defaultUrl)!!
GET("$baseUrl/artists?o=${PAGE_SIZE * (page - 1)}", headers)
override fun popularMangaParse(response: Response): MangasPage { private val imgCdnUrl = when (name) {
val document = response.asJsoup() "Kemono" -> baseUrl
val cardList = document.selectFirst(Evaluator.Class("card-list"))!! else -> defaultUrl
val creators = cardList.select(Evaluator.Tag("article")).map { }.replace("//", "//img.")
val children = it.children()
val avatar = children[0].selectFirst(Evaluator.Tag("img"))!!.attr("src")
val link = children[1].child(0)
val service = children[2].ownText()
SManga.create().apply {
url = link.attr("href")
title = link.ownText()
author = service
thumbnail_url = baseUrl + avatar
description = PROMPT
initialized = true
}
}.filterUnsupported()
return MangasPage(creators, document.hasNextPage())
}
override fun latestUpdatesRequest(page: Int): Request = private fun String.formatAvatarUrl(): String = removePrefix("https://").replaceBefore('/', imgCdnUrl)
GET("$baseUrl/artists/updated?o=${PAGE_SIZE * (page - 1)}", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response) override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun fetchPopularManga(page: Int): Observable<MangasPage> { override fun fetchPopularManga(page: Int): Observable<MangasPage> {
if (!isNewDesign) return super.fetchPopularManga(page)
return Observable.fromCallable { return Observable.fromCallable {
fetchNewDesignListing(page, "/artists", compareByDescending { it.favorited }) fetchNewDesignListing(page, "/artists", compareByDescending { it.favorited })
} }
} }
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> { override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
if (!isNewDesign) return super.fetchLatestUpdates(page)
return Observable.fromCallable { return Observable.fromCallable {
fetchNewDesignListing(page, "/artists/updated", compareByDescending { it.updatedDate }) fetchNewDesignListing(page, "/artists/updated", compareByDescending { it.updatedDate })
} }
@ -106,7 +91,7 @@ open class Kemono(
url = it.attr("href") url = it.attr("href")
title = it.selectFirst(Evaluator.Class("user-card__name"))!!.ownText() title = it.selectFirst(Evaluator.Class("user-card__name"))!!.ownText()
author = it.selectFirst(Evaluator.Class("user-card__service"))!!.ownText() author = it.selectFirst(Evaluator.Class("user-card__service"))!!.ownText()
thumbnail_url = baseUrl + it.selectFirst(Evaluator.Tag("img"))!!.attr("src") thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.absUrl("src").formatAvatarUrl()
description = PROMPT description = PROMPT
initialized = true initialized = true
} }
@ -135,14 +120,14 @@ open class Kemono(
page: Int, page: Int,
block: (ArrayList<KemonoCreatorDto>) -> List<KemonoCreatorDto>, block: (ArrayList<KemonoCreatorDto>) -> List<KemonoCreatorDto>,
): MangasPage { ): MangasPage {
val baseUrl = this.baseUrl val imgCdnUrl = this.imgCdnUrl
val response = client.newCall(GET("$baseUrl/api/creators", headers)).execute() val response = client.newCall(GET("$baseUrl/api/creators", headers)).execute()
val allCreators = block(response.parseAs()) val allCreators = block(response.parseAs())
val count = allCreators.size val count = allCreators.size
val fromIndex = (page - 1) * NEW_PAGE_SIZE val fromIndex = (page - 1) * NEW_PAGE_SIZE
val toIndex = min(count, fromIndex + NEW_PAGE_SIZE) val toIndex = min(count, fromIndex + NEW_PAGE_SIZE)
val creators = allCreators.subList(fromIndex, toIndex) val creators = allCreators.subList(fromIndex, toIndex)
.map { it.toSManga(baseUrl) } .map { it.toSManga(imgCdnUrl) }
.filterUnsupported() .filterUnsupported()
return MangasPage(creators, toIndex < count) return MangasPage(creators, toIndex < count)
} }
@ -160,12 +145,15 @@ open class Kemono(
client.newCall(GET("$baseUrl/api/creators", headers)).enqueue(callback) client.newCall(GET("$baseUrl/api/creators", headers)).enqueue(callback)
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not used.") override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException("Not used.") override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.just(manga) override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
manga.thumbnail_url = manga.thumbnail_url!!.formatAvatarUrl()
return Observable.just(manga)
}
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException("Not used.") override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
KemonoPostDto.dateFormat.timeZone = when (manga.author) { KemonoPostDto.dateFormat.timeZone = when (manga.author) {
@ -187,7 +175,7 @@ open class Kemono(
result result
} }
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used.") override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
override fun pageListRequest(chapter: SChapter): Request = override fun pageListRequest(chapter: SChapter): Request =
GET("$baseUrl/api${chapter.url}", headers) GET("$baseUrl/api${chapter.url}", headers)
@ -197,7 +185,7 @@ open class Kemono(
return post[0].images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) } return post[0].images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
} }
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.") override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T = use { private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream()) json.decodeFromStream(it.body.byteStream())
@ -214,10 +202,18 @@ open class Kemono(
}.toTypedArray() }.toTypedArray()
setDefaultValue(POST_PAGES_DEFAULT) setDefaultValue(POST_PAGES_DEFAULT)
}.let { screen.addPreference(it) } }.let { screen.addPreference(it) }
ListPreference(screen.context).apply {
key = BASE_URL_PREF
title = "Mirror URL"
summary = "%s\nRequires app restart to take effect"
entries = mirrorUrls
entryValues = mirrorUrls
setDefaultValue(defaultUrl)
}.let(screen::addPreference)
} }
companion object { companion object {
private const val PAGE_SIZE = 25
private const val NEW_PAGE_SIZE = 50 private const val NEW_PAGE_SIZE = 50
const val PROMPT = "You can change how many posts to load in the extension preferences." const val PROMPT = "You can change how many posts to load in the extension preferences."
@ -226,11 +222,8 @@ open class Kemono(
private const val POST_PAGES_DEFAULT = "1" private const val POST_PAGES_DEFAULT = "1"
private const val POST_PAGES_MAX = 50 private const val POST_PAGES_MAX = 50
private fun Element.hasNextPage(): Boolean {
val pagination = selectFirst(Evaluator.Class("paginator"))!!
return pagination.selectFirst("a[title=Next page]") != null
}
private fun List<SManga>.filterUnsupported() = filterNot { it.author == "Discord" } private fun List<SManga>.filterUnsupported() = filterNot { it.author == "Discord" }
private const val BASE_URL_PREF = "BASE_URL"
} }
} }

View File

@ -21,11 +21,11 @@ class KemonoCreatorDto(
else -> (updated.double * 1000).toLong() else -> (updated.double * 1000).toLong()
} }
fun toSManga(baseUrl: String) = SManga.create().apply { fun toSManga(imgCdnUrl: String) = SManga.create().apply {
url = "/$service/user/$id" // should be /server/ for Discord but will be filtered anyway url = "/$service/user/$id" // should be /server/ for Discord but will be filtered anyway
title = name title = name
author = service.serviceName() author = service.serviceName()
thumbnail_url = "$baseUrl/icons/$service/$id" thumbnail_url = "$imgCdnUrl/icons/$service/$id"
description = Kemono.PROMPT description = Kemono.PROMPT
initialized = true initialized = true
} }

View File

@ -9,7 +9,7 @@ class KemonoGenerator : ThemeSourceGenerator {
override val themePkg = "kemono" override val themePkg = "kemono"
override val baseVersionCode = 5 override val baseVersionCode = 6
override val sources = listOf( override val sources = listOf(
SingleLang("Kemono", "https://kemono.party", "all", isNsfw = true), SingleLang("Kemono", "https://kemono.party", "all", isNsfw = true),