diff --git a/src/zh/xiaoxintv/build.gradle b/src/zh/xiaoxintv/build.gradle new file mode 100644 index 00000000..67d7a839 --- /dev/null +++ b/src/zh/xiaoxintv/build.gradle @@ -0,0 +1,7 @@ +ext { + extName = 'Xiaoxintv' + extClass = '.Xiaoxintv' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/xiaoxintv/res/mipmap-hdpi/ic_launcher.png b/src/zh/xiaoxintv/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..3f719488 Binary files /dev/null and b/src/zh/xiaoxintv/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/xiaoxintv/res/mipmap-mdpi/ic_launcher.png b/src/zh/xiaoxintv/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..a0c08099 Binary files /dev/null and b/src/zh/xiaoxintv/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/xiaoxintv/res/mipmap-xhdpi/ic_launcher.png b/src/zh/xiaoxintv/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..57026442 Binary files /dev/null and b/src/zh/xiaoxintv/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/xiaoxintv/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/xiaoxintv/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..f1e07f13 Binary files /dev/null and b/src/zh/xiaoxintv/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/xiaoxintv/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/xiaoxintv/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..149a7bee Binary files /dev/null and b/src/zh/xiaoxintv/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/xiaoxintv/src/eu/kanade/tachiyomi/animeextension/zh/xiaoxintv/Filters.kt b/src/zh/xiaoxintv/src/eu/kanade/tachiyomi/animeextension/zh/xiaoxintv/Filters.kt new file mode 100644 index 00000000..68930c85 --- /dev/null +++ b/src/zh/xiaoxintv/src/eu/kanade/tachiyomi/animeextension/zh/xiaoxintv/Filters.kt @@ -0,0 +1,221 @@ +@file:Suppress("LocalVariableName", "PropertyName") + +package eu.kanade.tachiyomi.animeextension.zh.xiaoxintv + +import android.content.SharedPreferences +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +private const val PREF_KEY_FILTER_CONFIG_PREFIX = "STORED_SEARCH_CONFIG" + +open class PathFilter(name: String, private val beans: Array) : + AnimeFilter.Select(name, beans.map { it.name }.toTypedArray()) { + val selected + get() = beans[state] +} + +class GroupFilter(name: String, filters: List) : + AnimeFilter.Group(name, filters) + +internal enum class FilterType(val title: String) { + TYPE("类型"), + CLASS("分类"), + YEAR("年份"), + LANG("语言"), + SORT("排序"), + REGION("地区"), +} + +interface SearchBean { + val name: String + val ignore: Boolean + fun toPath(): String +} + +@Serializable +data class SearchType( + override val name: String, + val id: String, + override val ignore: Boolean = false, +) : SearchBean { + constructor(name: String, id: Int) : this(name, "$id") + + override fun toPath() = "/id/$id" +} + +internal fun SearchType.toFilter(): PathFilter { + return PathFilter(name, arrayOf(this)) +} + +@Serializable +data class SearchSort( + override val name: String, + val by: String, + override val ignore: Boolean = false, +) : SearchBean { + override fun toPath() = "/by/$by" +} + +@Serializable +data class SearchYear(override val name: String, override val ignore: Boolean = false) : + SearchBean { + override fun toPath() = "/year/$name" +} + +@Serializable +data class SearchLang(override val name: String, override val ignore: Boolean = false) : + SearchBean { + override fun toPath() = "/lang/$name" +} + +@Serializable +data class SearchClass(override val name: String, override val ignore: Boolean = false) : + SearchBean { + override fun toPath() = "/class/$name" +} + +@Serializable +data class SearchRegion(override val name: String, override val ignore: Boolean = false) : + SearchBean { + override fun toPath() = "/area/$name" +} + +@Serializable +data class SearchFilterConfig( + val type: List, + val category: List = emptyList(), + val year: List = emptyList(), + val lang: List = emptyList(), + val region: List = emptyList(), +) { + fun isEmpty() = + type.isEmpty() && category.isEmpty() && year.isEmpty() && lang.isEmpty() && region.isEmpty() +} + +private inline fun c(): Class { + return T::class.java +} + +private val searchPriority = arrayOf( + c(), + c(), + c(), + c(), + c(), + c(), +) + +internal fun Iterable.toPath(): String { + return this.asSequence().filterNot { it.ignore } + .groupBy { it::class.java }.flatMap { it.value.subList(it.value.size - 1, it.value.size) } + .sortedBy { + searchPriority.indexOf(it::class.java) + } + .joinToString(separator = "") { it.toPath() }.removePrefix("/") +} + +private val defaultLangList = + listOf( + SearchLang("全部", ignore = true), + SearchLang("国语"), + SearchLang("粤语"), + SearchLang("英语"), + SearchLang("其他"), + ) + +private val typeAll = SearchType("全部", "-1", ignore = true) +private val categoryAll = SearchClass("全部", ignore = true) +private val yearAll = SearchYear("全部", ignore = true) +private val regionAll = SearchRegion("全部", ignore = true) + +private val defaultSearchFilterConfig = mapOf( + // anime + "5" to SearchFilterConfig( + type = listOf(typeAll, SearchType("国产动漫", 51), SearchType("日本动漫", 52)), + category = listOf( + categoryAll, + SearchClass("热血"), + SearchClass("格斗"), + SearchClass("其他"), + ), + year = listOf(yearAll), + lang = defaultLangList, + ), + // movie + "7" to SearchFilterConfig( + type = listOf(typeAll), + region = listOf(regionAll), + year = listOf(yearAll), + lang = defaultLangList, + ), + // tv + "6" to SearchFilterConfig( + type = listOf(typeAll), + category = listOf(categoryAll), + year = listOf(yearAll), + lang = defaultLangList, + ), + // variety show + "3" to SearchFilterConfig( + type = listOf(typeAll), + category = listOf(categoryAll), + year = listOf(yearAll), + lang = defaultLangList, + ), + // documentary + "21" to SearchFilterConfig( + type = emptyList(), + region = listOf(regionAll), + year = listOf(yearAll), + lang = defaultLangList, + ), + // short show + "64" to SearchFilterConfig( + type = listOf(typeAll), + ), +) + +private fun findDefaultSearchFilterConfig(majorTypeId: String): SearchFilterConfig { + return defaultSearchFilterConfig.getOrElse(majorTypeId) { + SearchFilterConfig( + listOf(typeAll), + ) + } +} + +private fun genFilterConfigKey(majorTypeId: String): String { + return PREF_KEY_FILTER_CONFIG_PREFIX + "_$majorTypeId" +} + +internal val defaultMajorSearchTypeSet = arrayOf( + SearchType("动漫", 5), + SearchType("电影", 7), + SearchType("电视剧", 6), + SearchType("综艺", 3), + SearchType("纪录片", 21), + SearchType("短剧", 64), +) + +internal val defaultSortTypeSet = + arrayOf( + SearchSort("时间", "time", ignore = true), + SearchSort("人气", "hits"), + SearchSort("评分", "score"), + ) + +fun SharedPreferences.findSearchFilterConfig(majorTypeId: String, json: Json): SearchFilterConfig { + // check shared preferences + return getString(genFilterConfigKey(majorTypeId), null)?.let { json.decodeFromString(it) } + ?: findDefaultSearchFilterConfig(majorTypeId) +} + +fun SharedPreferences.saveSearchFilterConfig( + majorTypeId: String, + searchFilterConfig: SearchFilterConfig, + json: Json, +) { + edit().putString(genFilterConfigKey(majorTypeId), json.encodeToString(searchFilterConfig)) + .apply() +} diff --git a/src/zh/xiaoxintv/src/eu/kanade/tachiyomi/animeextension/zh/xiaoxintv/Xiaoxintv.kt b/src/zh/xiaoxintv/src/eu/kanade/tachiyomi/animeextension/zh/xiaoxintv/Xiaoxintv.kt new file mode 100644 index 00000000..01d5e151 --- /dev/null +++ b/src/zh/xiaoxintv/src/eu/kanade/tachiyomi/animeextension/zh/xiaoxintv/Xiaoxintv.kt @@ -0,0 +1,278 @@ +package eu.kanade.tachiyomi.animeextension.zh.xiaoxintv + +import android.app.Application +import android.content.SharedPreferences +import eu.kanade.tachiyomi.animesource.model.AnimeFilter +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.network.GET +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.HttpUrl.Companion.toHttpUrl +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 + +private object HotSortFilter : + PathFilter(FilterType.SORT.title, arrayOf(SearchSort("人气", "hits"))) + +class Xiaoxintv : AnimeHttpSource() { + override val baseUrl: String + get() = "https://xiaoxintv.cc" + override val lang: String + get() = "zh" + override val name: String + get() = "小宝影院" + override val supportsLatest: Boolean + get() = true + + private val majorSearchTypeSet: Array + get() = defaultMajorSearchTypeSet + private val searchSortTypeSet: Array + get() = defaultSortTypeSet + private val filterUpdateRecord by lazy { + majorSearchTypeSet.associateWith { + false + }.toMutableMap() + } + private val json by injectLazy() + private val preferences: SharedPreferences by lazy { + Injekt.get().getSharedPreferences("source_$id", 0x0000) + } + + override fun animeDetailsParse(response: Response): SAnime { + val document = response.asJsoup() + return SAnime.create().apply { + thumbnail_url = + document.select(".myui-vodlist__thumb.picture img").attr("data-original") + url = document.select(".myui-vodlist__thumb.picture").attr("href") + title = document.select(".myui-content__detail .title").text() + author = document.selectFirst("p.data:contains(主演:)")?.text() + artist = document.selectFirst("p.data:contains(导演:)")?.text() + description = document.selectFirst("p.data:contains(简介:)")?.ownText() + } + } + + override fun episodeListParse(response: Response): List { + val document = response.asJsoup() + return document.select("#playlist1 ul li").mapIndexed { index, element -> + SEpisode.create().apply { + url = element.select("a").attr("href") + name = element.attr("title") + episode_number = index.toFloat() + } + }.reversed() + } + + 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) } + return info.jsonObject["url"]!!.jsonPrimitive.content + } + + override fun videoListParse(response: Response): List