forked from AlmightyHak/extensions-source
parent
033eeb0b6e
commit
80f3773bc1
10 changed files with 340 additions and 0 deletions
7
src/zh/xfani/build.gradle
Normal file
7
src/zh/xfani/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Xfani'
|
||||
extClass = '.Xfani'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/zh/xfani/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/zh/xfani/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.3 KiB |
BIN
src/zh/xfani/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/zh/xfani/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
src/zh/xfani/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/zh/xfani/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
src/zh/xfani/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/zh/xfani/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
src/zh/xfani/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/zh/xfani/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
|
@ -0,0 +1,64 @@
|
|||
package eu.kanade.tachiyomi.animeextension.zh.xfani
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
|
||||
abstract class SelectFilter(name: String, private val options: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(name, options.map { it.first }.toTypedArray()) {
|
||||
val selected
|
||||
get() = options[state].second
|
||||
}
|
||||
|
||||
abstract class TagFilter(name: String, values: Array<String>) :
|
||||
SelectFilter(
|
||||
name,
|
||||
values.mapIndexed { index, s ->
|
||||
if (index == 0) {
|
||||
s to ""
|
||||
} else {
|
||||
s to s
|
||||
}
|
||||
}.toTypedArray(),
|
||||
)
|
||||
|
||||
class TypeFilter(
|
||||
kv: Array<Pair<String, String>> = arrayOf(
|
||||
"连载新番" to "1",
|
||||
"完结旧番" to "2",
|
||||
"剧场版" to "3",
|
||||
),
|
||||
) : SelectFilter("频道", kv)
|
||||
|
||||
class ClassFilter(
|
||||
tags: Array<String> = arrayOf(
|
||||
"全部",
|
||||
"搞笑",
|
||||
"原创",
|
||||
"轻小说改",
|
||||
"恋爱",
|
||||
"百合",
|
||||
"漫改",
|
||||
),
|
||||
) : TagFilter("类型", tags)
|
||||
|
||||
class VersionFilter(
|
||||
tags: Array<String> = arrayOf(
|
||||
"全部",
|
||||
"BD",
|
||||
"OVA",
|
||||
"SP",
|
||||
"OAD",
|
||||
),
|
||||
) : TagFilter("版本", tags)
|
||||
|
||||
class LetterFilter(
|
||||
tags: Array<String> = "ABCDEFGHIJKLMNOPQRSTUYWXYZ".map { it.toString() }.toMutableList()
|
||||
.also { it.add("0-9") }.toTypedArray(),
|
||||
) : TagFilter("字母", tags)
|
||||
|
||||
class SortFilter(
|
||||
kv: Array<Pair<String, String>> = arrayOf(
|
||||
"按最新" to "time",
|
||||
"按热门" to "hits",
|
||||
"按评分" to "score",
|
||||
),
|
||||
) : SelectFilter("排序", kv)
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.animeextension.zh.xfani
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
private const val UID = "DCC147D11943AF75"
|
||||
|
||||
internal fun generateKey(time: Long): String {
|
||||
return "DS${time}$UID".md5()
|
||||
}
|
||||
|
||||
internal fun String.md5(): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val digest = md.digest(this.toByteArray())
|
||||
return digest.joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
package eu.kanade.tachiyomi.animeextension.zh.xfani
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
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.network.POST
|
||||
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.MultipartBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Xfani : AnimeHttpSource(), ConfigurableAnimeSource {
|
||||
override val baseUrl: String
|
||||
get() = "https://dick.xfani.com"
|
||||
override val lang: String
|
||||
get() = "zh"
|
||||
override val name: String
|
||||
get() = "稀饭动漫"
|
||||
override val supportsLatest: Boolean
|
||||
get() = true
|
||||
|
||||
private val json by injectLazy<Json>()
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
private val numberRegex = Regex("\\d+")
|
||||
|
||||
private val selectedVideoSource
|
||||
get() = preferences.getString(PREF_KEY_VIDEO_SOURCE, DEFAULT_VIDEO_SOURCE)!!.toInt()
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime = SAnime.create()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val jsoup = response.asJsoup()
|
||||
val result = jsoup.select("ul.anthology-list-play.size")
|
||||
val episodeList = if (result.size > selectedVideoSource) {
|
||||
result[selectedVideoSource]
|
||||
} else {
|
||||
result[0]
|
||||
}.select("li > a")
|
||||
return episodeList.map {
|
||||
SEpisode.create().apply {
|
||||
name = it.text()
|
||||
url = it.attr("href")
|
||||
episode_number = numberRegex.find(name)?.value?.toFloat() ?: -1F
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val script = response.asJsoup().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))
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
return vodListToAnimePageList(response)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
searchAnimeRequest(page, "", AnimeFilterList())
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
return vodListToAnimePageList(response)
|
||||
}
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request =
|
||||
searchAnimeRequest(page, "", AnimeFilterList(SortFilter().apply { state = 1 }))
|
||||
|
||||
private fun vodListToAnimePageList(response: Response): AnimesPage {
|
||||
val vodResponse = json.decodeFromString<VodResponse>(response.body.string())
|
||||
val animeList = vodResponse.list.map {
|
||||
SAnime.create().apply {
|
||||
url = "/bangumi/${it.vodId}.html"
|
||||
thumbnail_url = it.vodPicThumb.ifEmpty { it.vodPic }
|
||||
title = it.vodName
|
||||
author = it.vodActor
|
||||
description = it.vodBlurb
|
||||
genre = it.vodClass
|
||||
}
|
||||
}
|
||||
return AnimesPage(
|
||||
animeList,
|
||||
animeList.isNotEmpty() && vodResponse.page * vodResponse.limit < vodResponse.total,
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val jsoup = response.asJsoup()
|
||||
val items = jsoup.select("div.public-list-box.search-box.flex.rel")
|
||||
val animeList = items.map { item ->
|
||||
SAnime.create().apply {
|
||||
title = item.select(".thumb-txt").text()
|
||||
url = item.select("div.left.public-list-bj a.public-list-exp").attr("href")
|
||||
thumbnail_url =
|
||||
item.select("div.left.public-list-bj img[data-src]").attr("data-src")
|
||||
author = item.select("div.thumb-actor").text().removeSuffix("/")
|
||||
artist = item.select("div.thumb-director").text().removeSuffix("/")
|
||||
description = item.select(".thumb-blurb").text()
|
||||
genre = item.select("div.thumb-else").text()
|
||||
val statusString = item.select("div.left.public-list-bj .public-list-prb").text()
|
||||
status = STATUS_STR_MAPPING.getOrElse(statusString) { SAnime.ONGOING }
|
||||
}
|
||||
}
|
||||
val tip = jsoup.select("div.pages div.page-tip").text()
|
||||
return AnimesPage(animeList, tip.isNotEmpty() && hasMorePage(tip))
|
||||
}
|
||||
|
||||
private fun hasMorePage(tip: String): Boolean {
|
||||
val pageIndicator = tip.substringAfter("当前").substringBefore("页")
|
||||
val numbers = pageIndicator.split("/")
|
||||
return numbers.size == 2 && numbers[0] != numbers[1]
|
||||
}
|
||||
|
||||
override fun getFilterList(): AnimeFilterList {
|
||||
return AnimeFilterList(
|
||||
AnimeFilter.Header("设置筛选后关键字搜索会失效"),
|
||||
TypeFilter(),
|
||||
ClassFilter(),
|
||||
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)
|
||||
} else {
|
||||
url.addPathSegments("search/wd/")
|
||||
.addPathSegment(query)
|
||||
.addPathSegments("page/$page.html")
|
||||
}
|
||||
return GET(url.build())
|
||||
}
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
if (query.isNotBlank()) {
|
||||
return doSearch(page, query)
|
||||
}
|
||||
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))
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is TypeFilter -> formBody.addFormDataPart("type", filter.selected)
|
||||
is ClassFilter -> formBody.addFormDataPart("class", filter.selected)
|
||||
is VersionFilter -> formBody.addFormDataPart("version", filter.selected)
|
||||
is LetterFilter -> formBody.addFormDataPart("letter", filter.selected)
|
||||
is SortFilter -> formBody.addFormDataPart("by", filter.selected)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
if (filters.filterIsInstance<TypeFilter>().isEmpty()) {
|
||||
formBody.addFormDataPart("type", "1")
|
||||
}
|
||||
return POST(url.toString(), body = formBody.build())
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
screen.addPreference(
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_KEY_VIDEO_SOURCE
|
||||
title = "请设置首选视频源线路"
|
||||
entries = arrayOf("主线-1", "主线-2", "备用-1")
|
||||
entryValues = arrayOf("0", "1", "2")
|
||||
setDefaultValue(DEFAULT_VIDEO_SOURCE)
|
||||
summary = "当前选择:${entries[selectedVideoSource]}"
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
summary = "当前选择 ${entries[(newValue as String).toInt()]}"
|
||||
true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREF_KEY_VIDEO_SOURCE = "PREF_KEY_VIDEO_SOURCE"
|
||||
|
||||
const val DEFAULT_VIDEO_SOURCE = "0"
|
||||
|
||||
val STATUS_STR_MAPPING = mapOf(
|
||||
"已完结" to SAnime.COMPLETED,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package eu.kanade.tachiyomi.animeextension.zh.xfani
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VodInfo(
|
||||
@SerialName("vod_id")
|
||||
val vodId: Int,
|
||||
@SerialName("vod_level")
|
||||
val vodLevel: Int = 0,
|
||||
@SerialName("vod_name")
|
||||
val vodName: String,
|
||||
@SerialName("vod_pic")
|
||||
val vodPic: String,
|
||||
@SerialName("vod_pic_thumb")
|
||||
val vodPicThumb: String = "",
|
||||
@SerialName("vod_tag")
|
||||
val vodTag: String = "",
|
||||
@SerialName("vod_class")
|
||||
val vodClass: String,
|
||||
@SerialName("vod_remarks")
|
||||
val vodRemarks: String,
|
||||
@SerialName("vod_serial")
|
||||
val vodSerial: String,
|
||||
@SerialName("vod_sub")
|
||||
val vodSub: String,
|
||||
@SerialName("vod_actor")
|
||||
val vodActor: String,
|
||||
@SerialName("vod_blurb")
|
||||
val vodBlurb: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VodResponse(
|
||||
val page: Int,
|
||||
@SerialName("pagecount")
|
||||
val pageCount: Int,
|
||||
val limit: Int,
|
||||
val total: Int,
|
||||
val list: List<VodInfo>,
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue