Kemono: add mirror setting and Coomer new design (#17311)
This commit is contained in:
parent
bbcae007e8
commit
66cd665b92
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in New Issue