Add Kemono and Coomer (#12542)

* Add Kemono

* utilize more APIs

* make multisrc theme and add Coomer
This commit is contained in:
stevenyomi 2022-07-12 06:42:27 +08:00 committed by GitHub
parent 55538ab076
commit 53e135bc03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 275 additions and 0 deletions

View File

@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="KemonoGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="tachiyomi-extensions.multisrc" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.kemono.KemonoGenerator" />
<option name="WORKING_DIRECTORY" value="" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="" />
</method>
</configuration>
</component>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,155 @@
package eu.kanade.tachiyomi.multisrc.kemono
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
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.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
import rx.Single
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.TimeZone
open class Kemono(
override val name: String,
override val baseUrl: String,
override val lang: String = "all",
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client = network.client.newBuilder().rateLimit(2).build()
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/artists?o=${PAGE_SIZE * (page - 1)}", headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val cardList = document.selectFirst(Evaluator.Class("card-list"))
val creators = cardList.select(Evaluator.Tag("article")).map {
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 =
GET("$baseUrl/artists/updated?o=${PAGE_SIZE * (page - 1)}", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Single.create<MangasPage> { subscriber ->
val baseUrl = this.baseUrl
val response = client.newCall(GET("$baseUrl/api/creators", headers)).execute()
val result = response.parseAs<List<KemonoCreatorDto>>()
.filter { it.name.contains(query, ignoreCase = true) }
.sortedByDescending { it.updatedDate }
.map { it.toSManga(baseUrl) }
.filterUnsupported()
subscriber.onSuccess(MangasPage(result, false))
}.toObservable()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException("Not used.")
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException("Not used.")
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.just(manga)
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException("Not used.")
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Single.create<List<SChapter>> {
KemonoPostDto.dateFormat.timeZone = when (manga.author) {
"Pixiv Fanbox", "Fantia" -> TimeZone.getTimeZone("GMT+09:00")
else -> TimeZone.getTimeZone("GMT")
}
val maxPosts = preferences.getString(POST_PAGES_PREF, POST_PAGES_DEFAULT)!!
.toInt().coerceAtMost(POST_PAGES_MAX) * POST_PAGE_SIZE
var offset = 0
var hasNextPage = true
val result = ArrayList<SChapter>()
while (offset < maxPosts && hasNextPage) {
val request = GET("$baseUrl/api${manga.url}?limit=$POST_PAGE_SIZE&o=$offset", headers)
val page: List<KemonoPostDto> = client.newCall(request).execute().parseAs()
page.forEach { post -> if (post.images.isNotEmpty()) result.add(post.toSChapter()) }
offset += POST_PAGE_SIZE
hasNextPage = page.size == POST_PAGE_SIZE
}
it.onSuccess(result)
}.toObservable()
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Not used.")
override fun pageListRequest(chapter: SChapter): Request =
GET("$baseUrl/api${chapter.url}", headers)
override fun pageListParse(response: Response): List<Page> {
val post: List<KemonoPostDto> = response.parseAs()
return post[0].images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.")
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body!!.byteStream())
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = POST_PAGES_PREF
title = "Maximum posts to load"
summary = "Loading more posts costs more time and network traffic.\nCurrently: %s"
entryValues = (1..POST_PAGES_MAX).map { it.toString() }.toTypedArray()
entries = (1..POST_PAGES_MAX).map {
if (it == 1) "1 page ($POST_PAGE_SIZE posts)" else "$it pages (${it * POST_PAGE_SIZE} posts)"
}.toTypedArray()
setDefaultValue(POST_PAGES_DEFAULT)
}.let { screen.addPreference(it) }
}
companion object {
private const val PAGE_SIZE = 25
const val PROMPT = "You can change how many posts to load in the extension preferences."
private const val POST_PAGE_SIZE = 50
private const val POST_PAGES_PREF = "POST_PAGES"
private const val POST_PAGES_DEFAULT = "1"
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" }
}
}

View File

@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.multisrc.kemono
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class KemonoCreatorDto(
private val id: String,
val name: String,
private val service: String,
private val updated: String,
) {
val updatedDate get() = dateFormat.parse(updated)?.time ?: 0
fun toSManga(baseUrl: String) = SManga.create().apply {
url = "/$service/user/$id" // should be /server/ for Discord but will be filtered anyway
title = name
author = service.serviceName()
thumbnail_url = "$baseUrl/icons/$service/$id"
description = Kemono.PROMPT
initialized = true
}
companion object {
private val dateFormat by lazy { getApiDateFormat() }
fun String.serviceName() = when (this) {
"fanbox" -> "Pixiv Fanbox"
"subscribestar" -> "SubscribeStar"
"dlsite" -> "DLsite"
"onlyfans" -> "OnlyFans"
else -> replaceFirstChar { it.uppercase() }
}
}
}
@Serializable
class KemonoPostDto(
private val id: String,
private val service: String,
private val user: String,
private val title: String,
private val added: String,
private val published: String?,
private val edited: String?,
private val file: KemonoFileDto,
private val attachments: List<KemonoAttachmentDto>,
) {
val images: List<String>
get() = buildList(attachments.size + 1) {
file.path?.let { add(it) }
attachments.mapTo(this) { it.path }
}.filter {
when (it.substringAfterLast('.').lowercase()) {
"png", "jpg", "gif", "jpeg", "webp" -> true
else -> false
}
}.distinct()
fun toSChapter() = SChapter.create().apply {
url = "/$service/user/$user/post/$id"
name = title
date_upload = dateFormat.parse(edited ?: published ?: added)?.time ?: 0
chapter_number = -2f
}
companion object {
val dateFormat by lazy { getApiDateFormat() }
}
}
@Serializable
class KemonoFileDto(val path: String? = null)
@Serializable
class KemonoAttachmentDto(val path: String)
private fun getApiDateFormat() =
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH)

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.multisrc.kemono
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class KemonoGenerator : ThemeSourceGenerator {
override val themeClass = "Kemono"
override val themePkg = "kemono"
override val baseVersionCode = 1
override val sources = listOf(
SingleLang("Kemono", "https://kemono.party", "all", isNsfw = true, className = "KemonoParty", pkgName = "kemono"),
SingleLang("Coomer", "https://coomer.party", "all", isNsfw = true)
)
companion object {
@JvmStatic
fun main(args: Array<String>) {
KemonoGenerator().createAll()
}
}
}