Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(src/zh): new extension added: xiaoxintv and Anime1 #148

Merged
merged 1 commit into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/bangumi-scraper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
id("lib-android")
}

dependencies {
compileOnly(libs.aniyomi.lib)
}
Original file line number Diff line number Diff line change
@@ -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>)
Original file line number Diff line number Diff line change
@@ -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
}
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.lib.bangumiscraper

class BangumiScraperException(message: String) : Exception(message)
13 changes: 13 additions & 0 deletions src/zh/anime1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ext {
extName = 'Anime1.me'
extClass = '.Anime1'
extVersionCode = 3
}

apply from: "$rootDir/common.gradle"

dependencies {
implementation(project(":lib:bangumi-scraper"))
//noinspection UseTomlInstead
implementation "com.github.houbb:opencc4j:1.8.1"
}
Binary file added src/zh/anime1/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/anime1/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/anime1/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/anime1/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/anime1/res/mipmap-xxxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading