Add Jable #378
10 changed files with 371 additions and 0 deletions
8
src/all/jable/build.gradle
Normal file
8
src/all/jable/build.gradle
Normal file
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Jable'
|
||||
extClass = '.JableFactory'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/all/jable/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
src/all/jable/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
src/all/jable/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
src/all/jable/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
BIN
src/all/jable/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/jable/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue