forked from AlmightyHak/extensions-source
Add Jable (#378)
This commit is contained in:
parent
a274256d94
commit
5a9383dffb
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