refactor(en): Refactor some English extensions (#1726)

This commit is contained in:
Secozzi
2023-06-14 20:22:18 +02:00
committed by GitHub
parent 9beb40f9ef
commit 31f057a705
34 changed files with 1509 additions and 1843 deletions

View File

@ -26,6 +26,10 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -39,7 +43,7 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "AllAnime" override val name = "AllAnime"
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! } override val baseUrl by lazy { preferences.baseUrl }
override val lang = "en" override val lang = "en"
@ -56,7 +60,12 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request {
val variables = """{"type":"anime","size":$PAGE_SIZE,"dateRange":7,"page":$page}""" val variables = buildJsonObject {
put("type", "anime")
put("size", PAGE_SIZE)
put("dateRange", 7)
put("page", page)
}
return GET("$baseUrl/allanimeapi?variables=$variables&query=$POPULAR_QUERY", headers = headers) return GET("$baseUrl/allanimeapi?variables=$variables&query=$POPULAR_QUERY", headers = headers)
} }
@ -64,13 +73,11 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
val parsed = json.decodeFromString<PopularResult>(response.body.string()) val parsed = json.decodeFromString<PopularResult>(response.body.string())
val animeList = mutableListOf<SAnime>() val animeList = mutableListOf<SAnime>()
val titleStyle = preferences.getString(PREF_TITLE_STYLE_KEY, PREF_TITLE_STYLE_DEFAULT)!!
parsed.data.queryPopular.recommendations.forEach { parsed.data.queryPopular.recommendations.forEach {
if (it.anyCard != null) { if (it.anyCard != null) {
animeList.add( animeList.add(
SAnime.create().apply { SAnime.create().apply {
title = when (titleStyle) { title = when (preferences.titleStyle) {
"romaji" -> it.anyCard.name "romaji" -> it.anyCard.name
"eng" -> it.anyCard.englishName ?: it.anyCard.name "eng" -> it.anyCard.englishName ?: it.anyCard.name
else -> it.anyCard.nativeName ?: it.anyCard.name else -> it.anyCard.nativeName ?: it.anyCard.name
@ -88,15 +95,20 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
// Could be lazily loaded along with url, but would require user to restart val variables = buildJsonObject {
val subPref = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!! putJsonObject("search") {
val variables = """{"search":{"allowAdult":false,"allowUnknown":false},"limit":$PAGE_SIZE,"page":$page,"translationType":"$subPref","countryOrigin":"ALL"}""" put("allowAdult", false)
put("allowUnknown", false)
}
put("limit", PAGE_SIZE)
put("page", page)
put("translationType", preferences.subPref)
put("countryOrigin", "ALL")
}
return GET("$baseUrl/allanimeapi?variables=$variables&query=$SEARCH_QUERY", headers = headers) return GET("$baseUrl/allanimeapi?variables=$variables&query=$SEARCH_QUERY", headers = headers)
} }
override fun latestUpdatesParse(response: Response): AnimesPage { override fun latestUpdatesParse(response: Response): AnimesPage = parseAnime(response)
return parseAnime(response)
}
// =============================== Search =============================== // =============================== Search ===============================
@ -112,84 +124,105 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used") override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: AllAnimeFilters.FilterSearchParams): Request { private fun searchAnimeRequest(page: Int, query: String, filters: AllAnimeFilters.FilterSearchParams): Request {
val subPref = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
return if (query.isNotEmpty()) { return if (query.isNotEmpty()) {
val variables = """{"search":{"query":"$query","allowAdult":false,"allowUnknown":false},"limit":$PAGE_SIZE,"page":$page,"translationType":"$subPref","countryOrigin":"ALL"}""" val variables = buildJsonObject {
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")
}
GET("$baseUrl/allanimeapi?variables=$variables&query=$SEARCH_QUERY", headers = headers) GET("$baseUrl/allanimeapi?variables=$variables&query=$SEARCH_QUERY", headers = headers)
} else { } else {
val seasonString = if (filters.season == "all") "" else ""","season":"${filters.season}"""" val variables = buildJsonObject {
val yearString = if (filters.releaseYear == "all") "" else ""","year":${filters.releaseYear}""" putJsonObject("search") {
val genresString = if (filters.genres == "all") "" else ""","genres":${filters.genres},"excludeGenres":[]""" put("allowAdult", false)
val typesString = if (filters.types == "all") "" else ""","types":${filters.types}""" put("allowUnknown", false)
val sortByString = if (filters.sortBy == "update") "" else ""","sortBy":"${filters.sortBy}"""" if (filters.season != "all") put("season", filters.season)
if (filters.releaseYear != "all") put("year", filters.releaseYear.toInt())
var variables = """{"search":{"allowAdult":false,"allowUnknown":false$seasonString$yearString$genresString$typesString$sortByString""" if (filters.genres != "all") {
variables += """},"limit":$PAGE_SIZE,"page":$page,"translationType":"$subPref","countryOrigin":"${filters.origin}"}""" 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)
}
GET("$baseUrl/allanimeapi?variables=$variables&query=$SEARCH_QUERY", headers = headers) GET("$baseUrl/allanimeapi?variables=$variables&query=$SEARCH_QUERY", headers = headers)
} }
} }
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage = parseAnime(response)
return parseAnime(response)
} // ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AllAnimeFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = AllAnimeFilters.FILTER_LIST
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(response: Response): SAnime = throw Exception("Not used")
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return client.newCall(animeDetailsRequestInternal(anime)) return client.newCall(animeDetailsRequestInternal(anime))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
animeDetailsParse(response, anime).apply { initialized = true } animeDetailsParse(response).apply { initialized = true }
} }
} }
private fun animeDetailsRequestInternal(anime: SAnime): Request { private fun animeDetailsRequestInternal(anime: SAnime): Request {
val variables = """{"_id":"${anime.url.split("<&sep>").first()}"}""" val variables = buildJsonObject {
put("_id", anime.url.split("<&sep>").first())
}
return GET("$baseUrl/allanimeapi?variables=$variables&query=$DETAILS_QUERY", headers = headers) return GET("$baseUrl/allanimeapi?variables=$variables&query=$DETAILS_QUERY", headers = headers)
} }
override fun animeDetailsRequest(anime: SAnime): Request { override fun animeDetailsRequest(anime: SAnime): Request {
val (id, time, slug) = anime.url.split("<&sep>") val (id, time, slug) = anime.url.split("<&sep>")
val slugTime = if (time.isNotEmpty()) { val slugTime = if (time.isNotEmpty()) "-st-$time" else time
"-st-$time" val siteUrl = preferences.siteUrl
} else {
time
}
val siteUrl = preferences.getString(PREF_SITE_DOMAIN_KEY, PREF_SITE_DOMAIN_DEFAULT)!!
return GET("$siteUrl/anime/$id/$slug$slugTime") return GET("$siteUrl/anime/$id/$slug$slugTime")
} }
private fun animeDetailsParse(response: Response, animeOld: SAnime): SAnime { override fun animeDetailsParse(response: Response): SAnime {
val show = json.decodeFromString<DetailsResult>(response.body.string()).data.show val show = json.decodeFromString<DetailsResult>(response.body.string()).data.show
return SAnime.create().apply { return SAnime.create().apply {
title = animeOld.title
genre = show.genres?.joinToString(separator = ", ") ?: "" genre = show.genres?.joinToString(separator = ", ") ?: ""
status = parseStatus(show.status) status = parseStatus(show.status)
author = show.studios?.firstOrNull() author = show.studios?.firstOrNull()
description = Jsoup.parse( description = buildString {
show.description?.replace("<br>", "br2n") ?: "", append(
).text().replace("br2n", "\n") + Jsoup.parse(
"\n\n" + show.description?.replace("<br>", "br2n") ?: "",
"Type: ${show.type ?: "Unknown"}" + ).text().replace("br2n", "\n"),
"\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}" + )
"\nScore: ${show.score ?: "-"}" append("\n\n")
append("Type: ${show.type ?: "Unknown"}")
append("\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}")
append("\nScore: ${show.score ?: "-"}")
}
} }
} }
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request { override fun episodeListRequest(anime: SAnime): Request {
val variables = """{"_id":"${anime.url.split("<&sep>").first()}"}""" val variables = buildJsonObject {
put("_id", anime.url.split("<&sep>").first())
}
return GET("$baseUrl/allanimeapi?variables=$variables&query=$EPISODES_QUERY", headers = headers) return GET("$baseUrl/allanimeapi?variables=$variables&query=$EPISODES_QUERY", headers = headers)
} }
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val subPref = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!! val subPref = preferences.subPref
val medias = json.decodeFromString<SeriesResult>(response.body.string()) val medias = json.decodeFromString<SeriesResult>(response.body.string())
val episodesDetail = if (subPref == "sub") { val episodesDetail = if (subPref == "sub") {
@ -200,7 +233,12 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
return episodesDetail.map { ep -> return episodesDetail.map { ep ->
val numName = ep.toIntOrNull() ?: (ep.toFloatOrNull() ?: "1") val numName = ep.toIntOrNull() ?: (ep.toFloatOrNull() ?: "1")
val variables = """{"showId":"${medias.data.show._id}","translationType":"$subPref","episodeString":"$ep"}""" val variables = buildJsonObject {
put("showId", medias.data.show._id)
put("translationType", subPref)
put("episodeString", ep)
}
SEpisode.create().apply { SEpisode.create().apply {
episode_number = ep.toFloatOrNull() ?: 0F episode_number = ep.toFloatOrNull() ?: 0F
name = "Episode $numName ($subPref)" name = "Episode $numName ($subPref)"
@ -211,27 +249,13 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val headers = headers.newBuilder()
.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0")
.build()
return GET(baseUrl + episode.url, headers)
}
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val videoJson = json.decodeFromString<EpisodeResult>(response.body.string()) val videoJson = json.decodeFromString<EpisodeResult>(response.body.string())
val videoList = mutableListOf<Pair<Video, Float>>() val videoList = mutableListOf<Pair<Video, Float>>()
val serverList = mutableListOf<Server>() val serverList = mutableListOf<Server>()
val altHosterSelection = preferences.getStringSet( val hosterSelection = preferences.getHosters
PREF_ALT_HOSTER_KEY, val altHosterSelection = preferences.getAltHosters
ALT_HOSTER_NAMES.toSet(),
)!!
val hosterSelection = preferences.getStringSet(
PREF_HOSTER_KEY,
PREF_HOSTER_DEFAULT,
)!!
// list of alternative hosters // list of alternative hosters
val mappings = listOf( val mappings = listOf(
@ -356,9 +380,9 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> { private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
val prefServer = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!! val prefServer = preferences.prefServer
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.quality
val subPref = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!! val subPref = preferences.subPref
return pList.sortedWith( return pList.sortedWith(
compareBy( compareBy(
@ -392,11 +416,10 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
private fun parseAnime(response: Response): AnimesPage { private fun parseAnime(response: Response): AnimesPage {
val parsed = json.decodeFromString<SearchResult>(response.body.string()) val parsed = json.decodeFromString<SearchResult>(response.body.string())
val titleStyle = preferences.getString(PREF_TITLE_STYLE_KEY, PREF_TITLE_STYLE_DEFAULT)!!
val animeList = parsed.data.shows.edges.map { ani -> val animeList = parsed.data.shows.edges.map { ani ->
SAnime.create().apply { SAnime.create().apply {
title = when (titleStyle) { title = when (preferences.titleStyle) {
"romaji" -> ani.name "romaji" -> ani.name
"eng" -> ani.englishName ?: ani.name "eng" -> ani.englishName ?: ani.name
else -> ani.nativeName ?: ani.name else -> ani.nativeName ?: ani.name
@ -413,7 +436,7 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
return keywords.any { this.contains(it) } return keywords.any { this.contains(it) }
} }
fun hexToText(inputString: String): String { private fun hexToText(inputString: String): String {
return inputString.chunked(2).map { return inputString.chunked(2).map {
it.toInt(16).toByte() it.toInt(16).toByte()
}.toByteArray().toString(Charsets.UTF_8) }.toByteArray().toString(Charsets.UTF_8)
@ -425,138 +448,6 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainSitePref = ListPreference(screen.context).apply {
key = PREF_SITE_DOMAIN_KEY
title = PREF_SITE_DOMAIN_TITLE
entries = PREF_SITE_DOMAIN_ENTRIES
entryValues = PREF_SITE_DOMAIN_ENTRY_VALUES
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()
}
}
val domainPref = ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_ENTRY_VALUES
setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val serverPref = ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE
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()
}
}
val hostSelection = MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE
entries = PREF_HOSTER_ENTRIES
entryValues = PREF_HOSTER_ENTRY_VALUES
setDefaultValue(PREF_HOSTER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
val altHostSelection = MultiSelectListPreference(screen.context).apply {
key = PREF_ALT_HOSTER_KEY
title = PREF_ALT_HOSTER_TITLE
entries = ALT_HOSTER_NAMES
entryValues = ALT_HOSTER_NAMES
setDefaultValue(ALT_HOSTER_NAMES.toSet())
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
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()
}
}
val titleStylePref = ListPreference(screen.context).apply {
key = PREF_TITLE_STYLE_KEY
title = PREF_TITLE_STYLE_TITLE
entries = PREF_TITLE_STYLE_ENTRIES
entryValues = PREF_TITLE_STYLE_ENTRY_VALUES
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()
}
}
val subPref = ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_ENTRIES
entryValues = PREF_SUB_ENTRY_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()
}
}
screen.addPreference(domainSitePref)
screen.addPreference(domainPref)
screen.addPreference(serverPref)
screen.addPreference(hostSelection)
screen.addPreference(altHostSelection)
screen.addPreference(videoQualityPref)
screen.addPreference(titleStylePref)
screen.addPreference(subPref)
}
companion object { companion object {
private const val PAGE_SIZE = 26 // number of items to retrieve when calling API private const val PAGE_SIZE = 26 // number of items to retrieve when calling API
private val INTERAL_HOSTER_NAMES = arrayOf( private val INTERAL_HOSTER_NAMES = arrayOf(
@ -575,19 +466,12 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
) )
private const val PREF_SITE_DOMAIN_KEY = "preferred_site_domain" private const val PREF_SITE_DOMAIN_KEY = "preferred_site_domain"
private const val PREF_SITE_DOMAIN_TITLE = "Preferred domain for site (requires app restart)"
private val PREF_SITE_DOMAIN_ENTRIES = arrayOf("allanime.to", "allanime.co")
private val PREF_SITE_DOMAIN_ENTRY_VALUES = PREF_SITE_DOMAIN_ENTRIES.map { "https://$it" }.toTypedArray()
private const val PREF_SITE_DOMAIN_DEFAULT = "https://allanime.to" private const val PREF_SITE_DOMAIN_DEFAULT = "https://allanime.to"
private const val PREF_DOMAIN_KEY = "preferred_domain" private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private val PREF_DOMAIN_ENTRIES = arrayOf("api.allanime.to", "api.allanime.co")
private val PREF_DOMAIN_ENTRY_VALUES = PREF_DOMAIN_ENTRIES.map { "https://$it" }.toTypedArray()
private const val PREF_DOMAIN_DEFAULT = "https://api.allanime.to" private const val PREF_DOMAIN_DEFAULT = "https://api.allanime.to"
private const val PREF_SERVER_KEY = "preferred_server" private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred Video Server"
private val PREF_SERVER_ENTRIES = arrayOf("Site Default") + private val PREF_SERVER_ENTRIES = arrayOf("Site Default") +
INTERAL_HOSTER_NAMES.sliceArray(1 until INTERAL_HOSTER_NAMES.size) + INTERAL_HOSTER_NAMES.sliceArray(1 until INTERAL_HOSTER_NAMES.size) +
ALT_HOSTER_NAMES ALT_HOSTER_NAMES
@ -599,18 +483,14 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
private const val PREF_SERVER_DEFAULT = "site_default" private const val PREF_SERVER_DEFAULT = "site_default"
private const val PREF_HOSTER_KEY = "hoster_selection" private const val PREF_HOSTER_KEY = "hoster_selection"
private const val PREF_HOSTER_TITLE = "Enable/Disable Hosts"
private val PREF_HOSTER_ENTRIES = INTERAL_HOSTER_NAMES
private val PREF_HOSTER_ENTRY_VALUES = INTERAL_HOSTER_NAMES.map { private val PREF_HOSTER_ENTRY_VALUES = INTERAL_HOSTER_NAMES.map {
it.lowercase() it.lowercase()
}.toTypedArray() }.toTypedArray()
private val PREF_HOSTER_DEFAULT = setOf("default", "ac", "ak", "kir", "luf-mp4", "si-hls", "s-mp4", "ac-hls") 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_ALT_HOSTER_KEY = "alt_hoster_selection"
private const val PREF_ALT_HOSTER_TITLE = "Enable/Disable Alternative Hosts"
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private val PREF_QUALITY_ENTRIES = arrayOf( private val PREF_QUALITY_ENTRIES = arrayOf(
"1080p", "1080p",
"720p", "720p",
@ -627,15 +507,158 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
private const val PREF_QUALITY_DEFAULT = "1080" private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_TITLE_STYLE_KEY = "preferred_title_style" private const val PREF_TITLE_STYLE_KEY = "preferred_title_style"
private const val PREF_TITLE_STYLE_TITLE = "Preferred Title Style"
private val PREF_TITLE_STYLE_ENTRIES = arrayOf("Romaji", "English", "Native")
private val PREF_TITLE_STYLE_ENTRY_VALUES = arrayOf("romaji", "eng", "native")
private const val PREF_TITLE_STYLE_DEFAULT = "romaji" private const val PREF_TITLE_STYLE_DEFAULT = "romaji"
private const val PREF_SUB_KEY = "preferred_sub" private const val PREF_SUB_KEY = "preferred_sub"
private const val PREF_SUB_TITLE = "Prefer subs or dubs?"
private val PREF_SUB_ENTRIES = arrayOf("Subs", "Dubs")
private val PREF_SUB_ENTRY_VALUES = arrayOf("sub", "dub")
private const val PREF_SUB_DEFAULT = "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("allanime.to", "allanime.co")
entryValues = arrayOf("https://allanime.to", "https://allanime.co")
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.to", "api.allanime.co")
entryValues = arrayOf("https://api.allanime.to", "https://api.allanime.co")
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())!!
} }

View File

@ -11,57 +11,6 @@ import okhttp3.OkHttpClient
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Locale import java.util.Locale
@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,
)
}
}
}
class AllAnimeExtractor(private val client: OkHttpClient) { class AllAnimeExtractor(private val client: OkHttpClient) {
private val json: Json by injectLazy() private val json: Json by injectLazy()
@ -235,4 +184,55 @@ class AllAnimeExtractor(private val client: OkHttpClient) {
return videoList return videoList
} }
@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,
)
}
}
}
} }

View File

@ -44,7 +44,7 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeDao" override val name = "AnimeDao"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://animedao.to")!! } override val baseUrl = "https://animedao.to"
override val lang = "en" override val lang = "en"
@ -58,20 +58,12 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
}
}
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animelist/popular") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/animelist/popular")
override fun popularAnimeSelector(): String = "div.container > div.row > div.col-md-6" override fun popularAnimeSelector(): String = "div.container > div.row > div.col-md-6"
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeFromElement(element: Element): SAnime {
val thumbnailUrl = element.selectFirst("img")!!.attr("data-src") val thumbnailUrl = element.selectFirst("img")!!.attr("data-src")
@ -86,14 +78,14 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun popularAnimeNextPageSelector(): String? = null
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl) override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
override fun latestUpdatesSelector(): String = "div#latest-tab-pane > div.row > div.col-md-6" override fun latestUpdatesSelector(): String = "div#latest-tab-pane > div.row > div.col-md-6"
override fun latestUpdatesNextPageSelector(): String? = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesFromElement(element: Element): SAnime {
val thumbnailUrl = element.selectFirst("img")!!.attr("data-src") val thumbnailUrl = element.selectFirst("img")!!.attr("data-src")
@ -108,6 +100,8 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun latestUpdatesNextPageSelector(): String? = popularAnimeNextPageSelector()
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used") override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
@ -121,26 +115,6 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val animes = if (response.request.url.encodedPath.startsWith("/animelist/")) {
document.select(searchAnimeSelectorFilter()).map { element ->
searchAnimeFromElement(element)
}
} else {
document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
}
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
private fun searchAnimeRequest(page: Int, query: String, filters: AnimeDaoFilters.FilterSearchParams): Request { private fun searchAnimeRequest(page: Int, query: String, filters: AnimeDaoFilters.FilterSearchParams): Request {
return if (query.isNotBlank()) { return if (query.isNotBlank()) {
val cleanQuery = query.replace(" ", "+") val cleanQuery = query.replace(" ", "+")
@ -162,14 +136,33 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val selector = if (response.request.url.encodedPath.startsWith("/animelist/")) {
searchAnimeSelectorFilter()
} else {
searchAnimeSelector()
}
val animes = document.select(selector).map { element ->
searchAnimeFromElement(element)
}
val hasNextPage = searchAnimeNextPageSelector().let { selector ->
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
private fun searchAnimeSelectorFilter(): String = "div.container div.col-12 > div.row > div.col-md-6" private fun searchAnimeSelectorFilter(): String = "div.container div.col-12 > div.row > div.col-md-6"
override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.page-item:has(i.fa-arrow-right):not(.disabled)" override fun searchAnimeNextPageSelector(): String = "ul.pagination > li.page-item:has(i.fa-arrow-right):not(.disabled)"
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
// ============================== FILTERS =============================== // ============================== FILTERS ===============================
override fun getFilterList(): AnimeFilterList = AnimeDaoFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = AnimeDaoFilters.FILTER_LIST
@ -198,7 +191,7 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
return if (preferences.getBoolean("preferred_episode_sorting", false)) { return if (preferences.getBoolean(PREF_EPISODE_SORT_KEY, PREF_EPISODE_SORT_DEFAULT)) {
super.episodeListParse(response).sortedWith( super.episodeListParse(response).sortedWith(
compareBy( compareBy(
{ it.episode_number }, { it.episode_number },
@ -221,7 +214,7 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
episode_number = if (episodeName.contains("Episode ", true)) { episode_number = if (episodeName.contains("Episode ", true)) {
episodeName.substringAfter("Episode ").substringBefore(" ").toFloatOrNull() ?: 0F episodeName.substringAfter("Episode ").substringBefore(" ").toFloatOrNull() ?: 0F
} else { 0F } } else { 0F }
if (element.selectFirst("span.filler") != null && preferences.getBoolean("mark_fillers", true)) { if (element.selectFirst("span.filler") != null && preferences.getBoolean(PREF_MARK_FILLERS_KEY, PREF_MARK_FILLERS_DEFAULT)) {
scanlator = "Filler Episode" scanlator = "Filler Episode"
} }
date_upload = element.selectFirst("span.date")?.let { parseDate(it.text()) } ?: 0L date_upload = element.selectFirst("span.date")?.let { parseDate(it.text()) } ?: 0L
@ -298,8 +291,8 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString("preferred_server", "vstream")!! val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy( compareBy(
@ -333,28 +326,33 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { companion object {
val domainPref = ListPreference(screen.context).apply { private val DATE_FORMATTER by lazy {
key = "preferred_domain" SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
title = "Preferred domain (requires app restart)"
entries = arrayOf("animedao.to")
entryValues = arrayOf("https://animedao.to")
setDefaultValue("https://animedao.to")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
} }
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "vstream"
private const val PREF_EPISODE_SORT_KEY = "preferred_episode_sorting"
private const val PREF_EPISODE_SORT_DEFAULT = true
private const val PREF_MARK_FILLERS_KEY = "mark_fillers"
private const val PREF_MARK_FILLERS_DEFAULT = true
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality" title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p") entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360") entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080") setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -363,13 +361,14 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val videoServerPref = ListPreference(screen.context).apply {
key = "preferred_server" ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server" title = "Preferred server"
entries = arrayOf("Vidstreaming", "Vidstreaming2", "Vidstreaming3", "Mixdrop", "StreamSB", "Streamtape", "Vidstreaming4", "Doodstream") entries = arrayOf("Vidstreaming", "Vidstreaming2", "Vidstreaming3", "Mixdrop", "StreamSB", "Streamtape", "Vidstreaming4", "Doodstream")
entryValues = arrayOf("vstream", "src2", "src", "mixdrop", "streamsb", "streamtape", "vplayer", "doodstream") entryValues = arrayOf("vstream", "src2", "src", "mixdrop", "streamsb", "streamtape", "vplayer", "doodstream")
setDefaultValue("vstream") setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -378,33 +377,29 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val episodeSortPref = SwitchPreferenceCompat(screen.context).apply {
key = "preferred_episode_sorting" SwitchPreferenceCompat(screen.context).apply {
key = PREF_EPISODE_SORT_KEY
title = "Attempt episode sorting" title = "Attempt episode sorting"
summary = """AnimeDao displays the episodes in either ascending or descending order, summary = """AnimeDao displays the episodes in either ascending or descending order,
| enable to attempt order or disable to set same as website. | enable to attempt order or disable to set same as website.
""".trimMargin() """.trimMargin()
setDefaultValue(true) setDefaultValue(PREF_EPISODE_SORT_DEFAULT)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit() preferences.edit().putBoolean(key, new).commit()
} }
} }.also(screen::addPreference)
val markFillers = SwitchPreferenceCompat(screen.context).apply {
key = "mark_fillers" SwitchPreferenceCompat(screen.context).apply {
key = PREF_MARK_FILLERS_KEY
title = "Mark filler episodes" title = "Mark filler episodes"
setDefaultValue(true) setDefaultValue(PREF_MARK_FILLERS_DEFAULT)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit() preferences.edit().putBoolean(key, newValue as Boolean).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref)
screen.addPreference(videoServerPref)
screen.addPreference(episodeSortPref)
screen.addPreference(markFillers)
} }
} }

View File

@ -13,7 +13,7 @@ class MixDropExtractor(private val client: OkHttpClient) {
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)") val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
?.data() ?.data()
?.let { JsUnpacker.unpackAndCombine(it) } ?.let { JsUnpacker.unpackAndCombine(it) }
?: return emptyList<Video>() ?: return emptyList()
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"") val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
.substringBefore("\"") .substringBefore("\"")
val quality = ("MixDrop").let { val quality = ("MixDrop").let {

View File

@ -12,7 +12,7 @@ class Mp4uploadExtractor(private val client: OkHttpClient) {
val packed = body.substringAfter("<script type='text/javascript'>eval(function(p,a,c,k,e,d)") val packed = body.substringAfter("<script type='text/javascript'>eval(function(p,a,c,k,e,d)")
.substringBefore("</script>") .substringBefore("</script>")
val unpacked = JsUnpacker.unpackAndCombine("eval(function(p,a,c,k,e,d)" + packed) ?: return emptyList() val unpacked = JsUnpacker.unpackAndCombine("eval(function(p,a,c,k,e,d)$packed") ?: return emptyList()
val videoUrl = unpacked.substringAfter("player.src(\"").substringBefore("\");") val videoUrl = unpacked.substringAfter("player.src(\"").substringBefore("\");")
return listOf( return listOf(
Video(videoUrl, "$prefix Mp4upload", videoUrl, headers = Headers.headersOf("Referer", "https://www.mp4upload.com/")), Video(videoUrl, "$prefix Mp4upload", videoUrl, headers = Headers.headersOf("Referer", "https://www.mp4upload.com/")),

View File

@ -6,7 +6,7 @@ ext {
extName = 'AnimeFlix' extName = 'AnimeFlix'
pkgNameSuffix = 'en.animeflix' pkgNameSuffix = 'en.animeflix'
extClass = '.AnimeFlix' extClass = '.AnimeFlix'
extVersionCode = 4 extVersionCode = 5
libVersion = '13' libVersion = '13'
} }

View File

@ -8,13 +8,11 @@ import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList 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.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -26,7 +24,6 @@ import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
@ -58,95 +55,53 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "div#page > div#content_box > article" override fun popularAnimeSelector(): String = "div#page > div#content_box > article"
override fun popularAnimeNextPageSelector(): String = "div.nav-links > span.current ~ a" override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
override fun popularAnimeFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img")!!.attr("src")
return SAnime.create().apply { title = element.selectFirst("header")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img")!!.attr("src")
title = element.selectFirst("header")!!.text()
}
} }
override fun popularAnimeNextPageSelector(): String = "div.nav-links > span.current ~ a"
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
override fun latestUpdatesSelector(): String = popularAnimeSelector() override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used") override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val (request, isExact) = searchAnimeRequestExact(page, query, filters)
return client.newCall(request)
.asObservableSuccess()
.map { response ->
searchAnimeParse(response, isExact)
}
}
private fun searchAnimeParse(response: Response, isExact: Boolean): AnimesPage {
val document = response.asJsoup()
if (isExact) {
val anime = SAnime.create()
anime.title = document.selectFirst("div.single_post > header > h1")!!.text()
anime.thumbnail_url = document.selectFirst("div.imdbwp img")!!.attr("src")
anime.setUrlWithoutDomain(response.request.url.encodedPath)
return AnimesPage(listOf(anime), false)
}
val animes = document.select(searchAnimeSelector()).map { element ->
searchAnimeFromElement(element)
}
val hasNextPage = searchAnimeNextPageSelector()?.let { selector ->
document.selectFirst(selector)
} != null
return AnimesPage(animes, hasNextPage)
}
private fun searchAnimeRequestExact(page: Int, query: String, filters: AnimeFilterList): Pair<Request, Boolean> {
val cleanQuery = query.replace(" ", "+").lowercase() val cleanQuery = query.replace(" ", "+").lowercase()
val filterList = if (filters.isEmpty()) getFilterList() else filters val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
val urlFilter = filterList.find { it is URLFilter } as URLFilter
return when { return when {
query.isNotBlank() -> Pair(GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers), false) query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
genreFilter.state != 0 -> Pair(GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers), false) genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
subpageFilter.state != 0 -> Pair(GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers), false) subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
urlFilter.state.isNotEmpty() -> Pair(GET(urlFilter.state, headers = headers), true) else -> popularAnimeRequest(page)
else -> Pair(popularAnimeRequest(page), false)
} }
} }
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
// ============================== FILTERS =============================== override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"), AnimeFilter.Header("Text search ignores filters"),
GenreFilter(), GenreFilter(),
SubPageFilter(), SubPageFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("Get item url from webview"),
URLFilter(),
) )
private class GenreFilter : UriPartFilter( private class GenreFilter : UriPartFilter(
@ -185,14 +140,13 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }
private class URLFilter : AnimeFilter.Text("Url")
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
val animeInfo = document.select("div.thecontent h3:contains(Anime Info) ~ ul li").joinToString("\n") { it.text() }
return SAnime.create().apply { return SAnime.create().apply {
title = document.selectFirst("div.single_post > header > h1")!!.text() title = document.selectFirst("div.single_post > header > h1")!!.text()
val animeInfo = document.select("div.thecontent h3:contains(Anime Info) ~ ul 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" description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)").joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
} }
} }
@ -218,7 +172,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val serverListSeason = mutableListOf<List<EpUrl>>() val serverListSeason = mutableListOf<List<EpUrl>>()
season.forEach { season.forEach {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1] val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
val seasonNumber = seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1] val seasonNumber = seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
val url = it.selectFirst("a")!!.attr("href") val url = it.selectFirst("a")!!.attr("href")
@ -244,7 +198,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} else { } else {
document.select("div.thecontent p:has(span:contains(Gdrive))").forEach { document.select("div.thecontent p:has(span:contains(Gdrive))").forEach {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1] val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
driveList.add(Pair(it.selectFirst("a")!!.attr("href"), quality)) driveList.add(Pair(it.selectFirst("a")!!.attr("href"), quality))
} }
@ -314,6 +268,9 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}.getOrNull() }.getOrNull()
}.flatten(), }.flatten(),
) )
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return Observable.just(videoList.sort()) return Observable.just(videoList.sort())
} }
@ -323,7 +280,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
// https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt // https://github.com/jmir1/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> { private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
@ -331,11 +288,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val qualityRegex = """(\d+)p""".toRegex() val qualityRegex = """(\d+)p""".toRegex()
val matchResult = qualityRegex.find(epUrl.name) val matchResult = qualityRegex.find(epUrl.name)
val quality = if (matchResult == null) { val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
epUrl.quality
} else {
matchResult.groupValues[1]
}
for (type in 1..3) { for (type in 1..3) {
videoList.addAll( videoList.addAll(
@ -345,12 +298,10 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return Pair(videoList, epUrl.url) return Pair(videoList, epUrl.url)
} }
private val sizeRegex = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> { private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type" val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup() val resp = client.newCall(GET(reqLink)).execute().asJsoup()
val sizeMatch = sizeRegex.find(resp.select("div.card-header").text().trim()) val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: "" val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement -> return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
val link = linkElement.attr("href") val link = linkElement.attr("href")
@ -373,12 +324,12 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val response = tokenClient.newCall(GET(mediaUrl)).execute().asJsoup() val response = tokenClient.newCall(GET(mediaUrl)).execute().asJsoup()
val gdBtn = response.selectFirst("div.card-body a.btn")!! val gdBtn = response.selectFirst("div.card-body a.btn")!!
val gdLink = gdBtn.attr("href") val gdLink = gdBtn.attr("href")
val sizeMatch = sizeRegex.find(gdBtn.text()) val sizeMatch = SIZE_REGEX.find(gdBtn.text())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: "" val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup() val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
val link = gdResponse.select("form#download-form") val link = gdResponse.select("form#download-form")
return if (link.isNullOrEmpty()) { return if (link.isNullOrEmpty()) {
listOf() emptyList()
} else { } else {
val realLink = link.attr("action") val realLink = link.attr("action")
listOf(Video(realLink, "$quality - Gdrive$size", realLink)) listOf(Video(realLink, "$quality - Gdrive$size", realLink))
@ -386,7 +337,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy { it.quality.contains(quality) }, compareBy { it.quality.contains(quality) },
@ -407,25 +358,6 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("2160p", "1080p", "720p", "480p")
entryValues = arrayOf("2160", "1080", "720", "480")
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)
}
@Serializable @Serializable
data class EpUrl( data class EpUrl(
val quality: String, val quality: String,
@ -438,4 +370,31 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
runBlocking { runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
companion object {
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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)
}
} }

View File

@ -6,7 +6,7 @@ ext {
extName = 'AnimeKaizoku' extName = 'AnimeKaizoku'
pkgNameSuffix = 'en.animekaizoku' pkgNameSuffix = 'en.animekaizoku'
extClass = '.AnimeKaizoku' extClass = '.AnimeKaizoku'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
} }

View File

@ -741,9 +741,12 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
companion object { private val xmlHeaders = headers.newBuilder()
private val DDL_REGEX = Regex("""DDL\((.*?), ?(.*?), ?(.*?), ?(.*?)\)""") .add("Accept", "*/*")
} .add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.add("Host", baseUrl.toHttpUrl().host)
.add("Origin", baseUrl)
.add("X-Requested-With", "XMLHttpRequest")
// ============================== Popular =============================== // ============================== Popular ===============================
@ -751,26 +754,24 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "table > tbody > tr.post-row" override fun popularAnimeSelector(): String = "table > tbody > tr.post-row"
override fun popularAnimeNextPageSelector(): String? = null override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a:not([title])")!!.attr("abs:href"))
override fun popularAnimeFromElement(element: Element): SAnime { title = element.selectFirst("a:not([title])")!!.text()
return SAnime.create().apply { thumbnail_url = ""
setUrlWithoutDomain(element.selectFirst("a:not([title])")!!.attr("abs:href").toHttpUrl().encodedPath)
title = element.selectFirst("a:not([title])")!!.text()
thumbnail_url = ""
}
} }
override fun popularAnimeNextPageSelector(): String? = null
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not Used") override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not Used")
override fun latestUpdatesSelector(): String = throw Exception("Not Used") override fun latestUpdatesSelector(): String = throw Exception("Not Used")
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not Used") override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not Used")
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used") override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used")
@ -821,13 +822,8 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.add("layout", layout) .add("layout", layout)
.add("settings", settings) .add("settings", settings)
.build() .build()
val formHeaders = headers.newBuilder() val formHeaders = xmlHeaders
.add("Accept", "*/*")
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.add("Host", "animekaizoku.com")
.add("Origin", "https://animekaizoku.com")
.add("Referer", currentReferer) .add("Referer", currentReferer)
.add("X-Requested-With", "XMLHttpRequest")
.build() .build()
POST("$baseUrl/wp-admin/admin-ajax.php", body = formBody, headers = formHeaders) POST("$baseUrl/wp-admin/admin-ajax.php", body = formBody, headers = formHeaders)
} }
@ -884,18 +880,16 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
private fun searchAnimeFromElementPaginated(element: Element): SAnime { private fun searchAnimeFromElementPaginated(element: Element): SAnime = SAnime.create().apply {
return SAnime.create().apply { setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href").toHttpUrl().encodedPath) thumbnail_url = element.selectFirst("img[src]")?.attr("abs:src") ?: ""
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: "" title = element.selectFirst("h2.post-title")!!.text().substringBefore(" Episode")
title = element.selectFirst("h2.post-title")!!.text().substringBefore(" Episode")
}
} }
override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
// ============================== Filters =============================== // ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
@ -921,16 +915,14 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
return SAnime.create().apply { title = document.selectFirst("div.entry-header > h1")?.text()?.trim() ?: ""
title = document.selectFirst("div.entry-header > h1")?.text()?.trim() ?: "" thumbnail_url = document.selectFirst("script:containsData(primaryImageOfPage)")?.data()?.let {
thumbnail_url = document.selectFirst("script:containsData(primaryImageOfPage)")?.data()?.let { it.substringAfter("primaryImageOfPage").substringAfter("@id\":\"").substringBefore("\"")
it.substringAfter("primaryImageOfPage").substringAfter("@id\":\"").substringBefore("\"")
}
description = document.selectFirst("div.review-short-summary")?.text()
author = document.selectFirst("div.toggle-content > strong:contains(studio) + a")?.text()
genre = document.select("div.toggle-content > strong:contains(Genres) ~ a[href*=/genres/]").joinToString(", ") { it.text() }
} }
description = document.selectFirst("div.review-short-summary")?.text()
author = document.selectFirst("div.toggle-content > strong:contains(studio) + a")?.text()
genre = document.select("div.toggle-content > strong:contains(Genres) ~ a[href*=/genres/]").joinToString(", ") { it.text() }
} }
// ============================== Episodes ============================== // ============================== Episodes ==============================
@ -944,16 +936,11 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
.substringAfter("\"postId\":\"").substringBefore("\"") .substringAfter("\"postId\":\"").substringBefore("\"")
val serversList = mutableListOf<List<EpUrl>>() val serversList = mutableListOf<List<EpUrl>>()
val postHeaders = headers.newBuilder() val postHeaders = xmlHeaders
.add("Accept", "*/*")
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.add("Host", baseUrl.toHttpUrl().host)
.add("Origin", baseUrl)
.add("Referer", baseUrl + anime.url) .add("Referer", baseUrl + anime.url)
.add("X-Requested-With", "XMLHttpRequest")
.build() .build()
val prefServer = preferences.getString("preferred_server", "server")!! val prefServer = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
DDL_REGEX.findAll(document.data()).forEach { serverType -> DDL_REGEX.findAll(document.data()).forEach { serverType ->
val data = serverType.groupValues val data = serverType.groupValues
@ -1020,9 +1007,7 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
SEpisode.create().apply { SEpisode.create().apply {
name = serverList.first().name name = serverList.first().name
episode_number = (index + 1).toFloat() episode_number = (index + 1).toFloat()
setUrlWithoutDomain( url = serverList.toJsonString()
serverList.toJsonString(),
)
}, },
) )
} }
@ -1044,13 +1029,8 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val parsed = json.decodeFromString<List<EpUrl>>(episode.url) val parsed = json.decodeFromString<List<EpUrl>>(episode.url)
parsed.forEach { parsed.forEach {
val postHeaders = headers.newBuilder() val postHeaders = xmlHeaders
.add("Accept", "*/*")
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.add("Host", baseUrl.toHttpUrl().host)
.add("Origin", baseUrl)
.add("Referer", it.ref) .add("Referer", it.ref)
.add("X-Requested-With", "XMLHttpRequest")
.build() .build()
val ddlData = DDL_REGEX.find(it.url)!!.groupValues val ddlData = DDL_REGEX.find(it.url)!!.groupValues
@ -1076,6 +1056,8 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return Observable.just(videoList) return Observable.just(videoList)
} }
@ -1142,7 +1124,7 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy { it.quality.contains(quality) }, compareBy { it.quality.contains(quality) },
@ -1188,13 +1170,25 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return json.encodeToString(this) return json.encodeToString(this)
} }
companion object {
private val DDL_REGEX = Regex("""DDL\((.*?), ?(.*?), ?(.*?), ?(.*?)\)""")
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "server"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = "preferred_server" key = PREF_SERVER_KEY
title = "Preferred server" title = "Preferred server"
entries = arrayOf("Server direct", "Worker direct", "Both") entries = arrayOf("Server direct", "Worker direct", "Both")
entryValues = arrayOf("server", "worker", "both") entryValues = arrayOf("server", "worker", "both")
setDefaultValue("server") setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -1203,13 +1197,14 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality" ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality" title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p") entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360") entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080") setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -1218,9 +1213,6 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref)
} }
} }

