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)
|
|
@ -1,7 +1,13 @@
|
||||||
ext {
|
ext {
|
||||||
extName = 'Anime1.me'
|
extName = 'Anime1.me'
|
||||||
extClass = '.Anime1'
|
extClass = '.Anime1'
|
||||||
extVersionCode = 1
|
extVersionCode = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:bangumi-scraper"))
|
||||||
|
//noinspection UseTomlInstead
|
||||||
|
implementation "com.github.houbb:opencc4j:1.8.1"
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
package eu.kanade.tachiyomi.animeextension.zh.anime1
|
package eu.kanade.tachiyomi.animeextension.zh.anime1
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
|
import androidx.preference.CheckBoxPreference
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import com.github.houbb.opencc4j.util.ZhTwConverterUtil
|
||||||
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||||
|
import eu.kanade.tachiyomi.lib.bangumiscraper.BangumiFetchType
|
||||||
|
import eu.kanade.tachiyomi.lib.bangumiscraper.BangumiScraper
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
@ -24,10 +33,12 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
class Anime1 : AnimeHttpSource() {
|
class Anime1 : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||||
override val baseUrl: String
|
override val baseUrl: String
|
||||||
get() = "https://anime1.me"
|
get() = "https://anime1.me"
|
||||||
override val lang: String
|
override val lang: String
|
||||||
|
@ -47,11 +58,22 @@ class Anime1 : AnimeHttpSource() {
|
||||||
private lateinit var data: JsonArray
|
private lateinit var data: JsonArray
|
||||||
private val cookieManager
|
private val cookieManager
|
||||||
get() = CookieManager.getInstance()
|
get() = CookieManager.getInstance()
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
override fun animeDetailsParse(response: Response) = throw UnsupportedOperationException()
|
override fun animeDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
||||||
return SAnime.create().apply {
|
return if (bangumiEnable) {
|
||||||
thumbnail_url = FIX_COVER
|
BangumiScraper.fetchDetail(
|
||||||
|
client,
|
||||||
|
ZhTwConverterUtil.toSimple(anime.title.removeSuffixMark()),
|
||||||
|
fetchType = bangumiFetchType,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
anime.thumbnail_url = FIX_COVER
|
||||||
|
anime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,13 +190,78 @@ class Anime1 : AnimeHttpSource() {
|
||||||
return GET(url.build())
|
return GET(url.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val bangumiScraper = CheckBoxPreference(screen.context).apply {
|
||||||
|
key = PREF_KEY_BANGUMI
|
||||||
|
title = "啟用Bangumi刮削"
|
||||||
|
}
|
||||||
|
val bangumiFetchType = ListPreference(screen.context).apply {
|
||||||
|
key = PREF_KEY_BANGUMI_FETCH_TYPE
|
||||||
|
title = "詳情拉取設置"
|
||||||
|
setVisible(bangumiEnable)
|
||||||
|
entries = arrayOf("拉取部分數據", "拉取完整數據")
|
||||||
|
entryValues = arrayOf(BangumiFetchType.SHORT.name, BangumiFetchType.ALL.name)
|
||||||
|
setDefaultValue(entryValues[0])
|
||||||
|
summary = when (bangumiFetchType) {
|
||||||
|
BangumiFetchType.SHORT -> entries[0]
|
||||||
|
BangumiFetchType.ALL -> entries[1]
|
||||||
|
else -> entries[0]
|
||||||
|
}
|
||||||
|
setOnPreferenceChangeListener { _, value ->
|
||||||
|
summary = when (value) {
|
||||||
|
BangumiFetchType.SHORT.name -> entries[0]
|
||||||
|
BangumiFetchType.ALL.name -> entries[1]
|
||||||
|
else -> entries[0]
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bangumiScraper.setOnPreferenceChangeListener { _, value ->
|
||||||
|
bangumiFetchType.setVisible(value as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
screen.apply {
|
||||||
|
addPreference(bangumiScraper)
|
||||||
|
addPreference(bangumiFetchType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val bangumiEnable: Boolean
|
||||||
|
get() = preferences.getBoolean(PREF_KEY_BANGUMI, false)
|
||||||
|
private val bangumiFetchType: BangumiFetchType
|
||||||
|
get() {
|
||||||
|
val fetchTypeName =
|
||||||
|
preferences.getString(PREF_KEY_BANGUMI_FETCH_TYPE, BangumiFetchType.SHORT.name)
|
||||||
|
return when (fetchTypeName) {
|
||||||
|
BangumiFetchType.SHORT.name -> BangumiFetchType.SHORT
|
||||||
|
BangumiFetchType.ALL.name -> BangumiFetchType.ALL
|
||||||
|
else -> BangumiFetchType.SHORT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun JsonArray.getContent(index: Int): String? {
|
private fun JsonArray.getContent(index: Int): String? {
|
||||||
return getOrNull(index)?.jsonPrimitive?.contentOrNull
|
return getOrNull(index)?.jsonPrimitive?.contentOrNull
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.removeSuffixMark(): String {
|
||||||
|
return removeBracket("(", ")").removeBracket("[", "]").trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.removeBracket(start: String, end: String): String {
|
||||||
|
val seasonStart = indexOf(start)
|
||||||
|
val seasonEnd = indexOf(end)
|
||||||
|
if (seasonEnd > seasonStart) {
|
||||||
|
return removeRange(seasonStart, seasonEnd + 1)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PAGE_SIZE = 20
|
const val PAGE_SIZE = 20
|
||||||
const val FIX_COVER = "https://sta.anicdn.com/playerImg/8.jpg"
|
const val FIX_COVER = "https://sta.anicdn.com/playerImg/8.jpg"
|
||||||
|
|
||||||
|
const val PREF_KEY_BANGUMI = "PREF_KEY_BANGUMI"
|
||||||
|
const val PREF_KEY_BANGUMI_FETCH_TYPE = "PREF_KEY_BANGUMI_FETCH_TYPE"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue