This commit is contained in:
Secozzi
2023-04-19 12:03:46 +02:00
committed by GitHub
parent 0c031d7bc2
commit 1fe5b6c5d6
5 changed files with 156 additions and 50 deletions

View File

@ -9,7 +9,7 @@ ext {
pkgNameSuffix = 'en.kickassanime'
extClass = '.KickAssAnime'
libVersion = '13'
extVersionCode = 23
extVersionCode = 24
}
dependencies {

View File

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

View File

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

View File

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

View File

@ -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("&amp;", "&")
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())
}
}