View File

@ -6,7 +6,7 @@ ext {
extName = 'Ask4Movie' extName = 'Ask4Movie'
pkgNameSuffix = 'en.ask4movie' pkgNameSuffix = 'en.ask4movie'
extClass = '.Ask4Movie' extClass = '.Ask4Movie'
extVersionCode = 5 extVersionCode = 6
libVersion = '13' libVersion = '13'
} }

View File

@ -13,9 +13,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -49,7 +47,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "div.all-channels div.channel-content" override fun popularAnimeSelector(): String = "div.all-channels div.channel-content"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply { override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("p.channel-name a")!!.relative()) setUrlWithoutDomain(element.selectFirst("p.channel-name a")!!.attr("abs:href"))
thumbnail_url = element.select("div.channel-avatar a img").attr("src") thumbnail_url = element.select("div.channel-avatar a img").attr("src")
title = element.select("p.channel-name a").text() title = element.select("p.channel-name a").text()
} }
@ -66,7 +64,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val a = element.selectFirst("div.main-slide a[href]")!! val a = element.selectFirst("div.main-slide a[href]")!!
return SAnime.create().apply { return SAnime.create().apply {
setUrlWithoutDomain(a.relative()) setUrlWithoutDomain(a.attr("abs:href"))
thumbnail_url = element.select("div.item-thumb").attr("style") thumbnail_url = element.select("div.item-thumb").attr("style")
.substringAfter("background-image: url(").substringBefore(")") .substringAfter("background-image: url(").substringBefore(")")
title = a.text() title = a.text()
@ -101,7 +99,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = "div.cacus-sub-wrap > div.item,div#search-content > div.item" override fun searchAnimeSelector(): String = "div.cacus-sub-wrap > div.item,div#search-content > div.item"
override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply { override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("div.description a")!!.relative()) setUrlWithoutDomain(element.selectFirst("div.description a")!!.attr("abs:href"))
thumbnail_url = element.attr("style") thumbnail_url = element.attr("style")
.substringAfter("background-image: url(").substringBefore(")") .substringAfter("background-image: url(").substringBefore(")")
title = element.selectFirst("div.description a")!!.text() title = element.selectFirst("div.description a")!!.text()
@ -111,19 +109,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used") override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
return client.newCall(animeDetailsRequest(anime))
.asObservableSuccess()
.map { response ->
animeDetailsParse(response.asJsoup(), anime).apply { initialized = true }
}
}
private fun animeDetailsParse(document: Document, oldSAnime: SAnime): SAnime = SAnime.create().apply {
title = oldSAnime.title
thumbnail_url = oldSAnime.thumbnail_url
genre = document.select("div.categories:contains(Genres) a").joinToString(", ") { it.text() } genre = document.select("div.categories:contains(Genres) a").joinToString(", ") { it.text() }
.ifBlank { document.selectFirst("div.channel-description > p:has(span:contains(Genre)) em")?.text() } .ifBlank { document.selectFirst("div.channel-description > p:has(span:contains(Genre)) em")?.text() }
description = document.selectFirst("div.custom.video-the-content p")?.ownText() description = document.selectFirst("div.custom.video-the-content p")?.ownText()
@ -138,7 +124,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// Select multiple seasons // Select multiple seasons
val seasonsList = document.select("div.row > div.cactus-sub-wrap > div.item") val seasonsList = document.select("div.row > div.cactus-sub-wrap > div.item")
if (seasonsList.isEmpty().not()) { if (seasonsList.isNotEmpty()) {
seasonsList.forEach { season -> seasonsList.forEach { season ->
val link = season.selectFirst("a.btn-play-nf")!!.attr("abs:href") val link = season.selectFirst("a.btn-play-nf")!!.attr("abs:href")
val seasonName = "Season ${season.selectFirst("div.description p a")!!.text().substringAfter("(Season ").substringBefore(")")} " val seasonName = "Season ${season.selectFirst("div.description p a")!!.text().substringAfter("(Season ").substringBefore(")")} "
@ -157,17 +143,6 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return episodeList return episodeList
} }
override fun episodeListSelector() = "ul#episode_page li a"
override fun episodeFromElement(element: Element): SEpisode {
val ep = element.selectFirst("div.name")!!.ownText().substringAfter(" ")
return SEpisode.create().apply {
setUrlWithoutDomain(element.attr("abs:href"))
episode_number = ep.toFloat()
name = "Episode $ep"
}
}
// Returns episode list when episodes are in red boxes below the player // Returns episode list when episodes are in red boxes below the player
private fun episodesFromGroupLinks(document: Document, prefix: String = ""): List<SEpisode> { private fun episodesFromGroupLinks(document: Document, prefix: String = ""): List<SEpisode> {
return document.select("ul.group-links-list > li.group-link").mapNotNull { link -> return document.select("ul.group-links-list > li.group-link").mapNotNull { link ->
@ -190,11 +165,15 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
) )
} }
override fun episodeListSelector() = throw Exception("Not used")
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used")
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> { override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
val videoList = FilemoonExtractor(client, headers).videosFromUrl(episode.url) val videoList = FilemoonExtractor(client, headers).videosFromUrl(episode.url)
if (videoList.isEmpty()) throw Exception("Videos not found") require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return Observable.just(videoList) return Observable.just(videoList)
} }
@ -206,10 +185,6 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun Element.relative(): String {
return this.attr("abs:href").toHttpUrl().encodedPath
}
private fun Int.toPage(): String { private fun Int.toPage(): String {
return if (this == 1) { return if (this == 1) {
"" ""

View File

@ -6,7 +6,7 @@ ext {
extName = 'BestDubbedAnime' extName = 'BestDubbedAnime'
pkgNameSuffix = 'en.bestdubbedanime' pkgNameSuffix = 'en.bestdubbedanime'
extClass = '.BestDubbedAnime' extClass = '.BestDubbedAnime'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
} }

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.animeextension.en.bestdubbedanime
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
@ -20,13 +19,11 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -57,35 +54,138 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// Popular Anime // ============================== Popular ===============================
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/xz/trending.php?_=${System.currentTimeMillis() / 1000}")
val document = response.asJsoup()
val animes = document.select(popularAnimeSelector()).map { element ->
popularAnimeFromElement(element)
}
return AnimesPage(animes, false)
}
override fun popularAnimeSelector(): String = "li" override fun popularAnimeSelector(): String = "li"
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
return GET("$baseUrl/xz/trending.php?_=${System.currentTimeMillis() / 1000}") setUrlWithoutDomain(element.select("a").attr("abs:href"))
thumbnail_url = element.select("img").attr("abs:src")
title = element.select("div.cittx").text()
} }
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeNextPageSelector(): String? = null
val anime = SAnime.create()
anime.setUrlWithoutDomain(("https:" + element.select("a").attr("href")).toHttpUrl().encodedPath) // =============================== Latest ===============================
anime.title = element.select("div.cittx").text()
anime.thumbnail_url = "https:" + element.select("img").attr("src")
return anime override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/xz/gridgrabrecent.php?p=$page&limit=12&_=${System.currentTimeMillis() / 1000}", headers)
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val (animes, hasNextPage) = getAnimesFromLatest(document)
return AnimesPage(animes, hasNextPage)
} }
override fun popularAnimeNextPageSelector(): String = throw Exception("Not used") private fun getAnimesFromLatest(document: Document): Pair<List<SAnime>, Boolean> {
val animeList = document.select("div.grid > div.grid__item").map { item ->
latestUpdatesFromElement(item)
}
return Pair(animeList, animeList.size == 12)
}
override fun latestUpdatesFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.select("a").attr("abs:href"))
thumbnail_url = element.select("img").attr("abs:src")
title = element.select("div.tixtlis").text()
}
override fun latestUpdatesSelector(): String = throw Exception("Not used")
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotEmpty()) {
GET("$baseUrl/xz/searchgrid.php?p=$page&limit=12&s=$query&_=${System.currentTimeMillis() / 1000}", headers)
} else {
val genreFilter = (filters.find { it is TagFilter } as TagFilter).state.filter { it.state }
val categories = genreFilter.map { it.name }
GET("$baseUrl/xz/v3/taglist.php?tags=${categories.joinToString(separator = ",,")}&_=${System.currentTimeMillis() / 1000}", headers)
}
}
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
return if (response.request.url.encodedPath.startsWith("/xz/searchgrid")) {
getAnimesPageFromSearch(document)
} else {
getAnimesPageFromTags(document)
}
}
private fun getAnimesPageFromSearch(document: Document): AnimesPage {
val animeList = document.select("div.grid > div.grid__item").map { item ->
SAnime.create().apply {
setUrlWithoutDomain(item.select("a").attr("abs:href"))
thumbnail_url = item.select("img").attr("abs:src")
title = item.select("div.tixtlis").text()
}
}
return AnimesPage(animeList, animeList.size == 12)
}
private fun getAnimesPageFromTags(document: Document): AnimesPage {
val animeList = document.select("div.itemdtagk").map { item ->
SAnime.create().apply {
setUrlWithoutDomain(item.select("a").attr("abs:href"))
thumbnail_url = item.select("img").attr("abs:src")
title = item.select("div.titlekf").text()
}
}
return AnimesPage(animeList, false)
}
override fun searchAnimeSelector(): String = throw Exception("Not used")
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
override fun animeDetailsParse(response: Response): SAnime {
return if (response.request.url.encodedPath.startsWith("/movies/")) {
val slug = response.request.url.toString().split(".com/movies/")[1]
val apiResp = client.newCall(
GET(baseUrl + "/movies/jsonMovie.php?slug=" + slug + "&_=${System.currentTimeMillis() / 1000}"),
).execute()
val apiJson = apiResp.body.let { Json.decodeFromString<JsonObject>(it.string()) }
val animeJson = apiJson["result"]!!
.jsonObject["anime"]!!
.jsonArray[0]
.jsonObject
SAnime.create().apply {
title = animeJson["title"]!!.jsonPrimitive.content
description = animeJson["desc"]!!.jsonPrimitive.content
status = animeJson["status"]?.jsonPrimitive?.let { parseStatus(it.content) } ?: SAnime.UNKNOWN
genre = Jsoup.parse(animeJson["tags"]!!.jsonPrimitive.content).select("a").eachText().joinToString(separator = ", ")
}
} else {
val document = response.asJsoup()
val info = document.select("div.animeDescript")
SAnime.create().apply {
genre = document.select("div[itemprop=keywords] > a").eachText().joinToString(separator = ", ")
description = info.select("p").text()
status = info.select("div > div").firstOrNull {
it.text().contains("Status")
}?.let { parseStatus(it.text()) } ?: SAnime.UNKNOWN
}
}
}
// ============================== Episodes ==============================
// Episodes // Episodes
override fun episodeListSelector() = throw Exception("Not used") override fun episodeListSelector() = throw Exception("Not used")
@ -95,22 +195,23 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val episodeList = mutableListOf<SEpisode>() val episodeList = mutableListOf<SEpisode>()
if (response.request.url.encodedPath.startsWith("/movies/")) { if (response.request.url.encodedPath.startsWith("/movies/")) {
val episode = SEpisode.create() episodeList.add(
SEpisode.create().apply {
episode.name = document.select("div.tinywells > div > h4").text() name = document.select("div.tinywells > div > h4").text()
episode.episode_number = 1F episode_number = 1F
episode.setUrlWithoutDomain(response.request.url.encodedPath) setUrlWithoutDomain(response.request.url.toString())
episodeList.add(episode) },
)
} else { } else {
var counter = 1 var counter = 1
for (ep in document.select("div.eplistz > div > div > a")) { for (ep in document.select("div.eplistz > div > div > a")) {
val episode = SEpisode.create() episodeList.add(
SEpisode.create().apply {
episode.name = ep.select("div.inwel > span").text() name = ep.select("div.inwel > span").text()
episode.episode_number = counter.toFloat() episode_number = counter.toFloat()
episode.setUrlWithoutDomain(("https:" + ep.attr("href")).toHttpUrl().encodedPath) setUrlWithoutDomain(ep.attr("abs:href"))
episodeList.add(episode) },
)
counter++ counter++
} }
@ -118,10 +219,8 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val cacheUrlRegex = Regex("""url: '(.*?)'(?:.*?)episodesListxf""", RegexOption.DOT_MATCHES_ALL) val cacheUrlRegex = Regex("""url: '(.*?)'(?:.*?)episodesListxf""", RegexOption.DOT_MATCHES_ALL)
val jsText = document.selectFirst("script:containsData(episodesListxf)")!!.data() val jsText = document.selectFirst("script:containsData(episodesListxf)")!!.data()
val url = cacheUrlRegex.find(jsText)?.groupValues?.get(1) ?: "" cacheUrlRegex.find(jsText)?.groupValues?.get(1)?.let {
episodeList.addAll(extractFromCache(it))
if (url.isNotBlank()) {
episodeList.addAll(extractFromCache(url))
} }
} }
} }
@ -129,58 +228,9 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return episodeList.reversed() return episodeList.reversed()
} }
private fun extractFromCache(url: String): List<SEpisode> {
val headers = Headers.headersOf(
"Accept",
"*/*",
"Origin",
baseUrl,
"Referer",
"$baseUrl/",
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0",
)
val soup = client.newCall(GET(url, headers = headers)).execute().asJsoup()
return soup.select("a").mapIndexed { index, ep ->
SEpisode.create().apply {
name = ep.select("div.inwel > span").text()
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(("https:" + ep.attr("href")).toHttpUrl().encodedPath)
}
}
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used") override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used")
// Video urls // ============================ Video Links =============================
private fun String.decodeHex(): String {
require(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
.toString(Charsets.UTF_8)
}
private fun decodeAtob(inputStr: String): String {
return String(Base64.decode(inputStr.replace("\\x", "").decodeHex(), Base64.DEFAULT))
}
@Serializable
data class ServerResponse(
val result: ResultObject,
) {
@Serializable
data class ResultObject(
val anime: List<AnimeObject>,
) {
@Serializable
data class AnimeObject(
val serversHTML: String,
)
}
}
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
@ -219,11 +269,59 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return videoList.sort() return videoList.sort()
} }
override fun videoListSelector() = throw Exception("Not used") override fun videoListSelector() = throw Exception("Not used")
override fun videoFromElement(element: Element) = throw Exception("Not used")
override fun videoUrlParse(document: Document) = throw Exception("Not used")
// ============================= Utilities ==============================
private fun extractFromCache(url: String): List<SEpisode> {
val cacheHeaders = headers.newBuilder()
.add("Accept", "*/*")
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
.build()
val soup = client.newCall(GET(url, headers = cacheHeaders)).execute().asJsoup()
return soup.select("a").mapIndexed { index, ep ->
SEpisode.create().apply {
name = ep.select("div.inwel > span").text()
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(ep.attr("abs:href"))
}
}
}
@Serializable
data class ServerResponse(
val result: ResultObject,
) {
@Serializable
data class ResultObject(
val anime: List<AnimeObject>,
) {
@Serializable
data class AnimeObject(
val serversHTML: String,
)
}
}
private fun parseStatus(statusString: String): Int {
return when {
statusString.contains("Ongoing") -> SAnime.ONGOING
statusString.contains("Completed") -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
// From Dopebox // From Dopebox
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking { runBlocking {
@ -231,94 +329,39 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null) val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
if (quality != null) {
val newList = mutableListOf<Video>() return this.sortedWith(
var preferred = 0 compareBy { it.quality.contains(quality) },
for (video in this) { ).reversed()
if (video.quality.contains(quality)) { }
newList.add(preferred, video)
preferred++ companion object {
} else { private const val PREF_QUALITY_KEY = "preferred_quality"
newList.add(video) private const val PREF_QUALITY_DEFAULT = "1080"
} }
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
entryValues = arrayOf("1080", "720", "480", "360", "240")
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()
} }
return newList }.also(screen::addPreference)
}
return this
} }
override fun videoFromElement(element: Element) = throw Exception("Not used") // ============================== Filters ===============================
override fun videoUrlParse(document: Document) = throw Exception("Not used")
// search
override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val (animes, hasNextPage) = if (response.request.url.encodedPath.startsWith("/xz/searchgrid")) {
getAnimesFromSearch(document)
} else {
getAnimesFromTags(document)
}
return AnimesPage(animes, hasNextPage)
}
private fun getAnimesFromSearch(document: Document): Pair<List<SAnime>, Boolean> {
val animeList = mutableListOf<SAnime>()
for (item in document.select("div.grid > div.grid__item")) {
val anime = SAnime.create()
anime.title = item.select("div.tixtlis").text()
anime.thumbnail_url = item.select("img").attr("src").replace("^//".toRegex(), "https://")
anime.setUrlWithoutDomain(item.select("a").attr("href").toHttpUrl().encodedPath)
animeList.add(anime)
}
return Pair(animeList, animeList.size == 12)
}
private fun getAnimesFromTags(document: Document): Pair<List<SAnime>, Boolean> {
val animeList = mutableListOf<SAnime>()
for (item in document.select("div.itemdtagk")) {
val anime = SAnime.create()
anime.title = item.select("div.titlekf").text()
anime.thumbnail_url = item.select("img").attr("src").replace("^//".toRegex(), "https://")
anime.setUrlWithoutDomain(("https:" + item.select("a").attr("href")).toHttpUrl().encodedPath)
animeList.add(anime)
}
return Pair(animeList, false)
}
override fun searchAnimeSelector(): String = throw Exception("Not used")
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
// override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val url = if (query.isNotEmpty()) {
GET("$baseUrl/xz/searchgrid.php?p=$page&limit=12&s=$query&_=${System.currentTimeMillis() / 1000}", headers)
} else {
val genreFilter = (filters.find { it is TagFilter } as TagFilter).state.filter { it.state }
var categories = mutableListOf<String>()
genreFilter.forEach { categories.add(it.name) }
GET("$baseUrl/xz/v3/taglist.php?tags=${categories.joinToString(separator = ",,")}&_=${System.currentTimeMillis() / 1000}", headers)
}
return url
}
// Filters
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("NOTE: Ignored if using text search!"), AnimeFilter.Header("NOTE: Ignored if using text search!"),
@ -442,106 +485,4 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Pair("Work Life", "Work Life"), Pair("Work Life", "Work Life"),
Pair("Zombies", "Zombies"), Pair("Zombies", "Zombies"),
) )
// Details
override fun animeDetailsParse(response: Response): SAnime {
val anime = SAnime.create()
if (response.request.url.encodedPath.startsWith("/movies/")) {
val slug = response.request.url.toString().split(".com/movies/")[1]
val apiResp = client.newCall(
GET(baseUrl + "/movies/jsonMovie.php?slug=" + slug + "&_=${System.currentTimeMillis() / 1000}"),
).execute()
val apiJson = apiResp.body.let { Json.decodeFromString<JsonObject>(it.string()) }
val animeJson = apiJson!!["result"]!!
.jsonObject["anime"]!!
.jsonArray[0]
.jsonObject
anime.title = animeJson["title"]!!.jsonPrimitive.content
anime.description = animeJson["desc"]!!.jsonPrimitive.content
anime.status = animeJson["status"]?.jsonPrimitive?.let { parseStatus(it.content) } ?: SAnime.UNKNOWN
anime.genre = Jsoup.parse(animeJson["tags"]!!.jsonPrimitive.content).select("a").eachText().joinToString(separator = ", ")
} else {
val document = response.asJsoup()
val info = document.select("div.animeDescript")
anime.description = info.select("p").text()
for (header in info.select("div > div")) {
if (header.text().contains("Status")) {
anime.status = parseStatus(header.text())
}
}
anime.genre = document.select("div[itemprop=keywords] > a").eachText().joinToString(separator = ", ")
}
return anime
}
private fun parseStatus(statusString: String): Int {
return when {
statusString.contains("Ongoing") -> SAnime.ONGOING
statusString.contains("Completed") -> SAnime.COMPLETED
else -> SAnime.UNKNOWN
}
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
// Latest
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup()
val (animes, hasNextPage) = getAnimesFromLatest(document)
return AnimesPage(animes, hasNextPage)
}
private fun getAnimesFromLatest(document: Document): Pair<List<SAnime>, Boolean> {
val animeList = mutableListOf<SAnime>()
for (item in document.select("div.grid > div.grid__item")) {
val anime = SAnime.create()
anime.title = item.select("div.tixtlis").text()
anime.thumbnail_url = item.select("img").attr("src").replace("^//".toRegex(), "https://")
anime.setUrlWithoutDomain(item.select("a").attr("href").toHttpUrl().encodedPath)
animeList.add(anime)
}
return Pair(animeList, animeList.size == 12)
}
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/xz/gridgrabrecent.php?p=$page&limit=12&_=${System.currentTimeMillis() / 1000}", headers)
}
override fun latestUpdatesSelector(): String = throw Exception("Not used")
// 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)
}
} }

View File

@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.bestdubbedanime.extractors
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class DailyMotionExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String): List<Video> {
val videoList = mutableListOf<Video>()
val htmlString = client.newCall(GET(url)).execute().body.string()
val internalData = htmlString.substringAfter("\"dmInternalData\":").substringBefore("</script>")
val ts = internalData.substringAfter("\"ts\":").substringBefore(",")
val v1st = internalData.substringAfter("\"v1st\":\"").substringBefore("\",")
val jsonUrl = "https://www.dailymotion.com/player/metadata/video/${url.toHttpUrl().encodedPath}?locale=en-US&dmV1st=$v1st&dmTs=$ts&is_native_app=0"
val json = Json.decodeFromString<JsonObject>(
client.newCall(GET(jsonUrl))
.execute().body.string(),
)
val masterUrl = json["qualities"]!!
.jsonObject["auto"]!!
.jsonArray[0]
.jsonObject["url"]!!
.jsonPrimitive.content
val masterPlaylist = client.newCall(GET(masterUrl)).execute().body.string()
val separator = "#EXT-X-STREAM-INF"
masterPlaylist.substringAfter(separator).split(separator).map {
val quality = it.substringAfter("RESOLUTION=").substringAfter("x").substringBefore(",NAME") + "p"
var videoUrl = it.substringAfter("\n").substringBefore("\n")
videoList.add(Video(videoUrl, "$quality (DM)", videoUrl))
}
return videoList
}
}

