Changes (#1513)
This commit is contained in:
@ -9,7 +9,7 @@ ext {
|
||||
pkgNameSuffix = 'en.kickassanime'
|
||||
extClass = '.KickAssAnime'
|
||||
libVersion = '13'
|
||||
extVersionCode = 23
|
||||
extVersionCode = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -8,6 +8,7 @@ import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.AnimeInfoDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.EpisodeResponseDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.LanguagesDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.PopularItemDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.PopularResponseDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.RecentsResponseDto
|
||||
@ -79,22 +80,28 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
private fun episodeListRequest(anime: SAnime, page: Int) =
|
||||
GET("$API_URL/${anime.url}/episodes?page=$page&lang=ja-JP")
|
||||
private fun episodeListRequest(anime: SAnime, page: Int, lang: String) =
|
||||
GET("$API_URL${anime.url}/episodes?page=$page&lang=$lang")
|
||||
|
||||
private fun getEpisodeResponse(anime: SAnime, page: Int): EpisodeResponseDto {
|
||||
return client.newCall(episodeListRequest(anime, page))
|
||||
private fun getEpisodeResponse(anime: SAnime, page: Int, lang: String): EpisodeResponseDto {
|
||||
return client.newCall(episodeListRequest(anime, page, lang))
|
||||
.execute()
|
||||
.parseAs<EpisodeResponseDto>()
|
||||
}
|
||||
|
||||
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
||||
val first = getEpisodeResponse(anime, 1)
|
||||
val languages = client.newCall(
|
||||
GET("$API_URL${anime.url}/language"),
|
||||
).execute().parseAs<LanguagesDto>()
|
||||
val prefLang = preferences.getString(PREF_AUDIO_LANG_KEY, PREF_AUDIO_LANG_DEFAULT)!!
|
||||
val lang = languages.result.firstOrNull { it == prefLang } ?: PREF_AUDIO_LANG_DEFAULT
|
||||
|
||||
val first = getEpisodeResponse(anime, 1, lang)
|
||||
val items = buildList {
|
||||
addAll(first.result)
|
||||
|
||||
first.pages.drop(1).forEachIndexed { index, _ ->
|
||||
addAll(getEpisodeResponse(anime, index + 2).result)
|
||||
addAll(getEpisodeResponse(anime, index + 2, lang).result)
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,6 +110,7 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
name = "Ep. ${it.episode_string} - ${it.title}"
|
||||
url = "${anime.url}/ep-${it.episode_string}-${it.slug}"
|
||||
episode_number = it.episode_string.toFloatOrNull() ?: 0F
|
||||
scanlator = lang.getLocale()
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,9 +139,12 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
// tested with extensions-lib:9d3dcb0
|
||||
// override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime) = GET("$API_URL/${anime.url}")
|
||||
override fun animeDetailsRequest(anime: SAnime) = GET("$API_URL${anime.url}")
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val languages = client.newCall(
|
||||
GET("${response.request.url}/language"),
|
||||
).execute().parseAs<LanguagesDto>()
|
||||
val anime = response.parseAs<AnimeInfoDto>()
|
||||
return SAnime.create().apply {
|
||||
val useEnglish = preferences.getBoolean(PREF_USE_ENGLISH_KEY, PREF_USE_ENGLISH_DEFAULT)
|
||||
@ -147,6 +158,7 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
status = anime.status.parseStatus()
|
||||
description = buildString {
|
||||
append(anime.synopsis + "\n\n")
|
||||
append("Available Dub Languages: ${languages.result.joinToString(", ") { t -> t.getLocale() }}\n")
|
||||
append("Season: ${anime.season.capitalize()}\n")
|
||||
append("Year: ${anime.year}")
|
||||
}
|
||||
@ -158,12 +170,20 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
override fun searchAnimeParse(response: Response) = throw Exception("Not used")
|
||||
|
||||
private fun searchAnimeParse(response: Response, page: Int): AnimesPage {
|
||||
val data = response.parseAs<SearchResponseDto>()
|
||||
val animes = data.result.map(::popularAnimeFromObject)
|
||||
return AnimesPage(animes, page < data.maxPage)
|
||||
val path = response.request.url.encodedPath
|
||||
return if (path.endsWith("api/fsearch") || path.endsWith("/anime")) {
|
||||
val data = response.parseAs<SearchResponseDto>()
|
||||
val animes = data.result.map(::popularAnimeFromObject)
|
||||
AnimesPage(animes, page < data.maxPage)
|
||||
} else if (path.endsWith("/recent")) {
|
||||
latestUpdatesParse(response)
|
||||
} else {
|
||||
popularAnimeParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeRequest(page: Int, query: String, filters: KickAssAnimeFilters.FilterSearchParams): Request {
|
||||
if (filters.subPage.isNotBlank()) return GET("$baseUrl/api/${filters.subPage}?page=$page")
|
||||
if (query.isBlank()) throw Exception("Enter query to search")
|
||||
val data = if (filters.filters == "{}") {
|
||||
"""{"page":$page,"query":"$query"}"""
|
||||
@ -221,6 +241,21 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
val audioLangPref = ListPreference(screen.context).apply {
|
||||
key = PREF_AUDIO_LANG_KEY
|
||||
title = PREF_AUDIO_LANG_TITLE
|
||||
entries = locale.map { it.second }.toTypedArray()
|
||||
entryValues = locale.map { it.first }.toTypedArray()
|
||||
setDefaultValue(PREF_AUDIO_LANG_DEFAULT)
|
||||
summary = "%s"
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
@ -235,9 +270,25 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
val serverPref = ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = PREF_SERVER_TITLE
|
||||
entries = PREF_SERVER_VALUES
|
||||
entryValues = PREF_SERVER_VALUES
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(videoQualityPref)
|
||||
screen.addPreference(audioLangPref)
|
||||
screen.addPreference(titlePref)
|
||||
screen.addPreference(serverPref)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
@ -245,6 +296,10 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
return body.string().let(json::decodeFromString)
|
||||
}
|
||||
|
||||
private fun String.getLocale(): String {
|
||||
return locale.firstOrNull { it.first == this }?.second ?: ""
|
||||
}
|
||||
|
||||
private fun String.parseStatus() = when (this) {
|
||||
"finished_airing" -> SAnime.COMPLETED
|
||||
"currently_airing" -> SAnime.ONGOING
|
||||
@ -253,8 +308,12 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.contains(server, true) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
@ -270,5 +329,21 @@ class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "720p"
|
||||
private val PREF_QUALITY_VALUES = arrayOf("240p", "360p", "480p", "720p", "1080p")
|
||||
|
||||
private const val PREF_AUDIO_LANG_KEY = "preferred_audio_lang"
|
||||
private const val PREF_AUDIO_LANG_TITLE = "Preferred audio language"
|
||||
private const val PREF_AUDIO_LANG_DEFAULT = "ja-JP"
|
||||
|
||||
// Add new locales to the bottom so it doesn't mess with pref indexes
|
||||
private val locale = arrayOf(
|
||||
Pair("en-US", "English"),
|
||||
Pair("es-ES", "Spanish (España)"),
|
||||
Pair("ja-JP", "Japanese"),
|
||||
)
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_TITLE = "Preferred server"
|
||||
private const val PREF_SERVER_DEFAULT = "SapphireDuck"
|
||||
private val PREF_SERVER_VALUES = arrayOf("SapphireDuck", "PinkBird")
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.kickassanime
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.KickAssAnimeFilters.asQueryPart
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
@ -31,16 +32,21 @@ object KickAssAnimeFilters {
|
||||
class YearFilter : QueryPartFilter("Year", KickAssAnimeFiltersData.year)
|
||||
class StatusFilter : QueryPartFilter("Status", KickAssAnimeFiltersData.status)
|
||||
class TypeFilter : QueryPartFilter("Type", KickAssAnimeFiltersData.type)
|
||||
class SubPageFilter : QueryPartFilter("Sub-page", KickAssAnimeFiltersData.subpage)
|
||||
|
||||
val filterList = AnimeFilterList(
|
||||
GenreFilter(),
|
||||
YearFilter(),
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
AnimeFilter.Header("NOTE: Overrides & ignores search and other filters"),
|
||||
SubPageFilter(),
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val filters: String = "",
|
||||
val subPage: String = "",
|
||||
)
|
||||
|
||||
private fun getJsonList(listString: String, name: String): String {
|
||||
@ -66,7 +72,7 @@ object KickAssAnimeFilters {
|
||||
val status = filters.asQueryPart<StatusFilter>()
|
||||
val type = filters.asQueryPart<TypeFilter>()
|
||||
|
||||
val filters = "{${
|
||||
val filtersQuery = "{${
|
||||
listOf(
|
||||
getJsonList(genre, "genres"),
|
||||
getJsonItem(year, "year"),
|
||||
@ -74,7 +80,10 @@ object KickAssAnimeFilters {
|
||||
getJsonItem(type, "type"),
|
||||
).filter { it.isNotEmpty() }.joinToString(",")
|
||||
}}"
|
||||
return FilterSearchParams(filters)
|
||||
return FilterSearchParams(
|
||||
filtersQuery,
|
||||
filters.asQueryPart<SubPageFilter>(),
|
||||
)
|
||||
}
|
||||
|
||||
private object KickAssAnimeFiltersData {
|
||||
@ -279,5 +288,13 @@ object KickAssAnimeFilters {
|
||||
Pair("UNKNOWN", "\"unknown\""),
|
||||
Pair("MUSIC", "\"music\""),
|
||||
)
|
||||
|
||||
val subpage = arrayOf(
|
||||
Pair("<Select>", ""),
|
||||
Pair("Trending", "show/trending"),
|
||||
Pair("Anime", "anime"),
|
||||
Pair("Recently Added", "show/recent"),
|
||||
Pair("Popular Shows", "show/popular"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -75,3 +75,8 @@ data class VideoDto(
|
||||
@Serializable
|
||||
data class SubtitlesDto(val name: String, val language: String, val src: String)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LanguagesDto(
|
||||
val result: List<String>,
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.VideoDto
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
@ -9,15 +10,11 @@ import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.CharacterIterator
|
||||
import java.text.StringCharacterIterator
|
||||
|
||||
class KickAssAnimeExtractor(private val client: OkHttpClient, private val json: Json) {
|
||||
private val isStable by lazy {
|
||||
runCatching {
|
||||
Track("", "")
|
||||
false
|
||||
}.getOrDefault(true)
|
||||
}
|
||||
|
||||
fun videosFromUrl(url: String): List<Video> {
|
||||
val idQuery = url.substringAfterLast("?")
|
||||
val baseUrl = url.substringBeforeLast("/") // baseUrl + endpoint/player
|
||||
@ -44,23 +41,19 @@ class KickAssAnimeExtractor(private val client: OkHttpClient, private val json:
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val subtitles = if (isStable || videoObject.subtitles.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
videoObject.subtitles.map {
|
||||
val subUrl: String = it.src.let { src ->
|
||||
if (src.startsWith("/")) {
|
||||
baseUrl.substringBeforeLast("/") + "/$src"
|
||||
} else {
|
||||
src
|
||||
}
|
||||
val subtitles = videoObject.subtitles.map {
|
||||
val subUrl: String = it.src.let { src ->
|
||||
if (src.startsWith("/")) {
|
||||
baseUrl.substringBeforeLast("/") + "/$src"
|
||||
} else {
|
||||
src
|
||||
}
|
||||
|
||||
val language = "${it.name} (${it.language})"
|
||||
|
||||
println("subUrl -> $subUrl")
|
||||
Track(subUrl, language)
|
||||
}
|
||||
|
||||
val language = "${it.name} (${it.language})"
|
||||
|
||||
println("subUrl -> $subUrl")
|
||||
Track(subUrl, language)
|
||||
}
|
||||
|
||||
val masterPlaylist = client.newCall(GET(videoObject.playlistUrl)).execute()
|
||||
@ -85,24 +78,40 @@ class KickAssAnimeExtractor(private val client: OkHttpClient, private val json:
|
||||
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
|
||||
if (isStable) {
|
||||
Video(videoUrl, "$prefix - $resolution", videoUrl)
|
||||
} else {
|
||||
Video(videoUrl, "$prefix - $resolution", videoUrl, subtitleTracks = subs)
|
||||
}
|
||||
Video(videoUrl, "$prefix - $resolution", videoUrl, subtitleTracks = subs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractVideosFromDash(playlist: String, prefix: String, subs: List<Track>): List<Video> {
|
||||
return playlist.split("<Representation").drop(1).dropLast(1).map {
|
||||
val resolution = it.substringAfter("height=\"").substringBefore('"') + "p"
|
||||
val url = it.substringAfter("<BaseURL>").substringBefore("</Base")
|
||||
.replace("&", "&")
|
||||
if (isStable) {
|
||||
Video(url, "$prefix - $resolution", url)
|
||||
} else {
|
||||
Video(url, "$prefix - $resolution", url, subtitleTracks = subs)
|
||||
}
|
||||
// Parsing dash with Jsoup :YEP:
|
||||
val document = Jsoup.parse(playlist)
|
||||
val audioList = document.select("Representation[mimetype~=audio]").map { audioSrc ->
|
||||
Track(audioSrc.text(), formatBits(audioSrc.attr("bandwidth").toLongOrNull() ?: 0L) ?: "audio")
|
||||
}
|
||||
return document.select("Representation[mimetype~=video]").map { videoSrc ->
|
||||
Video(
|
||||
videoSrc.text(),
|
||||
"$prefix - ${videoSrc.attr("height")}p - ${formatBits(videoSrc.attr("bandwidth").toLongOrNull() ?: 0L)}",
|
||||
videoSrc.text(),
|
||||
audioTracks = audioList,
|
||||
subtitleTracks = subs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun formatBits(bits: Long): String? {
|
||||
var bits = bits
|
||||
if (-1000 < bits && bits < 1000) {
|
||||
return "${bits}b"
|
||||
}
|
||||
val ci: CharacterIterator = StringCharacterIterator("kMGTPE")
|
||||
while (bits <= -999950 || bits >= 999950) {
|
||||
bits /= 1000
|
||||
ci.next()
|
||||
}
|
||||
return java.lang.String.format("%.2f%cbs", bits / 1000.0, ci.current())
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user