Merge branch 'refs/heads/main' into aniplay
This commit is contained in:
commit
66a163541e
16 changed files with 687 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)
|
8
src/all/jable/build.gradle
Normal file
8
src/all/jable/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Jable'
|
||||
extClass = '.JableFactory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/jable/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
src/all/jable/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
src/all/jable/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
src/all/jable/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
BIN
src/all/jable/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -0,0 +1,248 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jable
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeUpdateStrategy
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Jable(override val lang: String) : AnimeHttpSource() {
|
||||
override val baseUrl: String
|
||||
get() = "https://jable.tv"
|
||||
override val name: String
|
||||
get() = "Jable"
|
||||
override val supportsLatest: Boolean
|
||||
get() = true
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val json by injectLazy<Json>()
|
||||
private var tagsUpdated = false
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
return GET("$baseUrl${anime.url}?lang=$lang", headers)
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val doc = response.asJsoup()
|
||||
return SAnime.create().apply {
|
||||
val info = doc.select(".info-header")
|
||||
title = info.select(".header-left h4").text()
|
||||
author = info.select(".header-left .model")
|
||||
.joinToString { it.select("span[title]").attr("title") }
|
||||
genre = doc.select(".tags a").joinToString { it.text() }
|
||||
update_strategy = AnimeUpdateStrategy.ONLY_FETCH_ONCE
|
||||
status = SAnime.COMPLETED
|
||||
description = info.select(".header-right").text()
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||
return listOf(
|
||||
SEpisode.create().apply {
|
||||
name = "Episode"
|
||||
url = anime.url
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val doc = response.asJsoup()
|
||||
val videoUrl = doc.selectFirst("script:containsData(var hlsUrl)")!!.data()
|
||||
.substringAfter("var hlsUrl = '").substringBefore("'")
|
||||
return listOf(Video(videoUrl, "Default", videoUrl))
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val doc = response.asJsoup()
|
||||
if (!tagsUpdated) {
|
||||
tagsUpdated = preferences.saveTags(
|
||||
doc.select("a.tag").associate {
|
||||
it.ownText() to it.attr("href").substringAfter(baseUrl).removePrefix("/")
|
||||
.removeSuffix("/")
|
||||
},
|
||||
)
|
||||
}
|
||||
return AnimesPage(
|
||||
doc.select(".container .video-img-box").map {
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain(it.select(".img-box a").attr("href"))
|
||||
thumbnail_url = it.select(".img-box img").attr("data-src")
|
||||
title = it.select(".detail .title").text()
|
||||
}
|
||||
},
|
||||
doc.select(".container .pagination .page-item .page-link.disabled").isNullOrEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
searchRequest("latest-updates", page, latestFilter)
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage = latestUpdatesParse(response)
|
||||
|
||||
override fun popularAnimeRequest(page: Int) =
|
||||
searchRequest("hot", page, popularFilter)
|
||||
|
||||
override fun searchAnimeParse(response: Response) = latestUpdatesParse(response)
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return if (query.isNotEmpty()) {
|
||||
searchRequest(
|
||||
"search/$query",
|
||||
page,
|
||||
AnimeFilterList(filters.list + defaultSearchFunctionFilter),
|
||||
query = query,
|
||||
)
|
||||
} else {
|
||||
val path = filters.list.filterIsInstance<TagFilter>()
|
||||
.firstOrNull()?.selected?.second?.takeUnless { it.isEmpty() } ?: "hot"
|
||||
searchRequest(path, page, AnimeFilterList(filters.list + commonVideoListFuncFilter))
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchRequest(
|
||||
path: String,
|
||||
page: Int,
|
||||
filters: AnimeFilterList = AnimeFilterList(),
|
||||
query: String = "",
|
||||
): Request {
|
||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||
.addPathSegments("$path/")
|
||||
.addQueryParameter("lang", lang)
|
||||
if (tagsUpdated) {
|
||||
// load whole page for update filter tags info
|
||||
urlBuilder.addQueryParameter("mode", "async")
|
||||
}
|
||||
filters.list.forEach {
|
||||
when (it) {
|
||||
is BlockFunctionFilter -> {
|
||||
urlBuilder.addQueryParameter("function", it.selected.functionName)
|
||||
.addQueryParameter("block_id", it.selected.blockId)
|
||||
}
|
||||
|
||||
is SortFilter -> {
|
||||
if (it.selected.second.isNotEmpty()) {
|
||||
urlBuilder.addQueryParameter("sort_by", it.selected.second)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (query.isNotEmpty()) {
|
||||
urlBuilder.addQueryParameter("q", query)
|
||||
}
|
||||
urlBuilder.addQueryParameter("from", "%02d".format(page))
|
||||
.addQueryParameter("_", System.currentTimeMillis().toString())
|
||||
return GET(urlBuilder.build())
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList {
|
||||
return AnimeFilterList(
|
||||
SortFilter(
|
||||
intl.filterPopularSortTitle,
|
||||
arrayOf(
|
||||
"" to "",
|
||||
intl.hotMonth to "video_viewed_month",
|
||||
intl.hotWeek to "video_viewed_week",
|
||||
intl.hotDay to "video_viewed_today",
|
||||
intl.hotAll to "video_viewed",
|
||||
),
|
||||
),
|
||||
TagFilter(
|
||||
intl.filterTagTitle,
|
||||
buildList {
|
||||
add("" to "")
|
||||
preferences.getTags()?.forEach {
|
||||
add(it.key to it.value)
|
||||
}
|
||||
}.toTypedArray(),
|
||||
),
|
||||
SortFilter(
|
||||
intl.filterTagsSortTitle,
|
||||
arrayOf(
|
||||
"" to "",
|
||||
intl.sortLatestUpdate to "post_date",
|
||||
intl.sortMostView to "video_viewed",
|
||||
intl.sortMostFavorite to "most_favourited",
|
||||
intl.sortRecentBest to "post_date_and_popularity",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun SharedPreferences.getTags(): Map<String, String>? {
|
||||
val savedStr = getString("${lang}_$PREF_KEY_TAGS", null)
|
||||
if (savedStr.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return json.decodeFromString<Map<String, String>>(savedStr)
|
||||
}
|
||||
|
||||
private fun SharedPreferences.saveTags(tags: Map<String, String>): Boolean {
|
||||
if (tags.isNotEmpty()) {
|
||||
edit().putString("${lang}_$PREF_KEY_TAGS", json.encodeToString(tags)).apply()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private val intl by lazy {
|
||||
JableIntl(lang)
|
||||
}
|
||||
|
||||
private val commonVideoListFuncFilter by lazy {
|
||||
BlockFunctionFilter(
|
||||
intl.popular,
|
||||
arrayOf(BlockFunction(intl.popular, "list_videos_common_videos_list")),
|
||||
)
|
||||
}
|
||||
|
||||
private val defaultSearchFunctionFilter by lazy {
|
||||
BlockFunctionFilter("", arrayOf(BlockFunction("", "list_videos_videos_list_search_result")))
|
||||
}
|
||||
|
||||
private val popularFilter by lazy {
|
||||
AnimeFilterList(
|
||||
commonVideoListFuncFilter,
|
||||
SortFilter(
|
||||
intl.hotWeek,
|
||||
arrayOf(intl.hotWeek to "video_viewed_week"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private val latestFilter by lazy {
|
||||
AnimeFilterList(
|
||||
BlockFunctionFilter(
|
||||
intl.latestUpdate,
|
||||
arrayOf(BlockFunction(intl.latestUpdate, "list_videos_latest_videos_list")),
|
||||
),
|
||||
SortFilter(
|
||||
intl.sortLatestUpdate,
|
||||
arrayOf(intl.sortLatestUpdate to "post_date"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREF_KEY_TAGS = "pref_key_tags"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jable
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.AnimeSourceFactory
|
||||
|
||||
class JableFactory : AnimeSourceFactory {
|
||||
override fun createSources(): List<AnimeSource> {
|
||||
return listOf(
|
||||
Jable("zh"),
|
||||
Jable("en"),
|
||||
Jable("jp"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jable
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
|
||||
data class BlockFunction(
|
||||
val name: String,
|
||||
val blockId: String,
|
||||
val functionName: String = "get_block",
|
||||
)
|
||||
|
||||
class BlockFunctionFilter(name: String, private val functions: Array<BlockFunction>) :
|
||||
AnimeFilter.Select<String>(name, functions.map { it.name }.toTypedArray()) {
|
||||
val selected
|
||||
get() = functions[state]
|
||||
}
|
||||
|
||||
open class UriPartFilter(name: String, private val pairs: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(name, pairs.map { it.first }.toTypedArray()) {
|
||||
val selected
|
||||
get() = pairs[state]
|
||||
}
|
||||
|
||||
class SortFilter(name: String, pairs: Array<Pair<String, String>>) : UriPartFilter(name, pairs)
|
||||
|
||||
class TagFilter(name: String, pairs: Array<Pair<String, String>>) : UriPartFilter(name, pairs)
|
|
@ -0,0 +1,76 @@
|
|||
package eu.kanade.tachiyomi.animeextension.all.jable
|
||||
|
||||
internal interface Intl {
|
||||
val popular: String
|
||||
val latestUpdate: String
|
||||
val sortLatestUpdate: String
|
||||
val sortMostView: String
|
||||
val sortMostFavorite: String
|
||||
val sortRecentBest: String
|
||||
val hotDay: String
|
||||
val hotWeek: String
|
||||
val hotMonth: String
|
||||
val hotAll: String
|
||||
val filterPopularSortTitle: String
|
||||
val filterTagsSortTitle: String
|
||||
val filterTagTitle: String
|
||||
}
|
||||
|
||||
internal class JableIntl private constructor(delegate: Intl) : Intl by delegate {
|
||||
constructor(lang: String) : this(
|
||||
when (lang) {
|
||||
"zh" -> ZH()
|
||||
"jp" -> JP()
|
||||
"en" -> EN()
|
||||
else -> ZH()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal class ZH : Intl {
|
||||
override val popular: String = "熱度優先"
|
||||
override val latestUpdate: String = "新片優先"
|
||||
override val sortLatestUpdate: String = "最近更新"
|
||||
override val sortMostView: String = "最多觀看"
|
||||
override val sortMostFavorite: String = "最高收藏"
|
||||
override val sortRecentBest: String = "近期最佳"
|
||||
override val hotDay: String = "今日熱門"
|
||||
override val hotWeek: String = "本周熱門"
|
||||
override val hotMonth: String = "本月熱門"
|
||||
override val hotAll: String = "所有時間"
|
||||
override val filterPopularSortTitle: String = "熱門排序"
|
||||
override val filterTagsSortTitle: String = "通用排序"
|
||||
override val filterTagTitle: String = "標籤"
|
||||
}
|
||||
|
||||
internal class JP : Intl {
|
||||
override val popular: String = "人気優先"
|
||||
override val latestUpdate: String = "新作優先"
|
||||
override val sortLatestUpdate: String = "最近更新"
|
||||
override val sortMostView: String = "最も見ら"
|
||||
override val sortMostFavorite: String = "最もお気に入"
|
||||
override val sortRecentBest: String = "最近ベスト"
|
||||
override val hotDay: String = "今日のヒット"
|
||||
override val hotWeek: String = "今週のヒット"
|
||||
override val hotMonth: String = "今月のヒット"
|
||||
override val hotAll: String = "全ての時間"
|
||||
override val filterPopularSortTitle: String = "人気ソート"
|
||||
override val filterTagsSortTitle: String = "一般ソート"
|
||||
override val filterTagTitle: String = "タグ"
|
||||
}
|
||||
|
||||
internal class EN : Intl {
|
||||
override val popular: String = "Hot"
|
||||
override val latestUpdate: String = "Newest"
|
||||
override val sortLatestUpdate: String = "Recent Update"
|
||||
override val sortMostView: String = "Most Viewed"
|
||||
override val sortMostFavorite: String = "Most Favorite"
|
||||
override val sortRecentBest: String = "Best Recently"
|
||||
override val hotDay: String = "Today"
|
||||
override val hotWeek: String = "This Week"
|
||||
override val hotMonth: String = "This Month"
|
||||
override val hotAll: String = "All Time"
|
||||
override val filterPopularSortTitle: String = "Popular Sorting"
|
||||
override val filterTagsSortTitle: String = "General Sorting"
|
||||
override val filterTagTitle: String = "Tag"
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
ext {
|
||||
extName = 'Anime1.me'
|
||||
extClass = '.Anime1'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 3
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
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.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
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.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
|
@ -24,10 +33,12 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Anime1 : AnimeHttpSource() {
|
||||
class Anime1 : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||
override val baseUrl: String
|
||||
get() = "https://anime1.me"
|
||||
override val lang: String
|
||||
|
@ -47,11 +58,22 @@ class Anime1 : AnimeHttpSource() {
|
|||
private lateinit var data: JsonArray
|
||||
private val cookieManager
|
||||
get() = CookieManager.getInstance()
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
||||
return SAnime.create().apply {
|
||||
thumbnail_url = FIX_COVER
|
||||
return if (bangumiEnable) {
|
||||
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())
|
||||
}
|
||||
|
||||
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? {
|
||||
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 {
|
||||
const val PAGE_SIZE = 20
|
||||
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