View File

@ -25,8 +25,6 @@ import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.net.URLEncoder import java.net.URLEncoder
import java.text.CharacterIterator
import java.text.StringCharacterIterator
class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -42,8 +40,6 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val chunkedSize = 300
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
@ -54,32 +50,31 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val animeList = mutableListOf<SAnime>()
val page = response.request.url.encodedFragment!!.toInt() val page = response.request.url.encodedFragment!!.toInt()
val path = response.request.url.encodedPath val path = response.request.url.encodedPath
val items = document.select(popularAnimeSelector()) val items = document.select(popularAnimeSelector())
items.chunked(chunkedSize)[page - 1].forEach { val animeList = items.chunked(CHUNKED_SIZE)[page - 1].mapNotNull {
val a = it.selectFirst("a")!! val a = it.selectFirst("a")!!
val name = a.text() val name = a.text()
if (a.attr("href") == "..") return@forEach if (a.attr("href") == "..") return@mapNotNull null
val anime = SAnime.create() SAnime.create().apply {
anime.title = name.removeSuffix("/") setUrlWithoutDomain(joinPaths(path, a.attr("href")))
anime.setUrlWithoutDomain(joinPaths(path, a.attr("href"))) title = name.removeSuffix("/")
anime.thumbnail_url = "" thumbnail_url = ""
animeList.add(anime) }
} }
return AnimesPage(animeList, (page + 1) * chunkedSize <= items.size) return AnimesPage(animeList, (page + 1) * CHUNKED_SIZE <= items.size)
} }
override fun popularAnimeSelector(): String = "table > tbody > tr:has(a)" override fun popularAnimeSelector(): String = "table > tbody > tr:has(a)"
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeFromElement(element: Element): SAnime = throw Exception("Not used") override fun popularAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
override fun popularAnimeNextPageSelector(): String? = null
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
@ -121,34 +116,33 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun searchAnimeParse(response: Response, query: String): AnimesPage { private fun searchAnimeParse(response: Response, query: String): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val animeList = mutableListOf<SAnime>()
val page = response.request.url.encodedFragment!!.toInt() val page = response.request.url.encodedFragment!!.toInt()
val path = response.request.url.encodedPath val path = response.request.url.encodedPath
val items = document.select(popularAnimeSelector()).filter { t -> val items = document.select(popularAnimeSelector()).filter { t ->
t.selectFirst("a")!!.text().contains(query, true) t.selectFirst("a")!!.text().contains(query, true)
} }
items.chunked(chunkedSize)[page - 1].forEach { val animeList = items.chunked(CHUNKED_SIZE)[page - 1].mapNotNull {
val a = it.selectFirst("a")!! val a = it.selectFirst("a")!!
val name = a.text() val name = a.text()
if (a.attr("href") == "..") return@forEach if (a.attr("href") == "..") return@mapNotNull null
val anime = SAnime.create() SAnime.create().apply {
anime.title = name.removeSuffix("/") setUrlWithoutDomain(joinPaths(path, a.attr("href")))
anime.setUrlWithoutDomain(joinPaths(path, a.attr("href"))) title = name.removeSuffix("/")
anime.thumbnail_url = "" thumbnail_url = ""
animeList.add(anime) }
} }
return AnimesPage(animeList, (page + 1) * chunkedSize <= items.size) return AnimesPage(animeList, (page + 1) * CHUNKED_SIZE <= items.size)
} }
override fun searchAnimeSelector(): String = throw Exception("Not used") override fun searchAnimeSelector(): String = throw Exception("Not used")
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not used") override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
// ============================== FILTERS =============================== // ============================== FILTERS ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
@ -172,9 +166,7 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = Observable.just(anime)
return Observable.just(anime)
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used") override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
@ -196,7 +188,6 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
traverseDirectory(fullUrl) traverseDirectory(fullUrl)
} }
if (videoFormats.any { t -> fullUrl.endsWith(t) }) { if (videoFormats.any { t -> fullUrl.endsWith(t) }) {
val episode = SEpisode.create()
val paths = fullUrl.toHttpUrl().pathSegments val paths = fullUrl.toHttpUrl().pathSegments
val seasonInfoRegex = """(\([\s\w-]+\))(?: ?\[[\s\w-]+\])?${'$'}""".toRegex() val seasonInfoRegex = """(\([\s\w-]+\))(?: ?\[[\s\w-]+\])?${'$'}""".toRegex()
@ -213,13 +204,15 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
val size = link.selectFirst("td[data-order]")?.let { formatBytes(it.text().toLongOrNull()) } val size = link.selectFirst("td[data-order]")?.let { formatBytes(it.text().toLongOrNull()) }
episode.name = "${videoFormats.fold(paths.last()) { acc, suffix -> acc.removeSuffix(suffix).trimInfo() }}${if (size == null) "" else " - $size"}" episodeList.add(
episode.url = fullUrl SEpisode.create().apply {
episode.scanlator = seasonInfo + extraInfo name = videoFormats.fold(paths.last()) { acc, suffix -> acc.removeSuffix(suffix).trimInfo() }
episode.episode_number = counter.toFloat() this.url = fullUrl
scanlator = "${if (size == null) "" else "$size"}$seasonInfo$extraInfo"
episode_number = counter.toFloat()
},
)
counter++ counter++
episodeList.add(episode)
} }
} }
} }
@ -238,9 +231,8 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> { override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> =
return Observable.just(listOf(Video(episode.url, "Video", episode.url))) Observable.just(listOf(Video(episode.url, "Video", episode.url)))
}
override fun videoFromElement(element: Element): Video = throw Exception("Not Used") override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
@ -264,25 +256,17 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
private fun formatBytes(bytes: Long?): String? { private fun formatBytes(bytes: Long?): String? {
if (bytes == null) return null val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) var value = bytes?.toDouble() ?: return null
if (absB < 1024) { var i = 0
return "$bytes B" while (value >= 1024 && i < units.size - 1) {
value /= 1024
i++
} }
var value = absB return String.format("%.1f %s", value, units[i])
val ci: CharacterIterator = StringCharacterIterator("KMGTPE")
var i = 40
while (i >= 0 && absB > 0xfffccccccccccccL shr i) {
value = value shr 10
ci.next()
i -= 10
}
value *= java.lang.Long.signum(bytes).toLong()
return java.lang.String.format("%.1f %ciB", value / 1024.0, ci.current())
} }
private fun String.trimInfo(): String { private fun String.trimInfo(): String {
var newString = this.replaceFirst("""^\[\w+\] """.toRegex(), "") var newString = this.replaceFirst("""^\[\w+\] ?""".toRegex(), "")
val regex = """( ?\[[\s\w-]+\]| ?\([\s\w-]+\))(\.mkv|\.mp4|\.avi)?${'$'}""".toRegex() val regex = """( ?\[[\s\w-]+\]| ?\([\s\w-]+\))(\.mkv|\.mp4|\.avi)?${'$'}""".toRegex()
while (regex.containsMatchIn(newString)) { while (regex.containsMatchIn(newString)) {
@ -294,15 +278,11 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return newString return newString
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { companion object {
val ignoreExtras = SwitchPreferenceCompat(screen.context).apply { private const val CHUNKED_SIZE = 300
key = "ignore_extras"
title = "Ignore \"Extras\" folder"
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
screen.addPreference(ignoreExtras)
} }
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { }
} }

View File

@ -6,7 +6,7 @@ ext {
extName = 'FMovies' extName = 'FMovies'
pkgNameSuffix = 'en.fmovies' pkgNameSuffix = 'en.fmovies'
extClass = '.FMovies' extClass = '.FMovies'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
} }

View File

@ -66,7 +66,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val a = element.selectFirst("div.meta a")!! val a = element.selectFirst("div.meta a")!!
return SAnime.create().apply { return SAnime.create().apply {
setUrlWithoutDomain(a.relative()) setUrlWithoutDomain(a.attr("abs:href"))
thumbnail_url = element.select("div.poster img").attr("data-src") thumbnail_url = element.select("div.poster img").attr("data-src")
title = a.text() title = a.text()
} }
@ -277,6 +277,8 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}.filterNotNull().flatten(), }.filterNotNull().flatten(),
) )
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return videoList.sort() return videoList.sort()
} }
@ -355,7 +357,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_TITLE)!! val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy( compareBy(
@ -377,10 +379,6 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
private fun Element.relative(): String {
return this.attr("abs:href").toHttpUrl().encodedPath
}
private fun Int.toPageQuery(first: Boolean = true): String { private fun Int.toPageQuery(first: Boolean = true): String {
return if (this == 1) "" else "${if (first) "?" else "&"}page=$this" return if (this == 1) "" else "${if (first) "?" else "&"}page=$this"
} }
@ -394,30 +392,15 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
) )
private const val PREF_DOMAIN_KEY = "preferred_domain" private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private val PREF_DOMAIN_ENTRIES = arrayOf(
"fmovies.to",
"fmovies.wtf",
"fmovies.taxi",
"fmovies.pub",
"fmovies.cafe",
"fmovies.world",
)
private val PREF_DOMAIN_ENTRY_VALUES = PREF_DOMAIN_ENTRIES.map { "https://$it" }.toTypedArray()
private val PREF_DOMAIN_DEFAULT = "https://fmovies.to" private val PREF_DOMAIN_DEFAULT = "https://fmovies.to"
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
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_QUALITY_DEFAULT = "1080" private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_SERVER_KEY = "preferred_server" private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred server"
private const val PREF_SERVER_DEFAULT = "Vidstream" private const val PREF_SERVER_DEFAULT = "Vidstream"
private const val PREF_HOSTER_KEY = "hoster_selection" private const val PREF_HOSTER_KEY = "hoster_selection"
private const val PREF_HOSTER_TITLE = "Enable/Disable Hosts"
private val PREF_HOSTER_DEFAULT = setOf("Vidstream", "Filemoon") private val PREF_HOSTER_DEFAULT = setOf("Vidstream", "Filemoon")
} }
// ============================== Settings ============================== // ============================== Settings ==============================
@ -425,9 +408,15 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE title = "Preferred domain (requires app restart)"
entries = PREF_DOMAIN_ENTRIES entries = arrayOf(
entryValues = PREF_DOMAIN_ENTRY_VALUES "fmovies.to", "fmovies.wtf", "fmovies.taxi",
"fmovies.pub", "fmovies.cafe", "fmovies.world"
)
entryValues = arrayOf(
"https://fmovies.to", "https://fmovies.wtf", "https://fmovies.taxi",
"https://fmovies.pub", "https://fmovies.cafe", "https://fmovies.world",
)
setDefaultValue(PREF_DOMAIN_DEFAULT) setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = "%s" summary = "%s"
@ -437,13 +426,13 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = PREF_QUALITY_ENTRY_VALUES entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue(PREF_QUALITY_DEFAULT) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
@ -453,11 +442,11 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_SERVER_KEY key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE title = "Preferred server"
entries = HOSTERS entries = HOSTERS
entryValues = HOSTERS entryValues = HOSTERS
setDefaultValue(PREF_SERVER_DEFAULT) setDefaultValue(PREF_SERVER_DEFAULT)
@ -469,11 +458,11 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply { MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE title = "Enable/Disable Hosts"
entries = HOSTERS entries = HOSTERS
entryValues = HOSTERS entryValues = HOSTERS
setDefaultValue(PREF_HOSTER_DEFAULT) setDefaultValue(PREF_HOSTER_DEFAULT)
@ -482,6 +471,6 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit() preferences.edit().putStringSet(key, newValue as Set<String>).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
} }
} }

View File

@ -210,7 +210,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_TITLE)!! val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy( compareBy(
@ -253,21 +253,15 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val PREF_DOMAIN_KEY = "preferred_domain_name" private const val PREF_DOMAIN_KEY = "preferred_domain_name"
private const val PREF_DOMAIN_TITLE = "Override BaseUrl" private const val PREF_DOMAIN_TITLE = "Override BaseUrl"
private const val PREF_DOMAIN_SUMMARY = "Override default domain (requires app restart)"
private const val PREF_DOMAIN_DEFAULT = "https://gogoanime.hu" private const val PREF_DOMAIN_DEFAULT = "https://gogoanime.hu"
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
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_QUALITY_DEFAULT = "1080" private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_SERVER_KEY = "preferred_server" private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_TITLE = "Preferred server"
private const val PREF_SERVER_DEFAULT = "Gogostream" private const val PREF_SERVER_DEFAULT = "Gogostream"
private const val PREF_HOSTER_KEY = "hoster_selection" private const val PREF_HOSTER_KEY = "hoster_selection"
private const val PREF_HOSTER_TITLE = "Enable/Disable Hosts"
private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet() private val PREF_HOSTER_DEFAULT = HOSTERS_NAMES.toSet()
} }
@ -277,7 +271,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
EditTextPreference(screen.context).apply { EditTextPreference(screen.context).apply {
key = PREF_DOMAIN_KEY key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE title = PREF_DOMAIN_TITLE
summary = PREF_DOMAIN_SUMMARY summary = "Override default domain (requires app restart)"
dialogTitle = PREF_DOMAIN_TITLE dialogTitle = PREF_DOMAIN_TITLE
dialogMessage = "Default: $PREF_DOMAIN_DEFAULT" dialogMessage = "Default: $PREF_DOMAIN_DEFAULT"
setDefaultValue(PREF_DOMAIN_DEFAULT) setDefaultValue(PREF_DOMAIN_DEFAULT)
@ -286,13 +280,13 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val newValueString = newValue as String val newValueString = newValue as String
preferences.edit().putString(key, newValueString.trim()).commit() preferences.edit().putString(key, newValueString.trim()).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = PREF_QUALITY_ENTRY_VALUES entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue(PREF_QUALITY_DEFAULT) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
@ -302,11 +296,11 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_SERVER_KEY key = PREF_SERVER_KEY
title = PREF_SERVER_TITLE title = "Preferred server"
entries = HOSTERS entries = HOSTERS
entryValues = HOSTERS entryValues = HOSTERS
setDefaultValue(PREF_SERVER_DEFAULT) setDefaultValue(PREF_SERVER_DEFAULT)
@ -318,11 +312,11 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
MultiSelectListPreference(screen.context).apply { MultiSelectListPreference(screen.context).apply {
key = PREF_HOSTER_KEY key = PREF_HOSTER_KEY
title = PREF_HOSTER_TITLE title = "Enable/Disable Hosts"
entries = HOSTERS entries = HOSTERS
entryValues = HOSTERS_NAMES entryValues = HOSTERS_NAMES
setDefaultValue(PREF_HOSTER_DEFAULT) setDefaultValue(PREF_HOSTER_DEFAULT)
@ -331,7 +325,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit() preferences.edit().putStringSet(key, newValue as Set<String>).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
} }
// ============================== Filters =============================== // ============================== Filters ===============================

View File

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -48,37 +47,23 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "div#content > div > div.row > div" override fun popularAnimeSelector(): String = "div#content > div > div.row > div"
override fun popularAnimeNextPageSelector(): String = "nav.gridlove-pagination > span.current + a" override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
override fun popularAnimeFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
return SAnime.create().apply { title = element.selectFirst("h1")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
title = element.selectFirst("h1")!!.text()
}
} }
override fun popularAnimeNextPageSelector(): String = "nav.gridlove-pagination > span.current + a"
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl) override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesSelector(): String = "div#latest-tab-pane > div.row > div.col-md-6" override fun latestUpdatesSelector(): String = throw Exception("Not used")
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector() override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime { override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
val thumbnailUrl = element.selectFirst("img")!!.attr("data-src")
return SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a.animeparent")!!.attr("href"))
thumbnail_url = if (thumbnailUrl.contains(baseUrl.toHttpUrl().host)) {
thumbnailUrl
} else {
baseUrl + thumbnailUrl
}
title = element.selectFirst("span.animename")!!.text()
}
}
// =============================== Search =============================== // =============================== Search ===============================
@ -113,23 +98,8 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
return client.newCall(animeDetailsRequest(anime)) description = document.selectFirst("div.entry-content > p")?.text()
.asObservableSuccess()
.map { response ->
animeDetailsParse(response, anime).apply { initialized = true }
}
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
private fun animeDetailsParse(response: Response, anime: SAnime): SAnime {
val document = response.asJsoup()
val oldAnime = anime
oldAnime.description = document.selectFirst("div.entry-content > p")?.text()
return oldAnime
} }
// ============================== Episodes ============================== // ============================== Episodes ==============================
@ -150,13 +120,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val size = sizeRegex.find(link.text())?.groupValues?.get(1) val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = link.text() SEpisode.create().apply {
episode.episode_number = 1F name = link.text()
episode.date_upload = -1L episode_number = 1F
episode.url = link.attr("href") date_upload = -1L
episode.scanlator = "${if (size == null) "" else "$size • "}$info" url = link.attr("href")
episodeList.add(episode) scanlator = "${if (size == null) "" else "$size • "}$info"
}
)
} }
} }
@ -174,13 +146,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val size = sizeRegex.find(link.text())?.groupValues?.get(1) val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = "Ep. $episodeNumber - ${link.text()}" SEpisode.create().apply {
episode.episode_number = episodeNumber name = "Ep. $episodeNumber - ${link.text()}"
episode.date_upload = -1L episode_number = episodeNumber
episode.url = link.attr("href") date_upload = -1L
episode.scanlator = "${if (size == null) "" else "$size • "}$info" url = link.attr("href")
episodeList.add(episode) scanlator = "${if (size == null) "" else "$size • "}$info"
}
)
} }
} }
} }
@ -196,13 +170,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val size = sizeRegex.find(title)?.groupValues?.get(1) val size = sizeRegex.find(title)?.groupValues?.get(1)
?: sizeRegex.find(link.text())?.groupValues?.get(1) ?: sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = "$title - ${link.text()}" SEpisode.create().apply {
episode.episode_number = 1F name = "$title - ${link.text()}"
episode.date_upload = -1L episode_number = 1F
episode.scanlator = size date_upload = -1L
episode.url = link.attr("href") scanlator = size
episodeList.add(episode) url = link.attr("href")
}
)
} }
} }
} }
@ -218,13 +194,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val size = sizeRegex.find(link.text())?.groupValues?.get(1) val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = "$title - ${link.text()}" SEpisode.create().apply {
episode.episode_number = 1F name = "$title - ${link.text()}"
episode.date_upload = -1L episode_number = 1F
episode.scanlator = size date_upload = -1L
episode.url = link.attr("href") scanlator = size
episodeList.add(episode) url = link.attr("href")
}
)
} }
} }
} }
@ -239,13 +217,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val size = sizeRegex.find(link.text())?.groupValues?.get(1) val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = link.text() SEpisode.create().apply {
episode.episode_number = 1F name = link.text()
episode.date_upload = -1L episode_number = 1F
episode.scanlator = "${if (size == null) "" else "$size • "}$info" date_upload = -1L
episode.url = link.attr("href") scanlator = "${if (size == null) "" else "$size • "}$info"
episodeList.add(episode) url = link.attr("href")
}
)
} }
} }
} }
@ -260,13 +240,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val size = sizeRegex.find(link.text())?.groupValues?.get(1) val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = link.text() SEpisode.create().apply {
episode.episode_number = 1F name = link.text()
episode.date_upload = -1L episode_number = 1F
episode.scanlator = "${if (size == null) "" else "$size • "}$info" date_upload = -1L
episode.url = link.attr("href") scanlator = "${if (size == null) "" else "$size • "}$info"
episodeList.add(episode) url = link.attr("href")
}
)
} }
} }
} }
@ -277,13 +259,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
if (zipRegex.find(link.text()) != null) return@forEach if (zipRegex.find(link.text()) != null) return@forEach
val size = sizeRegex.find(link.text())?.groupValues?.get(1) val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = link.text() SEpisode.create().apply {
episode.episode_number = 1F name = link.text()
episode.date_upload = -1L episode_number = 1F
episode.scanlator = size date_upload = -1L
episode.url = link.attr("href") scanlator = size
episodeList.add(episode) url = link.attr("href")
}
)
} }
} }
} }
@ -294,13 +278,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
if (zipRegex.find(link.text()) != null) return@forEach if (zipRegex.find(link.text()) != null) return@forEach
val size = sizeRegex.find(link.text())?.groupValues?.get(1) val size = sizeRegex.find(link.text())?.groupValues?.get(1)
val episode = SEpisode.create() episodeList.add(
episode.name = link.text() SEpisode.create().apply {
episode.episode_number = 1F name = link.text()
episode.date_upload = -1L episode_number = 1F
episode.scanlator = size date_upload = -1L
episode.url = link.attr("href") scanlator = size
episodeList.add(episode) url = link.attr("href")
}
)
} }
} }
} }
@ -326,16 +312,20 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
else -> { throw Exception("Unsupported url: ${episode.url}") } else -> { throw Exception("Unsupported url: ${episode.url}") }
} }
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return Observable.just(videoList.sort()) return Observable.just(videoList.sort())
} }
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used") override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {} override fun setupPreferenceScreen(screen: PreferenceScreen) {}
} }

View File

@ -6,7 +6,7 @@ ext {
extName = 'Kawaiifu' extName = 'Kawaiifu'
pkgNameSuffix = 'en.kawaiifu' pkgNameSuffix = 'en.kawaiifu'
extClass = '.Kawaiifu' extClass = '.Kawaiifu'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
containsNsfw = true containsNsfw = true
} }

View File

@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -54,7 +53,7 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "ul.list-film li" override fun popularAnimeSelector(): String = "ul.list-film li"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply { override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a.mv-namevn")!!.relative()) setUrlWithoutDomain(element.selectFirst("a.mv-namevn")!!.attr("abs:href"))
title = element.selectFirst("a.mv-namevn")!!.text() title = element.selectFirst("a.mv-namevn")!!.text()
thumbnail_url = element.selectFirst("a img")!!.attr("src") thumbnail_url = element.selectFirst("a img")!!.attr("src")
} }
@ -71,7 +70,7 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val a = element.selectFirst("div.info a:not([style])")!! val a = element.selectFirst("div.info a:not([style])")!!
return SAnime.create().apply { return SAnime.create().apply {
setUrlWithoutDomain(a.relative()) setUrlWithoutDomain(a.attr("abs:href"))
thumbnail_url = element.select("a.thumb img").attr("src") thumbnail_url = element.select("a.thumb img").attr("src")
title = a.text() title = a.text()
} }
@ -196,6 +195,8 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
) )
} }
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return Observable.just(videoList) return Observable.just(videoList)
} }
@ -218,10 +219,6 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
).reversed() ).reversed()
} }
private fun Element.relative(): String {
return this.attr("abs:href").toHttpUrl().encodedPath
}
private fun Int.toPage(): String { private fun Int.toPage(): String {
return if (this == 1) { return if (this == 1) {
"" ""
@ -244,9 +241,6 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
companion object { companion object {
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360", "240")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
private const val PREF_QUALITY_DEFAULT = "720" private const val PREF_QUALITY_DEFAULT = "720"
} }
@ -255,9 +249,9 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
entryValues = PREF_QUALITY_ENTRY_VALUES entryValues = arrayOf("1080", "720", "480", "360", "240")
setDefaultValue(PREF_QUALITY_DEFAULT) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
@ -267,6 +261,6 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
} }
} }

View File

@ -6,7 +6,7 @@ ext {
extName = 'Kayoanime' extName = 'Kayoanime'
pkgNameSuffix = 'en.kayoanime' pkgNameSuffix = 'en.kayoanime'
extClass = '.Kayoanime' extClass = '.Kayoanime'
extVersionCode = 5 extVersionCode = 6
libVersion = '13' libVersion = '13'
} }

View File

@ -4,7 +4,6 @@ import android.util.Base64
import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -18,7 +17,11 @@ class DriveIndexExtractor(private val client: OkHttpClient, private val headers:
private val json: Json by injectLazy() private val json: Json by injectLazy()
fun getEpisodesFromIndex(indexUrl: String, path: String, flipOrder: Boolean): List<SEpisode> { fun getEpisodesFromIndex(
indexUrl: String,
path: String,
trimName: Boolean,
): List<SEpisode> {
val episodeList = mutableListOf<SEpisode>() val episodeList = mutableListOf<SEpisode>()
val basePathCounter = indexUrl.toHttpUrl().pathSegments.size val basePathCounter = indexUrl.toHttpUrl().pathSegments.size
@ -52,11 +55,21 @@ class DriveIndexExtractor(private val client: OkHttpClient, private val headers:
traverseDirectory(newUrl) traverseDirectory(newUrl)
} }
if (item.mimeType.startsWith("video/")) { if (item.mimeType.startsWith("video/")) {
val episode = SEpisode.create()
val epUrl = joinUrl(url, item.name) val epUrl = joinUrl(url, item.name)
val paths = epUrl.toHttpUrl().pathSegments val paths = epUrl.toHttpUrl().pathSegments
// Get other info // Get other info
val season = if (paths.size == basePathCounter) {
""
} else {
paths[basePathCounter - 1]
}
val seasonInfoRegex = """(\([\s\w-]+\))(?: ?\[[\s\w-]+\])?${'$'}""".toRegex()
val seasonInfo = if (seasonInfoRegex.containsMatchIn(season)) {
"${seasonInfoRegex.find(season)!!.groups[1]!!.value}"
} else {
""
}
val extraInfo = if (paths.size > basePathCounter) { val extraInfo = if (paths.size > basePathCounter) {
"/$path/" + paths.subList(basePathCounter - 1, paths.size - 1).joinToString("/") { it.trimInfo() } "/$path/" + paths.subList(basePathCounter - 1, paths.size - 1).joinToString("/") { it.trimInfo() }
} else { } else {
@ -64,18 +77,16 @@ class DriveIndexExtractor(private val client: OkHttpClient, private val headers:
} }
val size = item.size?.toLongOrNull()?.let { formatFileSize(it) } val size = item.size?.toLongOrNull()?.let { formatFileSize(it) }
episode.name = item.name.trimInfo() episodeList.add(
episode.url = epUrl SEpisode.create().apply {
episode.scanlator = if (flipOrder) { name = if (trimName) item.name.trimInfo() else item.name
"$extraInfo${size ?: "N/A"}" this.url = epUrl
} else { scanlator = "${if (size == null) "" else "$size"}$seasonInfo$extraInfo"
"${size ?: "N/A"}$extraInfo" date_upload = -1L
} episode_number = counter.toFloat()
episode.episode_number = counter.toFloat() },
episode.date_upload = -1L )
counter++ counter++
episodeList.add(episode)
} }
} }

View File

@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -34,10 +33,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest import java.security.MessageDigest
import java.text.CharacterIterator
import java.text.SimpleDateFormat
import java.text.StringCharacterIterator
import java.util.Locale
class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -61,20 +56,12 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val maxRecursionDepth = 2
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
}
}
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request {
@ -146,32 +133,28 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "ul#posts-container > li.post-item" override fun popularAnimeSelector(): String = "ul#posts-container > li.post-item"
override fun popularAnimeNextPageSelector(): String = "div.pages-nav > a[data-text=load more]" override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
override fun popularAnimeFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
return SAnime.create().apply { title = element.selectFirst("h2.post-title")!!.text().substringBefore(" Episode")
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
title = element.selectFirst("h2.post-title")!!.text().substringBefore(" Episode")
}
} }
override fun popularAnimeNextPageSelector(): String = "div.pages-nav > a[data-text=load more]"
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl) override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
override fun latestUpdatesSelector(): String = "ul.tabs:has(a:contains(Recent)) + div.tab-content li.widget-single-post-item" override fun latestUpdatesSelector(): String = "ul.tabs:has(a:contains(Recent)) + div.tab-content li.widget-single-post-item"
override fun latestUpdatesNextPageSelector(): String? = null override fun latestUpdatesFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
override fun latestUpdatesFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
return SAnime.create().apply { title = element.selectFirst("a.post-title")!!.text().substringBefore(" Episode")
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
title = element.selectFirst("a.post-title")!!.text().substringBefore(" Episode")
}
} }
override fun latestUpdatesNextPageSelector(): String? = null
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
@ -226,14 +209,12 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun searchAnimeParse(response: Response): AnimesPage = popularAnimeParse(response)
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters =============================== // ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
@ -280,26 +261,13 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun animeDetailsParse(document: Document): SAnime {
return client.newCall(animeDetailsRequest(anime))
.asObservableSuccess()
.map { response ->
animeDetailsParse(response, anime).apply { initialized = true }
}
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
private fun animeDetailsParse(response: Response, anime: SAnime): SAnime {
val document = response.asJsoup()
val moreInfo = document.select("div.toggle-content > ul > li").joinToString("\n") { it.text() } val moreInfo = document.select("div.toggle-content > ul > li").joinToString("\n") { it.text() }
val realDesc = document.selectFirst("div.entry-content:has(div.toggle + div.clearfix + div.toggle:has(h3:contains(Information)))")?.let { val realDesc = document.selectFirst("div.entry-content:has(div.toggle + div.clearfix + div.toggle:has(h3:contains(Information)))")?.let {
it.selectFirst("div.toggle > div.toggle-content")!!.text() + "\n\n" it.selectFirst("div.toggle > div.toggle-content")!!.text() + "\n\n"
} ?: "" } ?: ""
return SAnime.create().apply { return SAnime.create().apply {
title = anime.title
thumbnail_url = anime.thumbnail_url
status = document.selectFirst("div.toggle-content > ul > li:contains(Status)")?.let { status = document.selectFirst("div.toggle-content > ul > li:contains(Status)")?.let {
parseStatus(it.text()) parseStatus(it.text())
} ?: SAnime.UNKNOWN } ?: SAnime.UNKNOWN
@ -320,13 +288,8 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val document = response.asJsoup() val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>() val episodeList = mutableListOf<SEpisode>()
val keyRegex = """"(\w{39})"""".toRegex()
val versionRegex = """"([^"]+web-frontend[^"]+)"""".toRegex()
val jsonRegex = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL)
val boundary = "=====vc17a3rwnndj====="
fun traverseFolder(url: String, path: String, recursionDepth: Int = 0) { fun traverseFolder(url: String, path: String, recursionDepth: Int = 0) {
if (recursionDepth == maxRecursionDepth) return if (recursionDepth == MAX_RECURSION_DEPTH) return
val folderId = url.substringAfter("/folders/") val folderId = url.substringAfter("/folders/")
val driveHeaders = headers.newBuilder() val driveHeaders = headers.newBuilder()
@ -342,14 +305,14 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return
val keyScript = driveDocument.select("script").first { script -> val keyScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.data()
val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: ""
val versionScript = driveDocument.select("script").first { script -> val versionScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.data()
val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" val driveVersion = VERSION_REGEX.find(versionScript)?.groupValues?.get(1) ?: ""
val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull { val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID" it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: "" }?.value ?: ""
@ -357,7 +320,7 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
var pageToken: String? = "" var pageToken: String? = ""
while (pageToken != null) { while (pageToken != null) {
val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1"
val body = """--$boundary val body = """--$BOUNDARY
|content-type: application/http |content-type: application/http
|content-transfer-encoding: binary |content-transfer-encoding: binary
| |
@ -366,12 +329,12 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|authorization: ${generateSapisidhashHeader(sapisid)} |authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0 |x-goog-authuser: 0
| |
|--$boundary |--$BOUNDARY
| |
""".trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType())
val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder()
.addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$BOUNDARY\"")
.addQueryParameter("key", key) .addQueryParameter("key", key)
.build() .build()
.toString() .toString()
@ -386,34 +349,23 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
POST(postUrl, body = body, headers = postHeaders), POST(postUrl, body = body, headers = postHeaders),
).execute() ).execute()
val parsed = json.decodeFromString<GDrivePostResponse>( val parsed = json.decodeFromString<GDrivePostResponse>(
jsonRegex.find(response.body.string())!!.groupValues[1], JSON_REGEX.find(response.body.string())!!.groupValues[1],
) )
if (parsed.items == null) throw Exception("Failed to load items, please log in to google drive through webview") if (parsed.items == null) throw Exception("Failed to load items, please log in to google drive through webview")
parsed.items.forEachIndexed { index, it -> parsed.items.forEachIndexed { index, it ->
if (it.mimeType.startsWith("video")) { if (it.mimeType.startsWith("video")) {
val episode = SEpisode.create()
val size = formatBytes(it.fileSize?.toLongOrNull()) val size = formatBytes(it.fileSize?.toLongOrNull())
val pathName = if (preferences.getBoolean("trim_info", false)) { val pathName = path.trimInfo()
path.trimInfo()
} else {
path
}
val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex() episodeList.add(
episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) { SEpisode.create().apply {
"/$pathName$size" name = if (preferences.trimEpisodeName) it.title.trimInfo() else it.title
} else { this.url = "https://drive.google.com/uc?id=${it.id}"
"$size • /$pathName" episode_number = ITEM_NUMBER_REGEX.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
} date_upload = -1L
episode.name = if (preferences.getBoolean("trim_episode", false)) { scanlator = "$size • /$pathName"
it.title.trimInfo() },
} else { )
it.title
}
episode.url = "https://drive.google.com/uc?id=${it.id}"
episode.episode_number = itemNumberRegex.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
episode.date_upload = -1L
episodeList.add(episode)
} }
if (it.mimeType.endsWith(".folder")) { if (it.mimeType.endsWith(".folder")) {
traverseFolder( traverseFolder(
@ -449,7 +401,7 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
indexExtractor.getEpisodesFromIndex( indexExtractor.getEpisodesFromIndex(
location, location,
getVideoPathsFromElement(season) + " " + it.text(), getVideoPathsFromElement(season) + " " + it.text(),
preferences.getBoolean("scanlator_order", false), preferences.trimEpisodeName,
), ),
) )
} }
@ -479,16 +431,18 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} else if (host.contains("workers.dev")) { } else if (host.contains("workers.dev")) {
getIndexVideoUrl(episode.url) getIndexVideoUrl(episode.url)
} else { } else {
emptyList() throw Exception("Unsupported url: ${episode.url}")
} }
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return Observable.just(videoList) return Observable.just(videoList)
} }
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used") override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
@ -519,7 +473,7 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
private fun String.trimInfo(): String { private fun String.trimInfo(): String {
var newString = this.replaceFirst("""^\[\w+\] """.toRegex(), "") var newString = this.replaceFirst("""^\[\w+\] ?""".toRegex(), "")
val regex = """( ?\[[\s\w-]+\]| ?\([\s\w-]+\))(\.mkv|\.mp4|\.avi)?${'$'}""".toRegex() val regex = """( ?\[[\s\w-]+\]| ?\([\s\w-]+\))(\.mkv|\.mp4|\.avi)?${'$'}""".toRegex()
while (regex.containsMatchIn(newString)) { while (regex.containsMatchIn(newString)) {
@ -570,21 +524,14 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
) )
private fun formatBytes(bytes: Long?): String? { private fun formatBytes(bytes: Long?): String? {
if (bytes == null) return null val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) var value = bytes?.toDouble() ?: return null
if (absB < 1024) { var i = 0
return "$bytes B" while (value >= 1024 && i < units.size - 1) {
value /= 1024
i++
} }
var value = absB return String.format("%.1f %s", value, units[i])
val ci: CharacterIterator = StringCharacterIterator("KMGTPE")
var i = 40
while (i >= 0 && absB > 0xfffccccccccccccL shr i) {
value = value shr 10
ci.next()
i -= 10
}
value *= java.lang.Long.signum(bytes).toLong()
return java.lang.String.format("%.1f %cB", value / 1024.0, ci.current())
} }
private fun getCookie(url: String): String { private fun getCookie(url: String): String {
@ -604,34 +551,32 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { companion object {
val scanlatorOrder = SwitchPreferenceCompat(screen.context).apply { private val ITEM_NUMBER_REGEX = """ - (?:S\d+E)?(\d+)""".toRegex()
key = "scanlator_order" private val KEY_REGEX = """"(\w{39})"""".toRegex()
title = "Switch order of file path and size" private val VERSION_REGEX = """"([^"]+web-frontend[^"]+)"""".toRegex()
setDefaultValue(false) private val JSON_REGEX = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL)
setOnPreferenceChangeListener { _, newValue -> private const val BOUNDARY = "=====vc17a3rwnndj====="
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
val trimEpisodeName = SwitchPreferenceCompat(screen.context).apply {
key = "trim_episode"
title = "Trim info from episode name"
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
val trimEpisodeInfo = SwitchPreferenceCompat(screen.context).apply {
key = "trim_info"
title = "Trim info from episode info"
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
screen.addPreference(scanlatorOrder) private const val MAX_RECURSION_DEPTH = 2
screen.addPreference(trimEpisodeName)
screen.addPreference(trimEpisodeInfo) private const val TRIM_EPISODE_NAME_KEY = "trim_episode"
private const val TRIM_EPISODE_NAME_DEFAULT = true
}
private val SharedPreferences.trimEpisodeName
get() = getBoolean(TRIM_EPISODE_NAME_KEY, TRIM_EPISODE_NAME_DEFAULT)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = TRIM_EPISODE_NAME_KEY
title = "Trim info from episode name"
setDefaultValue(TRIM_EPISODE_NAME_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
} }
} }

View File

@ -6,7 +6,7 @@ ext {
extName = 'KissAnime' extName = 'KissAnime'
pkgNameSuffix = 'en.kissanime' pkgNameSuffix = 'en.kissanime'
extClass = '.KissAnime' extClass = '.KissAnime'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
} }

View File

@ -24,7 +24,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
@ -46,7 +45,7 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "kissanime.com.ru" override val name = "kissanime.com.ru"
override val baseUrl by lazy { preferences.getString("preferred_domain", "https://kissanime.com.ru")!! } override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
override val lang = "en" override val lang = "en"
@ -60,38 +59,30 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH)
}
}
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/AnimeListOnline/Trending?page=$page") override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/AnimeListOnline/Trending?page=$page")
override fun popularAnimeSelector(): String = "div.listing > div.item_movies_in_cat" override fun popularAnimeSelector(): String = "div.listing > div.item_movies_in_cat"
override fun popularAnimeNextPageSelector(): String = "div.pagination > ul > li.current ~ li" override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
override fun popularAnimeFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img")!!.attr("src")
return SAnime.create().apply { title = element.selectFirst("div.title_in_cat_container > a")!!.text()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img")!!.attr("src")
title = element.selectFirst("div.title_in_cat_container > a")!!.text()
}
} }
override fun popularAnimeNextPageSelector(): String = "div.pagination > ul > li.current ~ li"
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/AnimeListOnline/LatestUpdate?page=$page") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/AnimeListOnline/LatestUpdate?page=$page")
override fun latestUpdatesSelector(): String = popularAnimeSelector() override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used") override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
@ -118,15 +109,15 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val document = response.asJsoup() val document = response.asJsoup()
val name = response.request.url.encodedFragment!! val name = response.request.url.encodedFragment!!
val animes = document.select("div.barContent > div.schedule_container > div.schedule_item:has(div.schedule_block_title:contains($name)) div.schedule_row > div.schedule_block").map { val animeList = document.select("div.barContent > div.schedule_container > div.schedule_item:has(div.schedule_block_title:contains($name)) div.schedule_row > div.schedule_block").map {
SAnime.create().apply { SAnime.create().apply {
title = it.selectFirst("h2 > a > span.jtitle")!!.text() title = it.selectFirst("h2 > a > span.jtitle")!!.text()
thumbnail_url = it.selectFirst("img")!!.attr("src") thumbnail_url = it.selectFirst("img")!!.attr("src")
setUrlWithoutDomain(it.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath) setUrlWithoutDomain(it.selectFirst("a")!!.attr("href"))
} }
} }
AnimesPage(animes, false) AnimesPage(animeList, false)
} else { } else {
super.searchAnimeParse(response) super.searchAnimeParse(response)
} }
@ -134,10 +125,10 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== FILTERS =============================== // ============================== FILTERS ===============================
override fun getFilterList(): AnimeFilterList = KissAnimeFilters.FILTER_LIST override fun getFilterList(): AnimeFilterList = KissAnimeFilters.FILTER_LIST
@ -146,6 +137,7 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun animeDetailsParse(document: Document): SAnime { override fun animeDetailsParse(document: Document): SAnime {
val rating = document.selectFirst("div.Votes > div.Prct > div[data-percent]")?.let { "\n\nUser rating: ${it.attr("data-percent")}%" } ?: "" val rating = document.selectFirst("div.Votes > div.Prct > div[data-percent]")?.let { "\n\nUser rating: ${it.attr("data-percent")}%" } ?: ""
return SAnime.create().apply { return SAnime.create().apply {
title = document.selectFirst("div.barContent > div.full > h2")!!.text() title = document.selectFirst("div.barContent > div.full > h2")!!.text()
thumbnail_url = document.selectFirst("div.cover_anime img")!!.attr("src") thumbnail_url = document.selectFirst("div.cover_anime img")!!.attr("src")
@ -159,13 +151,11 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListSelector(): String = "div.listing > div:not([class])" override fun episodeListSelector(): String = "div.listing > div:not([class])"
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element): SEpisode = SEpisode.create().apply {
return SEpisode.create().apply { name = element.selectFirst("a")!!.text()
name = element.selectFirst("a")!!.text() episode_number = element.selectFirst("a")!!.text().substringAfter("Episode ").toFloatOrNull() ?: 0F
episode_number = element.selectFirst("a")!!.text().substringAfter("Episode ").toFloatOrNull() ?: 0F date_upload = parseDate(element.selectFirst("div:not(:has(a))")!!.text())
date_upload = parseDate(element.selectFirst("div:not(:has(a))")!!.text()) setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").substringAfter(baseUrl))
}
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
@ -251,19 +241,21 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}.filterNotNull().flatten(), }.filterNotNull().flatten(),
) )
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return Observable.just(videoList.sort()) return Observable.just(videoList.sort())
} }
override fun videoFromElement(element: Element): Video = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used") override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy { it.quality.contains(quality) }, compareBy { it.quality.contains(quality) },
@ -294,45 +286,57 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = "preferred_domain"
title = "Preferred domain (requires app restart)"
entries = arrayOf("kissanime.com.ru", "kissanime.co", "kissanime.sx", "kissanime.org.ru")
entryValues = arrayOf("https://kissanime.com.ru", "https://kissanime.co", "https://kissanime.sx", "https://kissanime.org.ru")
setDefaultValue("https://kissanime.com.ru")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = "preferred_quality"
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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(domainPref)
screen.addPreference(videoQualityPref)
}
// From Dopebox // From Dopebox
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking { runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH)
}
private const val PREF_DOMAIN_KEY = "preferred_domain"
private const val PREF_DOMAIN_DEFAULT = "https://kissanime.com.ru"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain (requires app restart)"
entries = arrayOf("kissanime.com.ru", "kissanime.co", "kissanime.sx", "kissanime.org.ru")
entryValues = arrayOf("https://kissanime.com.ru", "https://kissanime.co", "https://kissanime.sx", "https://kissanime.org.ru")
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_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360")
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)
}
} }

View File

@ -113,21 +113,23 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
val dataPage = document.select("div#app").attr("data-page").replace("&quot;", "\"") val dataPage = document.select("div#app").attr("data-page").replace("&quot;", "\"")
val details = json.decodeFromString<AnimeDetails>(dataPage).props.anime val details = json.decodeFromString<AnimeDetails>(dataPage).props.anime
var desc = Jsoup.parse(
details.description.replace("<br />", "br2n"),
).text().replace("br2n", "\n") + "\n"
desc += "\nContent Rating: ${details.content_rating.name}"
desc += "\nRelease Date: ${details.release_date}"
desc += "\nType: ${details.type.name}"
desc += "\nSource: ${details.source_list.joinToString(separator = ", ") { it.name }}"
return SAnime.create().apply { return SAnime.create().apply {
title = details.title title = details.title
thumbnail_url = details.cover thumbnail_url = details.cover
genre = details.genre_list.joinToString(", ") { it.name } genre = details.genre_list.joinToString(", ") { it.name }
author = details.production_list.joinToString(", ") { it.name } author = details.production_list.joinToString(", ") { it.name }
status = parseStatus(details.status.name) status = parseStatus(details.status.name)
description = desc description = buildString {
append(
Jsoup.parse(
details.description.replace("<br />", "br2n"),
).text().replace("br2n", "\n"),
)
append("\n\nContent Rating: ${details.content_rating.name}")
append("\nRelease Date: ${details.release_date}")
append("\nType: ${details.type.name}")
append("\nSource: ${details.source_list.joinToString(separator = ", ") { it.name }}")
}
} }
} }
@ -311,23 +313,19 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
companion object { companion object {
private const val PREF_QUALITY_KEY = "preferred_quality" private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360") 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 val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
private const val PREF_QUALITY_DEFAULT = "1080" private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_GROUP_KEY = "preferred_group" private const val PREF_GROUP_KEY = "preferred_group"
private const val PREF_GROUP_TITLE = "Preferred group"
private const val PREF_GROUP_DEFAULT = "site_default" private const val PREF_GROUP_DEFAULT = "site_default"
private const val PREF_SUBS_KEY = "preferred_sub" private const val PREF_SUBS_KEY = "preferred_sub"
private const val PREF_SUBS_TITLE = "Prefer subs or dubs?"
private val PREF_SUBS_ENTRY_VALUES = arrayOf("sub", "dub") private val PREF_SUBS_ENTRY_VALUES = arrayOf("sub", "dub")
private val PREF_SUBS_ENTRIES = arrayOf("Subs", "Dubs") private val PREF_SUBS_ENTRIES = arrayOf("Subs", "Dubs")
private const val PREF_SUBS_DEFAULT = "sub" private const val PREF_SUBS_DEFAULT = "sub"
private const val PREF_SPECIAL_KEY = "preferred_special" private const val PREF_SPECIAL_KEY = "preferred_special"
private const val PREF_SPECIAL_TITLE = "Include Special Episodes"
private const val PREF_SPECIAL_DEFAULT = true private const val PREF_SPECIAL_DEFAULT = true
} }
@ -336,7 +334,7 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_VALUES entryValues = PREF_QUALITY_ENTRY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT) setDefaultValue(PREF_QUALITY_DEFAULT)
@ -348,11 +346,11 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_GROUP_KEY key = PREF_GROUP_KEY
title = PREF_GROUP_TITLE title = "Preferred group"
entries = MarinMoeConstants.GROUP_ENTRIES entries = MarinMoeConstants.GROUP_ENTRIES
entryValues = MarinMoeConstants.GROUP_ENTRY_VALUES entryValues = MarinMoeConstants.GROUP_ENTRY_VALUES
setDefaultValue(PREF_GROUP_DEFAULT) setDefaultValue(PREF_GROUP_DEFAULT)
@ -364,11 +362,11 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_SUBS_KEY key = PREF_SUBS_KEY
title = PREF_SUBS_TITLE title = "Prefer subs or dubs?"
entries = PREF_SUBS_ENTRIES entries = PREF_SUBS_ENTRIES
entryValues = PREF_SUBS_ENTRY_VALUES entryValues = PREF_SUBS_ENTRY_VALUES
setDefaultValue(PREF_SUBS_DEFAULT) setDefaultValue(PREF_SUBS_DEFAULT)
@ -380,17 +378,17 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = PREF_SPECIAL_KEY key = PREF_SPECIAL_KEY
title = PREF_SPECIAL_TITLE title = "Include Special Episodes"
setDefaultValue(PREF_SPECIAL_DEFAULT) setDefaultValue(PREF_SPECIAL_DEFAULT)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val new = newValue as Boolean val new = newValue as Boolean
preferences.edit().putBoolean(key, new).commit() preferences.edit().putBoolean(key, new).commit()
} }
}.let { screen.addPreference(it) } }.also(screen::addPreference)
} }
} }

View File

@ -6,7 +6,7 @@ ext {
extName = 'Myanime' extName = 'Myanime'
pkgNameSuffix = 'en.myanime' pkgNameSuffix = 'en.myanime'
extClass = '.Myanime' extClass = '.Myanime'
extVersionCode = 3 extVersionCode = 4
libVersion = '13' libVersion = '13'
} }

View File

@ -21,8 +21,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -31,9 +29,6 @@ import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -47,58 +42,40 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private var postBody = ""
private var postHeaders = headers.newBuilder()
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
}
}
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/category/donghua-list/page/$page/")
return GET("$baseUrl/category/donghua-list/page/$page/")
}
override fun popularAnimeSelector(): String = "main#main > article.post" override fun popularAnimeSelector(): String = "main#main > article.post"
override fun popularAnimeNextPageSelector(): String = "script:containsData(infiniteScroll)" override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("h2.entry-header-title > a")!!.attr("href"))
override fun popularAnimeFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
return SAnime.create().apply { title = element.selectFirst("h2.entry-header-title > a")!!.text().removePrefix("Playlist ")
setUrlWithoutDomain(element.selectFirst("h2.entry-header-title > a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
title = element.selectFirst("h2.entry-header-title > a")!!.text().removePrefix("Playlist ")
}
} }
override fun popularAnimeNextPageSelector(): String = "script:containsData(infiniteScroll)"
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/") override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
override fun latestUpdatesSelector(): String = popularAnimeSelector() override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector() override fun latestUpdatesFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("h2.entry-header-title > a")!!.attr("href"))
override fun latestUpdatesFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
return SAnime.create().apply { title = element.selectFirst("h2.entry-header-title > a")!!.text()
setUrlWithoutDomain(element.selectFirst("h2.entry-header-title > a")!!.attr("href").toHttpUrl().encodedPath) .substringBefore(" Episode")
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: "" .substringBefore(" episode")
title = element.selectFirst("h2.entry-header-title > a")!!.text()
.substringBefore(" Episode")
.substringBefore(" episode")
}
} }
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
@ -115,10 +92,10 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = latestUpdatesFromElement(element) override fun searchAnimeFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters =============================== // ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
@ -141,9 +118,7 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = Observable.just(anime)
return Observable.just(anime)
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used") override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
@ -164,7 +139,7 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
SEpisode.create().apply { SEpisode.create().apply {
name = a.text() name = a.text()
episode_number = a.text().substringAfter("pisode ").substringBefore(" ").toFloatOrNull() ?: 0F episode_number = a.text().substringAfter("pisode ").substringBefore(" ").toFloatOrNull() ?: 0F
setUrlWithoutDomain(a.attr("href").toHttpUrl().encodedPath) setUrlWithoutDomain(a.attr("href"))
} }
}, },
) )
@ -184,24 +159,26 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
epDocument.select("main#main > article.post").forEach { epDocument.select("main#main > article.post").forEach {
val a = it.selectFirst("h2.entry-header-title > a")!! val a = it.selectFirst("h2.entry-header-title > a")!!
val episode = SEpisode.create() episodeList.add(
SEpisode.create().apply {
episode.name = a.text() name = a.text()
episode.episode_number = a.text().substringAfter("pisode ").substringBefore(" ").toFloatOrNull() ?: 0F episode_number = a.text().substringAfter("pisode ").substringBefore(" ").toFloatOrNull() ?: 0F
episode.setUrlWithoutDomain(a.attr("href").toHttpUrl().encodedPath) setUrlWithoutDomain(a.attr("href"))
episodeList.add(episode) },
)
} }
infiniteScroll = epDocument.selectFirst("script:containsData(infiniteScroll)") != null infiniteScroll = epDocument.selectFirst("script:containsData(infiniteScroll)") != null
page++ page++
} }
} else if (document.selectFirst("iframe.youtube-player[src]") != null) { } else if (document.selectFirst("iframe.youtube-player[src]") != null) {
val episode = SEpisode.create() episodeList.add(
SEpisode.create().apply {
episode.name = document.selectFirst("title")!!.text() name = document.selectFirst("title")!!.text()
episode.episode_number = 0F episode_number = 0F
episode.setUrlWithoutDomain(response.request.url.encodedPath) setUrlWithoutDomain(response.request.url.toString())
episodeList.add(episode) },
)
} else if (document.selectFirst("span > a[href*=/tag/]") != null) { } else if (document.selectFirst("span > a[href*=/tag/]") != null) {
val url = document.selectFirst("span > a[href*=/tag/]")!!.attr("href") val url = document.selectFirst("span > a[href*=/tag/]")!!.attr("href")
episodeList.addAll( episodeList.addAll(
@ -247,20 +224,22 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}.filterNotNull().flatten(), }.filterNotNull().flatten(),
) )
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return videoList return videoList
} }
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = "div.entry-content iframe[src]" override fun videoListSelector(): String = "div.entry-content iframe[src]"
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", "1080")!! val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val server = preferences.getString("preferred_server", "dailymotion")!! val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
return this.sortedWith( return this.sortedWith(
compareBy( compareBy(
@ -276,13 +255,23 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }
companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "dailymotion"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val videoQualityPref = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = "preferred_quality" key = PREF_QUALITY_KEY
title = "Preferred quality" title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p") entries = arrayOf("1080p", "720p", "480p", "360p")
entryValues = arrayOf("1080", "720", "480", "360") entryValues = arrayOf("1080", "720", "480", "360")
setDefaultValue("1080") setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -291,13 +280,14 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val videoServerPref = ListPreference(screen.context).apply {
key = "preferred_server" ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server" title = "Preferred server"
entries = arrayOf("YouTube", "Dailymotion", "ok.ru") entries = arrayOf("YouTube", "Dailymotion", "ok.ru")
entryValues = arrayOf("youtube", "dailymotion", "okru") entryValues = arrayOf("youtube", "dailymotion", "okru")
setDefaultValue("dailymotion") setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -306,8 +296,6 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(videoQualityPref)
screen.addPreference(videoServerPref)
} }
} }

View File

@ -5,7 +5,7 @@ ext {
extName = 'NollyVerse' extName = 'NollyVerse'
pkgNameSuffix = 'en.nollyverse' pkgNameSuffix = 'en.nollyverse'
extClass = '.NollyVerse' extClass = '.NollyVerse'
extVersionCode = 1 extVersionCode = 2
libVersion = '13' libVersion = '13'
} }

View File

@ -42,21 +42,9 @@ class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// Popular Anime // ============================== Popular ===============================
private fun toImgUrl(inputUrl: String): String { override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/category/trending-movies/page/$page/")
val url = inputUrl.removeSuffix("/").toHttpUrl()
val pathSeg = url.encodedPathSegments.toMutableList()
pathSeg.add(1, "img")
return url.scheme +
"://" +
url.host +
"/" +
pathSeg.joinToString(separator = "/") +
".jpg"
}
override fun popularAnimeNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
@ -77,156 +65,72 @@ class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "div.col-md-8 div.row div.col-md-6" override fun popularAnimeSelector(): String = "div.col-md-8 div.row div.col-md-6"
override fun popularAnimeRequest(page: Int): Request { override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
return GET("$baseUrl/category/trending-movies/page/$page/") title = element.select("div.post-body h3 a").text()
thumbnail_url = element.select("a.post-img img").attr("data-src")
setUrlWithoutDomain(element.select("a.post-img").attr("href"))
} }
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
val anime = SAnime.create()
anime.title = element.select("div.post-body h3 a").text()
anime.thumbnail_url = element.select("a.post-img img").attr("data-src")
anime.setUrlWithoutDomain(element.select("a.post-img").attr("href").toHttpUrl().encodedPath)
return anime
}
// Episodes // =============================== Latest ===============================
override fun episodeListRequest(anime: SAnime): Request { override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/category/new-series/page/$page/")
return if (anime.url.startsWith("/movie/")) {
GET(baseUrl + anime.url + "/download/", headers)
} else {
GET(baseUrl + anime.url + "/seasons/", headers)
}
}
override fun episodeListSelector() = throw Exception("not used") override fun latestUpdatesParse(response: Response): AnimesPage {
override fun episodeListParse(response: Response): List<SEpisode> {
val path = response.request.url.encodedPath
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
if (path.startsWith("/movie/")) {
val episode = SEpisode.create()
episode.name = "Movie"
episode.episode_number = 1F
episode.setUrlWithoutDomain(path)
episodeList.add(episode)
} else {
var counter = 1
for (season in document.select("table.table.table-striped tbody tr").reversed()) {
val seasonUrl = season.select("td a[href]").attr("href")
val seasonSoup = client.newCall(
GET(seasonUrl, headers),
).execute().asJsoup()
val episodeTable = seasonSoup.select("table.table.table-striped")
val seasonNumber = episodeTable.select("thead th").eachText().find {
t ->
"""Season (\d+)""".toRegex().matches(t)
}?.split(" ")!![1]
for (ep in episodeTable.select("tbody tr")) {
val episode = SEpisode.create()
episode.name = "Episode S${seasonNumber}E${ep.selectFirst("td")!!.text().split(" ")!![1]}"
episode.episode_number = counter.toFloat()
episode.setUrlWithoutDomain(seasonUrl + "#$counter")
episodeList.add(episode)
counter++
}
// Stop API abuse
Thread.sleep(500)
}
}
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
}
// Video urls
override fun videoListRequest(episode: SEpisode): Request {
return if (episode.name == "Movie") {
GET(baseUrl + episode.url + "#movie", headers)
} else {
val episodeIndex = """Episode S(\d+)E(?<num>\d+)""".toRegex().matchEntire(
episode.name,
)!!.groups["num"]!!.value
GET(baseUrl + episode.url.replaceAfterLast("#", "") + episodeIndex, headers)
}
}
override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>()
val document = response.asJsoup() val document = response.asJsoup()
val fragment = response.request.url.fragment!! val animes = document.select(latestUpdatesSelector()).map { element ->
if (fragment == "movie") { latestUpdatesFromElement(element)
for (res in document.select("table.table.table-striped tbody tr")) { }
val url = res.select("td a").attr("href")
val name = res.select("td:not(:has(a))").text().trim() val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
videoList.add(Video(url, name, url)) if (document.select(selector).text() != ">") {
return AnimesPage(animes, false)
} }
document.select(selector).first()
} != null
return AnimesPage(animes, hasNextPage)
}
override fun latestUpdatesSelector(): String = "div.section div.container div.row div.post.post-row"
override fun latestUpdatesFromElement(element: Element): SAnime = SAnime.create().apply {
title = element.select("div.post-body h3 a").text()
thumbnail_url = element.select("a.post-img img").attr("data-src").ifEmpty {
element.select("a.post-img img").attr("src")
}
setUrlWithoutDomain(element.select("a.post-img").attr("href"))
}
override fun latestUpdatesNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotBlank()) {
val body = FormBody.Builder()
.add("name", "$query")
.build()
POST("$baseUrl/livesearch.php", body = body)
} else { } else {
val episodeIndex = fragment.toInt() - 1 var searchPath = ""
filters.filter { it.state != 0 }.forEach { filter ->
val episodeList = document.select("table.table.table-striped tbody tr").toList() when (filter) {
is CategoryFilter -> searchPath = if (filter.toUriPart() == "/series/") filter.toUriPart() else "${filter.toUriPart()}page/$page"
for (res in episodeList[episodeIndex].select("td").reversed()) { is MovieGenreFilter -> searchPath = "/movies/genre/${filter.toUriPart()}/page/$page"
val url = res.select("a").attr("href") is SeriesGenreFilter -> searchPath = "/series/genre/${filter.toUriPart()}/page/$page"
if (url.isNotEmpty()) { else -> ""
videoList.add(
Video(url, res.text().trim(), url),
)
} }
} }
require(searchPath.isNotEmpty()) { "Search must not be empty" }
GET(baseUrl + searchPath)
} }
return videoList.sort()
} }
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString("preferred_quality", null)
val codec = preferences.getString("preferred_codec", null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
if (codec?.let { video.quality.contains(it) } == true) {
newList.add(0, video)
} else {
newList.add(preferred, video)
}
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// search
override fun searchAnimeParse(response: Response): AnimesPage { override fun searchAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val path = response.request.url.encodedPath val path = response.request.url.encodedPath
@ -317,141 +221,250 @@ class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return AnimesPage(animes, hasNextPage) return AnimesPage(animes, hasNextPage)
} }
private fun nextPageSelector(): String = "ul.pagination.pagination-md li:nth-last-child(2)"
private fun movieGenreSelector(): String = "div.container > div.row > div.col-md-4" private fun movieGenreSelector(): String = "div.container > div.row > div.col-md-4"
private fun movieGenreFromElement(element: Element): SAnime { private fun movieGenreFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
return latestUpdatesFromElement(element)
}
private fun seriesGenreSelector(): String = "div.row div.col-md-8 div.col-md-6" private fun seriesGenreSelector(): String = "div.row div.col-md-8 div.col-md-6"
private fun seriesGenreFromElement(element: Element): SAnime { private fun seriesGenreFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
return latestUpdatesFromElement(element)
}
private fun koreanSelector(): String = "div.col-md-8 div.row div.col-md-6" private fun koreanSelector(): String = "div.col-md-8 div.row div.col-md-6"
private fun koreanFromElement(element: Element): SAnime { private fun koreanFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
return latestUpdatesFromElement(element)
}
private fun latestSelector(): String = latestUpdatesSelector() private fun latestSelector(): String = latestUpdatesSelector()
private fun latestFromElement(element: Element): SAnime { private fun latestFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
return latestUpdatesFromElement(element)
}
private fun seriesSelector(): String = "div.section-row ul.list-style li" private fun seriesSelector(): String = "div.section-row ul.list-style li"
private fun seriesFromElement(element: Element): SAnime { private fun seriesFromElement(element: Element): SAnime = SAnime.create().apply {
val anime = SAnime.create() title = element.select("a").text()
anime.title = element.select("a").text() thumbnail_url = toImgUrl(element.select("a").attr("href"))
anime.thumbnail_url = toImgUrl(element.select("a").attr("href")) setUrlWithoutDomain(element.select("a").attr("href"))
anime.setUrlWithoutDomain(element.select("a").attr("href").toHttpUrl().encodedPath)
return anime
} }
private fun movieSelector(): String = "div.container div.row div.col-md-12 div.col-md-4" private fun movieSelector(): String = "div.container div.row div.col-md-12 div.col-md-4"
private fun movieFromElement(element: Element): SAnime { private fun movieFromElement(element: Element): SAnime = SAnime.create().apply {
val anime = SAnime.create() title = element.select("h3 a").text()
anime.title = element.select("h3 a").text() thumbnail_url = element.select("a img").attr("src")
anime.thumbnail_url = element.select("a img").attr("src") setUrlWithoutDomain(element.select("a.post-img").attr("href"))
anime.setUrlWithoutDomain(element.select("a.post-img").attr("href").toHttpUrl().encodedPath)
return anime
} }
override fun searchAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.title = element.text()
anime.thumbnail_url = toImgUrl(element.attr("href"))
anime.setUrlWithoutDomain(element.attr("href").toHttpUrl().encodedPath)
return anime
}
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
override fun searchAnimeSelector(): String = "a" override fun searchAnimeSelector(): String = "a"
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
if (query.isNotBlank()) { title = element.text()
val body = FormBody.Builder() thumbnail_url = toImgUrl(element.attr("href"))
.add("name", "$query") setUrlWithoutDomain(element.attr("href"))
.build() }
return POST("$baseUrl/livesearch.php", body = body)
private fun nextPageSelector(): String = "ul.pagination.pagination-md li:nth-last-child(2)"
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
title = document.select("div.page-header div.container div.row div.text-center h1").text()
description = document.select("blockquote.blockquote small").text()
genre = document.select("div.col-md-8 ul.list-style li").firstOrNull {
it.text().startsWith("Genre: ")
}?.text()?.substringAfter("Genre: ")?.replace(",", ", ")
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
return if (anime.url.startsWith("/movie/")) {
GET(baseUrl + anime.url + "/download/", headers)
} else { } else {
var searchPath = "" GET(baseUrl + anime.url + "/seasons/", headers)
filters.filter { it.state != 0 }.forEach { filter -> }
when (filter) { }
is CategoryFilter -> searchPath = if (filter.toUriPart() == "/series/") filter.toUriPart() else "${filter.toUriPart()}page/$page"
is MovieGenreFilter -> searchPath = "/movies/genre/${filter.toUriPart()}/page/$page" override fun episodeListParse(response: Response): List<SEpisode> {
is SeriesGenreFilter -> searchPath = "/series/genre/${filter.toUriPart()}/page/$page" val path = response.request.url.encodedPath
else -> ""
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
if (path.startsWith("/movie/")) {
episodeList.add(
SEpisode.create().apply {
name = "Movie"
episode_number = 1F
setUrlWithoutDomain(path)
},
)
} else {
var counter = 1
for (season in document.select("table.table.table-striped tbody tr").reversed()) {
val seasonUrl = season.select("td a[href]").attr("href")
val seasonSoup = client.newCall(
GET(seasonUrl, headers),
).execute().asJsoup()
val episodeTable = seasonSoup.select("table.table.table-striped")
val seasonNumber = episodeTable.select("thead th").eachText().find {
t ->
"""Season (\d+)""".toRegex().matches(t)
}?.split(" ")!![1]
for (ep in episodeTable.select("tbody tr")) {
episodeList.add(
SEpisode.create().apply {
name = "Episode S${seasonNumber}E${ep.selectFirst("td")!!.text().split(" ")!![1]}"
episode_number = counter.toFloat()
setUrlWithoutDomain(seasonUrl + "#$counter")
},
)
counter++
} }
// Stop abuse
Thread.sleep(500)
} }
if (searchPath.isEmpty()) { }
throw Exception("Empty search")
} return episodeList.reversed()
return GET(baseUrl + searchPath) }
override fun episodeFromElement(element: Element): SEpisode {
val seasonNum = element.ownerDocument()!!.select("div.Title span").text()
return SEpisode.create().apply {
name = "Season $seasonNum" + "x" + element.select("td span.Num").text() + " : " + element.select("td.MvTbTtl > a").text()
episode_number = element.select("td > span.Num").text().toFloat()
setUrlWithoutDomain(element.select("td.MvTbPly > a.ClA").attr("abs:href"))
} }
} }
// Details override fun episodeListSelector() = throw Exception("not used")
override fun animeDetailsParse(document: Document): SAnime { // ============================ Video Links =============================
val anime = SAnime.create()
anime.title = document.select("div.page-header div.container div.row div.text-center h1").text() override fun videoListRequest(episode: SEpisode): Request {
anime.description = document.select("blockquote.blockquote small").text() return if (episode.name == "Movie") {
document.select("div.col-md-8 ul.list-style li").forEach { GET(baseUrl + episode.url + "#movie", headers)
if (it.text().startsWith("Genre: ")) { } else {
anime.genre = it.text().substringAfter("Genre: ").replace(",", ", ") val episodeIndex = """Episode S(\d+)E(?<num>\d+)""".toRegex().matchEntire(
} episode.name,
)!!.groups["num"]!!.value
GET(baseUrl + episode.url.replaceAfterLast("#", "") + episodeIndex, headers)
} }
return anime
} }
// Latest override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>()
override fun latestUpdatesNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
override fun latestUpdatesParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val animes = document.select(latestUpdatesSelector()).map { element -> val fragment = response.request.url.fragment!!
latestUpdatesFromElement(element) if (fragment == "movie") {
} for (res in document.select("table.table.table-striped tbody tr")) {
val url = res.select("td a").attr("href")
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector -> val name = res.select("td:not(:has(a))").text().trim()
if (document.select(selector).text() != ">") { videoList.add(Video(url, name, url))
return AnimesPage(animes, false)
} }
document.select(selector).first() } else {
} != null val episodeIndex = fragment.toInt() - 1
return AnimesPage(animes, hasNextPage) val episodeList = document.select("table.table.table-striped tbody tr").toList()
}
override fun latestUpdatesFromElement(element: Element): SAnime { for (res in episodeList[episodeIndex].select("td").reversed()) {
val anime = SAnime.create() val url = res.select("a").attr("href")
if (url.isNotEmpty()) {
anime.setUrlWithoutDomain(element.select("a.post-img").attr("href").toHttpUrl().encodedPath) videoList.add(
anime.title = element.select("div.post-body h3 a").text() Video(url, res.text().trim(), url),
anime.thumbnail_url = element.select("a.post-img img").attr("data-src") )
if (anime.thumbnail_url.toString().isEmpty()) { }
anime.thumbnail_url = element.select("a.post-img img").attr("src") }
} }
return anime
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
return videoList.sort()
} }
override fun latestUpdatesRequest(page: Int): Request { override fun videoListSelector() = throw Exception("not used")
return GET("$baseUrl/category/new-series/page/$page/")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val codec = preferences.getString(PREF_CODEC_KEY, PREF_CODEC_KEY)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(codec) },
),
).reversed()
} }
override fun latestUpdatesSelector(): String = "div.section div.container div.row div.post.post-row" private fun toImgUrl(inputUrl: String): String {
val url = inputUrl.removeSuffix("/").toHttpUrl()
val pathSeg = url.encodedPathSegments.toMutableList()
pathSeg.add(1, "img")
return url.scheme +
"://" +
url.host +
"/" +
pathSeg.joinToString(separator = "/") +
".jpg"
}
// Filters companion object {
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private const val PREF_CODEC_KEY = "preferred_codec"
private const val PREF_CODEC_DEFAULT = "265"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
entryValues = arrayOf("1080", "720", "480", "360", "240")
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_CODEC_KEY
title = "Preferred Video Codec"
entries = arrayOf("h265", "h264")
entryValues = arrayOf("265", "264")
setDefaultValue(PREF_CODEC_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)
}
// ============================== Filters ===============================
override fun getFilterList() = AnimeFilterList( override fun getFilterList() = AnimeFilterList(
AnimeFilter.Header("NOTE: Only one works at a time"), AnimeFilter.Header("NOTE: Only one works at a time"),
@ -539,41 +552,4 @@ class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) { AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second 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)
val videoCodecPref = ListPreference(screen.context).apply {
key = "preferred_codec"
title = "Preferred Video Codec"
entries = arrayOf("h265", "h264")
entryValues = arrayOf("265", "264")
setDefaultValue("265")
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(videoCodecPref)
}
} }

View File

@ -49,17 +49,17 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val badNames = arrayOf("../", "gifs/") val badNames = arrayOf("../", "gifs/")
val animeList = mutableListOf<SAnime>()
document.select(popularAnimeSelector()).forEach { val animeList = document.select(popularAnimeSelector()).mapNotNull {
val a = it.selectFirst("a")!! val a = it.selectFirst("a")!!
val name = a.text() val name = a.text()
if (name in badNames) return@forEach if (name in badNames) return@mapNotNull null
val anime = SAnime.create() SAnime.create().apply {
anime.title = name.removeSuffix("/") title = name.removeSuffix("/")
anime.setUrlWithoutDomain(a.attr("href")) setUrlWithoutDomain(a.attr("href"))
animeList.add(anime) thumbnail_url = ""
}
} }
return AnimesPage(animeList, false) return AnimesPage(animeList, false)
@ -67,20 +67,20 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "table tr:has(a)" override fun popularAnimeSelector(): String = "table tr:has(a)"
override fun popularAnimeNextPageSelector(): String? = null
override fun popularAnimeFromElement(element: Element): SAnime = throw Exception("Not used") override fun popularAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
override fun popularAnimeNextPageSelector(): String? = null
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
override fun latestUpdatesSelector(): String = throw Exception("Not used") override fun latestUpdatesSelector(): String = throw Exception("Not used")
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used") override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not used")
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
// =============================== Search =============================== // =============================== Search ===============================
override fun fetchSearchAnime( override fun fetchSearchAnime(
@ -107,17 +107,17 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun searchAnimeParse(response: Response, query: String): AnimesPage { private fun searchAnimeParse(response: Response, query: String): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val badNames = arrayOf("../", "gifs/") val badNames = arrayOf("../", "gifs/")
val animeList = mutableListOf<SAnime>()
document.select(popularAnimeSelector()).forEach { val animeList = document.select(popularAnimeSelector()).mapNotNull {
val name = it.text() val name = it.text()
if (name in badNames || !name.contains(query, ignoreCase = true)) return@forEach if (name in badNames || !name.contains(query, ignoreCase = true)) return@mapNotNull null
if (it.selectFirst("span.size")?.text()?.contains(" KiB") == true) return@forEach if (it.selectFirst("span.size")?.text()?.contains(" KiB") == true) return@mapNotNull null
val anime = SAnime.create() SAnime.create().apply {
anime.title = name.removeSuffix("/") title = name.removeSuffix("/")
anime.setUrlWithoutDomain(it.selectFirst("a")!!.attr("href")) setUrlWithoutDomain(it.selectFirst("a")!!.attr("href"))
animeList.add(anime) thumbnail_url = ""
}
} }
return AnimesPage(animeList, false) return AnimesPage(animeList, false)
@ -125,15 +125,13 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = throw Exception("Not used") override fun searchAnimeSelector(): String = throw Exception("Not used")
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not used") override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = Observable.just(anime)
return Observable.just(anime)
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used") override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
@ -150,7 +148,7 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val href = link.selectFirst("a")!!.attr("href") val href = link.selectFirst("a")!!.attr("href")
val text = link.selectFirst("a")!!.text() val text = link.selectFirst("a")!!.text()
if ("""\bOST\b""".toRegex().matches(text) || text.contains("original sound", true)) return@forEach if ("""\bOST\b""".toRegex().matches(text) || text.contains("original sound", true)) return@forEach
if (preferences.getBoolean("ignore_extras", true) && text.equals("extras", ignoreCase = true)) return@forEach if (preferences.getBoolean(PREF_IGNORE_EXTRA_KEY, PREF_IGNORE_EXTRA_DEFAULT) && text.equals("extras", ignoreCase = true)) return@forEach
if (href.isNotBlank() && href != "..") { if (href.isNotBlank() && href != "..") {
val fullUrl = baseUrl + href val fullUrl = baseUrl + href
@ -158,7 +156,6 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
traverseDirectory(fullUrl) traverseDirectory(fullUrl)
} }
if (videoFormats.any { t -> fullUrl.endsWith(t) }) { if (videoFormats.any { t -> fullUrl.endsWith(t) }) {
val episode = SEpisode.create()
val paths = fullUrl.toHttpUrl().pathSegments val paths = fullUrl.toHttpUrl().pathSegments
val seasonInfoRegex = """(\([\s\w-]+\))(?: ?\[[\s\w-]+\])?${'$'}""".toRegex() val seasonInfoRegex = """(\([\s\w-]+\))(?: ?\[[\s\w-]+\])?${'$'}""".toRegex()
@ -181,13 +178,15 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
val size = link.selectFirst("td.fb-s")?.text() val size = link.selectFirst("td.fb-s")?.text()
episode.name = "${season}${videoFormats.fold(paths.last()) { acc, suffix -> acc.removeSuffix(suffix).trimInfo() }}${if (size == null) "" else " - $size"}" episodeList.add(
episode.url = fullUrl SEpisode.create().apply {
episode.scanlator = seasonInfo + extraInfo name = "${season}${videoFormats.fold(paths.last()) { acc, suffix -> acc.removeSuffix(suffix).trimInfo() }}"
episode.episode_number = counter.toFloat() this.url = fullUrl
scanlator = "${if (size == null) "" else "$size • "}$seasonInfo$extraInfo"
episode_number = counter.toFloat()
},
)
counter++ counter++
episodeList.add(episode)
} }
} }
} }
@ -200,20 +199,19 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> = throw Exception("Not used") override fun episodeListParse(response: Response): List<SEpisode> = throw Exception("Not used")
override fun episodeListSelector(): String = throw Exception("Not Used")
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used") override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used")
override fun episodeListSelector(): String = throw Exception("Not Used")
// ============================ Video Links ============================= // ============================ Video Links =============================
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> { override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> =
return Observable.just(listOf(Video(episode.url, "Video", episode.url))) Observable.just(listOf(Video(episode.url, "Video", episode.url)))
}
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used") override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
@ -231,15 +229,21 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return newString return newString
} }
companion object {
private const val PREF_IGNORE_EXTRA_KEY = "ignore_extras"
private const val PREF_IGNORE_EXTRA_DEFAULT = true
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val ignoreExtras = SwitchPreferenceCompat(screen.context).apply { SwitchPreferenceCompat(screen.context).apply {
key = "ignore_extras" key = PREF_IGNORE_EXTRA_KEY
title = "Ignore \"Extras\" folder" title = "Ignore \"Extras\" folder"
setDefaultValue(true) setDefaultValue(PREF_IGNORE_EXTRA_DEFAULT)
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit() preferences.edit().putBoolean(key, newValue as Boolean).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(ignoreExtras)
} }
} }

View File

@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -32,8 +31,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest import java.security.MessageDigest
import java.text.CharacterIterator
import java.text.StringCharacterIterator
class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() { class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
@ -49,8 +46,6 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
private val maxRecursionDepth = 2
private val json: Json by injectLazy() private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
@ -63,26 +58,24 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "section#movies-list > div.movies-box" override fun popularAnimeSelector(): String = "section#movies-list > div.movies-box"
override fun popularAnimeNextPageSelector(): String? = null override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
override fun popularAnimeFromElement(element: Element): SAnime { thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
return SAnime.create().apply { title = element.selectFirst("a:matches(.)")!!.text().substringBefore(" | Episode").trimEnd()
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
title = element.selectFirst("a:matches(.)")!!.text().substringBefore(" | Episode").trimEnd()
}
} }
override fun popularAnimeNextPageSelector(): String? = null
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not Used") override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not Used")
override fun latestUpdatesSelector(): String = throw Exception("Not Used") override fun latestUpdatesSelector(): String = throw Exception("Not Used")
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not Used") override fun latestUpdatesFromElement(element: Element): SAnime = throw Exception("Not Used")
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
// =============================== Search =============================== // =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
@ -115,20 +108,20 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} else { } else {
val document = response.asJsoup() val document = response.asJsoup()
val animes = document.select(searchAnimeSelector()).map { element -> val animeList = document.select(searchAnimeSelector()).map { element ->
popularAnimeFromElement(element) popularAnimeFromElement(element)
} }
return AnimesPage(animes, animes.size == 40) return AnimesPage(animeList, animeList.size == 40)
} }
} }
override fun searchAnimeSelector(): String = "div#infinite-list" override fun searchAnimeSelector(): String = "div#infinite-list"
override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element) override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
// ============================== Filters =============================== // ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList( override fun getFilterList(): AnimeFilterList = AnimeFilterList(
@ -179,24 +172,11 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> { override fun animeDetailsParse(document: Document): SAnime {
return client.newCall(animeDetailsRequest(anime))
.asObservableSuccess()
.map { response ->
animeDetailsParse(response, anime).apply { initialized = true }
}
}
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
private fun animeDetailsParse(response: Response, anime: SAnime): SAnime {
val document = response.asJsoup()
val moreInfo = document.select("div.summery:not(:has(h2:contains(Summary))) ul li").joinToString("\n") { it.ownText().trim() } val moreInfo = document.select("div.summery:not(:has(h2:contains(Summary))) ul li").joinToString("\n") { it.ownText().trim() }
val realDesc = document.selectFirst("div.summery:has(h2:contains(Summary)) ul")?.let { "${it.text()}\n\n" } ?: "" val realDesc = document.selectFirst("div.summery:has(h2:contains(Summary)) ul")?.let { "${it.text()}\n\n" } ?: ""
return SAnime.create().apply { return SAnime.create().apply {
title = anime.title
thumbnail_url = anime.thumbnail_url
status = document.selectFirst("div.summery:not(:has(h2:contains(Summary))) ul li:contains(Status)")?.let { status = document.selectFirst("div.summery:not(:has(h2:contains(Summary))) ul li:contains(Status)")?.let {
parseStatus(it.text().substringAfter("Status: ")) parseStatus(it.text().substringAfter("Status: "))
} ?: SAnime.UNKNOWN } ?: SAnime.UNKNOWN
@ -217,13 +197,8 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val document = response.asJsoup() val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>() val episodeList = mutableListOf<SEpisode>()
val keyRegex = """"(\w{39})"""".toRegex()
val versionRegex = """"([^"]+web-frontend[^"]+)"""".toRegex()
val jsonRegex = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL)
val boundary = "=====vc17a3rwnndj====="
fun traverseFolder(url: String, path: String, recursionDepth: Int = 0) { fun traverseFolder(url: String, path: String, recursionDepth: Int = 0) {
if (recursionDepth == maxRecursionDepth) return if (recursionDepth == MAX_RECURSION_DEPTH) return
val folderId = url.substringAfter("/folders/") val folderId = url.substringAfter("/folders/")
val driveHeaders = headers.newBuilder() val driveHeaders = headers.newBuilder()
@ -239,14 +214,14 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return
val keyScript = driveDocument.select("script").first { script -> val keyScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.data()
val key = keyRegex.find(keyScript)?.groupValues?.get(1) ?: "" val key = KEY_REGEX.find(keyScript)?.groupValues?.get(1) ?: ""
val versionScript = driveDocument.select("script").first { script -> val versionScript = driveDocument.select("script").first { script ->
keyRegex.find(script.data()) != null KEY_REGEX.find(script.data()) != null
}.data() }.data()
val driveVersion = versionRegex.find(versionScript)?.groupValues?.get(1) ?: "" val driveVersion = VERSION_REGEX.find(versionScript)?.groupValues?.get(1) ?: ""
val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull { val sapisid = client.cookieJar.loadForRequest("https://drive.google.com".toHttpUrl()).firstOrNull {
it.name == "SAPISID" || it.name == "__Secure-3PAPISID" it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
}?.value ?: "" }?.value ?: ""
@ -254,7 +229,7 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
var pageToken: String? = "" var pageToken: String? = ""
while (pageToken != null) { while (pageToken != null) {
val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1" val requestUrl = "/drive/v2beta/files?openDrive=true&reason=102&syncType=0&errorRecovery=false&q=trashed%20%3D%20false%20and%20'$folderId'%20in%20parents&fields=kind%2CnextPageToken%2Citems(kind%2CmodifiedDate%2CmodifiedByMeDate%2ClastViewedByMeDate%2CfileSize%2Cowners(kind%2CpermissionId%2Cid)%2ClastModifyingUser(kind%2CpermissionId%2Cid)%2ChasThumbnail%2CthumbnailVersion%2Ctitle%2Cid%2CresourceKey%2Cshared%2CsharedWithMeDate%2CuserPermission(role)%2CexplicitlyTrashed%2CmimeType%2CquotaBytesUsed%2Ccopyable%2CfileExtension%2CsharingUser(kind%2CpermissionId%2Cid)%2Cspaces%2Cversion%2CteamDriveId%2ChasAugmentedPermissions%2CcreatedDate%2CtrashingUser(kind%2CpermissionId%2Cid)%2CtrashedDate%2Cparents(id)%2CshortcutDetails(targetId%2CtargetMimeType%2CtargetLookupStatus)%2Ccapabilities(canCopy%2CcanDownload%2CcanEdit%2CcanAddChildren%2CcanDelete%2CcanRemoveChildren%2CcanShare%2CcanTrash%2CcanRename%2CcanReadTeamDrive%2CcanMoveTeamDriveItem)%2Clabels(starred%2Ctrashed%2Crestricted%2Cviewed))%2CincompleteSearch&appDataFilter=NO_APP_DATA&spaces=drive&pageToken=$pageToken&maxResults=50&supportsTeamDrives=true&includeItemsFromAllDrives=true&corpora=default&orderBy=folder%2Ctitle_natural%20asc&retryCount=0&key=$key HTTP/1.1"
val body = """--$boundary val body = """--$BOUNDARY
|content-type: application/http |content-type: application/http
|content-transfer-encoding: binary |content-transfer-encoding: binary
| |
@ -263,12 +238,12 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|authorization: ${generateSapisidhashHeader(sapisid)} |authorization: ${generateSapisidhashHeader(sapisid)}
|x-goog-authuser: 0 |x-goog-authuser: 0
| |
|--$boundary |--$BOUNDARY
| |
""".trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$boundary\"".toMediaType()) """.trimMargin("|").toRequestBody("multipart/mixed; boundary=\"$BOUNDARY\"".toMediaType())
val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder() val postUrl = "https://clients6.google.com/batch/drive/v2beta".toHttpUrl().newBuilder()
.addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"") .addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$BOUNDARY\"")
.addQueryParameter("key", key) .addQueryParameter("key", key)
.build() .build()
.toString() .toString()
@ -283,34 +258,23 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
POST(postUrl, body = body, headers = postHeaders), POST(postUrl, body = body, headers = postHeaders),
).execute() ).execute()
val parsed = json.decodeFromString<PostResponse>( val parsed = json.decodeFromString<PostResponse>(
jsonRegex.find(response.body.string())!!.groupValues[1], JSON_REGEX.find(response.body.string())!!.groupValues[1],
) )
if (parsed.items == null) throw Exception("Failed to load items, please log in to google drive through webview") if (parsed.items == null) throw Exception("Failed to load items, please log in to google drive through webview")
parsed.items.forEachIndexed { index, it -> parsed.items.forEachIndexed { index, it ->
if (it.mimeType.startsWith("video")) { if (it.mimeType.startsWith("video")) {
val episode = SEpisode.create()
val size = formatBytes(it.fileSize?.toLongOrNull()) val size = formatBytes(it.fileSize?.toLongOrNull())
val pathName = if (preferences.getBoolean("trim_info", false)) { val pathName = path.trimInfo()
path.trimInfo()
} else {
path
}
val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex() episodeList.add(
episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) { SEpisode.create().apply {
"/$pathName$size" name = if (preferences.trimEpisodeName) it.title.trimInfo() else it.title
} else { this.url = "https://drive.google.com/uc?id=${it.id}"
"$size • /$pathName" episode_number = ITEM_NUMBER_REGEX.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
} date_upload = -1L
episode.name = if (preferences.getBoolean("trim_episode", false)) { scanlator = "$size • /$pathName"
it.title.trimInfo() },
} else { )
it.title
}
episode.url = "https://drive.google.com/uc?id=${it.id}"
episode.episode_number = itemNumberRegex.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
episode.date_upload = -1L
episodeList.add(episode)
} }
if (it.mimeType.endsWith(".folder")) { if (it.mimeType.endsWith(".folder")) {
traverseFolder( traverseFolder(
@ -358,10 +322,10 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return Observable.just(videoList) return Observable.just(videoList)
} }
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoListSelector(): String = throw Exception("Not Used") override fun videoListSelector(): String = throw Exception("Not Used")
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
override fun videoUrlParse(document: Document): String = throw Exception("Not Used") override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
// ============================= Utilities ============================== // ============================= Utilities ==============================
@ -405,21 +369,14 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
private fun formatBytes(bytes: Long?): String? { private fun formatBytes(bytes: Long?): String? {
if (bytes == null) return null val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) var value = bytes?.toDouble() ?: return null
if (absB < 1024) { var i = 0
return "$bytes B" while (value >= 1024 && i < units.size - 1) {
value /= 1024
i++
} }
var value = absB return String.format("%.1f %s", value, units[i])
val ci: CharacterIterator = StringCharacterIterator("KMGTPE")
var i = 40
while (i >= 0 && absB > 0xfffccccccccccccL shr i) {
value = value shr 10
ci.next()
i -= 10
}
value *= java.lang.Long.signum(bytes).toLong()
return java.lang.String.format("%.1f %cB", value / 1024.0, ci.current())
} }
private fun getCookie(url: String): String { private fun getCookie(url: String): String {
@ -439,34 +396,32 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
} }
override fun setupPreferenceScreen(screen: PreferenceScreen) { companion object {
val scanlatorOrder = SwitchPreferenceCompat(screen.context).apply { private val ITEM_NUMBER_REGEX = """ - (?:S\d+E)?(\d+)""".toRegex()
key = "scanlator_order" private val KEY_REGEX = """"(\w{39})"""".toRegex()
title = "Switch order of file path and size" private val VERSION_REGEX = """"([^"]+web-frontend[^"]+)"""".toRegex()
setDefaultValue(false) private val JSON_REGEX = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL)
setOnPreferenceChangeListener { _, newValue -> private const val BOUNDARY = "=====vc17a3rwnndj====="
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
val trimEpisodeName = SwitchPreferenceCompat(screen.context).apply {
key = "trim_episode"
title = "Trim info from episode name"
setDefaultValue(true)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
val trimEpisodeInfo = SwitchPreferenceCompat(screen.context).apply {
key = "trim_info"
title = "Trim info from episode info"
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}
screen.addPreference(scanlatorOrder) private const val MAX_RECURSION_DEPTH = 2
screen.addPreference(trimEpisodeName)
screen.addPreference(trimEpisodeInfo) private const val TRIM_EPISODE_NAME_KEY = "trim_episode"
private const val TRIM_EPISODE_NAME_DEFAULT = true
}
private val SharedPreferences.trimEpisodeName
get() = getBoolean(TRIM_EPISODE_NAME_KEY, TRIM_EPISODE_NAME_DEFAULT)
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = TRIM_EPISODE_NAME_KEY
title = "Trim info from episode name"
setDefaultValue(TRIM_EPISODE_NAME_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit().putBoolean(key, newValue as Boolean).commit()
}
}.also(screen::addPreference)
} }
} }