Add Jable #378

Merged
AlphaBoom merged 1 commit from jable into main 2024-11-19 00:53:57 -06:00
10 changed files with 371 additions and 0 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"
}