diff --git a/src/zh/xfani/build.gradle b/src/zh/xfani/build.gradle index a7ccc34c..71047c71 100644 --- a/src/zh/xfani/build.gradle +++ b/src/zh/xfani/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Xfani' extClass = '.Xfani' - extVersionCode = 2 + extVersionCode = 3 } apply from: "$rootDir/common.gradle" diff --git a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Filters.kt b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Filters.kt index 17c9656b..4072b453 100644 --- a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Filters.kt +++ b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Filters.kt @@ -52,7 +52,10 @@ class VersionFilter( class LetterFilter( tags: Array<String> = "ABCDEFGHIJKLMNOPQRSTUYWXYZ".map { it.toString() }.toMutableList() - .also { it.add("0-9") }.toTypedArray(), + .also { + it.add(0, "全部") + it.add("0-9") + }.toTypedArray(), ) : TagFilter("字母", tags) class SortFilter( @@ -62,3 +65,5 @@ class SortFilter( "按评分" to "score", ), ) : SelectFilter("排序", kv) + +class YearFilter(tags: Array<String>) : TagFilter("年份", tags) diff --git a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt index 77a1e455..c0f5041c 100644 --- a/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt +++ b/src/zh/xfani/src/eu/kanade/tachiyomi/animeextension/zh/xfani/Xfani.kt @@ -17,7 +17,12 @@ import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive @@ -27,6 +32,7 @@ import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import org.jsoup.nodes.Document import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -37,6 +43,13 @@ import javax.net.ssl.SSLHandshakeException import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager +enum class FilterUpdateState { + NONE, + UPDATING, + UPDATED, + FAILED, +} + class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { override val baseUrl: String get() = "https://dick.xfani.com" @@ -52,6 +65,8 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) } private val numberRegex = Regex("\\d+") + private var filterState = FilterUpdateState.NONE + private fun OkHttpClient.Builder.ignoreAllSSLErrors(): OkHttpClient.Builder { val naiveTrustManager = @SuppressLint("CustomX509TrustManager") @@ -74,12 +89,13 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { return this } - override val client: OkHttpClient - get() = if (preferences.getBoolean(PREF_KEY_IGNORE_SSL_ERROR, false)) { - network.client.newBuilder().ignoreAllSSLErrors().build() + override val client: OkHttpClient by lazy { + if (preferences.getBoolean(PREF_KEY_IGNORE_SSL_ERROR, false)) { + network.client.newBuilder().ignoreAllSSLErrors() } else { - network.client.newBuilder().addInterceptor(::checkSSLErrorInterceptor).build() - } + network.client.newBuilder().addInterceptor(::checkSSLErrorInterceptor) + }.addInterceptor(::updateFiltersInterceptor).build() + } private val selectedVideoSource get() = preferences.getString(PREF_KEY_VIDEO_SOURCE, DEFAULT_VIDEO_SOURCE)!!.toInt() @@ -92,6 +108,13 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { } } + private fun updateFiltersInterceptor(chain: Interceptor.Chain): Response { + if (filterState == FilterUpdateState.NONE) { + updateFilter() + } + return chain.proceed(chain.request()) + } + override fun animeDetailsParse(response: Response): SAnime { val jsoup = response.asJsoup() return SAnime.create().apply { @@ -118,10 +141,37 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { } override fun videoListParse(response: Response): List<Video> { - val script = response.asJsoup().select("script:containsData(player_aaaa)").first()!!.data() + val requestUrl = response.request.url + val currentPath = requestUrl.encodedPath + val currentAnthology = response.request.url.pathSegments.last() + val document = response.asJsoup() + val videoUrl = findVideoUrl(document) + val sourceList = + document.select(".player-anthology .anthology-list .anthology-list-box") + .map { element -> + element.select(".anthology-list-play li a").eachAttr("href") + .first { it.endsWith(currentAnthology) } + } + val sourceNameList = document.select(".anthology-tab .swiper-wrapper a").map { + it.ownText().trim() + } + return sourceList.zip(sourceNameList) { url, name -> + if (url.endsWith(currentPath)) { + Video("$baseUrl$url", name, videoUrl = videoUrl) + } else { + Video("$baseUrl$url", name, videoUrl = null) + } + }.sortedByDescending { it.videoUrl != null } + } + + override fun videoUrlParse(response: Response): String { + return findVideoUrl(response.asJsoup()) + } + + private fun findVideoUrl(document: Document): String { + val script = document.select("script:containsData(player_aaaa)").first()!!.data() val info = script.substringAfter("player_aaaa=").let { json.parseToJsonElement(it) } - val url = info.jsonObject["url"]!!.jsonPrimitive.content - return listOf(Video(url, "SingleFile", videoUrl = url)) + return info.jsonObject["url"]!!.jsonPrimitive.content } override fun latestUpdatesParse(response: Response): AnimesPage { @@ -157,6 +207,9 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { } override fun searchAnimeParse(response: Response): AnimesPage { + if (response.request.url.toString().contains("api/vod")) { + return vodListToAnimePageList(response) + } val jsoup = response.asJsoup() val items = jsoup.select("div.public-list-box.search-box.flex.rel") val animeList = items.map { item -> @@ -183,25 +236,71 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { return numbers.size == 2 && numbers[0] != numbers[1] } + private fun updateFilter() { + filterState = FilterUpdateState.UPDATING + val handler = CoroutineExceptionHandler { _, _ -> + filterState = FilterUpdateState.FAILED + } + CoroutineScope(Dispatchers.IO + handler).launch { + val jsoup = client.newCall(GET("$baseUrl/show/1/html")).awaitSuccess().asJsoup() + // update class and year filter type + val classList = jsoup.select("li[data-type=class]").eachAttr("data-val") + val yearList = jsoup.select("li[data-type=year]").eachAttr("data-val") + preferences.edit() + .putString(PREF_KEY_FILTER_CLASS, classList.joinToString()) + .putString(PREF_KEY_FILTER_YEAR, yearList.joinToString()) + .apply() + filterState = FilterUpdateState.UPDATED + } + } + + private fun SharedPreferences.createTagFilter( + key: String, + block: (tags: Array<String>) -> TagFilter?, + ): TagFilter? { + val savedTags = getString(key, "")!! + if (savedTags.isBlank()) { + return block(emptyArray()) + } + val tags = savedTags.split(", ").toMutableList() + if (tags[0].isBlank()) { + tags[0] = "全部" + } + return block(tags.toTypedArray()) + } + override fun getFilterList(): AnimeFilterList { return AnimeFilterList( - AnimeFilter.Header("设置筛选后关键字搜索会失效"), - TypeFilter(), - ClassFilter(), - VersionFilter(), - LetterFilter(), - SortFilter(), + listOfNotNull( + AnimeFilter.Header("以下筛选对搜索结果无效"), + TypeFilter(), + preferences.createTagFilter(PREF_KEY_FILTER_CLASS) { + if (it.isEmpty()) { + ClassFilter() + } else { + ClassFilter(it) + } + }, + preferences.createTagFilter(PREF_KEY_FILTER_YEAR) { + if (it.isEmpty()) { + null + } else { + YearFilter(it) + } + }, + VersionFilter(), + LetterFilter(), + SortFilter(), + ), ) } private fun doSearch(page: Int, query: String): Request { val url = baseUrl.toHttpUrl().newBuilder() if (page <= 1) { - url.addPathSegment("search.html") - .addQueryParameter("wd", query) + url.addPathSegment("search.html").addQueryParameter("wd", query) } else { - url.addPathSegments("search/wd/") - .addPathSegment(query) + url.addPathSegments("search/wd/").addPathSegment(query) .addPathSegments("page/$page.html") } return GET(url.build()) @@ -211,19 +310,16 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { if (query.isNotBlank()) { return doSearch(page, query) } - val url = baseUrl.toHttpUrl().newBuilder() - .addPathSegments("index.php/api/vod") - .build() + val url = baseUrl.toHttpUrl().newBuilder().addPathSegments("index.php/api/vod").build() val time = System.currentTimeMillis() / 1000 - val formBody = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("page", "$page") - .addFormDataPart("time", "$time") - .addFormDataPart("key", generateKey(time)) + val formBody = + MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("page", "$page") + .addFormDataPart("time", "$time").addFormDataPart("key", generateKey(time)) filters.forEach { filter -> when (filter) { is TypeFilter -> formBody.addFormDataPart("type", filter.selected) is ClassFilter -> formBody.addFormDataPart("class", filter.selected) + is YearFilter -> formBody.addFormDataPart("year", filter.selected) is VersionFilter -> formBody.addFormDataPart("version", filter.selected) is LetterFilter -> formBody.addFormDataPart("letter", filter.selected) is SortFilter -> formBody.addFormDataPart("by", filter.selected) @@ -247,7 +343,7 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { setDefaultValue(DEFAULT_VIDEO_SOURCE) summary = "当前选择:${entries[selectedVideoSource]}" setOnPreferenceChangeListener { _, newValue -> - summary = "当前选择 ${entries[(newValue as String).toInt()]}" + summary = "当前选择:${entries[(newValue as String).toInt()]}" true } }, @@ -270,6 +366,9 @@ class Xfani : AnimeHttpSource(), ConfigurableAnimeSource { const val PREF_KEY_VIDEO_SOURCE = "PREF_KEY_VIDEO_SOURCE" const val PREF_KEY_IGNORE_SSL_ERROR = "PREF_KEY_IGNORE_SSL_ERROR" + const val PREF_KEY_FILTER_CLASS = "PREF_KEY_FILTER_CLASS" + const val PREF_KEY_FILTER_YEAR = "PREF_KEY_FILTER_YEAR" + const val DEFAULT_VIDEO_SOURCE = "0" val STATUS_STR_MAPPING = mapOf(