Initial commit
This commit is contained in:
commit
98ed7e8839
2263 changed files with 108711 additions and 0 deletions
23
src/en/kickassanime/AndroidManifest.xml
Normal file
23
src/en/kickassanime/AndroidManifest.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".en.kickassanime.KickAssAnimeUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="kaas.am"
|
||||
android:pathPattern="/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
12
src/en/kickassanime/build.gradle
Normal file
12
src/en/kickassanime/build.gradle
Normal file
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'KickAssAnime'
|
||||
extClass = '.KickAssAnime'
|
||||
extVersionCode = 42
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:cryptoaes"))
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
BIN
src/en/kickassanime/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/kickassanime/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
BIN
src/en/kickassanime/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/kickassanime/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
BIN
src/en/kickassanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/kickassanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
src/en/kickassanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/kickassanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.5 KiB |
BIN
src/en/kickassanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/kickassanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/en/kickassanime/res/web_hi_res_512.png
Normal file
BIN
src/en/kickassanime/res/web_hi_res_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,434 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.kickassanime
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
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
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.SearchResponseDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.ServersDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors.KickAssAnimeExtractor
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
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.network.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
class KickAssAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "KickAssAnime"
|
||||
|
||||
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
|
||||
|
||||
private val apiUrl by lazy { "$baseUrl/api/show" }
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
.clearBaseUrl()
|
||||
}
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$apiUrl/popular?page=$page")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val data = response.parseAs<PopularResponseDto>()
|
||||
val animes = data.result.map(::popularAnimeFromObject)
|
||||
val page = response.request.url.queryParameter("page")?.toIntOrNull() ?: 0
|
||||
val hasNext = data.page_count > page
|
||||
return AnimesPage(animes, hasNext)
|
||||
}
|
||||
|
||||
private fun popularAnimeFromObject(anime: PopularItemDto): SAnime {
|
||||
return SAnime.create().apply {
|
||||
val useEnglish = preferences.getBoolean(PREF_USE_ENGLISH_KEY, PREF_USE_ENGLISH_DEFAULT)
|
||||
title = when {
|
||||
anime.title_en.isNotBlank() && useEnglish -> anime.title_en
|
||||
else -> anime.title
|
||||
}
|
||||
setUrlWithoutDomain("/${anime.slug}")
|
||||
thumbnail_url = "$baseUrl/${anime.poster.url}"
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
private fun episodeListRequest(anime: SAnime, page: Int, lang: String) =
|
||||
GET("$apiUrl${anime.url}/episodes?page=$page&lang=$lang")
|
||||
|
||||
private fun getEpisodeResponse(anime: SAnime, page: Int, lang: String): EpisodeResponseDto {
|
||||
return client.newCall(episodeListRequest(anime, page, lang))
|
||||
.execute()
|
||||
.parseAs()
|
||||
}
|
||||
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||
val languages = client.newCall(
|
||||
GET("$apiUrl${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, lang).result)
|
||||
}
|
||||
}
|
||||
|
||||
val episodes = items.map {
|
||||
SEpisode.create().apply {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
return episodes.reversed()
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val url = apiUrl + episode.url.replace("/ep-", "/episode/ep-")
|
||||
return GET(url)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val videos = response.parseAs<ServersDto>()
|
||||
val extractor = KickAssAnimeExtractor(client, json, headers)
|
||||
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
||||
|
||||
val videoList = videos.servers.mapNotNull {
|
||||
if (!hosterSelection.contains(it.name)) return@mapNotNull null
|
||||
runCatching { extractor.videosFromUrl(it.src, it.name) }.getOrNull()
|
||||
}.flatten()
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun getAnimeUrl(anime: SAnime) = "$baseUrl${anime.url}"
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime) = GET(apiUrl + 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)
|
||||
title = when {
|
||||
anime.title_en.isNotBlank() && useEnglish -> anime.title_en
|
||||
else -> anime.title
|
||||
}
|
||||
setUrlWithoutDomain("/${anime.slug}")
|
||||
thumbnail_url = "$baseUrl/${anime.poster.url}"
|
||||
genre = anime.genres.joinToString()
|
||||
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.replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(
|
||||
Locale.ROOT,
|
||||
)
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}}\n",
|
||||
)
|
||||
append("Year: ${anime.year}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw UnsupportedOperationException()
|
||||
override fun searchAnimeParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
private fun searchAnimeParse(response: Response, page: Int): AnimesPage {
|
||||
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 {
|
||||
val newHeaders = headers.newBuilder()
|
||||
.add("Accept", "application/json, text/plain, */*")
|
||||
.add("Host", baseUrl.toHttpUrl().host)
|
||||
.add("Referer", "$baseUrl/anime")
|
||||
.build()
|
||||
|
||||
if (filters.subPage.isNotBlank()) return GET("$baseUrl/api/${filters.subPage}?page=$page", headers = newHeaders)
|
||||
|
||||
val encodedFilters = if (filters.filters == "{}") "" else Base64.encodeToString(filters.filters.encodeToByteArray(), Base64.NO_WRAP)
|
||||
|
||||
return if (query.isBlank()) {
|
||||
val url = buildString {
|
||||
append(baseUrl)
|
||||
append("/api/anime")
|
||||
append("?page=$page")
|
||||
if (encodedFilters.isNotEmpty()) append("&filters=$encodedFilters")
|
||||
}
|
||||
|
||||
GET(url, headers = newHeaders)
|
||||
} else {
|
||||
val data = buildJsonObject {
|
||||
put("page", page)
|
||||
put("query", query)
|
||||
if (encodedFilters.isNotEmpty()) put("filters", encodedFilters)
|
||||
}.toString().toRequestBody("application/json".toMediaType())
|
||||
|
||||
POST("$baseUrl/api/fsearch", body = data, headers = newHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val slug = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$baseUrl/api/show/$slug"))
|
||||
.awaitSuccess()
|
||||
.use(::searchAnimeBySlugParse)
|
||||
} else {
|
||||
val params = KickAssAnimeFilters.getSearchParameters(filters)
|
||||
return client.newCall(searchAnimeRequest(page, query, params))
|
||||
.awaitSuccess()
|
||||
.let { response ->
|
||||
searchAnimeParse(response, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchAnimeBySlugParse(response: Response): AnimesPage {
|
||||
val details = animeDetailsParse(response)
|
||||
return AnimesPage(listOf(details), false)
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override fun getFilterList(): AnimeFilterList = KickAssAnimeFilters.FILTER_LIST
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val data = response.parseAs<RecentsResponseDto>()
|
||||
val animes = data.result.map(::popularAnimeFromObject)
|
||||
return AnimesPage(animes, data.hadNext)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$apiUrl/recent?type=all&page=$page")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
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
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
|
||||
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) },
|
||||
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
|
||||
{ it.quality.contains(server, true) },
|
||||
{ Regex("""([\d,]+) [KMGTPE]B/s""").find(it.quality)?.groupValues?.get(1)?.replace(",", ".")?.toFloatOrNull() ?: 0F },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private fun SharedPreferences.clearBaseUrl(): SharedPreferences {
|
||||
if (getString(PREF_DOMAIN_KEY, "")!! in PREF_DOMAIN_ENTRY_VALUES) {
|
||||
return this
|
||||
}
|
||||
edit()
|
||||
.remove(PREF_DOMAIN_KEY)
|
||||
.putString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)
|
||||
.apply()
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SERVERS = arrayOf("DuckStream", "BirdStream", "VidStreaming")
|
||||
|
||||
const val PREFIX_SEARCH = "slug:"
|
||||
|
||||
private const val PREF_USE_ENGLISH_KEY = "pref_use_english"
|
||||
private const val PREF_USE_ENGLISH_TITLE = "Use English titles"
|
||||
private const val PREF_USE_ENGLISH_SUMMARY = "Show Titles in English instead of Romanji when possible."
|
||||
private const val PREF_USE_ENGLISH_DEFAULT = false
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080p"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("240p", "360p", "480p", "720p", "1080p", "2160p")
|
||||
|
||||
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 = "DuckStream"
|
||||
private val PREF_SERVER_VALUES = SERVERS
|
||||
|
||||
private const val PREF_DOMAIN_KEY = "preferred_domain"
|
||||
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
|
||||
private const val PREF_DOMAIN_DEFAULT = "https://kaas.to"
|
||||
private val PREF_DOMAIN_ENTRIES = arrayOf("kaas.to", "kaas.ro", "kickassanimes.io", "www1.kickassanime.mx")
|
||||
private val PREF_DOMAIN_ENTRY_VALUES = PREF_DOMAIN_ENTRIES.map { "https://$it" }.toTypedArray()
|
||||
|
||||
private const val PREF_HOSTER_KEY = "hoster_selection"
|
||||
private const val PREF_HOSTER_TITLE = "Enable/Disable Hosts"
|
||||
private val PREF_HOSTER_DEFAULT = SERVERS.toSet()
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = PREF_DOMAIN_TITLE
|
||||
entries = PREF_DOMAIN_ENTRIES
|
||||
entryValues = PREF_DOMAIN_ENTRY_VALUES
|
||||
setDefaultValue(PREF_DOMAIN_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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_USE_ENGLISH_KEY
|
||||
title = PREF_USE_ENGLISH_TITLE
|
||||
summary = PREF_USE_ENGLISH_SUMMARY
|
||||
setDefaultValue(PREF_USE_ENGLISH_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRIES
|
||||
setDefaultValue(PREF_QUALITY_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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
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()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_HOSTER_KEY
|
||||
title = PREF_HOSTER_TITLE
|
||||
entries = SERVERS
|
||||
entryValues = SERVERS
|
||||
setDefaultValue(PREF_HOSTER_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.kickassanime
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object KickAssAnimeFilters {
|
||||
open class QueryPartFilter(
|
||||
displayName: String,
|
||||
val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Select<String>(
|
||||
displayName,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
fun toQueryPart() = vals[state].second
|
||||
}
|
||||
|
||||
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
|
||||
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
|
||||
return this.filterIsInstance<R>().joinToString("") {
|
||||
(it as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
}
|
||||
|
||||
class GenreFilter : CheckBoxFilterList(
|
||||
"Genre",
|
||||
KickAssAnimeFiltersData.GENRE.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
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 FILTER_LIST get() = 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 {
|
||||
if (listString.isEmpty()) return ""
|
||||
return "\"$name\":[$listString]"
|
||||
}
|
||||
|
||||
private fun getJsonItem(item: String, name: String): String {
|
||||
if (item.isEmpty()) return ""
|
||||
return "\"$name\":$item"
|
||||
}
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
val genre = filters.filterIsInstance<GenreFilter>()
|
||||
.first()
|
||||
.state.mapNotNull { format ->
|
||||
if (format.state) {
|
||||
KickAssAnimeFiltersData.GENRE.find { it.first == format.name }!!.second
|
||||
} else { null }
|
||||
}.joinToString(",") { "\"$it\"" }
|
||||
|
||||
val year = filters.asQueryPart<YearFilter>()
|
||||
val status = filters.asQueryPart<StatusFilter>()
|
||||
val type = filters.asQueryPart<TypeFilter>()
|
||||
|
||||
val filtersQuery = "{${
|
||||
listOf(
|
||||
getJsonList(genre, "genres"),
|
||||
getJsonItem(year, "year"),
|
||||
getJsonItem(status, "status"),
|
||||
getJsonItem(type, "type"),
|
||||
).filter { it.isNotEmpty() }.joinToString(",")
|
||||
}}"
|
||||
return FilterSearchParams(
|
||||
filtersQuery,
|
||||
filters.asQueryPart<SubPageFilter>(),
|
||||
)
|
||||
}
|
||||
|
||||
private object KickAssAnimeFiltersData {
|
||||
val GENRE = arrayOf(
|
||||
Pair("Action", "Action"),
|
||||
Pair("Adult Cast", "Adult Cast"),
|
||||
Pair("Adventure", "Adventure"),
|
||||
Pair("Anthropomorphic", "Anthropomorphic"),
|
||||
Pair("Avant Garde", "Avant Garde"),
|
||||
Pair("Award Winning", "Award Winning"),
|
||||
Pair("Boys Love", "Boys Love"),
|
||||
Pair("CGDCT", "CGDCT"),
|
||||
Pair("Childcare", "Childcare"),
|
||||
Pair("Combat Sports", "Combat Sports"),
|
||||
Pair("Comedy", "Comedy"),
|
||||
Pair("Crossdressing", "Crossdressing"),
|
||||
Pair("Delinquents", "Delinquents"),
|
||||
Pair("Detective", "Detective"),
|
||||
Pair("Drama", "Drama"),
|
||||
Pair("Ecchi", "Ecchi"),
|
||||
Pair("Educational", "Educational"),
|
||||
Pair("Erotica", "Erotica"),
|
||||
Pair("Fantasy", "Fantasy"),
|
||||
Pair("Gag Humor", "Gag Humor"),
|
||||
Pair("Girls Love", "Girls Love"),
|
||||
Pair("Gore", "Gore"),
|
||||
Pair("Gourmet", "Gourmet"),
|
||||
Pair("Harem", "Harem"),
|
||||
Pair("Hentai", "Hentai"),
|
||||
Pair("High Stakes Game", "High Stakes Game"),
|
||||
Pair("Historical", "Historical"),
|
||||
Pair("Horror", "Horror"),
|
||||
Pair("Idols (Female)", "Idols (Female)"),
|
||||
Pair("Idols (Male)", "Idols (Male)"),
|
||||
Pair("Isekai", "Isekai"),
|
||||
Pair("Iyashikei", "Iyashikei"),
|
||||
Pair("Josei", "Josei"),
|
||||
Pair("Kids", "Kids"),
|
||||
Pair("Love Polygon", "Love Polygon"),
|
||||
Pair("Magical Sex Shift", "Magical Sex Shift"),
|
||||
Pair("Mahou Shoujo", "Mahou Shoujo"),
|
||||
Pair("Martial Arts", "Martial Arts"),
|
||||
Pair("Mecha", "Mecha"),
|
||||
Pair("Medical", "Medical"),
|
||||
Pair("Military", "Military"),
|
||||
Pair("Music", "Music"),
|
||||
Pair("Mystery", "Mystery"),
|
||||
Pair("Mythology", "Mythology"),
|
||||
Pair("Organized Crime", "Organized Crime"),
|
||||
Pair("Otaku Culture", "Otaku Culture"),
|
||||
Pair("Parody", "Parody"),
|
||||
Pair("Performing Arts", "Performing Arts"),
|
||||
Pair("Pets", "Pets"),
|
||||
Pair("Psychological", "Psychological"),
|
||||
Pair("Racing", "Racing"),
|
||||
Pair("Reincarnation", "Reincarnation"),
|
||||
Pair("Reverse Harem", "Reverse Harem"),
|
||||
Pair("Romance", "Romance"),
|
||||
Pair("Romantic Subtext", "Romantic Subtext"),
|
||||
Pair("Samurai", "Samurai"),
|
||||
Pair("School", "School"),
|
||||
Pair("Sci-Fi", "Sci-Fi"),
|
||||
Pair("Seinen", "Seinen"),
|
||||
Pair("Shoujo", "Shoujo"),
|
||||
Pair("Shounen", "Shounen"),
|
||||
Pair("Showbiz", "Showbiz"),
|
||||
Pair("Slice of Life", "Slice of Life"),
|
||||
Pair("Space", "Space"),
|
||||
Pair("Sports", "Sports"),
|
||||
Pair("Strategy Game", "Strategy Game"),
|
||||
Pair("Super Power", "Super Power"),
|
||||
Pair("Supernatural", "Supernatural"),
|
||||
Pair("Survival", "Survival"),
|
||||
Pair("Suspense", "Suspense"),
|
||||
Pair("Team Sports", "Team Sports"),
|
||||
Pair("Time Travel", "Time Travel"),
|
||||
Pair("Vampire", "Vampire"),
|
||||
Pair("Video Game", "Video Game"),
|
||||
Pair("Visual Arts", "Visual Arts"),
|
||||
Pair("Workplace", "Workplace"),
|
||||
)
|
||||
|
||||
val YEAR = arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("1917", "1917"),
|
||||
Pair("1918", "1918"),
|
||||
Pair("1924", "1924"),
|
||||
Pair("1925", "1925"),
|
||||
Pair("1926", "1926"),
|
||||
Pair("1927", "1927"),
|
||||
Pair("1928", "1928"),
|
||||
Pair("1929", "1929"),
|
||||
Pair("1930", "1930"),
|
||||
Pair("1931", "1931"),
|
||||
Pair("1932", "1932"),
|
||||
Pair("1933", "1933"),
|
||||
Pair("1934", "1934"),
|
||||
Pair("1935", "1935"),
|
||||
Pair("1936", "1936"),
|
||||
Pair("1937", "1937"),
|
||||
Pair("1938", "1938"),
|
||||
Pair("1939", "1939"),
|
||||
Pair("1940", "1940"),
|
||||
Pair("1941", "1941"),
|
||||
Pair("1942", "1942"),
|
||||
Pair("1943", "1943"),
|
||||
Pair("1944", "1944"),
|
||||
Pair("1945", "1945"),
|
||||
Pair("1946", "1946"),
|
||||
Pair("1947", "1947"),
|
||||
Pair("1948", "1948"),
|
||||
Pair("1949", "1949"),
|
||||
Pair("1950", "1950"),
|
||||
Pair("1951", "1951"),
|
||||
Pair("1952", "1952"),
|
||||
Pair("1953", "1953"),
|
||||
Pair("1954", "1954"),
|
||||
Pair("1955", "1955"),
|
||||
Pair("1956", "1956"),
|
||||
Pair("1957", "1957"),
|
||||
Pair("1958", "1958"),
|
||||
Pair("1959", "1959"),
|
||||
Pair("1960", "1960"),
|
||||
Pair("1961", "1961"),
|
||||
Pair("1962", "1962"),
|
||||
Pair("1963", "1963"),
|
||||
Pair("1964", "1964"),
|
||||
Pair("1965", "1965"),
|
||||
Pair("1966", "1966"),
|
||||
Pair("1967", "1967"),
|
||||
Pair("1968", "1968"),
|
||||
Pair("1969", "1969"),
|
||||
Pair("1970", "1970"),
|
||||
Pair("1971", "1971"),
|
||||
Pair("1972", "1972"),
|
||||
Pair("1973", "1973"),
|
||||
Pair("1974", "1974"),
|
||||
Pair("1975", "1975"),
|
||||
Pair("1976", "1976"),
|
||||
Pair("1977", "1977"),
|
||||
Pair("1978", "1978"),
|
||||
Pair("1979", "1979"),
|
||||
Pair("1980", "1980"),
|
||||
Pair("1981", "1981"),
|
||||
Pair("1982", "1982"),
|
||||
Pair("1983", "1983"),
|
||||
Pair("1984", "1984"),
|
||||
Pair("1985", "1985"),
|
||||
Pair("1986", "1986"),
|
||||
Pair("1987", "1987"),
|
||||
Pair("1988", "1988"),
|
||||
Pair("1989", "1989"),
|
||||
Pair("1990", "1990"),
|
||||
Pair("1991", "1991"),
|
||||
Pair("1992", "1992"),
|
||||
Pair("1993", "1993"),
|
||||
Pair("1994", "1994"),
|
||||
Pair("1995", "1995"),
|
||||
Pair("1996", "1996"),
|
||||
Pair("1997", "1997"),
|
||||
Pair("1998", "1998"),
|
||||
Pair("1999", "1999"),
|
||||
Pair("2000", "2000"),
|
||||
Pair("2001", "2001"),
|
||||
Pair("2002", "2002"),
|
||||
Pair("2003", "2003"),
|
||||
Pair("2004", "2004"),
|
||||
Pair("2005", "2005"),
|
||||
Pair("2006", "2006"),
|
||||
Pair("2007", "2007"),
|
||||
Pair("2008", "2008"),
|
||||
Pair("2009", "2009"),
|
||||
Pair("2010", "2010"),
|
||||
Pair("2011", "2011"),
|
||||
Pair("2012", "2012"),
|
||||
Pair("2013", "2013"),
|
||||
Pair("2014", "2014"),
|
||||
Pair("2015", "2015"),
|
||||
Pair("2016", "2016"),
|
||||
Pair("2017", "2017"),
|
||||
Pair("2018", "2018"),
|
||||
Pair("2019", "2019"),
|
||||
Pair("2020", "2020"),
|
||||
Pair("2021", "2021"),
|
||||
Pair("2022", "2022"),
|
||||
Pair("2023", "2023"),
|
||||
Pair("2024", "2024"),
|
||||
)
|
||||
|
||||
val STATUS = arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Finished Airing", "\"finished\""),
|
||||
Pair("Currently Airing", "\"airing\""),
|
||||
)
|
||||
|
||||
val TYPE = arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("TV", "\"tv\""),
|
||||
Pair("OVA", "\"ova\""),
|
||||
Pair("ONA", "\"ona\""),
|
||||
Pair("SPECIAL", "\"special\""),
|
||||
Pair("MOVIE", "\"movie\""),
|
||||
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"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.kickassanime
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://kaas.am/<item> intents
|
||||
* and redirects them to the main Aniyomi process.
|
||||
*/
|
||||
class KickAssAnimeUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size >= 1) {
|
||||
val slug = pathSegments[0]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.ANIMESEARCH"
|
||||
putExtra("query", "${KickAssAnime.PREFIX_SEARCH}$slug")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(tag, e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e(tag, "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.kickassanime.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
data class PopularResponseDto(
|
||||
val page_count: Int,
|
||||
val result: List<PopularItemDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PopularItemDto(
|
||||
val title: String,
|
||||
val title_en: String = "",
|
||||
val slug: String,
|
||||
val poster: PosterDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchResponseDto(
|
||||
val result: List<PopularItemDto>,
|
||||
val maxPage: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PosterDto(@SerialName("hq") val slug: String) {
|
||||
val url by lazy { "image/poster/$slug.webp" }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RecentsResponseDto(
|
||||
val hadNext: Boolean,
|
||||
val result: List<PopularItemDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AnimeInfoDto(
|
||||
val genres: List<String>,
|
||||
val poster: PosterDto,
|
||||
val season: String,
|
||||
val slug: String,
|
||||
val status: String,
|
||||
val synopsis: String,
|
||||
val title: String,
|
||||
val title_en: String = "",
|
||||
val year: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EpisodeResponseDto(
|
||||
val pages: List<JsonObject>, // We dont care about its contents, only the size
|
||||
val result: List<EpisodeDto> = emptyList(),
|
||||
) {
|
||||
@Serializable
|
||||
data class EpisodeDto(
|
||||
val slug: String,
|
||||
val title: String? = "",
|
||||
val episode_string: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ServersDto(val servers: List<Server>) {
|
||||
@Serializable
|
||||
data class Server(
|
||||
val name: String,
|
||||
val src: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class VideoDto(
|
||||
val hls: String = "",
|
||||
val dash: String = "",
|
||||
val subtitles: List<SubtitlesDto> = emptyList(),
|
||||
) {
|
||||
val playlistUrl by lazy {
|
||||
hls.ifEmpty { dash }.let { uri ->
|
||||
when {
|
||||
uri.startsWith("//") -> "https:$uri"
|
||||
else -> uri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SubtitlesDto(val name: String, val language: String, val src: String)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LanguagesDto(
|
||||
val result: List<String>,
|
||||
)
|
|
@ -0,0 +1,146 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.kickassanime.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.en.kickassanime.dto.VideoDto
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES.decodeHex
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import java.security.MessageDigest
|
||||
|
||||
class KickAssAnimeExtractor(
|
||||
private val client: OkHttpClient,
|
||||
private val json: Json,
|
||||
private val headers: Headers,
|
||||
) {
|
||||
fun videosFromUrl(url: String, name: String): List<Video> {
|
||||
val host = url.toHttpUrl().host
|
||||
val mid = if (name == "DuckStream") "mid" else "id"
|
||||
val isBird = name == "BirdStream"
|
||||
|
||||
val query = url.toHttpUrl().queryParameter(mid)!!
|
||||
|
||||
val html = client.newCall(GET(url, headers)).execute().body.string()
|
||||
|
||||
val key = when (name) {
|
||||
"VidStreaming" -> "e13d38099bf562e8b9851a652d2043d3"
|
||||
"DuckStream" -> "4504447b74641ad972980a6b8ffd7631"
|
||||
"BirdStream" -> "4b14d0ff625163e3c9c7a47926484bf2"
|
||||
else -> return emptyList()
|
||||
}.toByteArray()
|
||||
|
||||
val (sig, timeStamp, route) = getSignature(html, name, query, key) ?: return emptyList()
|
||||
val sourceUrl = buildString {
|
||||
append("https://")
|
||||
append(host)
|
||||
append(route)
|
||||
append("?$mid=$query")
|
||||
if (!isBird) append("&e=$timeStamp")
|
||||
append("&s=$sig")
|
||||
}
|
||||
|
||||
val request = GET(sourceUrl, headers.newBuilder().add("Referer", url).build())
|
||||
val response = client.newCall(request).execute()
|
||||
.body.string()
|
||||
|
||||
val (encryptedData, ivhex) = response.substringAfter(":\"")
|
||||
.substringBefore('"')
|
||||
.replace("\\", "")
|
||||
.split(":")
|
||||
|
||||
val iv = ivhex.decodeHex()
|
||||
|
||||
val videoObject = try {
|
||||
val decrypted = CryptoAES.decrypt(encryptedData, key, iv)
|
||||
json.decodeFromString<VideoDto>(decrypted)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val subtitles = videoObject.subtitles.map {
|
||||
val subUrl: String = it.src.let { src ->
|
||||
if (src.startsWith("//")) {
|
||||
"https:$src"
|
||||
} else if (src.startsWith("/")) {
|
||||
"https://$host$src"
|
||||
} else {
|
||||
src
|
||||
}
|
||||
}
|
||||
|
||||
val language = "${it.name} (${it.language})"
|
||||
|
||||
Track(subUrl, language)
|
||||
}
|
||||
|
||||
fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers {
|
||||
return baseHeaders.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Accept-Language", "en-US,en;q=0.5")
|
||||
add("Origin", "https://$host")
|
||||
add("Sec-Fetch-Dest", "empty")
|
||||
add("Sec-Fetch-Mode", "cors")
|
||||
add("Sec-Fetch-Site", "cross-site")
|
||||
}.build()
|
||||
}
|
||||
|
||||
return when {
|
||||
videoObject.hls.isBlank() ->
|
||||
PlaylistUtils(client, headers).extractFromDash(videoObject.playlistUrl, videoNameGen = { res -> "$name - $res" }, subtitleList = subtitles)
|
||||
else -> PlaylistUtils(client, headers).extractFromHls(
|
||||
videoObject.playlistUrl,
|
||||
videoNameGen = { "$name - $it" },
|
||||
videoHeadersGen = ::getVideoHeaders,
|
||||
subtitleList = subtitles,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSignature(html: String, server: String, query: String, key: ByteArray): Triple<String, String, String>? {
|
||||
val order = when (server) {
|
||||
"VidStreaming" -> listOf("IP", "USERAGENT", "ROUTE", "MID", "TIMESTAMP", "KEY")
|
||||
"DuckStream" -> listOf("IP", "USERAGENT", "ROUTE", "MID", "TIMESTAMP", "KEY")
|
||||
"BirdStream" -> listOf("IP", "USERAGENT", "ROUTE", "MID", "KEY")
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val cid = String(html.substringAfter("cid: '").substringBefore("'").decodeHex()).split("|")
|
||||
val timeStamp = (System.currentTimeMillis() / 1000 + 60).toString()
|
||||
val route = cid[1].replace("player.php", "source.php")
|
||||
|
||||
val signature = buildString {
|
||||
order.forEach {
|
||||
when (it) {
|
||||
"IP" -> append(cid[0])
|
||||
"USERAGENT" -> append(headers["User-Agent"] ?: "")
|
||||
"ROUTE" -> append(route)
|
||||
"MID" -> append(query)
|
||||
"TIMESTAMP" -> append(timeStamp)
|
||||
"KEY" -> append(String(key))
|
||||
"SIG" -> append(html.substringAfter("signature: '").substringBefore("'"))
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(sha1sum(signature), timeStamp, route)
|
||||
}
|
||||
|
||||
private fun sha1sum(value: String): String {
|
||||
return try {
|
||||
val md = MessageDigest.getInstance("SHA-1")
|
||||
val bytes = md.digest(value.toByteArray())
|
||||
bytes.joinToString("") { "%02x".format(it) }
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Attempt to create the signature failed miserably.")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue