Initial commit
16
src/en/allanime/build.gradle
Normal file
|
@ -0,0 +1,16 @@
|
|||
ext {
|
||||
extName = 'AllAnime'
|
||||
extClass = '.AllAnime'
|
||||
extVersionCode = 31
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:gogostream-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/en/allanime/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src/en/allanime/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/en/allanime/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
src/en/allanime/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/en/allanime/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/en/allanime/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 126 KiB |
|
@ -0,0 +1,667 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanime
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.en.allanime.extractors.AllAnimeExtractor
|
||||
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.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.lib.gogostreamextractor.GogoStreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "AllAnime"
|
||||
|
||||
override val baseUrl by lazy { preferences.baseUrl }
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("type", "anime")
|
||||
put("size", PAGE_SIZE)
|
||||
put("dateRange", 7)
|
||||
put("page", page)
|
||||
}
|
||||
put("query", POPULAR_QUERY)
|
||||
}
|
||||
return buildPost(data)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<PopularResult>()
|
||||
|
||||
val animeList = parsed.data.queryPopular.recommendations.filter { it.anyCard != null }.map {
|
||||
SAnime.create().apply {
|
||||
title = when (preferences.titleStyle) {
|
||||
"romaji" -> it.anyCard!!.name
|
||||
"eng" -> it.anyCard!!.englishName ?: it.anyCard.name
|
||||
else -> it.anyCard!!.nativeName ?: it.anyCard.name
|
||||
}
|
||||
thumbnail_url = it.anyCard.thumbnail
|
||||
url = "${it.anyCard._id}<&sep>${it.anyCard.slugTime ?: ""}<&sep>${it.anyCard.name.slugify()}"
|
||||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
putJsonObject("search") {
|
||||
put("allowAdult", false)
|
||||
put("allowUnknown", false)
|
||||
}
|
||||
put("limit", PAGE_SIZE)
|
||||
put("page", page)
|
||||
put("translationType", preferences.subPref)
|
||||
put("countryOrigin", "ALL")
|
||||
}
|
||||
put("query", SEARCH_QUERY)
|
||||
}
|
||||
return buildPost(data)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage = parseAnime(response)
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val filters = AllAnimeFilters.getSearchParameters(filters)
|
||||
|
||||
return if (query.isNotEmpty()) {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
putJsonObject("search") {
|
||||
put("query", query)
|
||||
put("allowAdult", false)
|
||||
put("allowUnknown", false)
|
||||
}
|
||||
put("limit", PAGE_SIZE)
|
||||
put("page", page)
|
||||
put("translationType", preferences.subPref)
|
||||
put("countryOrigin", "ALL")
|
||||
}
|
||||
put("query", SEARCH_QUERY)
|
||||
}
|
||||
buildPost(data)
|
||||
} else {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
putJsonObject("search") {
|
||||
put("allowAdult", false)
|
||||
put("allowUnknown", false)
|
||||
if (filters.season != "all") put("season", filters.season)
|
||||
if (filters.releaseYear != "all") put("year", filters.releaseYear.toInt())
|
||||
if (filters.genres != "all") {
|
||||
put("genres", json.decodeFromString(filters.genres))
|
||||
put("excludeGenres", buildJsonArray { })
|
||||
}
|
||||
if (filters.types != "all") put("types", json.decodeFromString(filters.types))
|
||||
if (filters.sortBy != "update") put("sortBy", filters.sortBy)
|
||||
}
|
||||
put("limit", PAGE_SIZE)
|
||||
put("page", page)
|
||||
put("translationType", preferences.subPref)
|
||||
put("countryOrigin", filters.origin)
|
||||
}
|
||||
put("query", SEARCH_QUERY)
|
||||
}
|
||||
buildPost(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = parseAnime(response)
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AllAnimeFilters.FILTER_LIST
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("_id", anime.url.split("<&sep>").first())
|
||||
}
|
||||
put("query", DETAILS_QUERY)
|
||||
}
|
||||
return buildPost(data)
|
||||
}
|
||||
|
||||
override fun getAnimeUrl(anime: SAnime): String {
|
||||
val (id, time, slug) = anime.url.split("<&sep>")
|
||||
val slugTime = if (time.isNotEmpty()) "-st-$time" else time
|
||||
val siteUrl = preferences.siteUrl
|
||||
|
||||
return "$siteUrl/anime/$id/$slug$slugTime"
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val show = response.parseAs<DetailsResult>().data.show
|
||||
|
||||
return SAnime.create().apply {
|
||||
genre = show.genres?.joinToString(separator = ", ") ?: ""
|
||||
status = parseStatus(show.status)
|
||||
author = show.studios?.firstOrNull()
|
||||
description = buildString {
|
||||
append(
|
||||
Jsoup.parseBodyFragment(
|
||||
show.description?.replace("<br>", "br2n") ?: "",
|
||||
).text().replace("br2n", "\n"),
|
||||
)
|
||||
append("\n\n")
|
||||
append("Type: ${show.type ?: "Unknown"}")
|
||||
append("\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}")
|
||||
append("\nScore: ${show.score ?: "-"}★")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val data = buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("_id", anime.url.split("<&sep>").first())
|
||||
}
|
||||
put("query", EPISODES_QUERY)
|
||||
}
|
||||
return buildPost(data)
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val subPref = preferences.subPref
|
||||
val medias = response.parseAs<SeriesResult>()
|
||||
|
||||
val episodesDetail = if (subPref == "sub") {
|
||||
medias.data.show.availableEpisodesDetail.sub!!
|
||||
} else {
|
||||
medias.data.show.availableEpisodesDetail.dub!!
|
||||
}
|
||||
|
||||
return episodesDetail.map { ep ->
|
||||
val numName = ep.toIntOrNull() ?: (ep.toFloatOrNull() ?: "1")
|
||||
|
||||
SEpisode.create().apply {
|
||||
episode_number = ep.toFloatOrNull() ?: 0F
|
||||
name = "Episode $numName ($subPref)"
|
||||
url = json.encodeToString(
|
||||
buildJsonObject {
|
||||
putJsonObject("variables") {
|
||||
put("showId", medias.data.show._id)
|
||||
put("translationType", subPref)
|
||||
put("episodeString", ep)
|
||||
}
|
||||
put("query", STREAMS_QUERY)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val payload = episode.url
|
||||
.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val siteUrl = preferences.siteUrl
|
||||
val postHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Content-Length", payload.contentLength().toString())
|
||||
add("Content-Type", payload.contentType().toString())
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
add("Origin", siteUrl)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
return POST("$baseUrl/api", headers = postHeaders, body = payload)
|
||||
}
|
||||
|
||||
private val allAnimeExtractor by lazy { AllAnimeExtractor(client, headers, preferences.siteUrl) }
|
||||
private val gogoStreamExtractor by lazy { GogoStreamExtractor(client) }
|
||||
private val doodExtractor by lazy { DoodExtractor(client) }
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
|
||||
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val response = client.newCall(videoListRequest(episode)).await()
|
||||
|
||||
val videoJson = response.parseAs<EpisodeResult>()
|
||||
val videoList = mutableListOf<Pair<Video, Float>>()
|
||||
val serverList = mutableListOf<Server>()
|
||||
|
||||
val hosterSelection = preferences.getHosters
|
||||
val altHosterSelection = preferences.getAltHosters
|
||||
|
||||
// list of alternative hosters
|
||||
val mappings = listOf(
|
||||
"vidstreaming" to listOf("vidstreaming", "https://gogo", "playgo1.cc", "playtaku"),
|
||||
"doodstream" to listOf("dood"),
|
||||
"okru" to listOf("ok.ru"),
|
||||
"mp4upload" to listOf("mp4upload.com"),
|
||||
"streamlare" to listOf("streamlare.com"),
|
||||
)
|
||||
|
||||
videoJson.data.episode.sourceUrls.forEach { video ->
|
||||
val videoUrl = video.sourceUrl.decryptSource()
|
||||
|
||||
val matchingMapping = mappings.firstOrNull { (altHoster, urlMatches) ->
|
||||
altHosterSelection.contains(altHoster) && videoUrl.containsAny(urlMatches)
|
||||
}
|
||||
|
||||
when {
|
||||
videoUrl.startsWith("/apivtwo/") && INTERAL_HOSTER_NAMES.any {
|
||||
Regex("""\b${it.lowercase()}\b""").find(video.sourceName.lowercase()) != null &&
|
||||
hosterSelection.contains(it.lowercase())
|
||||
} -> {
|
||||
serverList.add(Server(videoUrl, "internal ${video.sourceName}", video.priority))
|
||||
}
|
||||
altHosterSelection.contains("player") && video.type == "player" -> {
|
||||
serverList.add(Server(videoUrl, "player@${video.sourceName}", video.priority))
|
||||
}
|
||||
matchingMapping != null -> {
|
||||
serverList.add(Server(videoUrl, matchingMapping.first, video.priority))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videoList.addAll(
|
||||
serverList.parallelCatchingFlatMap { server ->
|
||||
val sName = server.sourceName
|
||||
when {
|
||||
sName.startsWith("internal ") -> {
|
||||
allAnimeExtractor.videoFromUrl(server.sourceUrl, server.sourceName)
|
||||
}
|
||||
sName.startsWith("player@") -> {
|
||||
val endPoint = client.newCall(GET("${preferences.siteUrl}/getVersion")).await()
|
||||
.parseAs<AllAnimeExtractor.VersionResponse>()
|
||||
.episodeIframeHead
|
||||
|
||||
val videoHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5")
|
||||
add("Host", server.sourceUrl.toHttpUrl().host)
|
||||
add("Referer", "$endPoint/")
|
||||
}.build()
|
||||
|
||||
listOf(
|
||||
Video(
|
||||
server.sourceUrl,
|
||||
"Original (player ${server.sourceName.substringAfter("player@")})",
|
||||
server.sourceUrl,
|
||||
headers = videoHeaders,
|
||||
),
|
||||
)
|
||||
}
|
||||
sName == "vidstreaming" -> {
|
||||
gogoStreamExtractor.videosFromUrl(server.sourceUrl.replace(Regex("^//"), "https://"))
|
||||
}
|
||||
sName == "dood" -> {
|
||||
doodExtractor.videosFromUrl(server.sourceUrl)
|
||||
}
|
||||
sName == "okru" -> {
|
||||
okruExtractor.videosFromUrl(server.sourceUrl)
|
||||
}
|
||||
sName == "mp4upload" -> {
|
||||
mp4uploadExtractor.videosFromUrl(server.sourceUrl, headers)
|
||||
}
|
||||
sName == "streamlare" -> {
|
||||
streamlareExtractor.videosFromUrl(server.sourceUrl)
|
||||
}
|
||||
else -> emptyList()
|
||||
}.let { it.map { v -> Pair(v, server.priority) } }
|
||||
},
|
||||
)
|
||||
|
||||
return prioritySort(videoList)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun String.decryptSource(): String {
|
||||
return if (this.startsWith("-")) {
|
||||
this.substringAfterLast('-').chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray().map {
|
||||
(it.toInt() xor 56).toChar()
|
||||
}.joinToString("")
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
|
||||
val prefServer = preferences.prefServer
|
||||
val quality = preferences.quality
|
||||
val subPref = preferences.subPref
|
||||
|
||||
return pList.sortedWith(
|
||||
compareBy(
|
||||
{ if (prefServer == "site_default") it.second else it.first.quality.contains(prefServer, true) },
|
||||
{ it.first.quality.contains(quality, true) },
|
||||
{ it.first.quality.contains(subPref, true) },
|
||||
),
|
||||
).reversed().map { t -> t.first }
|
||||
}
|
||||
|
||||
private fun buildPost(dataObject: JsonObject): Request {
|
||||
val payload = json.encodeToString(dataObject)
|
||||
.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val siteUrl = preferences.siteUrl
|
||||
val postHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Content-Length", payload.contentLength().toString())
|
||||
add("Content-Type", payload.contentType().toString())
|
||||
add("Host", baseUrl.toHttpUrl().host)
|
||||
add("Origin", siteUrl)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
return POST("$baseUrl/api", headers = postHeaders, body = payload)
|
||||
}
|
||||
|
||||
data class Server(
|
||||
val sourceUrl: String,
|
||||
val sourceName: String,
|
||||
val priority: Float,
|
||||
)
|
||||
|
||||
private fun parseStatus(string: String?): Int {
|
||||
return when (string) {
|
||||
"Releasing" -> SAnime.ONGOING
|
||||
"Finished" -> SAnime.COMPLETED
|
||||
"Not Yet Released" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.slugify(): String {
|
||||
return this.replace("""[^a-zA-Z0-9]""".toRegex(), "-")
|
||||
.replace("""-{2,}""".toRegex(), "-")
|
||||
.lowercase()
|
||||
}
|
||||
|
||||
private fun parseAnime(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<SearchResult>()
|
||||
|
||||
val animeList = parsed.data.shows.edges.map { ani ->
|
||||
SAnime.create().apply {
|
||||
title = when (preferences.titleStyle) {
|
||||
"romaji" -> ani.name
|
||||
"eng" -> ani.englishName ?: ani.name
|
||||
else -> ani.nativeName ?: ani.name
|
||||
}
|
||||
thumbnail_url = ani.thumbnail
|
||||
url = "${ani._id}<&sep>${ani.slugTime ?: ""}<&sep>${ani.name.slugify()}"
|
||||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
private fun String.containsAny(keywords: List<String>): Boolean {
|
||||
return keywords.any { this.contains(it) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGE_SIZE = 26 // number of items to retrieve when calling API
|
||||
private val INTERAL_HOSTER_NAMES = arrayOf(
|
||||
"Default", "Ac", "Ak", "Kir", "Rab", "Luf-mp4",
|
||||
"Si-Hls", "S-mp4", "Ac-Hls", "Uv-mp4", "Pn-Hls",
|
||||
)
|
||||
|
||||
private val ALT_HOSTER_NAMES = arrayOf(
|
||||
"player",
|
||||
"vidstreaming",
|
||||
"okru",
|
||||
"mp4upload",
|
||||
"streamlare",
|
||||
"doodstream",
|
||||
)
|
||||
|
||||
private const val PREF_SITE_DOMAIN_KEY = "preferred_site_domain"
|
||||
private const val PREF_SITE_DOMAIN_DEFAULT = "https://allanime.to"
|
||||
|
||||
private const val PREF_DOMAIN_KEY = "preferred_domain"
|
||||
private const val PREF_DOMAIN_DEFAULT = "https://api.allanime.day"
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Site Default") +
|
||||
INTERAL_HOSTER_NAMES.sliceArray(1 until INTERAL_HOSTER_NAMES.size) +
|
||||
ALT_HOSTER_NAMES
|
||||
private val PREF_SERVER_ENTRY_VALUES = arrayOf("site_default") +
|
||||
INTERAL_HOSTER_NAMES.sliceArray(1 until INTERAL_HOSTER_NAMES.size).map {
|
||||
it.lowercase()
|
||||
}.toTypedArray() +
|
||||
ALT_HOSTER_NAMES
|
||||
private const val PREF_SERVER_DEFAULT = "site_default"
|
||||
|
||||
private const val PREF_HOSTER_KEY = "hoster_selection"
|
||||
private val PREF_HOSTER_ENTRY_VALUES = INTERAL_HOSTER_NAMES.map {
|
||||
it.lowercase()
|
||||
}.toTypedArray()
|
||||
private val PREF_HOSTER_DEFAULT = setOf("default", "ac", "ak", "kir", "luf-mp4", "si-hls", "s-mp4", "ac-hls")
|
||||
|
||||
private const val PREF_ALT_HOSTER_KEY = "alt_hoster_selection"
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf(
|
||||
"2160p",
|
||||
"1440p",
|
||||
"1080p",
|
||||
"720p",
|
||||
"480p",
|
||||
"360p",
|
||||
"240p",
|
||||
"80p",
|
||||
)
|
||||
private val PREF_QUALITY_ENTRY_VALUES = PREF_QUALITY_ENTRIES.map {
|
||||
it.substringBefore("p")
|
||||
}.toTypedArray()
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
|
||||
private const val PREF_TITLE_STYLE_KEY = "preferred_title_style"
|
||||
private const val PREF_TITLE_STYLE_DEFAULT = "romaji"
|
||||
|
||||
private const val PREF_SUB_KEY = "preferred_sub"
|
||||
private const val PREF_SUB_DEFAULT = "sub"
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SITE_DOMAIN_KEY
|
||||
title = "Preferred domain for site (requires app restart)"
|
||||
entries = arrayOf("allmanga.to")
|
||||
entryValues = arrayOf("https://allmanga.to")
|
||||
setDefaultValue(PREF_SITE_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)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = "Preferred domain (requires app restart)"
|
||||
entries = arrayOf("api.allanime.day")
|
||||
entryValues = arrayOf("https://api.allanime.day")
|
||||
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)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred Video Server"
|
||||
entries = PREF_SERVER_ENTRIES
|
||||
entryValues = PREF_SERVER_ENTRY_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 = "Enable/Disable Hosts"
|
||||
entries = INTERAL_HOSTER_NAMES
|
||||
entryValues = PREF_HOSTER_ENTRY_VALUES
|
||||
setDefaultValue(PREF_HOSTER_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_ALT_HOSTER_KEY
|
||||
title = "Enable/Disable Alternative Hosts"
|
||||
entries = ALT_HOSTER_NAMES
|
||||
entryValues = ALT_HOSTER_NAMES
|
||||
setDefaultValue(ALT_HOSTER_NAMES.toSet())
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
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_TITLE_STYLE_KEY
|
||||
title = "Preferred Title Style"
|
||||
entries = arrayOf("Romaji", "English", "Native")
|
||||
entryValues = arrayOf("romaji", "eng", "native")
|
||||
setDefaultValue(PREF_TITLE_STYLE_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_SUB_KEY
|
||||
title = "Prefer subs or dubs?"
|
||||
entries = arrayOf("Subs", "Dubs")
|
||||
entryValues = arrayOf("sub", "dub")
|
||||
setDefaultValue(PREF_SUB_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)
|
||||
}
|
||||
|
||||
private val SharedPreferences.subPref
|
||||
get() = getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.baseUrl
|
||||
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.siteUrl
|
||||
get() = getString(PREF_SITE_DOMAIN_KEY, PREF_SITE_DOMAIN_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.titleStyle
|
||||
get() = getString(PREF_TITLE_STYLE_KEY, PREF_TITLE_STYLE_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.quality
|
||||
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.prefServer
|
||||
get() = getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getHosters
|
||||
get() = getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getAltHosters
|
||||
get() = getStringSet(PREF_ALT_HOSTER_KEY, ALT_HOSTER_NAMES.toSet())!!
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanime
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PopularResult(
|
||||
val data: PopularResultData,
|
||||
) {
|
||||
@Serializable
|
||||
data class PopularResultData(
|
||||
val queryPopular: QueryPopularData,
|
||||
) {
|
||||
@Serializable
|
||||
data class QueryPopularData(
|
||||
val recommendations: List<Recommendation>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Recommendation(
|
||||
val anyCard: Card? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class Card(
|
||||
val _id: String,
|
||||
val name: String,
|
||||
val thumbnail: String,
|
||||
val englishName: String? = null,
|
||||
val nativeName: String? = null,
|
||||
val slugTime: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SearchResult(
|
||||
val data: SearchResultData,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultData(
|
||||
val shows: SearchResultShows,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultShows(
|
||||
val edges: List<SearchResultEdge>,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultEdge(
|
||||
val _id: String,
|
||||
val name: String,
|
||||
val thumbnail: String,
|
||||
val englishName: String? = null,
|
||||
val nativeName: String? = null,
|
||||
val slugTime: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DetailsResult(
|
||||
val data: DataShow,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataShow(
|
||||
val show: SeriesShows,
|
||||
) {
|
||||
@Serializable
|
||||
data class SeriesShows(
|
||||
val thumbnail: String,
|
||||
val genres: List<String>? = null,
|
||||
val studios: List<String>? = null,
|
||||
val season: AirSeason? = null,
|
||||
val status: String? = null,
|
||||
val score: Float? = null,
|
||||
val type: String? = null,
|
||||
val description: String? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class AirSeason(
|
||||
val quarter: String,
|
||||
val year: Int,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SeriesResult(
|
||||
val data: DataShow,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataShow(
|
||||
val show: SeriesShows,
|
||||
) {
|
||||
@Serializable
|
||||
data class SeriesShows(
|
||||
val _id: String,
|
||||
val availableEpisodesDetail: AvailableEps,
|
||||
) {
|
||||
@Serializable
|
||||
data class AvailableEps(
|
||||
val sub: List<String>? = null,
|
||||
val dub: List<String>? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EpisodeResult(
|
||||
val data: DataEpisode,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataEpisode(
|
||||
val episode: Episode,
|
||||
) {
|
||||
@Serializable
|
||||
data class Episode(
|
||||
val sourceUrls: List<SourceUrl>,
|
||||
) {
|
||||
@Serializable
|
||||
data class SourceUrl(
|
||||
val sourceUrl: String,
|
||||
val type: String,
|
||||
val sourceName: String,
|
||||
val priority: Float = 0F,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanime
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object AllAnimeFilters {
|
||||
|
||||
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.getFirst<R>() as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return this.filterIsInstance<R>().first()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
||||
options: Array<Pair<String, String>>,
|
||||
): String {
|
||||
return (this.getFirst<R>() as CheckBoxFilterList).state
|
||||
.mapNotNull { checkbox ->
|
||||
if (checkbox.state) {
|
||||
options.find { it.first == checkbox.name }!!.second
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.joinToString("\",\"").let {
|
||||
if (it.isBlank()) {
|
||||
"all"
|
||||
} else {
|
||||
"[\"$it\"]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OriginFilter : QueryPartFilter("Origin", AllAnimeFiltersData.ORIGIN)
|
||||
class SeasonFilter : QueryPartFilter("Season", AllAnimeFiltersData.SEASONS)
|
||||
class ReleaseYearFilter : QueryPartFilter("Released at", AllAnimeFiltersData.YEARS)
|
||||
class SortByFilter : QueryPartFilter("Sort By", AllAnimeFiltersData.SORT_BY)
|
||||
|
||||
class TypesFilter : CheckBoxFilterList(
|
||||
"Types",
|
||||
AllAnimeFiltersData.TYPES.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
class GenresFilter : CheckBoxFilterList(
|
||||
"Genres",
|
||||
AllAnimeFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
val FILTER_LIST get() = AnimeFilterList(
|
||||
OriginFilter(),
|
||||
SeasonFilter(),
|
||||
ReleaseYearFilter(),
|
||||
SortByFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
TypesFilter(),
|
||||
GenresFilter(),
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val origin: String = "",
|
||||
val season: String = "",
|
||||
val releaseYear: String = "",
|
||||
val sortBy: String = "",
|
||||
val types: String = "",
|
||||
val genres: String = "",
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<OriginFilter>(),
|
||||
filters.asQueryPart<SeasonFilter>(),
|
||||
filters.asQueryPart<ReleaseYearFilter>(),
|
||||
filters.asQueryPart<SortByFilter>(),
|
||||
filters.parseCheckbox<TypesFilter>(AllAnimeFiltersData.TYPES),
|
||||
filters.parseCheckbox<GenresFilter>(AllAnimeFiltersData.GENRES),
|
||||
)
|
||||
}
|
||||
|
||||
private object AllAnimeFiltersData {
|
||||
val ALL = Pair("All", "all")
|
||||
|
||||
val ORIGIN = arrayOf(
|
||||
Pair("All", "ALL"),
|
||||
Pair("Japan", "JP"),
|
||||
Pair("China", "CN"),
|
||||
Pair("Korea", "KR"),
|
||||
)
|
||||
|
||||
val SEASONS = arrayOf(
|
||||
ALL,
|
||||
Pair("Winter", "Winter"),
|
||||
Pair("Spring", "Spring"),
|
||||
Pair("Summer", "Summer"),
|
||||
Pair("Fall", "Fall"),
|
||||
)
|
||||
|
||||
val YEARS = arrayOf(
|
||||
ALL,
|
||||
Pair("2024", "2024"),
|
||||
Pair("2023", "2023"),
|
||||
Pair("2022", "2022"),
|
||||
Pair("2021", "2021"),
|
||||
Pair("2020", "2020"),
|
||||
Pair("2019", "2019"),
|
||||
Pair("2018", "2018"),
|
||||
Pair("2017", "2017"),
|
||||
Pair("2016", "2016"),
|
||||
Pair("2015", "2015"),
|
||||
Pair("2014", "2014"),
|
||||
Pair("2013", "2013"),
|
||||
Pair("2012", "2012"),
|
||||
Pair("2011", "2011"),
|
||||
Pair("2010", "2010"),
|
||||
Pair("2009", "2009"),
|
||||
Pair("2008", "2008"),
|
||||
Pair("2007", "2007"),
|
||||
Pair("2006", "2006"),
|
||||
Pair("2005", "2005"),
|
||||
Pair("2004", "2004"),
|
||||
Pair("2003", "2003"),
|
||||
Pair("2002", "2002"),
|
||||
Pair("2001", "2001"),
|
||||
Pair("2000", "2000"),
|
||||
Pair("1999", "1999"),
|
||||
Pair("1998", "1998"),
|
||||
Pair("1997", "1997"),
|
||||
Pair("1996", "1996"),
|
||||
Pair("1995", "1995"),
|
||||
Pair("1994", "1994"),
|
||||
Pair("1993", "1993"),
|
||||
Pair("1992", "1992"),
|
||||
Pair("1991", "1991"),
|
||||
Pair("1990", "1990"),
|
||||
Pair("1989", "1989"),
|
||||
Pair("1988", "1988"),
|
||||
Pair("1987", "1987"),
|
||||
Pair("1986", "1986"),
|
||||
Pair("1985", "1985"),
|
||||
Pair("1984", "1984"),
|
||||
Pair("1983", "1983"),
|
||||
Pair("1982", "1982"),
|
||||
Pair("1981", "1981"),
|
||||
Pair("1980", "1980"),
|
||||
Pair("1979", "1979"),
|
||||
Pair("1978", "1978"),
|
||||
Pair("1977", "1977"),
|
||||
Pair("1976", "1976"),
|
||||
Pair("1975", "1975"),
|
||||
)
|
||||
|
||||
val SORT_BY = arrayOf(
|
||||
Pair("Update", "update"),
|
||||
Pair("Name Asc", "Name_ASC"),
|
||||
Pair("Name Desc", "Name_DESC"),
|
||||
Pair("Ratings", "Top"),
|
||||
)
|
||||
|
||||
val TYPES = arrayOf(
|
||||
Pair("Movie", "Movie"),
|
||||
Pair("ONA", "ONA"),
|
||||
Pair("OVA", "OVA"),
|
||||
Pair("Special", "Special"),
|
||||
Pair("TV", "TV"),
|
||||
Pair("Unknown", "Unknown"),
|
||||
)
|
||||
|
||||
val GENRES = arrayOf(
|
||||
Pair("Action", "Action"),
|
||||
Pair("Adventure", "Adventure"),
|
||||
Pair("Cars", "Cars"),
|
||||
Pair("Comedy", "Comedy"),
|
||||
Pair("Dementia", "Dementia"),
|
||||
Pair("Demons", "Demons"),
|
||||
Pair("Drama", "Drama"),
|
||||
Pair("Ecchi", "Ecchi"),
|
||||
Pair("Fantasy", "Fantasy"),
|
||||
Pair("Game", "Game"),
|
||||
Pair("Harem", "Harem"),
|
||||
Pair("Historical", "Historical"),
|
||||
Pair("Horror", "Horror"),
|
||||
Pair("Isekai", "Isekai"),
|
||||
Pair("Josei", "Josei"),
|
||||
Pair("Kids", "Kids"),
|
||||
Pair("Magic", "Magic"),
|
||||
Pair("Martial Arts", "Martial Arts"),
|
||||
Pair("Mecha", "Mecha"),
|
||||
Pair("Military", "Military"),
|
||||
Pair("Music", "Music"),
|
||||
Pair("Mystery", "Mystery"),
|
||||
Pair("Parody", "Parody"),
|
||||
Pair("Police", "Police"),
|
||||
Pair("Psychological", "Psychological"),
|
||||
Pair("Romance", "Romance"),
|
||||
Pair("Samurai", "Samurai"),
|
||||
Pair("School", "School"),
|
||||
Pair("Sci-Fi", "Sci-Fi"),
|
||||
Pair("Seinen", "Seinen"),
|
||||
Pair("Shoujo", "Shoujo"),
|
||||
Pair("Shoujo Ai", "Shoujo Ai"),
|
||||
Pair("Shounen", "Shounen"),
|
||||
Pair("Shounen Ai", "Shounen Ai"),
|
||||
Pair("Slice of Life", "Slice of Life"),
|
||||
Pair("Space", "Space"),
|
||||
Pair("Sports", "Sports"),
|
||||
Pair("Super Power", "Super Power"),
|
||||
Pair("Supernatural", "Supernatural"),
|
||||
Pair("Thriller", "Thriller"),
|
||||
Pair("Unknown", "Unknown"),
|
||||
Pair("Vampire", "Vampire"),
|
||||
Pair("Yaoi", "Yaoi"),
|
||||
Pair("Yuri", "Yuri"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanime
|
||||
|
||||
fun buildQuery(queryAction: () -> String): String {
|
||||
return queryAction()
|
||||
.trimIndent()
|
||||
.replace("%", "$")
|
||||
}
|
||||
|
||||
val POPULAR_QUERY: String = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%type: VaildPopularTypeEnumType!
|
||||
%size: Int!
|
||||
%page: Int
|
||||
%dateRange: Int
|
||||
) {
|
||||
queryPopular(
|
||||
type: %type
|
||||
size: %size
|
||||
dateRange: %dateRange
|
||||
page: %page
|
||||
) {
|
||||
total
|
||||
recommendations {
|
||||
anyCard {
|
||||
_id
|
||||
name
|
||||
thumbnail
|
||||
englishName
|
||||
nativeName
|
||||
slugTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val SEARCH_QUERY: String = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%search: SearchInput
|
||||
%limit: Int
|
||||
%page: Int
|
||||
%translationType: VaildTranslationTypeEnumType
|
||||
%countryOrigin: VaildCountryOriginEnumType
|
||||
) {
|
||||
shows(
|
||||
search: %search
|
||||
limit: %limit
|
||||
page: %page
|
||||
translationType: %translationType
|
||||
countryOrigin: %countryOrigin
|
||||
) {
|
||||
pageInfo {
|
||||
total
|
||||
}
|
||||
edges {
|
||||
_id
|
||||
name
|
||||
thumbnail
|
||||
englishName
|
||||
nativeName
|
||||
slugTime
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val DETAILS_QUERY = buildQuery {
|
||||
"""
|
||||
query (%_id: String!) {
|
||||
show(
|
||||
_id: %_id
|
||||
) {
|
||||
thumbnail
|
||||
description
|
||||
type
|
||||
season
|
||||
score
|
||||
genres
|
||||
status
|
||||
studios
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val EPISODES_QUERY = buildQuery {
|
||||
"""
|
||||
query (%_id: String!) {
|
||||
show(
|
||||
_id: %_id
|
||||
) {
|
||||
_id
|
||||
availableEpisodesDetail
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
val STREAMS_QUERY = buildQuery {
|
||||
"""
|
||||
query(
|
||||
%showId: String!,
|
||||
%translationType: VaildTranslationTypeEnumType!,
|
||||
%episodeString: String!
|
||||
) {
|
||||
episode(
|
||||
showId: %showId
|
||||
translationType: %translationType
|
||||
episodeString: %episodeString
|
||||
) {
|
||||
sourceUrls
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanime.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
class AllAnimeExtractor(private val client: OkHttpClient, private val headers: Headers, private val siteUrl: String) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private fun bytesIntoHumanReadable(bytes: Long): String {
|
||||
val kilobyte: Long = 1000
|
||||
val megabyte = kilobyte * 1000
|
||||
val gigabyte = megabyte * 1000
|
||||
val terabyte = gigabyte * 1000
|
||||
return if (bytes in 0 until kilobyte) {
|
||||
"$bytes b/s"
|
||||
} else if (bytes in kilobyte until megabyte) {
|
||||
(bytes / kilobyte).toString() + " kb/s"
|
||||
} else if (bytes in megabyte until gigabyte) {
|
||||
(bytes / megabyte).toString() + " mb/s"
|
||||
} else if (bytes in gigabyte until terabyte) {
|
||||
(bytes / gigabyte).toString() + " gb/s"
|
||||
} else if (bytes >= terabyte) {
|
||||
(bytes / terabyte).toString() + " tb/s"
|
||||
} else {
|
||||
"$bytes bits/s"
|
||||
}
|
||||
}
|
||||
|
||||
fun videoFromUrl(url: String, name: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
|
||||
val endPoint = json.decodeFromString<VersionResponse>(
|
||||
client.newCall(GET("$siteUrl/getVersion")).execute().body.string(),
|
||||
).episodeIframeHead
|
||||
|
||||
val resp = client.newCall(
|
||||
GET(endPoint + url.replace("/clock?", "/clock.json?")),
|
||||
).execute()
|
||||
|
||||
if (resp.code != 200) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val body = resp.body.string()
|
||||
val linkJson = json.decodeFromString<VideoLink>(body)
|
||||
|
||||
for (link in linkJson.links) {
|
||||
val subtitles = mutableListOf<Track>()
|
||||
if (!link.subtitles.isNullOrEmpty()) {
|
||||
subtitles.addAll(
|
||||
link.subtitles.map { sub ->
|
||||
val label = if (sub.label != null) {
|
||||
" - ${sub.label}"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
Track(sub.src, Locale(sub.lang).displayLanguage + label)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (link.mp4 == true) {
|
||||
videoList.add(
|
||||
Video(
|
||||
link.link,
|
||||
"Original ($name - ${link.resolutionStr})",
|
||||
link.link,
|
||||
subtitleTracks = subtitles,
|
||||
),
|
||||
)
|
||||
} else if (link.hls == true) {
|
||||
val newClient = OkHttpClient()
|
||||
|
||||
val masterHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Host", link.link.toHttpUrl().host)
|
||||
.add("Origin", endPoint)
|
||||
.add("Referer", "$endPoint/")
|
||||
.build()
|
||||
|
||||
val resp = runCatching {
|
||||
newClient.newCall(
|
||||
GET(link.link, headers = masterHeaders),
|
||||
).execute()
|
||||
}.getOrNull()
|
||||
|
||||
if (resp != null && resp.code == 200) {
|
||||
val masterPlaylist = resp.body.string()
|
||||
|
||||
val audioList = mutableListOf<Track>()
|
||||
if (masterPlaylist.contains("#EXT-X-MEDIA:TYPE=AUDIO")) {
|
||||
val audioInfo = masterPlaylist.substringAfter("#EXT-X-MEDIA:TYPE=AUDIO")
|
||||
.substringBefore("\n")
|
||||
val language = audioInfo.substringAfter("NAME=\"").substringBefore("\"")
|
||||
val url = audioInfo.substringAfter("URI=\"").substringBefore("\"")
|
||||
audioList.add(
|
||||
Track(url, language),
|
||||
)
|
||||
}
|
||||
|
||||
if (!masterPlaylist.contains("#EXT-X-STREAM-INF:")) {
|
||||
return if (audioList.isEmpty()) {
|
||||
listOf(Video(link.link, "$name - ${link.resolutionStr}", link.link, subtitleTracks = subtitles, headers = masterHeaders))
|
||||
} else {
|
||||
listOf(Video(link.link, "$name - ${link.resolutionStr}", link.link, subtitleTracks = subtitles, audioTracks = audioList, headers = masterHeaders))
|
||||
}
|
||||
}
|
||||
|
||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
|
||||
.forEach {
|
||||
val bandwidth = if (it.contains("AVERAGE-BANDWIDTH")) {
|
||||
" " + bytesIntoHumanReadable(it.substringAfter("AVERAGE-BANDWIDTH=").substringBefore(",").toLong())
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p$bandwidth ($name - ${link.resolutionStr})"
|
||||
var videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
|
||||
if (!videoUrl.startsWith("http")) {
|
||||
videoUrl = resp.request.url.toString().substringBeforeLast("/") + "/$videoUrl"
|
||||
}
|
||||
|
||||
val plHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Host", videoUrl.toHttpUrl().host)
|
||||
.add("Origin", endPoint)
|
||||
.add("Referer", "$endPoint/")
|
||||
.build()
|
||||
|
||||
if (audioList.isEmpty()) {
|
||||
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitles, headers = plHeaders))
|
||||
} else {
|
||||
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitles, audioTracks = audioList, headers = plHeaders))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (link.crIframe == true) {
|
||||
link.portData!!.streams.forEach {
|
||||
if (it.format == "adaptive_dash") {
|
||||
videoList.add(
|
||||
Video(
|
||||
it.url,
|
||||
"Original (AC - Dash${if (it.hardsub_lang.isEmpty()) "" else " - Hardsub: ${it.hardsub_lang}"})",
|
||||
it.url,
|
||||
subtitleTracks = subtitles,
|
||||
),
|
||||
)
|
||||
} else if (it.format == "adaptive_hls") {
|
||||
val resp = runCatching {
|
||||
client.newCall(
|
||||
GET(it.url, headers = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0")),
|
||||
).execute()
|
||||
}.getOrNull()
|
||||
|
||||
if (resp != null && resp.code == 200) {
|
||||
val masterPlaylist = resp.body.string()
|
||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:")
|
||||
.forEach { t ->
|
||||
val quality = t.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p (AC - HLS${if (it.hardsub_lang.isEmpty()) "" else " - Hardsub: ${it.hardsub_lang}"})"
|
||||
var videoUrl = t.substringAfter("\n").substringBefore("\n")
|
||||
|
||||
videoList.add(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitles))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (link.dash == true) {
|
||||
val audioList = link.rawUrls?.audios?.map {
|
||||
Track(it.url, bytesIntoHumanReadable(it.bandwidth))
|
||||
}
|
||||
val videos = link.rawUrls?.vids?.map {
|
||||
if (audioList == null) {
|
||||
Video(it.url, "$name - ${it.height} ${bytesIntoHumanReadable(it.bandwidth)}", it.url, subtitleTracks = subtitles)
|
||||
} else {
|
||||
Video(it.url, "$name - ${it.height} ${bytesIntoHumanReadable(it.bandwidth)}", it.url, audioTracks = audioList, subtitleTracks = subtitles)
|
||||
}
|
||||
}
|
||||
if (videos != null) {
|
||||
videoList.addAll(videos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class VersionResponse(
|
||||
val episodeIframeHead: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VideoLink(
|
||||
val links: List<Link>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Link(
|
||||
val link: String,
|
||||
val hls: Boolean? = null,
|
||||
val mp4: Boolean? = null,
|
||||
val dash: Boolean? = null,
|
||||
val crIframe: Boolean? = null,
|
||||
val resolutionStr: String,
|
||||
val subtitles: List<Subtitles>? = null,
|
||||
val rawUrls: RawUrl? = null,
|
||||
val portData: Stream? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class Subtitles(
|
||||
val lang: String,
|
||||
val src: String,
|
||||
val label: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Stream(
|
||||
val streams: List<StreamObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class StreamObject(
|
||||
val format: String,
|
||||
val url: String,
|
||||
val audio_lang: String,
|
||||
val hardsub_lang: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RawUrl(
|
||||
val vids: List<DashStreamObject>? = null,
|
||||
val audios: List<DashStreamObject>? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class DashStreamObject(
|
||||
val bandwidth: Long,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
src/en/allanimechi/build.gradle
Normal file
|
@ -0,0 +1,17 @@
|
|||
ext {
|
||||
extName = 'AllAnimeChi'
|
||||
extClass = '.AllAnimeChi'
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:mp4upload-extractor'))
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:gogostream-extractor'))
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:streamlare-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation(project(':lib:playlist-utils'))
|
||||
}
|
BIN
src/en/allanimechi/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/en/allanimechi/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/en/allanimechi/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/en/allanimechi/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/en/allanimechi/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
src/en/allanimechi/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 165 KiB |
|
@ -0,0 +1,680 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanimechi
|
||||
|
||||
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.allanimechi.extractors.AllAnimeExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.en.allanimechi.extractors.InternalExtractor
|
||||
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.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.gogostreamextractor.GogoStreamExtractor
|
||||
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamlareextractor.StreamlareExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
class AllAnimeChi : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "AllAnimeChi"
|
||||
|
||||
override val baseUrl = "aHR0cHM6Ly9hY2FwaS5hbGxhbmltZS5kYXk=".decodeBase64()
|
||||
private val siteUrl = "aHR0cHM6Ly9hbGxhbmltZS50bw==".decodeBase64()
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val apiHeaders = Headers.Builder().apply {
|
||||
add("app-version", "android_c-253")
|
||||
add("content-type", "application/json; charset=UTF-8")
|
||||
add("from-app", "YW5pbWVjaGlja2Vu".decodeBase64())
|
||||
add("host", baseUrl.toHttpUrl().host)
|
||||
add("platformstr", "android_c")
|
||||
add("Referer", "$siteUrl/")
|
||||
add("user-agent", "Dart/2.19 (dart:io)")
|
||||
}.build()
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
val variables = buildJsonObject {
|
||||
put("type", "anime")
|
||||
put("size", PAGE_SIZE)
|
||||
put("dateRange", 7)
|
||||
put("page", page)
|
||||
put("allowAdult", false)
|
||||
put("allowUnknown", false)
|
||||
put("denyEcchi", false)
|
||||
}.encode()
|
||||
|
||||
val extensions = buildJsonObject {
|
||||
putJsonObject("persistedQuery") {
|
||||
put("version", 1)
|
||||
put("sha256Hash", POPULAR_HASH)
|
||||
}
|
||||
}.encode()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addQueryParameter("variables", variables)
|
||||
addQueryParameter("extensions", extensions)
|
||||
}.build().toString().replace("%3A", ":")
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<PopularResult>()
|
||||
val animeList = parsed.data.queryPopular.recommendations.mapNotNull {
|
||||
it.anyCard?.toSAnime(preferences.titleStyle)
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val variables = buildJsonObject {
|
||||
putJsonObject("search") {
|
||||
put("allowAdult", false)
|
||||
put("allowUnknown", false)
|
||||
put("denyEcchi", false)
|
||||
}
|
||||
put("translationType", preferences.subPref)
|
||||
put("limit", PAGE_SIZE)
|
||||
put("page", page)
|
||||
put("countryOrigin", "ALL")
|
||||
}.encode()
|
||||
|
||||
val extensions = buildJsonObject {
|
||||
putJsonObject("persistedQuery") {
|
||||
put("version", 1)
|
||||
put("sha256Hash", LATEST_HASH)
|
||||
}
|
||||
}.encode()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addQueryParameter("variables", variables)
|
||||
addQueryParameter("extensions", extensions)
|
||||
}.build().toString().replace("%3A", ":")
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<SearchResult>()
|
||||
val animeList = parsed.data.shows.edges.map {
|
||||
it.toSAnime(preferences.titleStyle)
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val params = AllAnimeChiFilters.getSearchParameters(filters)
|
||||
|
||||
val variables = buildJsonObject {
|
||||
putJsonObject("search") {
|
||||
if (query.isBlank()) {
|
||||
if (params.genres.isNotEmpty()) {
|
||||
putJsonArray("genres") {
|
||||
params.genres.forEach {
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (params.type != "all") {
|
||||
putJsonArray("types") {
|
||||
add(params.type)
|
||||
}
|
||||
}
|
||||
if (params.season != "all") {
|
||||
put("season", params.season)
|
||||
}
|
||||
if (params.releaseYear != "all") {
|
||||
put("year", params.releaseYear.toInt())
|
||||
}
|
||||
if (params.episodeCount != "all") {
|
||||
val (start, end) = params.episodeCount.split("-")
|
||||
if (start.isNotBlank()) put("epRangeStart", start.toInt())
|
||||
if (end.isNotBlank()) put("epRangeEnd", end.toInt())
|
||||
}
|
||||
} else {
|
||||
put("query", query)
|
||||
}
|
||||
put("sortBy", "Latest_Update")
|
||||
put("allowAdult", false)
|
||||
put("allowUnknown", false)
|
||||
put("denyEcchi", false)
|
||||
}
|
||||
put("translationType", preferences.subPref)
|
||||
put("limit", PAGE_SIZE)
|
||||
put("page", page)
|
||||
put("countryOrigin", params.origin)
|
||||
}.encode()
|
||||
|
||||
val extensions = buildJsonObject {
|
||||
putJsonObject("persistedQuery") {
|
||||
put("version", 1)
|
||||
put("sha256Hash", LATEST_HASH)
|
||||
}
|
||||
}.encode()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addQueryParameter("variables", variables)
|
||||
addQueryParameter("extensions", extensions)
|
||||
}.build().toString().replace("%3A", ":")
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = latestUpdatesParse(response)
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AllAnimeChiFilters.FILTER_LIST
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
val variables = buildJsonObject {
|
||||
put("_id", anime.url)
|
||||
}.encode()
|
||||
|
||||
val extensions = buildJsonObject {
|
||||
putJsonObject("persistedQuery") {
|
||||
put("version", 1)
|
||||
put("sha256Hash", DETAILS_HASH)
|
||||
}
|
||||
}.encode()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addQueryParameter("variables", variables)
|
||||
addQueryParameter("extensions", extensions)
|
||||
}.build().toString()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun getAnimeUrl(anime: SAnime): String {
|
||||
return "data:text/plain,This%20extension%20does%20not%20have%20a%20website."
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val show = response.parseAs<DetailsResult>().data.show
|
||||
|
||||
return SAnime.create().apply {
|
||||
genre = show.genres?.joinToString(separator = ", ") ?: ""
|
||||
status = parseStatus(show.status)
|
||||
author = show.studios?.firstOrNull()
|
||||
description = buildString {
|
||||
append(
|
||||
Jsoup.parseBodyFragment(
|
||||
show.description?.replace("<br>", "br2n") ?: "",
|
||||
).text().replace("br2n", "\n"),
|
||||
)
|
||||
append("\n\n")
|
||||
append("Type: ${show.type ?: "Unknown"}")
|
||||
append("\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}")
|
||||
append("\nScore: ${show.score ?: "-"}★")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime) = animeDetailsRequest(anime)
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val media = response.parseAs<SeriesResult>()
|
||||
val subPref = preferences.subPref
|
||||
|
||||
val episodesDetail = if (subPref == "sub") {
|
||||
media.data.show.availableEpisodesDetail.sub!!
|
||||
} else {
|
||||
media.data.show.availableEpisodesDetail.dub!!
|
||||
}
|
||||
|
||||
return episodesDetail.map { ep ->
|
||||
val numName = ep.toIntOrNull() ?: (ep.toFloatOrNull() ?: "1")
|
||||
|
||||
SEpisode.create().apply {
|
||||
episode_number = ep.toFloatOrNull() ?: 0F
|
||||
name = "Episode $numName ($subPref)"
|
||||
url = buildJsonObject {
|
||||
put("showId", media.data.show._id)
|
||||
put("translationType", subPref)
|
||||
put("episodeString", ep)
|
||||
}.encode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
private val internalExtractor by lazy { InternalExtractor(client, apiHeaders, headers) }
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val variables = episode.url
|
||||
|
||||
val extensions = buildJsonObject {
|
||||
putJsonObject("persistedQuery") {
|
||||
put("version", 1)
|
||||
put("sha256Hash", STREAMS_HASH)
|
||||
}
|
||||
}.encode()
|
||||
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("api")
|
||||
addQueryParameter("variables", variables)
|
||||
addQueryParameter("extensions", extensions)
|
||||
}.build().toString()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val videoJson = response.parseAs<EpisodeResult>()
|
||||
|
||||
val hosterBlackList = preferences.getHosterBlacklist
|
||||
val altHosterBlackList = preferences.getAltHosterBlacklist
|
||||
val useHosterNames = preferences.useHosterName
|
||||
|
||||
val serverList = videoJson.data.episode.sourceUrls.mapNotNull { video ->
|
||||
when {
|
||||
// "Internal" sources
|
||||
video.sourceUrl.startsWith("/apivtwo/") && !hosterBlackList.any {
|
||||
Regex("""\b${it.lowercase()}\b""").find(video.sourceName.lowercase()) != null
|
||||
} -> {
|
||||
Server(video.sourceUrl, video.sourceName, video.priority, "internal")
|
||||
}
|
||||
|
||||
// Player, direct video links.
|
||||
video.type == "player" && !altHosterBlackList.contains(video.sourceName, true) -> {
|
||||
Server(video.sourceUrl, video.sourceName, video.priority, "player")
|
||||
}
|
||||
|
||||
// External video players
|
||||
!altHosterBlackList.contains(video.sourceName, true) && !video.sourceUrl.startsWith("/apivtwo/") -> {
|
||||
Server(video.sourceUrl, video.sourceName, video.priority, "external")
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
return prioritySort(
|
||||
serverList.parallelCatchingFlatMapBlocking { getVideoFromServer(it, useHosterNames) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getVideoFromServer(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||
return when (server.type) {
|
||||
"player" -> getFromPlayer(server, useHosterName)
|
||||
"internal" -> internalExtractor.videosFromServer(server, useHosterName, removeRaw = preferences.removeRaw)
|
||||
"external" -> getFromExternal(server, useHosterName)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFromPlayer(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||
val name = if (useHosterName) {
|
||||
getHostName(server.sourceUrl, server.sourceName)
|
||||
} else {
|
||||
server.sourceName
|
||||
}
|
||||
|
||||
val videoHeaders = headers.newBuilder().apply {
|
||||
add("origin", siteUrl)
|
||||
add("referer", "$siteUrl/")
|
||||
}.build()
|
||||
|
||||
val video = Video(server.sourceUrl, name, server.sourceUrl, headers = videoHeaders)
|
||||
return listOf(
|
||||
Pair(video, server.priority),
|
||||
)
|
||||
}
|
||||
|
||||
private val okruExtractor by lazy { OkruExtractor(client) }
|
||||
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
|
||||
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
|
||||
private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
|
||||
private val gogoExtractor by lazy { GogoStreamExtractor(client) }
|
||||
private val allanimeExtractor by lazy { AllAnimeExtractor(client, headers) }
|
||||
private val streamwishExtractor by lazy { StreamWishExtractor(client, headers) }
|
||||
|
||||
private fun getFromExternal(server: Server, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||
val url = server.sourceUrl.replace(Regex("""^//"""), "https://")
|
||||
val prefix = if (useHosterName) {
|
||||
"${getHostName(url, server.sourceName)} - "
|
||||
} else {
|
||||
"${server.sourceName} - "
|
||||
}
|
||||
|
||||
val videoList = when {
|
||||
url.startsWith("https://ok") -> okruExtractor.videosFromUrl(url, prefix = prefix)
|
||||
url.startsWith("https://filemoon") -> filemoonExtractor.videosFromUrl(url, prefix = prefix)
|
||||
url.startsWith("https://streamlare") -> streamlareExtractor.videosFromUrl(url, prefix = prefix)
|
||||
url.startsWith("https://mp4upload") -> mp4uploadExtractor.videosFromUrl(url, headers, prefix = prefix)
|
||||
server.sourceName.equals("Vid-mp4", true) -> gogoExtractor.videosFromUrl(url)
|
||||
url.startsWith("https://allanime") -> allanimeExtractor.videosFromUrl(url, prefix = prefix)
|
||||
url.startsWith("https://streamwish") -> streamwishExtractor.videosFromUrl(url, videoNameGen = { q -> prefix + q })
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
return videoList.map { Pair(it, server.priority) }
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun getHostName(host: String, fallback: String): String {
|
||||
return host.toHttpUrlOrNull()?.host?.split(".")?.let {
|
||||
it.getOrNull(it.size - 2)?.replaceFirstChar { c ->
|
||||
if (c.isLowerCase()) c.titlecase(Locale.ROOT) else c.toString()
|
||||
}
|
||||
} ?: fallback
|
||||
}
|
||||
|
||||
private fun String.decodeBase64(): String {
|
||||
return String(Base64.decode(this, Base64.DEFAULT))
|
||||
}
|
||||
|
||||
private fun JsonObject.encode(): String {
|
||||
return json.encodeToString(this)
|
||||
}
|
||||
|
||||
data class Server(
|
||||
val sourceUrl: String,
|
||||
val sourceName: String,
|
||||
val priority: Float,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
fun Set<String>.contains(element: String, ignoreCase: Boolean): Boolean {
|
||||
return this.any { it.equals(element, ignoreCase = ignoreCase) }
|
||||
}
|
||||
|
||||
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
|
||||
val prefServer = preferences.prefServer
|
||||
val quality = preferences.quality
|
||||
val subPref = preferences.subPref
|
||||
|
||||
return pList.sortedWith(
|
||||
compareBy(
|
||||
{ if (prefServer == "site_default") it.second else it.first.quality.contains(prefServer, true) },
|
||||
{ it.first.quality.contains(quality, true) },
|
||||
{ it.first.quality.contains(subPref, true) },
|
||||
),
|
||||
).reversed().map { t -> t.first }
|
||||
}
|
||||
|
||||
private fun parseStatus(string: String?): Int {
|
||||
return when (string) {
|
||||
"Releasing" -> SAnime.ONGOING
|
||||
"Finished" -> SAnime.COMPLETED
|
||||
"Not Yet Released" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGE_SIZE = 30 // number of items to retrieve when calling API
|
||||
|
||||
private const val POPULAR_HASH = "31a117653812a2547fd981632e8c99fa8bf8a75c4ef1a77a1567ef1741a7ab9c"
|
||||
private const val LATEST_HASH = "e42a4466d984b2c0a2cecae5dd13aa68867f634b16ee0f17b380047d14482406"
|
||||
private const val DETAILS_HASH = "bb263f91e5bdd048c1c978f324613aeccdfe2cbc694a419466a31edb58c0cc0b"
|
||||
private const val STREAMS_HASH = "5e7e17cdd0166af5a2d8f43133d9ce3ce9253d1fdb5160a0cfd515564f98d061"
|
||||
|
||||
private val INTERNAL_HOSTER_NAMES = arrayOf(
|
||||
"Default 0 (Cr/vrv)",
|
||||
"Default",
|
||||
"Luf-mp4 (Gogo)",
|
||||
"S-mp4",
|
||||
"Sak",
|
||||
"Uv-mp4",
|
||||
)
|
||||
|
||||
private val EXTERNAL_HOSTER_NAMES = arrayOf(
|
||||
"AK",
|
||||
"Fm-Hls",
|
||||
"Mp4",
|
||||
"Ok",
|
||||
"Sw",
|
||||
"Vid-mp4 (Vidstreaming)",
|
||||
"Yt-mp4",
|
||||
)
|
||||
|
||||
private const val PREF_HOSTER_BLACKLIST_KEY = "pref_hoster_blacklist"
|
||||
private val PREF_HOSTER_BLACKLIST_ENTRY_VALUES = INTERNAL_HOSTER_NAMES.map {
|
||||
it.lowercase().substringBefore(" (")
|
||||
}.toTypedArray()
|
||||
|
||||
private const val PREF_ALT_HOSTER_BLACKLIST_KEY = "pref_alt_hoster_blacklist"
|
||||
private val PREF_ALT_HOSTER_BLACKLIST_ENTRY_VALUES = EXTERNAL_HOSTER_NAMES.map {
|
||||
it.lowercase().substringBefore(" (")
|
||||
}.toTypedArray()
|
||||
|
||||
private const val PREF_REMOVE_RAW_KEY = "pref_remove_raw"
|
||||
private const val PREF_REMOVE_RAW_DEFAULT = true
|
||||
|
||||
// Names as they appear in video list
|
||||
private val HOSTER_NAMES = arrayOf(
|
||||
"AK", "Crunchyroll", "Default", "Fm-Hls", "Luf-mp4",
|
||||
"Mp4", "Ok", "S-mp4", "Sak", "Sw", "Uv-mp4",
|
||||
"Vidstreaming", "Vrv", "Yt-mp4",
|
||||
)
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Site Default") +
|
||||
HOSTER_NAMES
|
||||
private val PREF_SERVER_ENTRY_VALUES = arrayOf("site_default") +
|
||||
HOSTER_NAMES.map { it.lowercase() }
|
||||
private const val PREF_SERVER_DEFAULT = "site_default"
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf(
|
||||
"2160p",
|
||||
"1440p",
|
||||
"1080p",
|
||||
"720p",
|
||||
"480p",
|
||||
"360p",
|
||||
"240p",
|
||||
"80p",
|
||||
)
|
||||
private val PREF_QUALITY_ENTRY_VALUES = PREF_QUALITY_ENTRIES.map {
|
||||
it.substringBefore("p")
|
||||
}.toTypedArray()
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
|
||||
private const val PREF_TITLE_STYLE_KEY = "preferred_title_style"
|
||||
private const val PREF_TITLE_STYLE_DEFAULT = "romaji"
|
||||
|
||||
private const val PREF_SUB_KEY = "preferred_sub"
|
||||
private const val PREF_SUB_DEFAULT = "sub"
|
||||
|
||||
private const val PREF_USE_HOSTER_NAMES_KEY = "use_host_prefix"
|
||||
private const val PREF_USE_HOSTER_NAMES_DEFAULT = false
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred Video Server"
|
||||
entries = PREF_SERVER_ENTRIES
|
||||
entryValues = PREF_SERVER_ENTRY_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_BLACKLIST_KEY
|
||||
title = "Internal hoster blacklist"
|
||||
entries = INTERNAL_HOSTER_NAMES
|
||||
entryValues = PREF_HOSTER_BLACKLIST_ENTRY_VALUES
|
||||
setDefaultValue(emptySet<String>())
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_ALT_HOSTER_BLACKLIST_KEY
|
||||
title = "External hoster blacklist"
|
||||
entries = EXTERNAL_HOSTER_NAMES
|
||||
entryValues = PREF_ALT_HOSTER_BLACKLIST_ENTRY_VALUES
|
||||
setDefaultValue(emptySet<String>())
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
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_TITLE_STYLE_KEY
|
||||
title = "Preferred Title Style"
|
||||
entries = arrayOf("Romaji", "English", "Native")
|
||||
entryValues = arrayOf("romaji", "eng", "native")
|
||||
setDefaultValue(PREF_TITLE_STYLE_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_SUB_KEY
|
||||
title = "Prefer subs or dubs?"
|
||||
entries = arrayOf("Subs", "Dubs")
|
||||
entryValues = arrayOf("sub", "dub")
|
||||
setDefaultValue(PREF_SUB_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_REMOVE_RAW_KEY
|
||||
title = "Attempt to filter out raw"
|
||||
setDefaultValue(PREF_REMOVE_RAW_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_USE_HOSTER_NAMES_KEY
|
||||
title = "Use names of video hoster"
|
||||
setDefaultValue(PREF_USE_HOSTER_NAMES_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private val SharedPreferences.subPref
|
||||
get() = getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.titleStyle
|
||||
get() = getString(PREF_TITLE_STYLE_KEY, PREF_TITLE_STYLE_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.quality
|
||||
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.prefServer
|
||||
get() = getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.getHosterBlacklist
|
||||
get() = getStringSet(PREF_HOSTER_BLACKLIST_KEY, emptySet())!!
|
||||
|
||||
private val SharedPreferences.getAltHosterBlacklist
|
||||
get() = getStringSet(PREF_ALT_HOSTER_BLACKLIST_KEY, emptySet())!!
|
||||
|
||||
private val SharedPreferences.removeRaw
|
||||
get() = getBoolean(PREF_REMOVE_RAW_KEY, PREF_REMOVE_RAW_DEFAULT)
|
||||
|
||||
private val SharedPreferences.useHosterName
|
||||
get() = getBoolean(PREF_USE_HOSTER_NAMES_KEY, PREF_USE_HOSTER_NAMES_DEFAULT)
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanimechi
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PopularResult(
|
||||
val data: PopularResultData,
|
||||
) {
|
||||
@Serializable
|
||||
data class PopularResultData(
|
||||
val queryPopular: QueryPopularData,
|
||||
) {
|
||||
@Serializable
|
||||
data class QueryPopularData(
|
||||
val recommendations: List<Recommendation>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Recommendation(
|
||||
val anyCard: Card? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class Card(
|
||||
val _id: String,
|
||||
val name: String,
|
||||
val thumbnail: String,
|
||||
val englishName: String? = null,
|
||||
val nativeName: String? = null,
|
||||
) {
|
||||
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
|
||||
title = when (titlePref) {
|
||||
"romaji" -> name
|
||||
"eng" -> englishName ?: name
|
||||
else -> nativeName ?: name
|
||||
}
|
||||
thumbnail_url = thumbnail
|
||||
url = _id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SearchResult(
|
||||
val data: SearchResultData,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultData(
|
||||
val shows: SearchResultShows,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultShows(
|
||||
val edges: List<SearchResultEdge>,
|
||||
) {
|
||||
@Serializable
|
||||
data class SearchResultEdge(
|
||||
val _id: String,
|
||||
val name: String,
|
||||
val thumbnail: String,
|
||||
val englishName: String? = null,
|
||||
val nativeName: String? = null,
|
||||
) {
|
||||
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
|
||||
title = when (titlePref) {
|
||||
"romaji" -> name
|
||||
"eng" -> englishName ?: name
|
||||
else -> nativeName ?: name
|
||||
}
|
||||
thumbnail_url = thumbnail
|
||||
url = _id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DetailsResult(
|
||||
val data: DataShow,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataShow(
|
||||
val show: SeriesShows,
|
||||
) {
|
||||
@Serializable
|
||||
data class SeriesShows(
|
||||
val thumbnail: String,
|
||||
val genres: List<String>? = null,
|
||||
val studios: List<String>? = null,
|
||||
val season: AirSeason? = null,
|
||||
val status: String? = null,
|
||||
val score: Float? = null,
|
||||
val type: String? = null,
|
||||
val description: String? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class AirSeason(
|
||||
val quarter: String,
|
||||
val year: Int,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SeriesResult(
|
||||
val data: DataShow,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataShow(
|
||||
val show: SeriesShows,
|
||||
) {
|
||||
@Serializable
|
||||
data class SeriesShows(
|
||||
val _id: String,
|
||||
val availableEpisodesDetail: AvailableEps,
|
||||
) {
|
||||
@Serializable
|
||||
data class AvailableEps(
|
||||
val sub: List<String>? = null,
|
||||
val dub: List<String>? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EpisodeResult(
|
||||
val data: DataEpisode,
|
||||
) {
|
||||
@Serializable
|
||||
data class DataEpisode(
|
||||
val episode: Episode,
|
||||
) {
|
||||
@Serializable
|
||||
data class Episode(
|
||||
val sourceUrls: List<SourceUrl>,
|
||||
) {
|
||||
@Serializable
|
||||
data class SourceUrl(
|
||||
val sourceUrl: String,
|
||||
val type: String,
|
||||
val sourceName: String,
|
||||
val priority: Float = 0F,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanimechi
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
|
||||
object AllAnimeChiFilters {
|
||||
|
||||
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.getFirst<R>() as QueryPartFilter).toQueryPart()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.getFirst(): R {
|
||||
return this.filterIsInstance<R>().first()
|
||||
}
|
||||
|
||||
private inline fun <reified R> AnimeFilterList.parseCheckbox(
|
||||
options: Array<Pair<String, String>>,
|
||||
): List<String> {
|
||||
return (this.getFirst<R>() as CheckBoxFilterList).state
|
||||
.mapNotNull { checkbox ->
|
||||
if (checkbox.state) {
|
||||
options.find { it.first == checkbox.name }!!.second
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OriginFilter : QueryPartFilter("Origin", AllAnimeChiFiltersData.ORIGIN)
|
||||
class SeasonFilter : QueryPartFilter("Season", AllAnimeChiFiltersData.SEASONS)
|
||||
class ReleaseYearFilter : QueryPartFilter("Released at", AllAnimeChiFiltersData.YEARS)
|
||||
class EpisodeCountFilter : QueryPartFilter("Episode Count", AllAnimeChiFiltersData.EPISODE_COUNT)
|
||||
class TypeFilter : QueryPartFilter("Type", AllAnimeChiFiltersData.TYPES)
|
||||
|
||||
class GenresFilter : CheckBoxFilterList(
|
||||
"Genres",
|
||||
AllAnimeChiFiltersData.GENRES.map { CheckBoxVal(it.first, false) },
|
||||
)
|
||||
|
||||
val FILTER_LIST get() = AnimeFilterList(
|
||||
AnimeFilter.Header("NOTE: Ignored if using text search!"),
|
||||
OriginFilter(),
|
||||
SeasonFilter(),
|
||||
ReleaseYearFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
TypeFilter(),
|
||||
EpisodeCountFilter(),
|
||||
GenresFilter(),
|
||||
)
|
||||
|
||||
data class FilterSearchParams(
|
||||
val origin: String = "",
|
||||
val season: String = "",
|
||||
val releaseYear: String = "",
|
||||
val type: String = "",
|
||||
val episodeCount: String = "",
|
||||
val genres: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
|
||||
if (filters.isEmpty()) return FilterSearchParams()
|
||||
|
||||
return FilterSearchParams(
|
||||
filters.asQueryPart<OriginFilter>(),
|
||||
filters.asQueryPart<SeasonFilter>(),
|
||||
filters.asQueryPart<ReleaseYearFilter>(),
|
||||
filters.asQueryPart<TypeFilter>(),
|
||||
filters.asQueryPart<EpisodeCountFilter>(),
|
||||
filters.parseCheckbox<GenresFilter>(AllAnimeChiFiltersData.GENRES),
|
||||
)
|
||||
}
|
||||
|
||||
private object AllAnimeChiFiltersData {
|
||||
val ALL = Pair("All", "all")
|
||||
|
||||
val ORIGIN = arrayOf(
|
||||
Pair("All", "ALL"),
|
||||
Pair("Japan", "JP"),
|
||||
Pair("China", "CN"),
|
||||
Pair("Korea", "KR"),
|
||||
)
|
||||
|
||||
val SEASONS = arrayOf(
|
||||
ALL,
|
||||
Pair("Winter", "Winter"),
|
||||
Pair("Spring", "Spring"),
|
||||
Pair("Summer", "Summer"),
|
||||
Pair("Fall", "Fall"),
|
||||
)
|
||||
|
||||
val YEARS = arrayOf(
|
||||
ALL,
|
||||
Pair("2024", "2024"),
|
||||
Pair("2023", "2023"),
|
||||
Pair("2022", "2022"),
|
||||
Pair("2021", "2021"),
|
||||
Pair("2020", "2020"),
|
||||
Pair("2019", "2019"),
|
||||
Pair("2018", "2018"),
|
||||
Pair("2017", "2017"),
|
||||
Pair("2016", "2016"),
|
||||
Pair("2015", "2015"),
|
||||
Pair("2014", "2014"),
|
||||
Pair("2013", "2013"),
|
||||
Pair("2012", "2012"),
|
||||
Pair("2011", "2011"),
|
||||
Pair("2010", "2010"),
|
||||
Pair("2009", "2009"),
|
||||
Pair("2008", "2008"),
|
||||
Pair("2007", "2007"),
|
||||
Pair("2006", "2006"),
|
||||
Pair("2005", "2005"),
|
||||
Pair("2004", "2004"),
|
||||
Pair("2003", "2003"),
|
||||
Pair("2002", "2002"),
|
||||
Pair("2001", "2001"),
|
||||
Pair("2000", "2000"),
|
||||
Pair("1999", "1999"),
|
||||
Pair("1998", "1998"),
|
||||
Pair("1997", "1997"),
|
||||
Pair("1996", "1996"),
|
||||
Pair("1995", "1995"),
|
||||
Pair("1994", "1994"),
|
||||
Pair("1993", "1993"),
|
||||
Pair("1992", "1992"),
|
||||
Pair("1991", "1991"),
|
||||
Pair("1990", "1990"),
|
||||
Pair("1989", "1989"),
|
||||
Pair("1988", "1988"),
|
||||
Pair("1987", "1987"),
|
||||
Pair("1986", "1986"),
|
||||
Pair("1985", "1985"),
|
||||
Pair("1984", "1984"),
|
||||
Pair("1983", "1983"),
|
||||
Pair("1982", "1982"),
|
||||
Pair("1981", "1981"),
|
||||
Pair("1980", "1980"),
|
||||
Pair("1979", "1979"),
|
||||
Pair("1978", "1978"),
|
||||
Pair("1977", "1977"),
|
||||
Pair("1976", "1976"),
|
||||
Pair("1975", "1975"),
|
||||
)
|
||||
|
||||
val TYPES = arrayOf(
|
||||
ALL,
|
||||
Pair("TV", "TV"),
|
||||
Pair("OVA", "OVA"),
|
||||
Pair("Movie", "Movie"),
|
||||
Pair("Special", "Special"),
|
||||
Pair("ONA", "ONA"),
|
||||
Pair("Unknown", "Unknown"),
|
||||
)
|
||||
|
||||
val GENRES = arrayOf(
|
||||
Pair("Samurai", "Samurai"),
|
||||
Pair("School", "School"),
|
||||
Pair("Sci-Fi", "Sci-Fi"),
|
||||
Pair("Isekai", "Isekai"),
|
||||
Pair("Action", "Action"),
|
||||
Pair("Adventure", "Adventure"),
|
||||
Pair("Cars", "Cars"),
|
||||
Pair("Comedy", "Comedy"),
|
||||
Pair("Dementia", "Dementia"),
|
||||
Pair("Demons", "Demons"),
|
||||
Pair("Drama", "Drama"),
|
||||
Pair("Ecchi", "Ecchi"),
|
||||
Pair("Fantasy", "Fantasy"),
|
||||
Pair("Game", "Game"),
|
||||
Pair("Harem", "Harem"),
|
||||
Pair("Historical", "Historical"),
|
||||
Pair("Horror", "Horror"),
|
||||
Pair("Josei", "Josei"),
|
||||
Pair("Kids", "Kids"),
|
||||
Pair("Magic", "Magic"),
|
||||
Pair("Martial Arts", "Martial Arts"),
|
||||
Pair("Mecha", "Mecha"),
|
||||
Pair("Parody", "Parody"),
|
||||
Pair("Police", "Police"),
|
||||
Pair("Psychological", "Psychological"),
|
||||
Pair("Romance", "Romance"),
|
||||
Pair("Thriller", "Thriller"),
|
||||
Pair("Unknown", "Unknown"),
|
||||
Pair("Vampire", "Vampire"),
|
||||
Pair("Yaoi", "Yaoi"),
|
||||
Pair("Yuri", "Yuri"),
|
||||
)
|
||||
|
||||
val EPISODE_COUNT = arrayOf(
|
||||
ALL,
|
||||
Pair("1-5", "1-5"),
|
||||
Pair("6-10", "6-10"),
|
||||
Pair("11-15", "11-15"),
|
||||
Pair("16-25", "16-25"),
|
||||
Pair("26-70", "26-70"),
|
||||
Pair("71-255", "71-255"),
|
||||
Pair("256+", "256-"),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanimechi.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AllAnimeExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String): List<Video> {
|
||||
val jsonHeaders = headers.newBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", url.toHttpUrl().host)
|
||||
add("Referer", url)
|
||||
add("Sec-Fetch-Dest", "empty")
|
||||
add("Sec-Fetch-Mode", "cors")
|
||||
add("Sec-Fetch-Site", "same-origin")
|
||||
add("X-Requested-With", "Y29tLmFsbGFuaW1lLmFuaW1lY2hpY2tlbg==".decodeBase64())
|
||||
}.build()
|
||||
|
||||
val decoded = url.toHttpUrl().queryParameter("source")?.decodeBase64() ?: return emptyList()
|
||||
val slug = json.decodeFromString<SourceUrl>(decoded).idUrl
|
||||
|
||||
val data = client.newCall(
|
||||
GET("https://${url.toHttpUrl().host}$slug", headers = jsonHeaders),
|
||||
).execute().parseAs<VideoData>()
|
||||
|
||||
return data.links.flatMap { link ->
|
||||
val subtitleList = link.subtitles.map {
|
||||
Track(it.src, it.label)
|
||||
}
|
||||
|
||||
val audioList = link.rawUrls.audios.map {
|
||||
Track(it.url, formatBytes(it.bandwidth) + "/s")
|
||||
}
|
||||
|
||||
link.rawUrls.vids.map { vid ->
|
||||
val bandwidth = formatBytes(vid.bandwidth) + "/s"
|
||||
val name = "$prefix${vid.height}p ($bandwidth)"
|
||||
|
||||
Video(vid.url, name, vid.url, subtitleTracks = subtitleList, audioTracks = audioList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun String.decodeBase64(): String {
|
||||
return String(Base64.decode(this, Base64.DEFAULT))
|
||||
}
|
||||
|
||||
private fun formatBytes(bytes: Long): String {
|
||||
return when {
|
||||
bytes >= 1_000_000_000 -> "%.2f GB".format(bytes / 1_000_000_000.0)
|
||||
bytes >= 1_000_000 -> "%.2f MB".format(bytes / 1_000_000.0)
|
||||
bytes >= 1_000 -> "%.2f KB".format(bytes / 1_000.0)
|
||||
bytes > 1 -> "$bytes bytes"
|
||||
bytes == 1L -> "$bytes byte"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SourceUrl(
|
||||
val idUrl: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VideoData(
|
||||
val links: List<LinkObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class LinkObject(
|
||||
val resolutionStr: String,
|
||||
val rawUrls: UrlObject,
|
||||
val subtitles: List<SubtitleObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class UrlObject(
|
||||
val vids: List<VideoObject>,
|
||||
val audios: List<AudioObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class VideoObject(
|
||||
val bandwidth: Long,
|
||||
val height: Int,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AudioObject(
|
||||
val bandwidth: Long,
|
||||
val url: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SubtitleObject(
|
||||
val label: String,
|
||||
val src: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allanimechi.extractors
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.animeextension.en.allanimechi.AllAnimeChi
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
class InternalExtractor(private val client: OkHttpClient, private val apiHeaders: Headers, private val headers: Headers) {
|
||||
|
||||
private val blogUrl = "aHR0cHM6Ly9ibG9nLmFsbGFuaW1lLnBybw==".decodeBase64()
|
||||
|
||||
private val playlistHeaders = Headers.headersOf(
|
||||
"User-Agent",
|
||||
"Dalvik/2.1.0 (Linux; U; Android 13; Pixel 5 Build/TQ3A.230705.001.B4)",
|
||||
)
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromServer(server: AllAnimeChi.Server, useHosterName: Boolean, removeRaw: Boolean): List<Pair<Video, Float>> {
|
||||
val blogHeaders = apiHeaders.newBuilder().apply {
|
||||
set("host", blogUrl.toHttpUrl().host)
|
||||
}.build()
|
||||
|
||||
val videoData = client.newCall(
|
||||
GET(blogUrl + server.sourceUrl.replace("clock?id=", "clock.json?id="), blogHeaders),
|
||||
).execute().parseAs<VideoData>()
|
||||
|
||||
val videoList = videoData.links.flatMap {
|
||||
when {
|
||||
it.hls == true -> getFromHls(server, it, useHosterName, removeRaw)
|
||||
it.mp4 == true -> getFromMp4(server, it, useHosterName)
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun getFromMp4(server: AllAnimeChi.Server, data: VideoData.LinkObject, useHosterName: Boolean): List<Pair<Video, Float>> {
|
||||
val host = if (useHosterName) getHostName(data.link, server.sourceName) else server.sourceName
|
||||
|
||||
val baseName = "$host - ${data.resolutionStr}"
|
||||
val video = Video(data.link, baseName, data.link, headers = playlistHeaders)
|
||||
return listOf(
|
||||
Pair(video, server.priority),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFromHls(server: AllAnimeChi.Server, data: VideoData.LinkObject, useHosterName: Boolean, removeRaw: Boolean): List<Pair<Video, Float>> {
|
||||
if (removeRaw && data.resolutionStr.contains("raw", true)) return emptyList()
|
||||
val host = if (useHosterName) getHostName(data.link, server.sourceName) else server.sourceName
|
||||
|
||||
val linkHost = data.link.toHttpUrl().host
|
||||
|
||||
// Doesn't seem to work
|
||||
if (server.sourceName.equals("Luf-mp4", true)) {
|
||||
return getFromGogo(server, data, host)
|
||||
}
|
||||
|
||||
// Hardcode some names
|
||||
val baseName = if (linkHost.contains("crunchyroll")) {
|
||||
"Crunchyroll - ${data.resolutionStr}"
|
||||
.replace("m_SUB", "SoftSub")
|
||||
.replace("vo_SUB", "Sub")
|
||||
.replace("SUB", "Sub")
|
||||
} else if (linkHost.contains("vrv")) {
|
||||
"Vrv - ${data.resolutionStr}"
|
||||
.replace("m_SUB", "SoftSub")
|
||||
.replace("vo_SUB", "Sub")
|
||||
.replace("SUB", "Sub")
|
||||
} else {
|
||||
"$host - ${data.resolutionStr}"
|
||||
}
|
||||
|
||||
// Get stuff
|
||||
|
||||
val hlsHeaders = playlistHeaders.newBuilder().apply {
|
||||
set("host", linkHost)
|
||||
}.build()
|
||||
|
||||
val masterHeaders = hlsHeaders.newBuilder().apply {
|
||||
data.headers?.entries?.forEach {
|
||||
set(it.key, it.value)
|
||||
}
|
||||
}.build()
|
||||
|
||||
val videoList = playlistUtils.extractFromHls(
|
||||
data.link,
|
||||
videoNameGen = { q -> "$baseName - ${data.resolutionStr} - $q" },
|
||||
masterHeadersGen = { _, _ -> masterHeaders },
|
||||
)
|
||||
|
||||
return videoList.map {
|
||||
Pair(it, server.priority)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFromGogo(server: AllAnimeChi.Server, data: VideoData.LinkObject, hostName: String): List<Pair<Video, Float>> {
|
||||
val host = data.link.toHttpUrl().host
|
||||
|
||||
// Seems to be dead
|
||||
if (host.contains("maverickki", true)) return emptyList()
|
||||
|
||||
val videoList = playlistUtils.extractFromHls(
|
||||
data.link,
|
||||
videoNameGen = { q -> "$hostName - ${data.resolutionStr} - $q" },
|
||||
referer = "https://playtaku.net/",
|
||||
)
|
||||
|
||||
return videoList.map {
|
||||
Pair(it, server.priority)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun getHostName(host: String, fallback: String): String {
|
||||
return host.toHttpUrlOrNull()?.host?.split(".")?.let {
|
||||
it.getOrNull(it.size - 2)?.replaceFirstChar { c ->
|
||||
if (c.isLowerCase()) c.titlecase(Locale.ROOT) else c.toString()
|
||||
}
|
||||
} ?: fallback
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class VideoData(
|
||||
val links: List<LinkObject>,
|
||||
) {
|
||||
@Serializable
|
||||
data class LinkObject(
|
||||
val link: String,
|
||||
val resolutionStr: String,
|
||||
val mp4: Boolean? = null,
|
||||
val hls: Boolean? = null,
|
||||
val headers: Map<String, String>? = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun String.decodeBase64(): String {
|
||||
return String(Base64.decode(this, Base64.DEFAULT))
|
||||
}
|
||||
}
|
7
src/en/allmovies/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AllMovies'
|
||||
extClass = '.AllMovies'
|
||||
extVersionCode = 12
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/allmovies/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
src/en/allmovies/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/en/allmovies/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
src/en/allmovies/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/en/allmovies/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src/en/allmovies/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 97 KiB |
|
@ -0,0 +1,337 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.allmovies
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
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.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AllMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AllMoviesForYou"
|
||||
|
||||
override val baseUrl = "https://allmoviesforyou.net"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// Popular Anime
|
||||
|
||||
override fun popularAnimeSelector(): String = "article.TPost > a"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/series/page/$page")
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.attr("href"))
|
||||
anime.title = element.select("h2.Title").text()
|
||||
anime.thumbnail_url = "https:" + element.select("div.Image figure img").attr("data-src")
|
||||
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.nav-links a:last-child"
|
||||
|
||||
// Episodes
|
||||
|
||||
override fun episodeListSelector() = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val document = response.asJsoup()
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
val seriesLink = document.select("link[rel=canonical]").attr("abs:href")
|
||||
if (seriesLink.contains("/series/")) {
|
||||
val seasonsHtml = client.newCall(
|
||||
GET(
|
||||
seriesLink,
|
||||
headers = Headers.headersOf("Referer", document.location()),
|
||||
),
|
||||
).execute().asJsoup()
|
||||
val seasonsElements = seasonsHtml.select("section.SeasonBx.AACrdn a")
|
||||
seasonsElements.forEach {
|
||||
val seasonEpList = parseEpisodesFromSeries(it)
|
||||
episodeList.addAll(seasonEpList)
|
||||
}
|
||||
} else {
|
||||
val episode = SEpisode.create()
|
||||
episode.name = document.select("div.TPMvCn h1.Title").text()
|
||||
episode.episode_number = 1F
|
||||
episode.setUrlWithoutDomain(seriesLink)
|
||||
episodeList.add(episode)
|
||||
}
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode {
|
||||
val episode = SEpisode.create()
|
||||
episode.episode_number = element.select("td > span.Num").text().toFloat()
|
||||
val seasonNum = element.ownerDocument()!!.select("div.Title span").text()
|
||||
episode.name = "Season $seasonNum" + "x" + element.select("td span.Num").text() + " : " + element.select("td.MvTbTtl > a").text()
|
||||
episode.setUrlWithoutDomain(element.select("td.MvTbPly > a.ClA").attr("abs:href"))
|
||||
return episode
|
||||
}
|
||||
|
||||
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
|
||||
val seasonId = element.attr("abs:href")
|
||||
val episodesHtml = client.newCall(GET(seasonId)).execute().asJsoup()
|
||||
val episodeElements = episodesHtml.select("tr.Viewed")
|
||||
return episodeElements.map { episodeFromElement(it) }
|
||||
}
|
||||
|
||||
// Video urls
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val document = client.newCall(GET(baseUrl + episode.url)).execute().asJsoup()
|
||||
val iframe = document.select("iframe[src*=/?trembed]").attr("abs:src")
|
||||
return GET(iframe)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
return videosFromElement(document)
|
||||
}
|
||||
|
||||
override fun videoListSelector() = "iframe"
|
||||
|
||||
private fun videosFromElement(document: Document): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val elements = document.select(videoListSelector())
|
||||
for (element in elements) {
|
||||
val url = element.attr("abs:src")
|
||||
val location = element.ownerDocument()!!.location()
|
||||
val videoHeaders = Headers.headersOf("Referer", location)
|
||||
when {
|
||||
url.contains("https://dood") -> {
|
||||
val newQuality = "Doodstream mirror"
|
||||
val video = Video(url, newQuality, doodUrlParse(url), headers = videoHeaders)
|
||||
videoList.add(video)
|
||||
}
|
||||
url.contains("streamhub") -> {
|
||||
val response = client.newCall(GET(url, videoHeaders)).execute().asJsoup()
|
||||
val script = response.selectFirst("script:containsData(m3u8)")!!
|
||||
val data = script.data()
|
||||
val masterUrl = masterExtractor(data)
|
||||
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
|
||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
|
||||
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p"
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
videoList.add(Video(videoUrl, quality, videoUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun masterExtractor(code: String): String {
|
||||
val stringsRegex = """(?<!\\)'.+?(?<!\\)'""".toRegex()
|
||||
val strings = stringsRegex.findAll(code).map {
|
||||
it.value
|
||||
}.toList()
|
||||
var p = strings[3]
|
||||
val k = strings[4].split('|')
|
||||
|
||||
val numbersRegex = """(?<=,)\d+(?=,)""".toRegex()
|
||||
val numbers = numbersRegex.findAll(code).map {
|
||||
it.value.toInt()
|
||||
}.toList()
|
||||
val a = numbers[0]
|
||||
var c = numbers[1] - 1
|
||||
|
||||
while (c >= 0) {
|
||||
val replaceRegex = ("""\b""" + c.toString(a) + """\b""").toRegex()
|
||||
p = p.replace(replaceRegex, k[c])
|
||||
c--
|
||||
}
|
||||
|
||||
val sourcesRegex = """(?<=sources':\[\{src:").+?(?=")""".toRegex()
|
||||
return sourcesRegex.find(p)!!.value
|
||||
}
|
||||
|
||||
private fun doodUrlParse(url: String): String? {
|
||||
val response = client.newCall(GET(url.replace("/d/", "/e/"))).execute()
|
||||
val content = response.body.string()
|
||||
if (!content.contains("'/pass_md5/")) return null
|
||||
val md5 = content.substringAfter("'/pass_md5/").substringBefore("',")
|
||||
val token = md5.substringAfterLast("/")
|
||||
val doodTld = url.substringAfter("https://dood.").substringBefore("/")
|
||||
val randomString = getRandomString()
|
||||
val expiry = System.currentTimeMillis()
|
||||
val videoUrlStart = client.newCall(
|
||||
GET(
|
||||
"https://dood.$doodTld/pass_md5/$md5",
|
||||
Headers.headersOf("referer", url),
|
||||
),
|
||||
).execute().body.string()
|
||||
return "$videoUrlStart$randomString?token=$token&expiry=$expiry"
|
||||
}
|
||||
|
||||
private fun getRandomString(length: Int = 10): String {
|
||||
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
|
||||
return (1..length)
|
||||
.map { allowedChars.random() }
|
||||
.joinToString("")
|
||||
}
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", null)
|
||||
if (quality != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(quality)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||
|
||||
// search
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.setUrlWithoutDomain(element.select("article a").attr("href"))
|
||||
anime.title = element.select("h2.Title").text()
|
||||
anime.thumbnail_url = "https:" + element.select("div.Image figure img").attr("data-src")
|
||||
return anime
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = "div.nav-links a:last-child"
|
||||
|
||||
override fun searchAnimeSelector(): String = "ul.MovieList li"
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/page/$page?s=$query", headers)
|
||||
} else {
|
||||
val url = "$baseUrl/category/".toHttpUrlOrNull()!!.newBuilder()
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> url.addPathSegment(filter.toUriPart())
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
url.addPathSegment("page")
|
||||
url.addPathSegment("$page")
|
||||
GET(url.toString(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = document.select("div.TPMvCn h1.Title").text()
|
||||
anime.genre = document.select("p.Genre a").joinToString(", ") { it.text() }
|
||||
anime.status = parseStatus(document.select("div.Info").text()) // span.Qlty
|
||||
anime.author = document.select("p.Director span a").joinToString(", ") { it.text() }
|
||||
anime.description = document.select("div.TPMvCn div.Description p:first-of-type").text()
|
||||
return anime
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SAnime.UNKNOWN
|
||||
status.contains("AIR", ignoreCase = true) -> SAnime.ONGOING
|
||||
else -> SAnime.COMPLETED
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = AnimeFilterList(
|
||||
AnimeFilter.Header("NOTE: Ignored if using text search!"),
|
||||
AnimeFilter.Separator(),
|
||||
GenreFilter(getGenreList()),
|
||||
)
|
||||
|
||||
private class GenreFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Genres", vals)
|
||||
|
||||
private fun getGenreList() = arrayOf(
|
||||
Pair("Action & Adventure", "action-adventure"),
|
||||
Pair("Adventure", "aventure"),
|
||||
Pair("Animation", "animation"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Crime", "crime"),
|
||||
Pair("Disney", "disney"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Family", "family"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("History", "fistory"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Kids", "kids"),
|
||||
Pair("Music", "music"),
|
||||
Pair("Mystery", "mystery"),
|
||||
Pair("Reality", "reality"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Sci-Fi & Fantasy", "sci-fi-fantasy"),
|
||||
Pair("Science Fiction", "science-fiction"),
|
||||
Pair("Thriller", "thriller"),
|
||||
Pair("War", "war"),
|
||||
Pair("War & Politics", "war-politics"),
|
||||
Pair("Western", "western"),
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// settings
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "240")
|
||||
setDefaultValue("1080")
|
||||
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)
|
||||
}
|
||||
}
|
7
src/en/animeflix/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeFlix'
|
||||
extClass = '.AnimeFlix'
|
||||
extVersionCode = 7
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/animeflix/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/en/animeflix/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/en/animeflix/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
src/en/animeflix/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/en/animeflix/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/en/animeflix/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 92 KiB |
|
@ -0,0 +1,398 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeflix
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
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.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeFlix"
|
||||
|
||||
override val baseUrl = "https://animeflix.mobi"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page/")
|
||||
|
||||
override fun popularAnimeSelector() = "div#content_box > div.post-cards > article"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
// prevent base64 images
|
||||
thumbnail_url = element.selectFirst("img")!!.run {
|
||||
attr("data-pagespeed-high-res-src").ifEmpty { attr("src") }
|
||||
}
|
||||
title = element.selectFirst("header")!!.text()
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val cleanQuery = query.replace(" ", "+").lowercase()
|
||||
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
|
||||
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
|
||||
subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("Text search ignores filters"),
|
||||
GenreFilter(),
|
||||
SubPageFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Genres",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Action", "action"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Isekai", "isekai"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Psychological", "psychological"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Sci-Fi", "sci-fi"),
|
||||
Pair("Magic", "magic"),
|
||||
Pair("Slice Of Life", "slice-of-life"),
|
||||
Pair("Sports", "sports"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
),
|
||||
)
|
||||
|
||||
private class SubPageFilter : UriPartFilter(
|
||||
"Sub-page",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Latest Release", "latest-release"),
|
||||
Pair("Movies", "movies"),
|
||||
),
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
|
||||
title = document.selectFirst("div.single_post > header > h1")!!.text()
|
||||
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
|
||||
|
||||
val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
|
||||
status = when (infosDiv.getInfo("Status").toString()) {
|
||||
"Completed" -> SAnime.COMPLETED
|
||||
"Currently Airing" -> SAnime.ONGOING
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
artist = infosDiv.getInfo("Studios")
|
||||
author = infosDiv.getInfo("Producers")
|
||||
genre = infosDiv.getInfo("Genres")
|
||||
val animeInfo = infosDiv.select("li").joinToString("\n") { it.text() }
|
||||
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)")
|
||||
.joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
|
||||
}
|
||||
|
||||
private fun Element.getInfo(info: String) =
|
||||
selectFirst("li:contains($info)")?.ownText()?.trim()
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
val seasonRegex by lazy { Regex("""season (\d+)""", RegexOption.IGNORE_CASE) }
|
||||
val qualityRegex by lazy { """(\d+)p""".toRegex() }
|
||||
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||
val document = client.newCall(GET(baseUrl + anime.url)).execute()
|
||||
.asJsoup()
|
||||
|
||||
val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)")
|
||||
|
||||
val episodeList = if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) {
|
||||
val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy {
|
||||
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
|
||||
}
|
||||
|
||||
seasonsLinks.flatMap { (seasonNumber, season) ->
|
||||
val serverListSeason = season.map {
|
||||
val previousText = it.previousElementSibling()!!.text()
|
||||
val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
|
||||
|
||||
val url = it.selectFirst("a")!!.attr("href")
|
||||
val episodesDocument = client.newCall(GET(url)).execute()
|
||||
.asJsoup()
|
||||
episodesDocument.select("div.entry-content > h3 > a").map {
|
||||
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
|
||||
}
|
||||
}
|
||||
|
||||
transposeEpisodes(serverListSeason)
|
||||
}
|
||||
} else {
|
||||
val driveList = document.select("div.thecontent p:has(span:contains(Gdrive))").map {
|
||||
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
|
||||
Pair(it.selectFirst("a")!!.attr("href"), quality)
|
||||
}
|
||||
|
||||
// Load episodes
|
||||
val serversList = driveList.map { drive ->
|
||||
val episodesDocument = client.newCall(GET(drive.first)).execute()
|
||||
.asJsoup()
|
||||
episodesDocument.select("div.entry-content > h3 > a").map {
|
||||
EpUrl(drive.second, it.attr("href"), it.text())
|
||||
}
|
||||
}
|
||||
|
||||
transposeEpisodes(serversList)
|
||||
}
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
private fun transposeEpisodes(serversList: List<List<EpUrl>>) =
|
||||
transpose(serversList).mapIndexed { index, serverList ->
|
||||
SEpisode.create().apply {
|
||||
name = serverList.first().name
|
||||
episode_number = (index + 1).toFloat()
|
||||
setUrlWithoutDomain(json.encodeToString(serverList))
|
||||
}
|
||||
}
|
||||
|
||||
override fun episodeListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
val urls = json.decodeFromString<List<EpUrl>>(episode.url)
|
||||
|
||||
val leechUrls = urls.map {
|
||||
val firstLeech = client.newCall(GET(it.url)).execute()
|
||||
.asJsoup()
|
||||
.selectFirst("script:containsData(downlaod_button)")!!
|
||||
.data()
|
||||
.substringAfter("<a href=\"")
|
||||
.substringBefore("\">")
|
||||
|
||||
val path = client.newCall(GET(firstLeech)).execute()
|
||||
.body.string()
|
||||
.substringAfter("replace(\"")
|
||||
.substringBefore("\"")
|
||||
|
||||
val link = "https://" + firstLeech.toHttpUrl().host + path
|
||||
EpUrl(it.quality, link, it.name)
|
||||
}
|
||||
|
||||
val videoList = leechUrls.parallelCatchingFlatMap { url ->
|
||||
if (url.url.toHttpUrl().encodedPath == "/404") return@parallelCatchingFlatMap emptyList()
|
||||
val (videos, mediaUrl) = extractVideo(url)
|
||||
when {
|
||||
videos.isEmpty() -> {
|
||||
extractGDriveLink(mediaUrl, url.quality).ifEmpty {
|
||||
getDirectLink(mediaUrl, "instant", "/mfile/")?.let {
|
||||
listOf(Video(it, "${url.quality}p - GDrive Instant link", it))
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
||||
else -> videos
|
||||
}
|
||||
}
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
// https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
|
||||
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
|
||||
val matchResult = qualityRegex.find(epUrl.name)
|
||||
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
|
||||
|
||||
return (1..3).toList().flatMap { type ->
|
||||
extractWorkerLinks(epUrl.url, quality, type)
|
||||
}.let { Pair(it, epUrl.url) }
|
||||
}
|
||||
|
||||
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
|
||||
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
|
||||
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
|
||||
val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
|
||||
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
|
||||
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
|
||||
val link = linkElement.attr("href")
|
||||
val decodedLink = if (link.contains("workers.dev")) {
|
||||
link
|
||||
} else {
|
||||
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
|
||||
}
|
||||
|
||||
Video(
|
||||
url = decodedLink,
|
||||
quality = "${quality}p - CF $type Worker ${index + 1}$size",
|
||||
videoUrl = decodedLink,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDirectLink(url: String, action: String = "direct", newPath: String = "/file/"): String? {
|
||||
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
|
||||
val script = doc.selectFirst("script:containsData(async function taskaction)")
|
||||
?.data()
|
||||
?: return url
|
||||
|
||||
val key = script.substringAfter("key\", \"").substringBefore('"')
|
||||
val form = MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("action", action)
|
||||
.addFormDataPart("key", key)
|
||||
.addFormDataPart("action_token", "")
|
||||
.build()
|
||||
|
||||
val headers = headersBuilder().set("x-token", url.toHttpUrl().host).build()
|
||||
|
||||
val req = client.newCall(POST(url.replace("/file/", newPath), headers, form)).execute()
|
||||
return runCatching {
|
||||
json.decodeFromString<DriveLeechDirect>(req.body.string()).url
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
|
||||
val neoUrl = getDirectLink(mediaUrl) ?: mediaUrl
|
||||
val response = client.newCall(GET(neoUrl)).execute().asJsoup()
|
||||
val gdBtn = response.selectFirst("div.card-body a.btn")!!
|
||||
val gdLink = gdBtn.attr("href")
|
||||
val sizeMatch = SIZE_REGEX.find(gdBtn.text())
|
||||
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
|
||||
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
|
||||
val link = gdResponse.select("form#download-form")
|
||||
return if (link.isNullOrEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
val realLink = link.attr("action")
|
||||
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
|
||||
}
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
return sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private fun <E> transpose(xs: List<List<E>>): List<List<E>> {
|
||||
// Helpers
|
||||
fun <E> List<E>.head(): E = this.first()
|
||||
fun <E> List<E>.tail(): List<E> = this.takeLast(this.size - 1)
|
||||
fun <E> E.append(xs: List<E>): List<E> = listOf(this).plus(xs)
|
||||
|
||||
xs.filter { it.isNotEmpty() }.let { ys ->
|
||||
return when (ys.isNotEmpty()) {
|
||||
true -> ys.map { it.head() }.append(transpose(ys.map { it.tail() }))
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EpUrl(
|
||||
val quality: String,
|
||||
val url: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DriveLeechDirect(val url: String? = null)
|
||||
|
||||
companion object {
|
||||
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private const val PREF_QUALITY_KEY = "pref_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
|
||||
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_VALUES
|
||||
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)
|
||||
}
|
||||
}
|
12
src/en/animeflixlive/build.gradle
Normal file
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'Animeflix.live'
|
||||
extClass = '.AnimeflixLive'
|
||||
extVersionCode = 4
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:gogostream-extractor'))
|
||||
implementation(project(':lib:playlist-utils'))
|
||||
}
|
BIN
src/en/animeflixlive/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
src/en/animeflixlive/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/en/animeflixlive/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
src/en/animeflixlive/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/en/animeflixlive/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,503 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
|
||||
|
||||
import GenreFilter
|
||||
import SortFilter
|
||||
import SubPageFilter
|
||||
import TypeFilter
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
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.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLDecoder
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.min
|
||||
|
||||
class AnimeflixLive : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
override val name = "Animeflix.live"
|
||||
|
||||
override val baseUrl by lazy { preferences.baseUrl }
|
||||
|
||||
private val apiUrl by lazy { preferences.apiUrl }
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val apiHeaders = headersBuilder().apply {
|
||||
add("Accept", "*/*")
|
||||
add("Host", apiUrl.toHttpUrl().host)
|
||||
add("Origin", baseUrl)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
private val docHeaders = headersBuilder().apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
add("Host", apiUrl.toHttpUrl().host)
|
||||
add("Referer", "$baseUrl/")
|
||||
}.build()
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request =
|
||||
GET("$apiUrl/popular?page=${page - 1}", apiHeaders)
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<List<AnimeDto>>()
|
||||
val titlePref = preferences.titleType
|
||||
|
||||
val animeList = parsed.map {
|
||||
it.toSAnime(titlePref)
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$apiUrl/trending?page=${page - 1}", apiHeaders)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<TrendingDto>()
|
||||
val titlePref = preferences.titleType
|
||||
|
||||
val animeList = parsed.trending.map {
|
||||
it.toSAnime(titlePref)
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val sort = filters.filterIsInstance<SortFilter>().first().getValue()
|
||||
val type = filters.filterIsInstance<TypeFilter>().first().getValues()
|
||||
val genre = filters.filterIsInstance<GenreFilter>().first().getValues()
|
||||
val subPage = filters.filterIsInstance<SubPageFilter>().first().getValue()
|
||||
|
||||
if (subPage.isNotBlank()) {
|
||||
return GET("$apiUrl/$subPage?page=${page - 1}", apiHeaders)
|
||||
}
|
||||
|
||||
if (query.isEmpty()) {
|
||||
throw Exception("Search must not be empty")
|
||||
}
|
||||
|
||||
val filtersObj = buildJsonObject {
|
||||
put("sort", sort)
|
||||
if (type.isNotEmpty()) {
|
||||
put("type", json.encodeToString(type))
|
||||
}
|
||||
if (genre.isNotEmpty()) {
|
||||
put("genre", json.encodeToString(genre))
|
||||
}
|
||||
}.toJsonString()
|
||||
|
||||
val url = apiUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("info")
|
||||
addPathSegment("")
|
||||
addQueryParameter("query", query)
|
||||
addQueryParameter("limit", "15")
|
||||
addQueryParameter("filters", filtersObj)
|
||||
addQueryParameter("k", query.substr(0, 3).sk())
|
||||
}.build()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val parsed = response.parseAs<List<AnimeDto>>()
|
||||
val titlePref = preferences.titleType
|
||||
|
||||
val animeList = parsed.map {
|
||||
it.toSAnime(titlePref)
|
||||
}
|
||||
|
||||
val hasNextPage = if (response.request.url.queryParameter("limit") == null) {
|
||||
animeList.size == 44
|
||||
} else {
|
||||
animeList.size == 15
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
SortFilter(),
|
||||
TypeFilter(),
|
||||
GenreFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
AnimeFilter.Header("NOTE: Subpage overrides search and other filters!"),
|
||||
SubPageFilter(),
|
||||
)
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
return GET("$apiUrl/getslug/${anime.url}", apiHeaders)
|
||||
}
|
||||
|
||||
override fun getAnimeUrl(anime: SAnime): String {
|
||||
return "$baseUrl/search/${anime.title}?anime=${anime.url}"
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val titlePref = preferences.titleType
|
||||
return response.parseAs<DetailsDto>().toSAnime(titlePref)
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val lang = preferences.lang
|
||||
|
||||
val url = apiUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("episodes")
|
||||
addQueryParameter("id", anime.url)
|
||||
addQueryParameter("dub", (lang == "Dub").toString())
|
||||
addQueryParameter("c", anime.url.sk())
|
||||
}.build()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val slug = response.request.url.queryParameter("id")!!
|
||||
|
||||
return response.parseAs<EpisodeResponseDto>().episodes.map {
|
||||
it.toSEpisode(slug)
|
||||
}.sortedByDescending { it.episode_number }
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListRequest(episode: SEpisode): Request {
|
||||
val url = "$apiUrl${episode.url}".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("server", "")
|
||||
addQueryParameter("c", episode.url.substringAfter("/watch/").sk())
|
||||
}.build()
|
||||
|
||||
return GET(url, apiHeaders)
|
||||
}
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val initialPlayerUrl = apiUrl + response.parseAs<ServerDto>().source
|
||||
val initialServer = initialPlayerUrl.toHttpUrl().queryParameter("server")!!
|
||||
|
||||
val initialPlayerDocument = client.newCall(
|
||||
GET(initialPlayerUrl, docHeaders),
|
||||
).execute().asJsoup().unescape()
|
||||
|
||||
videoList.addAll(
|
||||
videosFromPlayer(
|
||||
initialPlayerDocument,
|
||||
initialServer.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
|
||||
),
|
||||
)
|
||||
|
||||
// Go through rest of servers
|
||||
val servers = initialPlayerDocument.selectFirst("script:containsData(server-settings)")!!.data()
|
||||
val serversHtml = SERVER_REGEX.findAll(servers).map {
|
||||
Jsoup.parseBodyFragment(it.groupValues[1])
|
||||
}.toList()
|
||||
|
||||
videoList.addAll(
|
||||
serversHtml.parallelCatchingFlatMapBlocking {
|
||||
val server = serverMapping[
|
||||
it.selectFirst("button")!!
|
||||
.attr("onclick")
|
||||
.substringAfter("postMessage('")
|
||||
.substringBefore("'"),
|
||||
]
|
||||
if (server == initialServer) {
|
||||
return@parallelCatchingFlatMapBlocking emptyList()
|
||||
}
|
||||
|
||||
val serverUrl = response.request.url.newBuilder()
|
||||
.setQueryParameter("server", server)
|
||||
.build()
|
||||
val playerUrl = apiUrl + client.newCall(
|
||||
GET(serverUrl, apiHeaders),
|
||||
).execute().parseAs<ServerDto>().source
|
||||
|
||||
if (server != playerUrl.toHttpUrl().queryParameter("server")!!) {
|
||||
return@parallelCatchingFlatMapBlocking emptyList()
|
||||
}
|
||||
|
||||
val playerDocument = client.newCall(
|
||||
GET(playerUrl, docHeaders),
|
||||
).execute().asJsoup().unescape()
|
||||
|
||||
videosFromPlayer(
|
||||
playerDocument,
|
||||
server.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
private val serverMapping = mapOf(
|
||||
"settings-0" to "moon",
|
||||
"settings-1" to "sun",
|
||||
"settings-2" to "zoro",
|
||||
"settings-3" to "gogo",
|
||||
)
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
|
||||
private 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("Host", videoUrl.toHttpUrl().host)
|
||||
add("Origin", "https://${apiUrl.toHttpUrl().host}")
|
||||
add("Referer", "$apiUrl/")
|
||||
add("Sec-Fetch-Dest", "empty")
|
||||
add("Sec-Fetch-Mode", "cors")
|
||||
add("Sec-Fetch-Site", "cross-site")
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun Document.unescape(): Document {
|
||||
val unescapeScript = this.selectFirst("script:containsData(unescape)")
|
||||
return if (unescapeScript == null) {
|
||||
this
|
||||
} else {
|
||||
val data = URLDecoder.decode(unescapeScript.data(), "UTF-8")
|
||||
Jsoup.parse(data, this.location())
|
||||
}
|
||||
}
|
||||
|
||||
private fun videosFromPlayer(document: Document, name: String): List<Video> {
|
||||
val dataScript = document.selectFirst("script:containsData(m3u8)")
|
||||
?.data() ?: return emptyList()
|
||||
|
||||
val subtitleList = document.select("video > track[kind=captions]").map {
|
||||
Track(it.attr("id"), it.attr("label"))
|
||||
}
|
||||
|
||||
var masterPlaylist = M3U8_REGEX.find(dataScript)?.groupValues?.get(1)
|
||||
?: return emptyList()
|
||||
|
||||
if (name.equals("moon", true)) {
|
||||
masterPlaylist += dataScript.substringAfter("`${'$'}{url}")
|
||||
.substringBefore("`")
|
||||
}
|
||||
|
||||
return playlistUtils.extractFromHls(
|
||||
masterPlaylist,
|
||||
videoHeadersGen = ::getVideoHeaders,
|
||||
videoNameGen = { q -> "$name - $q" },
|
||||
subtitleList = subtitleList,
|
||||
)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.quality
|
||||
val server = preferences.server
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.contains(server, true) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private fun JsonObject.toJsonString(): String {
|
||||
return json.encodeToString(this)
|
||||
}
|
||||
|
||||
private fun String.sk(): String {
|
||||
val t = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
|
||||
val n = 17 + (t.get(Calendar.DAY_OF_MONTH) - t.get(Calendar.MONTH)) / 2
|
||||
return this.toCharArray().fold("") { acc, c ->
|
||||
acc + c.code.toString(n).padStart(2, '0')
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.substr(start: Int, end: Int): String {
|
||||
val stop = min(end, this.length)
|
||||
return this.substring(start, stop)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SERVER_REGEX = Regex("""'1' === '1'.*?(<button.*?</button>)""", RegexOption.DOT_MATCHES_ALL)
|
||||
private val M3U8_REGEX = Regex("""const ?\w*? ?= ?`(.*?)`""")
|
||||
private const val PAGE_SIZE = 24
|
||||
|
||||
private const val PREF_DOMAIN_KEY = "pref_domain_key"
|
||||
private const val PREF_DOMAIN_DEFAULT = "https://animeflix.live,https://api.animeflix.dev"
|
||||
private val PREF_DOMAIN_ENTRIES = arrayOf("animeflix.live", "animeflix.ro")
|
||||
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf(
|
||||
"https://animeflix.live,https://api.animeflix.dev",
|
||||
"https://animeflix.ro,https://api.animeflixtv.to",
|
||||
)
|
||||
|
||||
private const val PREF_TITLE_KEY = "pref_title_type_key"
|
||||
private const val PREF_TITLE_DEFAULT = "English"
|
||||
private val PREF_TITLE_ENTRIES = arrayOf("English", "Native", "Romaji")
|
||||
|
||||
private const val PREF_LANG_KEY = "pref_lang_key"
|
||||
private const val PREF_LANG_DEFAULT = "Sub"
|
||||
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
|
||||
|
||||
private const val PREF_QUALITY_KEY = "pref_quality_key"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
|
||||
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
|
||||
|
||||
private const val PREF_SERVER_KEY = "pref_server_key"
|
||||
private const val PREF_SERVER_DEFAULT = "Moon"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Moon", "Sun", "Zoro", "Gogo")
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = "Preferred domain (requires app restart)"
|
||||
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)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_TITLE_KEY
|
||||
title = "Preferred Title Type"
|
||||
entries = PREF_TITLE_ENTRIES
|
||||
entryValues = PREF_TITLE_ENTRIES
|
||||
setDefaultValue(PREF_TITLE_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_LANG_KEY
|
||||
title = "Preferred Language"
|
||||
entries = PREF_LANG_ENTRIES
|
||||
entryValues = PREF_LANG_ENTRIES
|
||||
setDefaultValue(PREF_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 = "Preferred quality"
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
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 = "Preferred server"
|
||||
entries = PREF_SERVER_ENTRIES
|
||||
entryValues = PREF_SERVER_ENTRIES
|
||||
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)
|
||||
}
|
||||
|
||||
private val SharedPreferences.baseUrl
|
||||
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
.split(",").first()
|
||||
|
||||
private val SharedPreferences.apiUrl
|
||||
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
.split(",").last()
|
||||
|
||||
private val SharedPreferences.titleType
|
||||
get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.lang
|
||||
get() = getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.quality
|
||||
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
private val SharedPreferences.server
|
||||
get() = getString(PREF_SERVER_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
@Serializable
|
||||
class TrendingDto(
|
||||
val trending: List<AnimeDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AnimeDto(
|
||||
val slug: String,
|
||||
@SerialName("title") val titleObj: TitleObject,
|
||||
val images: ImageObject,
|
||||
) {
|
||||
@Serializable
|
||||
class TitleObject(
|
||||
val english: String? = null,
|
||||
val native: String? = null,
|
||||
val romaji: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ImageObject(
|
||||
val large: String? = null,
|
||||
val medium: String? = null,
|
||||
val small: String? = null,
|
||||
)
|
||||
|
||||
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
|
||||
title = when (titlePref) {
|
||||
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
|
||||
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
|
||||
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
|
||||
}
|
||||
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
|
||||
url = slug
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class DetailsDto(
|
||||
val slug: String,
|
||||
@SerialName("title") val titleObj: TitleObject,
|
||||
val description: String,
|
||||
val genres: List<String>,
|
||||
val status: String? = null,
|
||||
val images: ImageObject,
|
||||
) {
|
||||
@Serializable
|
||||
class TitleObject(
|
||||
val english: String? = null,
|
||||
val native: String? = null,
|
||||
val romaji: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ImageObject(
|
||||
val large: String? = null,
|
||||
val medium: String? = null,
|
||||
val small: String? = null,
|
||||
)
|
||||
|
||||
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
|
||||
title = when (titlePref) {
|
||||
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
|
||||
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
|
||||
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
|
||||
}
|
||||
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
|
||||
url = slug
|
||||
genre = genres.joinToString()
|
||||
status = this@DetailsDto.status.parseStatus()
|
||||
description = Jsoup.parseBodyFragment(
|
||||
this@DetailsDto.description.replace("<br>", "br2n"),
|
||||
).text().replace("br2n", "\n")
|
||||
}
|
||||
|
||||
private fun String?.parseStatus(): Int = when (this?.lowercase()) {
|
||||
"releasing" -> SAnime.ONGOING
|
||||
"finished" -> SAnime.COMPLETED
|
||||
"cancelled" -> SAnime.CANCELLED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class EpisodeResponseDto(
|
||||
val episodes: List<EpisodeDto>,
|
||||
) {
|
||||
@Serializable
|
||||
class EpisodeDto(
|
||||
val number: Float,
|
||||
val title: String? = null,
|
||||
) {
|
||||
fun toSEpisode(slug: String): SEpisode = SEpisode.create().apply {
|
||||
val epNum = if (floor(number) == ceil(number)) {
|
||||
number.toInt().toString()
|
||||
} else {
|
||||
number.toString()
|
||||
}
|
||||
|
||||
url = "/watch/$slug-episode-$epNum"
|
||||
episode_number = number
|
||||
name = if (title == null) {
|
||||
"Episode $epNum"
|
||||
} else {
|
||||
"Ep. $epNum - $title"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ServerDto(
|
||||
val source: String,
|
||||
)
|
|
@ -0,0 +1,81 @@
|
|||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
|
||||
open class UriPartFilter(
|
||||
name: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
defaultValue: String? = null,
|
||||
) : AnimeFilter.Select<String>(
|
||||
name,
|
||||
vals.map { it.first }.toTypedArray(),
|
||||
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
|
||||
) {
|
||||
fun getValue(): String {
|
||||
return vals[state].second
|
||||
}
|
||||
}
|
||||
|
||||
open class UriMultiSelectOption(name: String, val value: String) : AnimeFilter.CheckBox(name)
|
||||
|
||||
open class UriMultiSelectFilter(
|
||||
name: String,
|
||||
private val vals: Array<Pair<String, String>>,
|
||||
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }) {
|
||||
fun getValues(): List<String> {
|
||||
return state.filter { it.state }.map { it.value }
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : UriPartFilter(
|
||||
"Sort",
|
||||
arrayOf(
|
||||
Pair("Recently Updated", "recently_updated"),
|
||||
Pair("Recently Added", "recently_added"),
|
||||
Pair("Release Date ↓", "release_date_down"),
|
||||
Pair("Release Date ↑", "release_date_up"),
|
||||
Pair("Name A-Z", "title_az"),
|
||||
Pair("Best Rating", "scores"),
|
||||
Pair("Most Watched", "most_watched"),
|
||||
Pair("Anime Length", "number_of_episodes"),
|
||||
),
|
||||
)
|
||||
|
||||
class TypeFilter : UriMultiSelectFilter(
|
||||
"Type",
|
||||
arrayOf(
|
||||
Pair("TV", "TV"),
|
||||
Pair("Movie", "MOVIE"),
|
||||
Pair("OVA", "OVA"),
|
||||
Pair("ONA", "ONA"),
|
||||
Pair("Special", "SPECIAL"),
|
||||
),
|
||||
)
|
||||
|
||||
class GenreFilter : UriMultiSelectFilter(
|
||||
"Genre",
|
||||
arrayOf(
|
||||
Pair("Action", "Action"),
|
||||
Pair("Adventure", "Adventure"),
|
||||
Pair("Comedy", "Comedy"),
|
||||
Pair("Drama", "Drama"),
|
||||
Pair("Ecchi", "Ecchi"),
|
||||
Pair("Fantasy", "Fantasy"),
|
||||
Pair("Horror", "Horror"),
|
||||
Pair("Mecha", "Mecha"),
|
||||
Pair("Mystery", "Mystery"),
|
||||
Pair("Psychological", "Psychological"),
|
||||
Pair("Romance", "Romance"),
|
||||
Pair("Sci-Fi", "Sci-Fi"),
|
||||
Pair("Sports", "Sports"),
|
||||
Pair("Supernatural", "Supernatural"),
|
||||
Pair("Thriller", "Thriller"),
|
||||
),
|
||||
)
|
||||
|
||||
class SubPageFilter : UriPartFilter(
|
||||
"Sub-page",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Movies", "movies"),
|
||||
Pair("Series", "series"),
|
||||
),
|
||||
)
|
15
src/en/animekhor/build.gradle
Normal file
|
@ -0,0 +1,15 @@
|
|||
ext {
|
||||
extName = 'AnimeKhor'
|
||||
extClass = '.AnimeKhor'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animekhor.xyz'
|
||||
overrideVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:okru-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/en/animekhor/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/en/animekhor/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
src/en/animekhor/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/en/animekhor/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
src/en/animekhor/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,38 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animekhor
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.en.animekhor.extractors.StreamHideExtractor
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.okruextractor.OkruExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||
|
||||
class AnimeKhor : AnimeStream(
|
||||
"en",
|
||||
"AnimeKhor",
|
||||
"https://animekhor.xyz",
|
||||
) {
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun getVideoList(url: String, name: String): List<Video> {
|
||||
val prefix = "$name - "
|
||||
return when {
|
||||
url.contains("ahvsh.com") || name.equals("streamhide", true) -> {
|
||||
StreamHideExtractor(client, headers).videosFromUrl(url, prefix = prefix)
|
||||
}
|
||||
url.contains("ok.ru") -> {
|
||||
OkruExtractor(client).videosFromUrl(url, prefix = prefix)
|
||||
}
|
||||
url.contains("streamwish") -> {
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.build()
|
||||
StreamWishExtractor(client, docHeaders).videosFromUrl(url, prefix)
|
||||
}
|
||||
// TODO: Videos won't play
|
||||
// url.contains("animeabc.xyz") -> {
|
||||
// AnimeABCExtractor(client, headers).videosFromUrl(url, prefix = prefix)
|
||||
// }
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animekhor.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AnimeABCExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val document = client.newCall(GET(url, headers = headers)).execute().asJsoup()
|
||||
val data = document.selectFirst("script:containsData(m3u8)")?.data() ?: return emptyList()
|
||||
val sources = json.decodeFromString<List<Source>>(
|
||||
"[${data.substringAfter("sources:")
|
||||
.substringAfter("[")
|
||||
.substringBefore("]")}]",
|
||||
)
|
||||
sources.forEach { src ->
|
||||
val masterplHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Connection", "keep-alive")
|
||||
.add("Host", url.toHttpUrl().host)
|
||||
.add("Referer", url)
|
||||
.build()
|
||||
val masterPlaylist = client.newCall(
|
||||
GET(src.file.replace("^//".toRegex(), "https://"), headers = masterplHeaders),
|
||||
).execute().body.string()
|
||||
|
||||
masterPlaylist.substringAfter("#EXT-X-STREAM-INF:").split("#EXT-X-STREAM-INF:").forEach {
|
||||
val quality = prefix + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore("\n") + "p ${src.label ?: ""}"
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
videoList.add(Video(videoUrl, quality, videoUrl, headers = masterplHeaders))
|
||||
}
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Source(
|
||||
val file: String,
|
||||
val label: String? = null,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animekhor.extractors
|
||||
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
import eu.kanade.tachiyomi.animesource.model.Track
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class StreamHideExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
// from nineanime / ask4movie FilemoonExtractor
|
||||
private val subtitleRegex = Regex("""#EXT-X-MEDIA:TYPE=SUBTITLES.*?NAME="(.*?)".*?URI="(.*?)"""")
|
||||
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
val page = client.newCall(GET(url, headers = headers)).execute().body.string()
|
||||
val unpacked = JsUnpacker.unpackAndCombine(page) ?: page
|
||||
val playlistUrl = unpacked.substringAfter("sources:")
|
||||
.substringAfter("file:\"")
|
||||
.substringBefore('"')
|
||||
|
||||
val playlistData = client.newCall(GET(playlistUrl, headers = headers)).execute().body.string()
|
||||
|
||||
val subs = subtitleRegex.findAll(playlistData).map {
|
||||
val subUrl = fixUrl(it.groupValues[2], playlistUrl)
|
||||
Track(subUrl, it.groupValues[1])
|
||||
}.toList()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF"
|
||||
return playlistData.substringAfter(separator).split(separator).map {
|
||||
val resolution = it.substringAfter("RESOLUTION=")
|
||||
.substringBefore("\n")
|
||||
.substringAfter("x")
|
||||
.substringBefore(",") + "p"
|
||||
|
||||
val urlPart = it.substringAfter("\n").substringBefore("\n")
|
||||
val videoUrl = fixUrl(urlPart, playlistUrl)
|
||||
Video(videoUrl, "${prefix}StreamHide:$resolution", videoUrl, subtitleTracks = subs)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixUrl(urlPart: String, playlistUrl: String) =
|
||||
when {
|
||||
!urlPart.startsWith("https:") -> playlistUrl.substringBeforeLast("/") + "/$urlPart"
|
||||
else -> urlPart
|
||||
}
|
||||
}
|
16
src/en/animenosub/build.gradle
Normal file
|
@ -0,0 +1,16 @@
|
|||
ext {
|
||||
extName = 'Animenosub'
|
||||
extClass = '.Animenosub'
|
||||
themePkg = 'animestream'
|
||||
baseUrl = 'https://animenosub.com'
|
||||
overrideVersionCode = 5
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:filemoon-extractor'))
|
||||
implementation(project(':lib:streamwish-extractor'))
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/en/animenosub/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/en/animenosub/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/en/animenosub/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/en/animenosub/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
src/en/animenosub/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
|
@ -0,0 +1,122 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animenosub
|
||||
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.en.animenosub.extractors.VidMolyExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.en.animenosub.extractors.VtubeExtractor
|
||||
import eu.kanade.tachiyomi.animeextension.en.animenosub.extractors.WolfstreamExtractor
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
|
||||
import eu.kanade.tachiyomi.multisrc.animestream.AnimeStream
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class Animenosub : AnimeStream(
|
||||
"en",
|
||||
"Animenosub",
|
||||
"https://animenosub.com",
|
||||
) {
|
||||
// ============================== Episodes ==============================
|
||||
override fun getEpisodeName(element: Element, epNum: String): String {
|
||||
val episodeTitle = element.selectFirst("div.epl-title")?.text() ?: ""
|
||||
val complement = if (episodeTitle.contains("Episode $epNum", true)) "" else episodeTitle
|
||||
return "Ep. $epNum $complement"
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun getVideoList(url: String, name: String): List<Video> {
|
||||
val prefix = "$name - "
|
||||
return when {
|
||||
url.contains("streamwish") -> {
|
||||
StreamWishExtractor(client, headers).videosFromUrl(url, prefix)
|
||||
}
|
||||
url.contains("vidmoly") -> {
|
||||
VidMolyExtractor(client).getVideoList(url, name)
|
||||
}
|
||||
url.contains("https://vtbe") -> {
|
||||
VtubeExtractor(client, headers).videosFromUrl(url, baseUrl, prefix)
|
||||
}
|
||||
url.contains("wolfstream") -> {
|
||||
WolfstreamExtractor(client).videosFromUrl(url, prefix)
|
||||
}
|
||||
url.contains("filemoon") -> {
|
||||
FilemoonExtractor(client).videosFromUrl(url, prefix, headers)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
super.setupPreferenceScreen(screen) // Quality preferences
|
||||
val videoTypePref = ListPreference(screen.context).apply {
|
||||
key = PREF_TYPE_KEY
|
||||
title = PREF_TYPE_TITLE
|
||||
entries = PREF_TYPE_VALUES
|
||||
entryValues = PREF_TYPE_VALUES
|
||||
setDefaultValue(PREF_TYPE_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 videoServer = 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(videoTypePref)
|
||||
screen.addPreference(videoServer)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(prefQualityKey, prefQualityDefault)!!
|
||||
val type = preferences.getString(PREF_TYPE_KEY, PREF_TYPE_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.startsWith(type, true) },
|
||||
{ it.quality.contains(quality) },
|
||||
{ it.quality.contains(server, true) },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_TYPE_KEY = "preferred_type"
|
||||
private const val PREF_TYPE_TITLE = "Preferred Video Type"
|
||||
private const val PREF_TYPE_DEFAULT = "SUB"
|
||||
private val PREF_TYPE_VALUES = arrayOf("SUB", "RAW")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_TITLE = "Preferred Video Server"
|
||||
private const val PREF_SERVER_DEFAULT = "StreamWish"
|
||||
private val PREF_SERVER_VALUES = arrayOf(
|
||||
"StreamWish",
|
||||
"VidMoly",
|
||||
"Vtube",
|
||||
"WolfStream",
|
||||
"Filemoon",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animenosub.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class VidMolyExtractor(private val client: OkHttpClient) {
|
||||
|
||||
private val regexPlaylist = Regex("file:\"(\\S+?)\"")
|
||||
|
||||
fun getVideoList(url: String, lang: String): List<Video> {
|
||||
val body = client.newCall(GET(url)).execute()
|
||||
.body.string()
|
||||
val playlistUrl = regexPlaylist.find(body)!!.groupValues.get(1)
|
||||
val headers = Headers.headersOf("Referer", "https://vidmoly.to")
|
||||
val playlistData = client.newCall(GET(playlistUrl, headers)).execute()
|
||||
.body.string()
|
||||
|
||||
val separator = "#EXT-X-STREAM-INF:"
|
||||
return playlistData.substringAfter(separator).split(separator).map {
|
||||
val quality = it.substringAfter("RESOLUTION=")
|
||||
.substringAfter("x")
|
||||
.substringBefore(",") + "p"
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
Video(videoUrl, "$lang - $quality", videoUrl, headers)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animenosub.extractors
|
||||
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class VtubeExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||
fun videosFromUrl(url: String, baseUrl: String, prefix: String): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
|
||||
val docHeaders = headers.newBuilder()
|
||||
.add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
|
||||
.add("Host", url.toHttpUrl().host)
|
||||
.add("Referer", "$baseUrl/")
|
||||
.build()
|
||||
val doc = client.newCall(GET(url, headers = docHeaders)).execute().asJsoup()
|
||||
|
||||
val jsEval = doc.selectFirst("script:containsData(m3u8)")!!.data()
|
||||
|
||||
val masterUrl = JsUnpacker.unpackAndCombine(jsEval)
|
||||
?.substringAfter("source")
|
||||
?.substringAfter("file:\"")
|
||||
?.substringBefore("\"")
|
||||
?: return emptyList()
|
||||
|
||||
val playlistHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Host", masterUrl.toHttpUrl().host)
|
||||
.add("Origin", "https://${url.toHttpUrl().host}")
|
||||
.add("Referer", "https://${url.toHttpUrl().host}/")
|
||||
.build()
|
||||
|
||||
val masterPlaylist = client.newCall(
|
||||
GET(masterUrl, headers = playlistHeaders),
|
||||
).execute().body.string()
|
||||
val separator = "#EXT-X-STREAM-INF:"
|
||||
masterPlaylist.substringAfter(separator).split(separator).forEach {
|
||||
val quality = prefix + it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",") + "p "
|
||||
val videoUrl = it.substringAfter("\n").substringBefore("\n")
|
||||
videoList.add(Video(videoUrl, quality, videoUrl, headers = playlistHeaders))
|
||||
}
|
||||
|
||||
return videoList
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animenosub.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class WolfstreamExtractor(private val client: OkHttpClient) {
|
||||
fun videosFromUrl(url: String, prefix: String = ""): List<Video> {
|
||||
val url = client.newCall(
|
||||
GET(url),
|
||||
).execute().asJsoup().selectFirst("script:containsData(sources)")?.let {
|
||||
it.data().substringAfter("{file:\"").substringBefore("\"")
|
||||
} ?: return emptyList()
|
||||
return listOf(
|
||||
Video(url, "${prefix}WolfStream", url),
|
||||
)
|
||||
}
|
||||
}
|
11
src/en/animension/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'Animension'
|
||||
extClass = '.Animension'
|
||||
extVersionCode = 20
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(':lib:dood-extractor'))
|
||||
}
|
BIN
src/en/animension/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/en/animension/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/en/animension/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
src/en/animension/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
src/en/animension/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/en/animension/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 59 KiB |
|
@ -0,0 +1,213 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animension
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
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.lib.doodextractor.DoodExtractor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.float
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class Animension() : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
override val lang = "en"
|
||||
|
||||
override val name = "Animension"
|
||||
|
||||
override val baseUrl = "https://animension.to/"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val apiUrl = "https://animension.to/public-api"
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// Popular
|
||||
override fun popularAnimeRequest(page: Int): Request =
|
||||
GET("$apiUrl/search.php?dub=0&sort=popular-week&page=$page")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
|
||||
val animes = responseJson.map { anime ->
|
||||
val data = anime.jsonArray
|
||||
|
||||
SAnime.create().apply {
|
||||
title = data[0].jsonPrimitive.content
|
||||
url = data[1].jsonPrimitive.content
|
||||
thumbnail_url = data[2].jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
val hasNextPage = responseJson.size >= 25
|
||||
|
||||
return AnimesPage(animes, hasNextPage)
|
||||
}
|
||||
|
||||
// Episode
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
return GET("$apiUrl/episodes.php?id=${anime.url}", headers)
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
|
||||
val episodes = responseJson.map { episode ->
|
||||
val data = episode.jsonArray
|
||||
|
||||
SEpisode.create().apply {
|
||||
name = "Episode ${data[2]}"
|
||||
url = data[1].jsonPrimitive.content
|
||||
episode_number = data[2].jsonPrimitive.float
|
||||
date_upload = data[3].jsonPrimitive.long.toMilli()
|
||||
}
|
||||
}
|
||||
return episodes
|
||||
}
|
||||
|
||||
// Video urls
|
||||
override fun videoListRequest(episode: SEpisode): Request =
|
||||
GET("$apiUrl/episode.php?id=${episode.url}", headers)
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
|
||||
val videos = json.decodeFromString<JsonObject>(responseJson[3].jsonPrimitive.content)
|
||||
val videoList = mutableListOf<Video>()
|
||||
|
||||
for (key in videos.keys.toList()) {
|
||||
val url = videos[key]!!.jsonPrimitive.content
|
||||
when {
|
||||
url.contains("dood") -> {
|
||||
val video = DoodExtractor(client).videoFromUrl(url)
|
||||
if (video != null) {
|
||||
videoList.add(video)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", null)
|
||||
if (quality != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(quality)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
override fun videoUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
// Anime details
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
val anime = SAnime.create()
|
||||
anime.thumbnail_url = document.select("div.thumb img").attr("src")
|
||||
anime.title = document.select("h1.entry-title").text()
|
||||
anime.description = document.select("div.desc").text()
|
||||
anime.genre = document.select("div.genxed span a").joinToString { it.text() }
|
||||
anime.status = parseStatus(document.select("div.spe span:contains(Status)").text().substringAfter("Status: "))
|
||||
return anime
|
||||
}
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$apiUrl/index.php?page=$page&mode=sub")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
|
||||
val animes = responseJson.map { anime ->
|
||||
val data = anime.jsonArray
|
||||
SAnime.create().apply {
|
||||
title = data[0].jsonPrimitive.content
|
||||
url = data[1].jsonPrimitive.content
|
||||
thumbnail_url = data[4].jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
val hasNextPage = responseJson.size >= 25
|
||||
|
||||
return AnimesPage(animes, hasNextPage)
|
||||
}
|
||||
|
||||
// Search
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
|
||||
GET("$apiUrl/search.php?search_text=$query&page=$page", headers)
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val responseJson = json.decodeFromString<JsonArray>(response.body.string())
|
||||
val animes = responseJson.map { anime ->
|
||||
val data = anime.jsonArray
|
||||
|
||||
SAnime.create().apply {
|
||||
title = data[0].jsonPrimitive.content
|
||||
url = data[1].jsonPrimitive.content
|
||||
thumbnail_url = data[2].jsonPrimitive.content
|
||||
}
|
||||
}
|
||||
val hasNextPage = responseJson.size >= 25
|
||||
|
||||
return AnimesPage(animes, hasNextPage)
|
||||
}
|
||||
|
||||
// Utilities
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when (statusString) {
|
||||
"Ongoing" -> SAnime.ONGOING
|
||||
"Finished" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.toMilli(): Long = this * 1000
|
||||
|
||||
// Preferences
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "Doodstream")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "Doodstream")
|
||||
setDefaultValue("1080")
|
||||
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)
|
||||
}
|
||||
}
|
12
src/en/animeowl/build.gradle
Normal file
|
@ -0,0 +1,12 @@
|
|||
ext {
|
||||
extName = 'AnimeOwl'
|
||||
extClass = '.AnimeOwl'
|
||||
extVersionCode = 18
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:synchrony"))
|
||||
implementation(project(":lib:playlist-utils"))
|
||||
}
|
BIN
src/en/animeowl/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
src/en/animeowl/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src/en/animeowl/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/en/animeowl/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/en/animeowl/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/en/animeowl/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 88 KiB |
|
@ -0,0 +1,314 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeowl
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animeextension.en.animeowl.extractors.OwlExtractor
|
||||
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.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parallelFlatMap
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlinx.serialization.json.putJsonArray
|
||||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.math.ceil
|
||||
|
||||
@ExperimentalSerializationApi
|
||||
class AnimeOwl : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val name = "AnimeOwl"
|
||||
|
||||
override val baseUrl = "https://animeowl.us"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val owlServersExtractor by lazy { OwlExtractor(client, baseUrl) }
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending?page=$page")
|
||||
|
||||
override fun popularAnimeSelector(): String = "div#anime-list > div"
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "ul.pagination > li > a[rel=next]"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.select("a.title-link").attr("href"))
|
||||
thumbnail_url = element.select("img[data-src]").attr("data-src")
|
||||
title = element.select("a.title-link h3").text()
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override suspend fun getLatestUpdates(page: Int): AnimesPage =
|
||||
advancedSearchAnime(page, sort = Sort.Latest)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// =============================== Search ===============================
|
||||
override suspend fun getSearchAnime(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: AnimeFilterList,
|
||||
): AnimesPage = advancedSearchAnime(page, sort = Sort.Search, query = query)
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime =
|
||||
throw UnsupportedOperationException()
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
||||
genre = document.select("div.genre > a").joinToString { it.text() }
|
||||
author = document.select("div.type > a").text()
|
||||
status = parseStatus(document.select("div.status > span").text())
|
||||
description = buildString {
|
||||
document.select("div.anime-desc.desc-content").text()
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let {
|
||||
appendLine(it)
|
||||
appendLine()
|
||||
}
|
||||
document.select("h4.anime-alternatives").text()
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.let {
|
||||
append("Other name(s): ")
|
||||
append(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val animeId = response.asJsoup().select("div#unq-anime-id").attr("animeId")
|
||||
val episodes = client.newCall(
|
||||
GET("$baseUrl/api/anime/$animeId/episodes"),
|
||||
).execute()
|
||||
.parseAs<EpisodeResponse>()
|
||||
|
||||
return listOf(
|
||||
episodes.sub.map { it.copy(lang = "Sub") },
|
||||
episodes.dub.map { it.copy(lang = "Dub") },
|
||||
).flatten()
|
||||
.groupBy { it.name }
|
||||
.map { (epNum, epList) ->
|
||||
SEpisode.create().apply {
|
||||
url = LinkData(
|
||||
epList.map { ep ->
|
||||
Link(
|
||||
ep.buildUrl(episodes.subSlug, episodes.dubSlug),
|
||||
ep.lang!!,
|
||||
)
|
||||
},
|
||||
).toJsonString()
|
||||
episode_number = epNum.toFloatOrNull() ?: 0F
|
||||
name = "Episode $epNum"
|
||||
}
|
||||
}
|
||||
.sortedByDescending { it.episode_number }
|
||||
}
|
||||
|
||||
override fun episodeListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> =
|
||||
json.decodeFromString<LinkData>(episode.url)
|
||||
.links.parallelFlatMap { owlServersExtractor.extractOwlVideo(it) }.sort()
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoListSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
enum class Sort(val code: String) {
|
||||
Latest("1"),
|
||||
Search("4"),
|
||||
}
|
||||
|
||||
private fun advancedSearchAnime(
|
||||
page: Int,
|
||||
sort: Sort,
|
||||
query: String? = "",
|
||||
limit: Int? = 30,
|
||||
): AnimesPage {
|
||||
val body = buildJsonObject {
|
||||
put("lang22", 3)
|
||||
put("value", query)
|
||||
put("sortt", sort.code)
|
||||
put("limit", limit)
|
||||
put("page", page - 1)
|
||||
putJsonObject("selected") {
|
||||
putJsonArray("type") { emptyList<String>() }
|
||||
putJsonArray("sort") { emptyList<String>() }
|
||||
putJsonArray("year") { emptyList<String>() }
|
||||
putJsonArray("genre") { emptyList<String>() }
|
||||
putJsonArray("season") { emptyList<String>() }
|
||||
putJsonArray("status") { emptyList<String>() }
|
||||
putJsonArray("country") { emptyList<String>() }
|
||||
putJsonArray("language") { emptyList<String>() }
|
||||
}
|
||||
}.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val result = client.newCall(
|
||||
POST("$baseUrl/api/advance-search", body = body, headers = headers),
|
||||
).execute()
|
||||
.parseAs<SearchResponse>()
|
||||
|
||||
val nextPage = ceil(result.total.toFloat() / limit!!).toInt() > page
|
||||
val animes = result.results.map { anime ->
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain("/anime/${anime.animeSlug}?mal=${anime.malId}")
|
||||
thumbnail_url = "$baseUrl${anime.image}"
|
||||
title = anime.animeName
|
||||
}
|
||||
}
|
||||
return AnimesPage(animes, nextPage)
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val lang = preferences.getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareByDescending<Video> { it.quality.contains(lang) }
|
||||
.thenByDescending { it.quality.contains(quality) }
|
||||
.thenByDescending { it.quality.contains(server, true) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun LinkData.toJsonString(): String {
|
||||
return json.encodeToString(this)
|
||||
}
|
||||
|
||||
private fun EpisodeResponse.Episode.buildUrl(subSlug: String, dubSlug: String): String =
|
||||
when (lang) {
|
||||
"dub" -> dubSlug
|
||||
else -> subSlug
|
||||
}.let { "$baseUrl/watch/$it/$episodeIndex" }
|
||||
|
||||
private fun parseStatus(statusString: String): Int =
|
||||
when (statusString) {
|
||||
"Currently Airing", "Not yet aired" -> SAnime.ONGOING
|
||||
"Finished Airing" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_LIST
|
||||
entryValues = PREF_QUALITY_LIST
|
||||
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_LANG_KEY
|
||||
title = PREF_LANG_TITLE
|
||||
entries = PREF_LANG_TYPES
|
||||
entryValues = PREF_LANG_TYPES
|
||||
setDefaultValue(PREF_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_SERVER_KEY
|
||||
title = PREF_SERVER_TITLE
|
||||
entries = PREF_SERVER_LIST
|
||||
entryValues = PREF_SERVER_LIST
|
||||
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)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF_LANG_KEY = "preferred_language"
|
||||
private const val PREF_LANG_TITLE = "Preferred type"
|
||||
private const val PREF_LANG_DEFAULT = "Sub"
|
||||
private val PREF_LANG_TYPES = arrayOf("Sub", "Dub")
|
||||
|
||||
private const val PREF_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_TITLE = "Preferred server"
|
||||
private const val PREF_SERVER_DEFAULT = "Luffy"
|
||||
private val PREF_SERVER_LIST = arrayOf("Luffy", "Kaido", "Boa")
|
||||
|
||||
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_LIST = arrayOf("1080p", "720p", "480p", "360p")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeowl
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SearchResponse(
|
||||
val total: Int,
|
||||
val results: List<Result>,
|
||||
) {
|
||||
@Serializable
|
||||
data class Result(
|
||||
@SerialName("mal_id")
|
||||
val malId: Int,
|
||||
@SerialName("anime_name")
|
||||
val animeName: String,
|
||||
@SerialName("anime_slug")
|
||||
val animeSlug: String,
|
||||
val image: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class EpisodeResponse(
|
||||
val sub: List<Episode>,
|
||||
val dub: List<Episode>,
|
||||
@SerialName("sub_slug")
|
||||
val subSlug: String,
|
||||
@SerialName("dub_slug")
|
||||
val dubSlug: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class Episode(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val lang: String? = null,
|
||||
@SerialName("episode_index")
|
||||
val episodeIndex: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LinkData(
|
||||
val links: List<Link>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Link(
|
||||
val url: String,
|
||||
val lang: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OwlServers(
|
||||
val kaido: String? = null,
|
||||
val luffy: String? = null,
|
||||
val zoro: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Stream(
|
||||
val url: String,
|
||||
)
|
|
@ -0,0 +1,87 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animeowl.extractors
|
||||
|
||||
import eu.kanade.tachiyomi.animeextension.en.animeowl.Link
|
||||
import eu.kanade.tachiyomi.animeextension.en.animeowl.OwlServers
|
||||
import eu.kanade.tachiyomi.animeextension.en.animeowl.Stream
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||
import eu.kanade.tachiyomi.lib.synchrony.Deobfuscator
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
class OwlExtractor(private val client: OkHttpClient, private val baseUrl: String) {
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client) }
|
||||
|
||||
private val noRedirectClient by lazy {
|
||||
client.newBuilder()
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun extractOwlVideo(link: Link): List<Video> {
|
||||
val dataSrc = client.newCall(GET(link.url)).execute()
|
||||
.asJsoup()
|
||||
.select("button#hot-anime-tab")
|
||||
.attr("data-source")
|
||||
val epJS = dataSrc.substringAfterLast("/")
|
||||
.let {
|
||||
client.newCall(GET("$baseUrl/players/$it.v2.js")).execute().body.string()
|
||||
}
|
||||
.let(Deobfuscator::deobfuscateScript)
|
||||
?: throw Exception("Unable to get clean JS")
|
||||
val jwt = JWT_REGEX.find(epJS)?.groupValues?.get(1) ?: throw Exception("Unable to get jwt")
|
||||
|
||||
val videoList = mutableListOf<Video>()
|
||||
val servers = client.newCall(GET("$baseUrl$dataSrc")).execute()
|
||||
.parseAs<OwlServers>()
|
||||
|
||||
coroutineScope {
|
||||
val lufDeferred = async {
|
||||
servers.luffy?.let { luffy ->
|
||||
noRedirectClient.newCall(GET("${luffy}$jwt")).execute()
|
||||
.use { it.headers["Location"] }
|
||||
?.let { videoList.add(Video(it, "Luffy - ${link.lang} - 1080p", it)) }
|
||||
}
|
||||
}
|
||||
val kaiDeferred = async {
|
||||
servers.kaido?.let {
|
||||
videoList.addAll(
|
||||
getHLS("${it}$jwt", "Kaido", link.lang),
|
||||
)
|
||||
}
|
||||
}
|
||||
val zorDeferred = async {
|
||||
servers.zoro?.let {
|
||||
videoList.addAll(
|
||||
getHLS("${it}$jwt", "Boa", link.lang),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
awaitAll(lufDeferred, kaiDeferred, zorDeferred)
|
||||
}
|
||||
return videoList
|
||||
}
|
||||
|
||||
private fun getHLS(url: String, server: String, lang: String): List<Video> =
|
||||
client.newCall(GET(url)).execute()
|
||||
.parseAs<Stream>()
|
||||
.url
|
||||
.let {
|
||||
playlistUtils.extractFromHls(
|
||||
it,
|
||||
videoNameGen = { qty -> "$server - $lang - $qty" },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val JWT_REGEX by lazy { "const\\s+(?:[A-Za-z0-9_]*)\\s*=\\s*'([^']+)'".toRegex() }
|
||||
}
|
||||
}
|
11
src/en/animepahe/build.gradle
Normal file
|
@ -0,0 +1,11 @@
|
|||
ext {
|
||||
extName = 'AnimePahe'
|
||||
extClass = '.AnimePahe'
|
||||
extVersionCode = 27
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
||||
}
|
BIN
src/en/animepahe/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
src/en/animepahe/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 857 B |
BIN
src/en/animepahe/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/en/animepahe/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/en/animepahe/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.7 KiB |
BIN
src/en/animepahe/res/web_hi_res_512.png
Normal file
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,381 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animepahe
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.animeextension.en.animepahe.dto.EpisodeDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.animepahe.dto.LatestAnimeDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.animepahe.dto.ResponseDto
|
||||
import eu.kanade.tachiyomi.animeextension.en.animepahe.dto.SearchResultDto
|
||||
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.util.asJsoup
|
||||
import eu.kanade.tachiyomi.util.parseAs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
class AnimePahe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val interceptor = DdosGuardInterceptor(network.client)
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.build()
|
||||
|
||||
override val name = "AnimePahe"
|
||||
|
||||
override val baseUrl by lazy {
|
||||
preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
|
||||
}
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
/**
|
||||
* This override is necessary because AnimePahe does not provide permanent
|
||||
* URLs to its animes, so we need to fetch the anime session every time.
|
||||
*
|
||||
* @see episodeListRequest
|
||||
*/
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
val animeId = anime.getId()
|
||||
// We're using coroutines here to run it inside another thread and
|
||||
// prevent android.os.NetworkOnMainThreadException when trying to open
|
||||
// webview or share it.
|
||||
val session = runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
fetchSession(anime.title, animeId)
|
||||
}
|
||||
}
|
||||
return GET("$baseUrl/anime/$session?anime_id=$animeId")
|
||||
}
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime {
|
||||
val document = response.asJsoup()
|
||||
return SAnime.create().apply {
|
||||
title = document.selectFirst("div.title-wrapper > h1 > span")!!.text()
|
||||
author = document.selectFirst("div.col-sm-4.anime-info p:contains(Studio:)")
|
||||
?.text()
|
||||
?.replace("Studio: ", "")
|
||||
status = parseStatus(document.selectFirst("div.col-sm-4.anime-info p:contains(Status:) a")!!.text())
|
||||
thumbnail_url = document.selectFirst("div.anime-poster a")!!.attr("href")
|
||||
genre = document.select("div.anime-genre ul li").joinToString { it.text() }
|
||||
val synonyms = document.selectFirst("div.col-sm-4.anime-info p:contains(Synonyms:)")
|
||||
?.text()
|
||||
description = document.select("div.anime-summary").text() +
|
||||
if (synonyms.isNullOrEmpty()) "" else "\n\n$synonyms"
|
||||
}
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/api?m=airing&page=$page")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val latestData = response.parseAs<ResponseDto<LatestAnimeDto>>()
|
||||
val hasNextPage = latestData.currentPage < latestData.lastPage
|
||||
val animeList = latestData.items.map { anime ->
|
||||
SAnime.create().apply {
|
||||
title = anime.title
|
||||
thumbnail_url = anime.snapshot
|
||||
val animeId = anime.id
|
||||
setUrlWithoutDomain("/anime/?anime_id=$animeId")
|
||||
artist = anime.fansub
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request =
|
||||
GET("$baseUrl/api?m=search&l=8&q=$query")
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
val searchData = response.parseAs<ResponseDto<SearchResultDto>>()
|
||||
val animeList = searchData.items.map { anime ->
|
||||
SAnime.create().apply {
|
||||
title = anime.title
|
||||
thumbnail_url = anime.poster
|
||||
val animeId = anime.id
|
||||
setUrlWithoutDomain("/anime/?anime_id=$animeId")
|
||||
}
|
||||
}
|
||||
return AnimesPage(animeList, false)
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
// This source doesnt have a popular animes page,
|
||||
// so we use latest animes page instead.
|
||||
override suspend fun getPopularAnime(page: Int) = getLatestUpdates(page)
|
||||
override fun popularAnimeParse(response: Response): AnimesPage = TODO()
|
||||
override fun popularAnimeRequest(page: Int): Request = TODO()
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
/**
|
||||
* This override is necessary because AnimePahe does not provide permanent
|
||||
* URLs to its animes, so we need to fetch the anime session every time.
|
||||
*
|
||||
* @see animeDetailsRequest
|
||||
*/
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val session = fetchSession(anime.title, anime.getId())
|
||||
return GET("$baseUrl/api?m=release&id=$session&sort=episode_desc&page=1")
|
||||
}
|
||||
|
||||
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||
val url = response.request.url.toString()
|
||||
val session = url.substringAfter("&id=").substringBefore("&")
|
||||
return recursivePages(response, session)
|
||||
}
|
||||
|
||||
private fun parseEpisodePage(episodes: List<EpisodeDto>, animeSession: String): MutableList<SEpisode> {
|
||||
return episodes.map { episode ->
|
||||
SEpisode.create().apply {
|
||||
date_upload = episode.createdAt.toDate()
|
||||
val session = episode.session
|
||||
setUrlWithoutDomain("/play/$animeSession/$session")
|
||||
val epNum = episode.episodeNumber
|
||||
episode_number = epNum
|
||||
val epName = if (floor(epNum) == ceil(epNum)) {
|
||||
epNum.toInt().toString()
|
||||
} else {
|
||||
epNum.toString()
|
||||
}
|
||||
name = "Episode $epName"
|
||||
}
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
private fun recursivePages(response: Response, animeSession: String): List<SEpisode> {
|
||||
val episodesData = response.parseAs<ResponseDto<EpisodeDto>>()
|
||||
val page = episodesData.currentPage
|
||||
val hasNextPage = page < episodesData.lastPage
|
||||
val returnList = parseEpisodePage(episodesData.items, animeSession)
|
||||
if (hasNextPage) {
|
||||
val nextPage = nextPageRequest(response.request.url.toString(), page + 1)
|
||||
returnList += recursivePages(nextPage, animeSession)
|
||||
}
|
||||
return returnList
|
||||
}
|
||||
|
||||
private fun nextPageRequest(url: String, page: Int): Response {
|
||||
val request = GET(url.substringBeforeLast("&page=") + "&page=$page")
|
||||
return client.newCall(request).execute()
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val document = response.asJsoup()
|
||||
val downloadLinks = document.select("div#pickDownload > a")
|
||||
return document.select("div#resolutionMenu > button").mapIndexed { index, btn ->
|
||||
val kwikLink = btn.attr("data-src")
|
||||
val quality = btn.text()
|
||||
val paheWinLink = downloadLinks[index].attr("href")
|
||||
getVideo(paheWinLink, kwikLink, quality)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getVideo(paheUrl: String, kwikUrl: String, quality: String): Video {
|
||||
return if (preferences.getBoolean(PREF_LINK_TYPE_KEY, PREF_LINK_TYPE_DEFAULT)) {
|
||||
val videoUrl = KwikExtractor(client).getHlsStreamUrl(kwikUrl, referer = baseUrl)
|
||||
Video(
|
||||
videoUrl,
|
||||
quality,
|
||||
videoUrl,
|
||||
headers = Headers.headersOf("referer", "https://kwik.cx"),
|
||||
)
|
||||
} else {
|
||||
val videoUrl = KwikExtractor(client).getStreamUrlFromKwik(paheUrl)
|
||||
Video(videoUrl, quality, videoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val subPreference = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val shouldBeAv1 = preferences.getBoolean(PREF_AV1_KEY, PREF_AV1_DEFAULT)
|
||||
val shouldEndWithEng = subPreference == "eng"
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
{ it.quality.contains(quality) },
|
||||
{ Regex("""\beng\b""").containsMatchIn(it.quality.lowercase()) == shouldEndWithEng },
|
||||
{ it.quality.lowercase().contains("av1") == shouldBeAv1 },
|
||||
),
|
||||
).reversed()
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val videoQualityPref = 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()
|
||||
}
|
||||
}
|
||||
val domainPref = ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = PREF_DOMAIN_TITLE
|
||||
entries = PREF_DOMAIN_ENTRIES
|
||||
entryValues = PREF_DOMAIN_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()
|
||||
}
|
||||
}
|
||||
val subPref = ListPreference(screen.context).apply {
|
||||
key = PREF_SUB_KEY
|
||||
title = PREF_SUB_TITLE
|
||||
entries = PREF_SUB_ENTRIES
|
||||
entryValues = PREF_SUB_VALUES
|
||||
setDefaultValue(PREF_SUB_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 linkPref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_LINK_TYPE_KEY
|
||||
title = PREF_LINK_TYPE_TITLE
|
||||
summary = PREF_LINK_TYPE_SUMMARY
|
||||
setDefaultValue(PREF_LINK_TYPE_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}
|
||||
val av1Pref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_AV1_KEY
|
||||
title = PREF_AV1_TITLE
|
||||
summary = PREF_AV1_SUMMARY
|
||||
setDefaultValue(PREF_AV1_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
screen.addPreference(domainPref)
|
||||
screen.addPreference(subPref)
|
||||
screen.addPreference(linkPref)
|
||||
screen.addPreference(av1Pref)
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
private fun fetchSession(title: String, animeId: String): String {
|
||||
return client.newCall(GET("$baseUrl/api?m=search&q=$title"))
|
||||
.execute()
|
||||
.body.string()
|
||||
.substringAfter("\"id\":$animeId")
|
||||
.substringAfter("\"session\":\"")
|
||||
.substringBefore("\"")
|
||||
}
|
||||
|
||||
private fun parseStatus(statusString: String): Int {
|
||||
return when (statusString) {
|
||||
"Currently Airing" -> SAnime.ONGOING
|
||||
"Finished Airing" -> SAnime.COMPLETED
|
||||
else -> SAnime.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun SAnime.getId() = url.substringAfterLast("?anime_id=").substringBefore("\"")
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
return runCatching {
|
||||
DATE_FORMATTER.parse(this)?.time ?: 0L
|
||||
}.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preffered_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private const val PREF_QUALITY_DEFAULT = "1080p"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "360p")
|
||||
|
||||
private const val PREF_DOMAIN_KEY = "preffered_domain"
|
||||
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
|
||||
private const val PREF_DOMAIN_DEFAULT = "https://animepahe.com"
|
||||
private val PREF_DOMAIN_ENTRIES = arrayOf("animepahe.com", "animepahe.ru", "animepahe.org")
|
||||
private val PREF_DOMAIN_VALUES by lazy {
|
||||
PREF_DOMAIN_ENTRIES.map { "https://" + it }.toTypedArray()
|
||||
}
|
||||
|
||||
private const val PREF_SUB_KEY = "preffered_sub"
|
||||
private const val PREF_SUB_TITLE = "Prefer subs or dubs?"
|
||||
private const val PREF_SUB_DEFAULT = "jpn"
|
||||
private val PREF_SUB_ENTRIES = arrayOf("sub", "dub")
|
||||
private val PREF_SUB_VALUES = arrayOf("jpn", "eng")
|
||||
|
||||
private const val PREF_LINK_TYPE_KEY = "preffered_link_type"
|
||||
private const val PREF_LINK_TYPE_TITLE = "Use HLS links"
|
||||
private const val PREF_LINK_TYPE_DEFAULT = false
|
||||
private val PREF_LINK_TYPE_SUMMARY by lazy {
|
||||
"""Enable this if you are having Cloudflare issues.
|
||||
|Note that this will break the ability to seek inside of the video unless the episode is downloaded in advance.
|
||||
""".trimMargin()
|
||||
}
|
||||
|
||||
// Big slap to whoever misspelled `preferred`
|
||||
private const val PREF_AV1_KEY = "preffered_av1"
|
||||
private const val PREF_AV1_TITLE = "Use AV1 codec"
|
||||
private const val PREF_AV1_DEFAULT = false
|
||||
private val PREF_AV1_SUMMARY by lazy {
|
||||
"""Enable to use AV1 if available
|
||||
|Turn off to never select av1 as preferred codec
|
||||
""".trimMargin()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animepahe
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
|
||||
class DdosGuardInterceptor(private val client: OkHttpClient) : Interceptor {
|
||||
|
||||
private val cookieManager by lazy { CookieManager.getInstance() }
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
val response = chain.proceed(originalRequest)
|
||||
|
||||
// Check if DDos-GUARD is on
|
||||
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
|
||||
return response
|
||||
}
|
||||
|
||||
response.close()
|
||||
val cookies = cookieManager.getCookie(originalRequest.url.toString())
|
||||
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(originalRequest.url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
|
||||
if (!ddg2Cookie?.value.isNullOrEmpty()) {
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
|
||||
val newCookie = getNewCookie(originalRequest.url) ?: return chain.proceed(originalRequest)
|
||||
val newCookieHeader = (oldCookie + newCookie).joinToString("; ") {
|
||||
"${it.name}=${it.value}"
|
||||
}
|
||||
|
||||
return chain.proceed(originalRequest.newBuilder().addHeader("cookie", newCookieHeader).build())
|
||||
}
|
||||
|
||||
fun getNewCookie(url: HttpUrl): Cookie? {
|
||||
val cookies = cookieManager.getCookie(url.toString())
|
||||
val oldCookie = if (cookies != null && cookies.isNotEmpty()) {
|
||||
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val ddg2Cookie = oldCookie.firstOrNull { it.name == "__ddg2_" }
|
||||
if (!ddg2Cookie?.value.isNullOrEmpty()) {
|
||||
return ddg2Cookie
|
||||
}
|
||||
val wellKnown = client.newCall(GET("https://check.ddos-guard.net/check.js"))
|
||||
.execute().body.string()
|
||||
.substringAfter("'", "")
|
||||
.substringBefore("'", "")
|
||||
val checkUrl = "${url.scheme}://${url.host + wellKnown}"
|
||||
return client.newCall(GET(checkUrl)).execute().header("set-cookie")?.let {
|
||||
Cookie.parse(url, it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ERROR_CODES = listOf(403)
|
||||
private val SERVER_CHECK = listOf("ddos-guard")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/** The following file is slightly modified and taken from: https://github.com/LagradOst/CloudStream-3/blob/4d6050219083d675ba9c7088b59a9492fcaa32c7/app/src/main/java/com/lagradost/cloudstream3/animeproviders/AnimePaheProvider.kt
|
||||
* It is published under the following license:
|
||||
*
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Osten
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package eu.kanade.tachiyomi.animeextension.en.animepahe
|
||||
|
||||
import dev.datlag.jsunpacker.JsUnpacker
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import kotlin.math.pow
|
||||
|
||||
class KwikExtractor(private val client: OkHttpClient) {
|
||||
private var cookies: String = ""
|
||||
|
||||
private val kwikParamsRegex = Regex("""\("(\w+)",\d+,"(\w+)",(\d+),(\d+),\d+\)""")
|
||||
private val kwikDUrl = Regex("action=\"([^\"]+)\"")
|
||||
private val kwikDToken = Regex("value=\"([^\"]+)\"")
|
||||
|
||||
private fun isNumber(s: String?): Boolean {
|
||||
return s?.toIntOrNull() != null
|
||||
}
|
||||
|
||||
fun getHlsStreamUrl(kwikUrl: String, referer: String): String {
|
||||
val eContent = client.newCall(GET(kwikUrl, Headers.headersOf("referer", referer)))
|
||||
.execute().asJsoup()
|
||||
val script = eContent.selectFirst("script:containsData(eval\\(function)")!!.data().substringAfterLast("eval(function(")
|
||||
val unpacked = JsUnpacker.unpackAndCombine("eval(function($script")!!
|
||||
return unpacked.substringAfter("const source=\\'").substringBefore("\\';")
|
||||
}
|
||||
|
||||
fun getStreamUrlFromKwik(paheUrl: String): String {
|
||||
val noRedirects = client.newBuilder()
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(false)
|
||||
.build()
|
||||
val kwikUrl = "https://" + noRedirects.newCall(GET("$paheUrl/i")).execute()
|
||||
.header("location")!!.substringAfterLast("https://")
|
||||
val fContent =
|
||||
client.newCall(GET(kwikUrl, Headers.headersOf("referer", "https://kwik.cx/"))).execute()
|
||||
cookies += fContent.header("set-cookie")!!
|
||||
val fContentString = fContent.body.string()
|
||||
|
||||
val (fullString, key, v1, v2) = kwikParamsRegex.find(fContentString)!!.destructured
|
||||
val decrypted = decrypt(fullString, key, v1.toInt(), v2.toInt())
|
||||
val uri = kwikDUrl.find(decrypted)!!.destructured.component1()
|
||||
val tok = kwikDToken.find(decrypted)!!.destructured.component1()
|
||||
var content: Response? = null
|
||||
|
||||
var code = 419
|
||||
var tries = 0
|
||||
|
||||
val noRedirectClient = OkHttpClient().newBuilder()
|
||||
.followRedirects(false)
|
||||
.followSslRedirects(false)
|
||||
.cookieJar(client.cookieJar)
|
||||
.build()
|
||||
|
||||
while (code != 302 && tries < 20) {
|
||||
content = noRedirectClient.newCall(
|
||||
POST(
|
||||
uri,
|
||||
Headers.headersOf(
|
||||
"referer",
|
||||
fContent.request.url.toString(),
|
||||
"cookie",
|
||||
fContent.header("set-cookie")!!.replace("path=/;", ""),
|
||||
),
|
||||
FormBody.Builder().add("_token", tok).build(),
|
||||
),
|
||||
).execute()
|
||||
code = content.code
|
||||
++tries
|
||||
}
|
||||
if (tries > 19) {
|
||||
throw Exception("Failed to extract the stream uri from kwik.")
|
||||
}
|
||||
val location = content?.header("location").toString()
|
||||
content?.close()
|
||||
return location
|
||||
}
|
||||
|
||||
private fun decrypt(fullString: String, key: String, v1: Int, v2: Int): String {
|
||||
var r = ""
|
||||
var i = 0
|
||||
|
||||
while (i < fullString.length) {
|
||||
var s = ""
|
||||
|
||||
while (fullString[i] != key[v2]) {
|
||||
s += fullString[i]
|
||||
++i
|
||||
}
|
||||
var j = 0
|
||||
|
||||
while (j < key.length) {
|
||||
s = s.replace(key[j].toString(), j.toString())
|
||||
++j
|
||||
}
|
||||
r += (getString(s, v2).toInt() - v1).toChar()
|
||||
++i
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
private fun getString(content: String, s1: Int): String {
|
||||
val s2 = 10
|
||||
val characterMap = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
|
||||
|
||||
val slice2 = characterMap.slice(0 until s2)
|
||||
var acc: Long = 0
|
||||
|
||||
for ((n, i) in content.reversed().withIndex()) {
|
||||
acc += when (isNumber("$i")) {
|
||||
true -> "$i".toLong()
|
||||
false -> 0L
|
||||
} * s1.toDouble().pow(n.toDouble()).toInt()
|
||||
}
|
||||
|
||||
var k = ""
|
||||
|
||||
while (acc > 0) {
|
||||
k = slice2[(acc % s2).toInt()] + k
|
||||
acc = (acc - (acc % s2)) / s2
|
||||
}
|
||||
|
||||
return when (k != "") {
|
||||
true -> k
|
||||
false -> "0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package eu.kanade.tachiyomi.animeextension.en.animepahe.dto
|
||||
|
||||
import kotlinx.serialization.EncodeDefault
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ResponseDto<T>(
|
||||
@SerialName("current_page")
|
||||
val currentPage: Int,
|
||||
@SerialName("last_page")
|
||||
val lastPage: Int,
|
||||
@EncodeDefault
|
||||
@SerialName("data")
|
||||
val items: List<T> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LatestAnimeDto(
|
||||
@SerialName("anime_title")
|
||||
val title: String,
|
||||
val snapshot: String,
|
||||
@SerialName("anime_id")
|
||||
val id: Int,
|
||||
val fansub: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SearchResultDto(
|
||||
val title: String,
|
||||
val poster: String,
|
||||
val id: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EpisodeDto(
|
||||
@SerialName("created_at")
|
||||
val createdAt: String,
|
||||
val session: String,
|
||||
@SerialName("episode")
|
||||
val episodeNumber: Float,
|
||||
)
|
7
src/en/animeparadise/build.gradle
Normal file
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'AnimeParadise'
|
||||
extClass = '.AnimeParadise'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/en/animeparadise/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/en/animeparadise/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.1 KiB |