Merge branch 'refs/heads/main' into aniplay

This commit is contained in:
Josef František Straka 2024-11-21 23:30:03 +01:00
commit 66a163541e
16 changed files with 687 additions and 4 deletions

View file

@ -0,0 +1,8 @@
ext {
extName = 'Jable'
extClass = '.JableFactory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -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"
}
}

View file

@ -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"),
)
}
}

View file

@ -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)

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}
}