forked from AlmightyHak/extensions-source
Anime1.me(zh/hant): Fetch anime detail from bangumi. (#369)
This commit is contained in:
parent
29dc8515eb
commit
d8fdc0b702
6 changed files with 316 additions and 4 deletions
7
lib/bangumi-scraper/build.gradle.kts
Normal file
7
lib/bangumi-scraper/build.gradle.kts
Normal file
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
id("lib-android")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.aniyomi.lib)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
@file:UseSerializers(BoxItemSerializer::class)
|
||||
package eu.kanade.tachiyomi.lib.bangumiscraper
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
internal data class Images(
|
||||
val large: String,
|
||||
val common: String,
|
||||
val medium: String,
|
||||
val small: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class BoxItem(
|
||||
val key: String,
|
||||
val value: String,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializer(forClass = BoxItem::class)
|
||||
internal object BoxItemSerializer : KSerializer<BoxItem> {
|
||||
override fun deserialize(decoder: Decoder): BoxItem {
|
||||
val item = (decoder as JsonDecoder).decodeJsonElement().jsonObject
|
||||
val key = item["key"]!!.jsonPrimitive.content
|
||||
val value = (item["value"] as? JsonPrimitive)?.contentOrNull ?: ""
|
||||
return BoxItem(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class Subject(
|
||||
val name: String,
|
||||
@SerialName("name_cn")
|
||||
val nameCN: String,
|
||||
val summary: String,
|
||||
val images: Images,
|
||||
@SerialName("meta_tags")
|
||||
val metaTags: List<String>,
|
||||
@SerialName("infobox")
|
||||
val infoBox: List<BoxItem>,
|
||||
) {
|
||||
fun findAuthor(): String? {
|
||||
return findInfo("导演", "原作")
|
||||
}
|
||||
|
||||
fun findArtist(): String? {
|
||||
return findInfo("美术监督", "总作画监督", "动画制作")
|
||||
}
|
||||
|
||||
fun findInfo(vararg keys: String): String? {
|
||||
keys.forEach { key ->
|
||||
return infoBox.find { item ->
|
||||
item.key == key
|
||||
}?.value ?: return@forEach
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class SearchItem(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
@SerialName("name_cn")
|
||||
val nameCN: String,
|
||||
val summary: String,
|
||||
val images: Images,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class SearchResponse(val results: Int, val list: List<SearchItem>)
|
|
@ -0,0 +1,126 @@
|
|||
package eu.kanade.tachiyomi.lib.bangumiscraper
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
enum class BangumiSubjectType(val value: Int) {
|
||||
BOOK(1),
|
||||
ANIME(2),
|
||||
MUSIC(3),
|
||||
GAME(4),
|
||||
REAL(6),
|
||||
}
|
||||
|
||||
enum class BangumiFetchType {
|
||||
/**
|
||||
* Give cover and summary info.
|
||||
*/
|
||||
SHORT,
|
||||
|
||||
/**
|
||||
* Give all require info include genre and author info.
|
||||
*/
|
||||
ALL,
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class to fetch anime details from Bangumi
|
||||
*/
|
||||
object BangumiScraper {
|
||||
private const val SEARCH_URL = "https://api.bgm.tv/search/subject"
|
||||
private const val SUBJECTS_URL = "https://api.bgm.tv/v0/subjects"
|
||||
|
||||
/**
|
||||
* Fetch anime details info from Bangumi
|
||||
* @param fetchType check [BangumiFetchType] to get detail
|
||||
* @param subjectType check [BangumiSubjectType] to get detail
|
||||
* @param requestProducer used to custom request
|
||||
*/
|
||||
suspend fun fetchDetail(
|
||||
client: OkHttpClient,
|
||||
keyword: String,
|
||||
fetchType: BangumiFetchType = BangumiFetchType.SHORT,
|
||||
subjectType: BangumiSubjectType = BangumiSubjectType.ANIME,
|
||||
requestProducer: (url: HttpUrl) -> Request = { url -> GET(url) },
|
||||
): SAnime {
|
||||
val httpUrl = SEARCH_URL.toHttpUrl().newBuilder()
|
||||
.addPathSegment(keyword)
|
||||
.addQueryParameter(
|
||||
"responseGroup",
|
||||
if (fetchType == BangumiFetchType.ALL) {
|
||||
"small"
|
||||
} else {
|
||||
"medium"
|
||||
},
|
||||
)
|
||||
.addQueryParameter("type", "${subjectType.value}")
|
||||
.addQueryParameter("start", "0")
|
||||
.addQueryParameter("max_results", "1")
|
||||
.build()
|
||||
val searchResponse = client.newCall(requestProducer(httpUrl)).awaitSuccess()
|
||||
.checkErrorMessage().parseAs<SearchResponse>()
|
||||
return if (searchResponse.list.isEmpty()) {
|
||||
SAnime.create()
|
||||
} else {
|
||||
val item = searchResponse.list[0]
|
||||
if (fetchType == BangumiFetchType.ALL) {
|
||||
fetchSubject(client, "${item.id}", requestProducer)
|
||||
} else {
|
||||
SAnime.create().apply {
|
||||
thumbnail_url = item.images.large
|
||||
description = item.summary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchSubject(
|
||||
client: OkHttpClient,
|
||||
id: String,
|
||||
requestProducer: (url: HttpUrl) -> Request,
|
||||
): SAnime {
|
||||
val httpUrl = SUBJECTS_URL.toHttpUrl().newBuilder().addPathSegment(id).build()
|
||||
val subject = client.newCall(requestProducer(httpUrl)).awaitSuccess()
|
||||
.checkErrorMessage().parseAs<Subject>()
|
||||
return SAnime.create().apply {
|
||||
thumbnail_url = subject.images.large
|
||||
description = subject.summary
|
||||
genre = buildList {
|
||||
addAll(subject.metaTags)
|
||||
subject.findInfo("动画制作")?.let { add(it) }
|
||||
subject.findInfo("放送开始")?.let { add(it) }
|
||||
}.joinToString()
|
||||
author = subject.findAuthor()
|
||||
artist = subject.findArtist()
|
||||
if (subject.findInfo("播放结束") != null) {
|
||||
status = SAnime.COMPLETED
|
||||
} else if (subject.findInfo("放送开始") != null) {
|
||||
status = SAnime.ONGOING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Response.checkErrorMessage(): String {
|
||||
val responseStr = body.string()
|
||||
val errorMessage =
|
||||
responseStr.parseAs<JsonElement>().jsonObject["error"]?.jsonPrimitive?.contentOrNull
|
||||
if (errorMessage != null) {
|
||||
throw BangumiScraperException(errorMessage)
|
||||
}
|
||||
return responseStr
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package eu.kanade.tachiyomi.lib.bangumiscraper
|
||||
|
||||
class BangumiScraperException(message: String) : Exception(message)
|
Loading…
Add table
Add a link
Reference in a new issue