refactor(en): Refactor some English extensions (#1726)
This commit is contained in:
@ -26,6 +26,10 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
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.Request
|
||||
import okhttp3.Response
|
||||
@ -39,7 +43,7 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
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"
|
||||
|
||||
@ -56,7 +60,12 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -64,13 +73,11 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
val parsed = json.decodeFromString<PopularResult>(response.body.string())
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
|
||||
val titleStyle = preferences.getString(PREF_TITLE_STYLE_KEY, PREF_TITLE_STYLE_DEFAULT)!!
|
||||
|
||||
parsed.data.queryPopular.recommendations.forEach {
|
||||
if (it.anyCard != null) {
|
||||
animeList.add(
|
||||
SAnime.create().apply {
|
||||
title = when (titleStyle) {
|
||||
title = when (preferences.titleStyle) {
|
||||
"romaji" -> it.anyCard.name
|
||||
"eng" -> it.anyCard.englishName ?: it.anyCard.name
|
||||
else -> it.anyCard.nativeName ?: it.anyCard.name
|
||||
@ -88,15 +95,20 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
// Could be lazily loaded along with url, but would require user to restart
|
||||
val subPref = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||
val variables = """{"search":{"allowAdult":false,"allowUnknown":false},"limit":$PAGE_SIZE,"page":$page,"translationType":"$subPref","countryOrigin":"ALL"}"""
|
||||
val variables = buildJsonObject {
|
||||
putJsonObject("search") {
|
||||
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)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
return parseAnime(response)
|
||||
}
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage = parseAnime(response)
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
@ -112,84 +124,105 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
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 {
|
||||
val subPref = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||
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)
|
||||
} else {
|
||||
val seasonString = if (filters.season == "all") "" else ""","season":"${filters.season}""""
|
||||
val yearString = if (filters.releaseYear == "all") "" else ""","year":${filters.releaseYear}"""
|
||||
val genresString = if (filters.genres == "all") "" else ""","genres":${filters.genres},"excludeGenres":[]"""
|
||||
val typesString = if (filters.types == "all") "" else ""","types":${filters.types}"""
|
||||
val sortByString = if (filters.sortBy == "update") "" else ""","sortBy":"${filters.sortBy}""""
|
||||
|
||||
var variables = """{"search":{"allowAdult":false,"allowUnknown":false$seasonString$yearString$genresString$typesString$sortByString"""
|
||||
variables += """},"limit":$PAGE_SIZE,"page":$page,"translationType":"$subPref","countryOrigin":"${filters.origin}"}"""
|
||||
|
||||
val variables = buildJsonObject {
|
||||
putJsonObject("search") {
|
||||
put("allowAdult", false)
|
||||
put("allowUnknown", false)
|
||||
if (filters.season != "all") put("season", filters.season)
|
||||
if (filters.releaseYear != "all") put("year", filters.releaseYear.toInt())
|
||||
if (filters.genres != "all") {
|
||||
put("genres", json.decodeFromString(filters.genres))
|
||||
put("excludeGenres", buildJsonArray { })
|
||||
}
|
||||
if (filters.types != "all") put("types", json.decodeFromString(filters.types))
|
||||
if (filters.sortBy != "update") put("sortBy", filters.sortBy)
|
||||
}
|
||||
put("limit", PAGE_SIZE)
|
||||
put("page", page)
|
||||
put("translationType", preferences.subPref)
|
||||
put("countryOrigin", filters.origin)
|
||||
}
|
||||
GET("$baseUrl/allanimeapi?variables=$variables&query=$SEARCH_QUERY", headers = headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage {
|
||||
return parseAnime(response)
|
||||
}
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = parseAnime(response)
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AllAnimeFilters.FILTER_LIST
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(response: Response): SAnime = throw Exception("Not used")
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||
return client.newCall(animeDetailsRequestInternal(anime))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
animeDetailsParse(response, anime).apply { initialized = true }
|
||||
animeDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override fun animeDetailsRequest(anime: SAnime): Request {
|
||||
val (id, time, slug) = anime.url.split("<&sep>")
|
||||
val slugTime = if (time.isNotEmpty()) {
|
||||
"-st-$time"
|
||||
} else {
|
||||
time
|
||||
}
|
||||
val siteUrl = preferences.getString(PREF_SITE_DOMAIN_KEY, PREF_SITE_DOMAIN_DEFAULT)!!
|
||||
val slugTime = if (time.isNotEmpty()) "-st-$time" else time
|
||||
val siteUrl = preferences.siteUrl
|
||||
|
||||
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
|
||||
|
||||
return SAnime.create().apply {
|
||||
title = animeOld.title
|
||||
genre = show.genres?.joinToString(separator = ", ") ?: ""
|
||||
status = parseStatus(show.status)
|
||||
author = show.studios?.firstOrNull()
|
||||
description = Jsoup.parse(
|
||||
show.description?.replace("<br>", "br2n") ?: "",
|
||||
).text().replace("br2n", "\n") +
|
||||
"\n\n" +
|
||||
"Type: ${show.type ?: "Unknown"}" +
|
||||
"\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}" +
|
||||
"\nScore: ${show.score ?: "-"}★"
|
||||
description = buildString {
|
||||
append(
|
||||
Jsoup.parse(
|
||||
show.description?.replace("<br>", "br2n") ?: "",
|
||||
).text().replace("br2n", "\n"),
|
||||
)
|
||||
append("\n\n")
|
||||
append("Type: ${show.type ?: "Unknown"}")
|
||||
append("\nAired: ${show.season?.quarter ?: "-"} ${show.season?.year ?: "-"}")
|
||||
append("\nScore: ${show.score ?: "-"}★")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
val 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)
|
||||
}
|
||||
|
||||
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 episodesDetail = if (subPref == "sub") {
|
||||
@ -200,7 +233,12 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
return episodesDetail.map { ep ->
|
||||
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 {
|
||||
episode_number = ep.toFloatOrNull() ?: 0F
|
||||
name = "Episode $numName ($subPref)"
|
||||
@ -211,27 +249,13 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
// ============================ 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> {
|
||||
val videoJson = json.decodeFromString<EpisodeResult>(response.body.string())
|
||||
val videoList = mutableListOf<Pair<Video, Float>>()
|
||||
val serverList = mutableListOf<Server>()
|
||||
|
||||
val altHosterSelection = preferences.getStringSet(
|
||||
PREF_ALT_HOSTER_KEY,
|
||||
ALT_HOSTER_NAMES.toSet(),
|
||||
)!!
|
||||
|
||||
val hosterSelection = preferences.getStringSet(
|
||||
PREF_HOSTER_KEY,
|
||||
PREF_HOSTER_DEFAULT,
|
||||
)!!
|
||||
val hosterSelection = preferences.getHosters
|
||||
val altHosterSelection = preferences.getAltHosters
|
||||
|
||||
// list of alternative hosters
|
||||
val mappings = listOf(
|
||||
@ -356,9 +380,9 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun prioritySort(pList: List<Pair<Video, Float>>): List<Video> {
|
||||
val prefServer = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val subPref = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
|
||||
val prefServer = preferences.prefServer
|
||||
val quality = preferences.quality
|
||||
val subPref = preferences.subPref
|
||||
|
||||
return pList.sortedWith(
|
||||
compareBy(
|
||||
@ -392,11 +416,10 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
|
||||
private fun parseAnime(response: Response): AnimesPage {
|
||||
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 ->
|
||||
SAnime.create().apply {
|
||||
title = when (titleStyle) {
|
||||
title = when (preferences.titleStyle) {
|
||||
"romaji" -> ani.name
|
||||
"eng" -> ani.englishName ?: ani.name
|
||||
else -> ani.nativeName ?: ani.name
|
||||
@ -413,7 +436,7 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
return keywords.any { this.contains(it) }
|
||||
}
|
||||
|
||||
fun hexToText(inputString: String): String {
|
||||
private fun hexToText(inputString: String): String {
|
||||
return inputString.chunked(2).map {
|
||||
it.toInt(16).toByte()
|
||||
}.toByteArray().toString(Charsets.UTF_8)
|
||||
@ -425,138 +448,6 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
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 {
|
||||
private const val PAGE_SIZE = 26 // number of items to retrieve when calling API
|
||||
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_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_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_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_TITLE = "Preferred Video Server"
|
||||
private val PREF_SERVER_ENTRIES = arrayOf("Site Default") +
|
||||
INTERAL_HOSTER_NAMES.sliceArray(1 until INTERAL_HOSTER_NAMES.size) +
|
||||
ALT_HOSTER_NAMES
|
||||
@ -599,18 +483,14 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
private const val PREF_SERVER_DEFAULT = "site_default"
|
||||
|
||||
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 {
|
||||
it.lowercase()
|
||||
}.toTypedArray()
|
||||
private val PREF_HOSTER_DEFAULT = setOf("default", "ac", "ak", "kir", "luf-mp4", "si-hls", "s-mp4", "ac-hls")
|
||||
|
||||
private const val PREF_ALT_HOSTER_KEY = "alt_hoster_selection"
|
||||
private const val PREF_ALT_HOSTER_TITLE = "Enable/Disable Alternative Hosts"
|
||||
|
||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||
private const val PREF_QUALITY_TITLE = "Preferred quality"
|
||||
private val PREF_QUALITY_ENTRIES = arrayOf(
|
||||
"1080p",
|
||||
"720p",
|
||||
@ -627,15 +507,158 @@ class AllAnime : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||
|
||||
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_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"
|
||||
}
|
||||
|
||||
// ============================== 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())!!
|
||||
}
|
||||
|
@ -11,57 +11,6 @@ import okhttp3.OkHttpClient
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
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) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
@ -235,4 +184,55 @@ class AllAnimeExtractor(private val client: OkHttpClient) {
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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"
|
||||
|
||||
@ -58,20 +58,12 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== 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 popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val thumbnailUrl = element.selectFirst("img")!!.attr("data-src")
|
||||
|
||||
@ -86,14 +78,14 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
|
||||
|
||||
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 {
|
||||
val thumbnailUrl = element.selectFirst("img")!!.attr("data-src")
|
||||
|
||||
@ -108,6 +100,8 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
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 {
|
||||
return if (query.isNotBlank()) {
|
||||
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 searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
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 searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
// ============================== FILTERS ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeDaoFilters.FILTER_LIST
|
||||
@ -198,7 +191,7 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
// ============================== Episodes ==============================
|
||||
|
||||
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(
|
||||
compareBy(
|
||||
{ it.episode_number },
|
||||
@ -221,7 +214,7 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
episode_number = if (episodeName.contains("Episode ", true)) {
|
||||
episodeName.substringAfter("Episode ").substringBefore(" ").toFloatOrNull() ?: 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"
|
||||
}
|
||||
date_upload = element.selectFirst("span.date")?.let { parseDate(it.text()) } ?: 0L
|
||||
@ -298,8 +291,8 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||
val server = preferences.getString("preferred_server", "vstream")!!
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
@ -333,28 +326,33 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val domainPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_domain"
|
||||
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()
|
||||
}
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
|
||||
}
|
||||
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"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360")
|
||||
setDefaultValue("1080")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@ -363,13 +361,14 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
val videoServerPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_server"
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = arrayOf("Vidstreaming", "Vidstreaming2", "Vidstreaming3", "Mixdrop", "StreamSB", "Streamtape", "Vidstreaming4", "Doodstream")
|
||||
entryValues = arrayOf("vstream", "src2", "src", "mixdrop", "streamsb", "streamtape", "vplayer", "doodstream")
|
||||
setDefaultValue("vstream")
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@ -378,33 +377,29 @@ class AnimeDao : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
val episodeSortPref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "preferred_episode_sorting"
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_EPISODE_SORT_KEY
|
||||
title = "Attempt episode sorting"
|
||||
summary = """AnimeDao displays the episodes in either ascending or descending order,
|
||||
| enable to attempt order or disable to set same as website.
|
||||
""".trimMargin()
|
||||
setDefaultValue(true)
|
||||
setDefaultValue(PREF_EPISODE_SORT_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}
|
||||
val markFillers = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "mark_fillers"
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_MARK_FILLERS_KEY
|
||||
title = "Mark filler episodes"
|
||||
setDefaultValue(true)
|
||||
setDefaultValue(PREF_MARK_FILLERS_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putBoolean(key, newValue as Boolean).commit()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(domainPref)
|
||||
screen.addPreference(videoQualityPref)
|
||||
screen.addPreference(videoServerPref)
|
||||
screen.addPreference(episodeSortPref)
|
||||
screen.addPreference(markFillers)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class MixDropExtractor(private val client: OkHttpClient) {
|
||||
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
|
||||
?.data()
|
||||
?.let { JsUnpacker.unpackAndCombine(it) }
|
||||
?: return emptyList<Video>()
|
||||
?: return emptyList()
|
||||
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
|
||||
.substringBefore("\"")
|
||||
val quality = ("MixDrop").let {
|
||||
|
@ -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)")
|
||||
.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("\");")
|
||||
return listOf(
|
||||
Video(videoUrl, "$prefix Mp4upload", videoUrl, headers = Headers.headersOf("Referer", "https://www.mp4upload.com/")),
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'AnimeFlix'
|
||||
pkgNameSuffix = 'en.animeflix'
|
||||
extClass = '.AnimeFlix'
|
||||
extVersionCode = 4
|
||||
extVersionCode = 5
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -8,13 +8,11 @@ import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@ -26,7 +24,6 @@ import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
@ -58,95 +55,53 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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 {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
title = element.selectFirst("header")!!.text()
|
||||
}
|
||||
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img")!!.attr("src")
|
||||
title = element.selectFirst("header")!!.text()
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.nav-links > span.current ~ a"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used")
|
||||
|
||||
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> {
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
val cleanQuery = query.replace(" ", "+").lowercase()
|
||||
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
|
||||
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
|
||||
val urlFilter = filterList.find { it is URLFilter } as URLFilter
|
||||
|
||||
return when {
|
||||
query.isNotBlank() -> Pair(GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers), false)
|
||||
genreFilter.state != 0 -> Pair(GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers), false)
|
||||
subpageFilter.state != 0 -> Pair(GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers), false)
|
||||
urlFilter.state.isNotEmpty() -> Pair(GET(urlFilter.state, headers = headers), true)
|
||||
else -> Pair(popularAnimeRequest(page), false)
|
||||
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
|
||||
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
|
||||
subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
|
||||
else -> popularAnimeRequest(page)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
// ============================== FILTERS ===============================
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("Text search ignores filters"),
|
||||
GenreFilter(),
|
||||
SubPageFilter(),
|
||||
AnimeFilter.Separator(),
|
||||
AnimeFilter.Header("Get item url from webview"),
|
||||
URLFilter(),
|
||||
)
|
||||
|
||||
private class GenreFilter : UriPartFilter(
|
||||
@ -185,14 +140,13 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private class URLFilter : AnimeFilter.Text("Url")
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
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 {
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -218,7 +172,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val serverListSeason = mutableListOf<List<EpUrl>>()
|
||||
|
||||
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 url = it.selectFirst("a")!!.attr("href")
|
||||
@ -244,7 +198,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
|
||||
@ -314,6 +268,9 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}.getOrNull()
|
||||
}.flatten(),
|
||||
)
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return Observable.just(videoList.sort())
|
||||
}
|
||||
|
||||
@ -323,7 +280,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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
|
||||
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
|
||||
@ -331,11 +288,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
val qualityRegex = """(\d+)p""".toRegex()
|
||||
val matchResult = qualityRegex.find(epUrl.name)
|
||||
val quality = if (matchResult == null) {
|
||||
epUrl.quality
|
||||
} else {
|
||||
matchResult.groupValues[1]
|
||||
}
|
||||
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
|
||||
|
||||
for (type in 1..3) {
|
||||
videoList.addAll(
|
||||
@ -345,12 +298,10 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return Pair(videoList, epUrl.url)
|
||||
}
|
||||
|
||||
private val sizeRegex = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
|
||||
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
|
||||
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
|
||||
val sizeMatch = 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" } ?: ""
|
||||
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
|
||||
val link = linkElement.attr("href")
|
||||
@ -373,12 +324,12 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val response = tokenClient.newCall(GET(mediaUrl)).execute().asJsoup()
|
||||
val gdBtn = response.selectFirst("div.card-body a.btn")!!
|
||||
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 gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
|
||||
val link = gdResponse.select("form#download-form")
|
||||
return if (link.isNullOrEmpty()) {
|
||||
listOf()
|
||||
emptyList()
|
||||
} else {
|
||||
val realLink = link.attr("action")
|
||||
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
|
||||
@ -386,7 +337,7 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
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(
|
||||
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
|
||||
data class EpUrl(
|
||||
val quality: String,
|
||||
@ -438,4 +370,31 @@ class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
runBlocking {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'AnimeKaizoku'
|
||||
pkgNameSuffix = 'en.animekaizoku'
|
||||
extClass = '.AnimeKaizoku'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -741,9 +741,12 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DDL_REGEX = Regex("""DDL\((.*?), ?(.*?), ?(.*?), ?(.*?)\)""")
|
||||
}
|
||||
private val xmlHeaders = headers.newBuilder()
|
||||
.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 ===============================
|
||||
|
||||
@ -751,26 +754,24 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun popularAnimeSelector(): String = "table > tbody > tr.post-row"
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a:not([title])")!!.attr("abs:href").toHttpUrl().encodedPath)
|
||||
title = element.selectFirst("a:not([title])")!!.text()
|
||||
thumbnail_url = ""
|
||||
}
|
||||
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a:not([title])")!!.attr("abs:href"))
|
||||
title = element.selectFirst("a:not([title])")!!.text()
|
||||
thumbnail_url = ""
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = 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 latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeParse(response: Response): AnimesPage = throw Exception("Not used")
|
||||
@ -821,13 +822,8 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
.add("layout", layout)
|
||||
.add("settings", settings)
|
||||
.build()
|
||||
val formHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", "animekaizoku.com")
|
||||
.add("Origin", "https://animekaizoku.com")
|
||||
val formHeaders = xmlHeaders
|
||||
.add("Referer", currentReferer)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
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 searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
private fun searchAnimeFromElementPaginated(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href").toHttpUrl().encodedPath)
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
|
||||
title = element.selectFirst("h2.post-title")!!.text().substringBefore(" Episode")
|
||||
}
|
||||
private fun searchAnimeFromElementPaginated(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("abs:src") ?: ""
|
||||
title = element.selectFirst("h2.post-title")!!.text().substringBefore(" Episode")
|
||||
}
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
@ -921,16 +915,14 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
return SAnime.create().apply {
|
||||
title = document.selectFirst("div.entry-header > h1")?.text()?.trim() ?: ""
|
||||
thumbnail_url = document.selectFirst("script:containsData(primaryImageOfPage)")?.data()?.let {
|
||||
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() }
|
||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
||||
title = document.selectFirst("div.entry-header > h1")?.text()?.trim() ?: ""
|
||||
thumbnail_url = document.selectFirst("script:containsData(primaryImageOfPage)")?.data()?.let {
|
||||
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() }
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
@ -944,16 +936,11 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
.substringAfter("\"postId\":\"").substringBefore("\"")
|
||||
val serversList = mutableListOf<List<EpUrl>>()
|
||||
|
||||
val postHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", baseUrl.toHttpUrl().host)
|
||||
.add("Origin", baseUrl)
|
||||
val postHeaders = xmlHeaders
|
||||
.add("Referer", baseUrl + anime.url)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.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 ->
|
||||
val data = serverType.groupValues
|
||||
@ -1020,9 +1007,7 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
SEpisode.create().apply {
|
||||
name = serverList.first().name
|
||||
episode_number = (index + 1).toFloat()
|
||||
setUrlWithoutDomain(
|
||||
serverList.toJsonString(),
|
||||
)
|
||||
url = serverList.toJsonString()
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -1044,13 +1029,8 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val parsed = json.decodeFromString<List<EpUrl>>(episode.url)
|
||||
|
||||
parsed.forEach {
|
||||
val postHeaders = headers.newBuilder()
|
||||
.add("Accept", "*/*")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Host", baseUrl.toHttpUrl().host)
|
||||
.add("Origin", baseUrl)
|
||||
val postHeaders = xmlHeaders
|
||||
.add("Referer", it.ref)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
.build()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1142,7 +1124,7 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
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(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
@ -1188,13 +1170,25 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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) {
|
||||
val domainPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_server"
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = arrayOf("Server direct", "Worker direct", "Both")
|
||||
entryValues = arrayOf("server", "worker", "both")
|
||||
setDefaultValue("server")
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@ -1203,13 +1197,14 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
}.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("1080")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@ -1218,9 +1213,6 @@ class AnimeKaizoku : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(domainPref)
|
||||
screen.addPreference(videoQualityPref)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'Ask4Movie'
|
||||
pkgNameSuffix = 'en.ask4movie'
|
||||
extClass = '.Ask4Movie'
|
||||
extVersionCode = 5
|
||||
extVersionCode = 6
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -13,9 +13,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@ -49,7 +47,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun popularAnimeSelector(): String = "div.all-channels div.channel-content"
|
||||
|
||||
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")
|
||||
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]")!!
|
||||
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(a.relative())
|
||||
setUrlWithoutDomain(a.attr("abs:href"))
|
||||
thumbnail_url = element.select("div.item-thumb").attr("style")
|
||||
.substringAfter("background-image: url(").substringBefore(")")
|
||||
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 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")
|
||||
.substringAfter("background-image: url(").substringBefore(")")
|
||||
title = element.selectFirst("div.description a")!!.text()
|
||||
@ -111,19 +109,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
|
||||
|
||||
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
|
||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
||||
genre = document.select("div.categories:contains(Genres) a").joinToString(", ") { it.text() }
|
||||
.ifBlank { document.selectFirst("div.channel-description > p:has(span:contains(Genre)) em")?.text() }
|
||||
description = document.selectFirst("div.custom.video-the-content p")?.ownText()
|
||||
@ -138,7 +124,7 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// Select multiple seasons
|
||||
val seasonsList = document.select("div.row > div.cactus-sub-wrap > div.item")
|
||||
if (seasonsList.isEmpty().not()) {
|
||||
if (seasonsList.isNotEmpty()) {
|
||||
seasonsList.forEach { season ->
|
||||
val link = season.selectFirst("a.btn-play-nf")!!.attr("abs:href")
|
||||
val seasonName = "Season ${season.selectFirst("div.description p a")!!.text().substringAfter("(Season ").substringBefore(")")} "
|
||||
@ -157,17 +143,6 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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
|
||||
private fun episodesFromGroupLinks(document: Document, prefix: String = ""): List<SEpisode> {
|
||||
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 =============================
|
||||
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -206,10 +185,6 @@ class Ask4Movie : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
private fun Element.relative(): String {
|
||||
return this.attr("abs:href").toHttpUrl().encodedPath
|
||||
}
|
||||
|
||||
private fun Int.toPage(): String {
|
||||
return if (this == 1) {
|
||||
""
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'BestDubbedAnime'
|
||||
pkgNameSuffix = 'en.bestdubbedanime'
|
||||
extClass = '.BestDubbedAnime'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.animeextension.en.bestdubbedanime
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Base64
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||
@ -20,13 +19,11 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
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.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@ -57,35 +54,138 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// Popular Anime
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val animes = document.select(popularAnimeSelector()).map { element ->
|
||||
popularAnimeFromElement(element)
|
||||
}
|
||||
|
||||
return AnimesPage(animes, false)
|
||||
}
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/xz/trending.php?_=${System.currentTimeMillis() / 1000}")
|
||||
|
||||
override fun popularAnimeSelector(): String = "li"
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
return GET("$baseUrl/xz/trending.php?_=${System.currentTimeMillis() / 1000}")
|
||||
override fun popularAnimeFromElement(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.cittx").text()
|
||||
}
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
anime.setUrlWithoutDomain(("https:" + element.select("a").attr("href")).toHttpUrl().encodedPath)
|
||||
anime.title = element.select("div.cittx").text()
|
||||
anime.thumbnail_url = "https:" + element.select("img").attr("src")
|
||||
// =============================== Latest ===============================
|
||||
|
||||
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
|
||||
override fun episodeListSelector() = throw Exception("Not used")
|
||||
@ -95,22 +195,23 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val episodeList = mutableListOf<SEpisode>()
|
||||
|
||||
if (response.request.url.encodedPath.startsWith("/movies/")) {
|
||||
val episode = SEpisode.create()
|
||||
|
||||
episode.name = document.select("div.tinywells > div > h4").text()
|
||||
episode.episode_number = 1F
|
||||
episode.setUrlWithoutDomain(response.request.url.encodedPath)
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = document.select("div.tinywells > div > h4").text()
|
||||
episode_number = 1F
|
||||
setUrlWithoutDomain(response.request.url.toString())
|
||||
},
|
||||
)
|
||||
} else {
|
||||
var counter = 1
|
||||
for (ep in document.select("div.eplistz > div > div > a")) {
|
||||
val episode = SEpisode.create()
|
||||
|
||||
episode.name = ep.select("div.inwel > span").text()
|
||||
episode.episode_number = counter.toFloat()
|
||||
episode.setUrlWithoutDomain(("https:" + ep.attr("href")).toHttpUrl().encodedPath)
|
||||
episodeList.add(episode)
|
||||
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = ep.select("div.inwel > span").text()
|
||||
episode_number = counter.toFloat()
|
||||
setUrlWithoutDomain(ep.attr("abs:href"))
|
||||
},
|
||||
)
|
||||
counter++
|
||||
}
|
||||
|
||||
@ -118,10 +219,8 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val cacheUrlRegex = Regex("""url: '(.*?)'(?:.*?)episodesListxf""", RegexOption.DOT_MATCHES_ALL)
|
||||
|
||||
val jsText = document.selectFirst("script:containsData(episodesListxf)")!!.data()
|
||||
val url = cacheUrlRegex.find(jsText)?.groupValues?.get(1) ?: ""
|
||||
|
||||
if (url.isNotBlank()) {
|
||||
episodeList.addAll(extractFromCache(url))
|
||||
cacheUrlRegex.find(jsText)?.groupValues?.get(1)?.let {
|
||||
episodeList.addAll(extractFromCache(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -129,58 +228,9 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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")
|
||||
|
||||
// Video urls
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
@ -219,11 +269,59 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
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
|
||||
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||
runBlocking {
|
||||
@ -231,94 +329,39 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", null)
|
||||
if (quality != null) {
|
||||
val newList = mutableListOf<Video>()
|
||||
var preferred = 0
|
||||
for (video in this) {
|
||||
if (video.quality.contains(quality)) {
|
||||
newList.add(preferred, video)
|
||||
preferred++
|
||||
} else {
|
||||
newList.add(video)
|
||||
}
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy { it.quality.contains(quality) },
|
||||
).reversed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
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", "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
|
||||
}
|
||||
return this
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
AnimeFilter.Header("NOTE: Ignored if using text search!"),
|
||||
@ -442,106 +485,4 @@ class BestDubbedAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
Pair("Work Life", "Work Life"),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -25,8 +25,6 @@ import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URLEncoder
|
||||
import java.text.CharacterIterator
|
||||
import java.text.StringCharacterIterator
|
||||
|
||||
class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
@ -42,8 +40,6 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val chunkedSize = 300
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
@ -54,32 +50,31 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
val page = response.request.url.encodedFragment!!.toInt()
|
||||
val path = response.request.url.encodedPath
|
||||
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 name = a.text()
|
||||
if (a.attr("href") == "..") return@forEach
|
||||
if (a.attr("href") == "..") return@mapNotNull null
|
||||
|
||||
val anime = SAnime.create()
|
||||
anime.title = name.removeSuffix("/")
|
||||
anime.setUrlWithoutDomain(joinPaths(path, a.attr("href")))
|
||||
anime.thumbnail_url = ""
|
||||
animeList.add(anime)
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain(joinPaths(path, a.attr("href")))
|
||||
title = name.removeSuffix("/")
|
||||
thumbnail_url = ""
|
||||
}
|
||||
}
|
||||
|
||||
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 popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
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 {
|
||||
val document = response.asJsoup()
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
val page = response.request.url.encodedFragment!!.toInt()
|
||||
val path = response.request.url.encodedPath
|
||||
val items = document.select(popularAnimeSelector()).filter { t ->
|
||||
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 name = a.text()
|
||||
if (a.attr("href") == "..") return@forEach
|
||||
if (a.attr("href") == "..") return@mapNotNull null
|
||||
|
||||
val anime = SAnime.create()
|
||||
anime.title = name.removeSuffix("/")
|
||||
anime.setUrlWithoutDomain(joinPaths(path, a.attr("href")))
|
||||
anime.thumbnail_url = ""
|
||||
animeList.add(anime)
|
||||
SAnime.create().apply {
|
||||
setUrlWithoutDomain(joinPaths(path, a.attr("href")))
|
||||
title = name.removeSuffix("/")
|
||||
thumbnail_url = ""
|
||||
}
|
||||
}
|
||||
|
||||
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 searchAnimeNextPageSelector(): String = throw Exception("Not used")
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = throw Exception("Not used")
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = throw Exception("Not used")
|
||||
|
||||
// ============================== FILTERS ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
@ -172,9 +166,7 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||
return Observable.just(anime)
|
||||
}
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = Observable.just(anime)
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
|
||||
|
||||
@ -196,7 +188,6 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
traverseDirectory(fullUrl)
|
||||
}
|
||||
if (videoFormats.any { t -> fullUrl.endsWith(t) }) {
|
||||
val episode = SEpisode.create()
|
||||
val paths = fullUrl.toHttpUrl().pathSegments
|
||||
|
||||
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()) }
|
||||
|
||||
episode.name = "${videoFormats.fold(paths.last()) { acc, suffix -> acc.removeSuffix(suffix).trimInfo() }}${if (size == null) "" else " - $size"}"
|
||||
episode.url = fullUrl
|
||||
episode.scanlator = seasonInfo + extraInfo
|
||||
episode.episode_number = counter.toFloat()
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = videoFormats.fold(paths.last()) { acc, suffix -> acc.removeSuffix(suffix).trimInfo() }
|
||||
this.url = fullUrl
|
||||
scanlator = "${if (size == null) "" else "$size"} • $seasonInfo$extraInfo"
|
||||
episode_number = counter.toFloat()
|
||||
},
|
||||
)
|
||||
counter++
|
||||
|
||||
episodeList.add(episode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -238,9 +231,8 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||
return Observable.just(listOf(Video(episode.url, "Video", episode.url)))
|
||||
}
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> =
|
||||
Observable.just(listOf(Video(episode.url, "Video", episode.url)))
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
|
||||
|
||||
@ -264,25 +256,17 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
private fun formatBytes(bytes: Long?): String? {
|
||||
if (bytes == null) return null
|
||||
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes)
|
||||
if (absB < 1024) {
|
||||
return "$bytes B"
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
|
||||
var value = bytes?.toDouble() ?: return null
|
||||
var i = 0
|
||||
while (value >= 1024 && i < units.size - 1) {
|
||||
value /= 1024
|
||||
i++
|
||||
}
|
||||
var value = absB
|
||||
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())
|
||||
return String.format("%.1f %s", value, units[i])
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
while (regex.containsMatchIn(newString)) {
|
||||
@ -294,15 +278,11 @@ class Edytjedhgmdhm : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return newString
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val ignoreExtras = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "ignore_extras"
|
||||
title = "Ignore \"Extras\" folder"
|
||||
setDefaultValue(true)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putBoolean(key, newValue as Boolean).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(ignoreExtras)
|
||||
companion object {
|
||||
private const val CHUNKED_SIZE = 300
|
||||
}
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) { }
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'FMovies'
|
||||
pkgNameSuffix = 'en.fmovies'
|
||||
extClass = '.FMovies'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val a = element.selectFirst("div.meta a")!!
|
||||
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(a.relative())
|
||||
setUrlWithoutDomain(a.attr("abs:href"))
|
||||
thumbnail_url = element.select("div.poster img").attr("data-src")
|
||||
title = a.text()
|
||||
}
|
||||
@ -277,6 +277,8 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}.filterNotNull().flatten(),
|
||||
)
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
@ -355,7 +357,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_TITLE)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
@ -377,10 +379,6 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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 {
|
||||
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_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 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_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_TITLE = "Preferred server"
|
||||
private const val PREF_SERVER_DEFAULT = "Vidstream"
|
||||
|
||||
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")
|
||||
}
|
||||
// ============================== Settings ==============================
|
||||
@ -425,9 +408,15 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = PREF_DOMAIN_TITLE
|
||||
entries = PREF_DOMAIN_ENTRIES
|
||||
entryValues = PREF_DOMAIN_ENTRY_VALUES
|
||||
title = "Preferred domain (requires app restart)"
|
||||
entries = arrayOf(
|
||||
"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)
|
||||
summary = "%s"
|
||||
|
||||
@ -437,13 +426,13 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
@ -453,11 +442,11 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = PREF_SERVER_TITLE
|
||||
title = "Preferred server"
|
||||
entries = HOSTERS
|
||||
entryValues = HOSTERS
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
@ -469,11 +458,11 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_HOSTER_KEY
|
||||
title = PREF_HOSTER_TITLE
|
||||
title = "Enable/Disable Hosts"
|
||||
entries = HOSTERS
|
||||
entryValues = HOSTERS
|
||||
setDefaultValue(PREF_HOSTER_DEFAULT)
|
||||
@ -482,6 +471,6 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_TITLE)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
@ -253,21 +253,15 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
private const val PREF_DOMAIN_KEY = "preferred_domain_name"
|
||||
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_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_SERVER_KEY = "preferred_server"
|
||||
private const val PREF_SERVER_TITLE = "Preferred server"
|
||||
private const val PREF_SERVER_DEFAULT = "Gogostream"
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@ -277,7 +271,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = PREF_DOMAIN_KEY
|
||||
title = PREF_DOMAIN_TITLE
|
||||
summary = PREF_DOMAIN_SUMMARY
|
||||
summary = "Override default domain (requires app restart)"
|
||||
dialogTitle = PREF_DOMAIN_TITLE
|
||||
dialogMessage = "Default: $PREF_DOMAIN_DEFAULT"
|
||||
setDefaultValue(PREF_DOMAIN_DEFAULT)
|
||||
@ -286,13 +280,13 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val newValueString = newValue as String
|
||||
preferences.edit().putString(key, newValueString.trim()).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
@ -302,11 +296,11 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = PREF_SERVER_TITLE
|
||||
title = "Preferred server"
|
||||
entries = HOSTERS
|
||||
entryValues = HOSTERS
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
@ -318,11 +312,11 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
MultiSelectListPreference(screen.context).apply {
|
||||
key = PREF_HOSTER_KEY
|
||||
title = PREF_HOSTER_TITLE
|
||||
title = "Enable/Disable Hosts"
|
||||
entries = HOSTERS
|
||||
entryValues = HOSTERS_NAMES
|
||||
setDefaultValue(PREF_HOSTER_DEFAULT)
|
||||
@ -331,7 +325,7 @@ class GogoAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
@ -48,37 +47,23 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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 {
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").toHttpUrl().encodedPath)
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
|
||||
title = element.selectFirst("h1")!!.text()
|
||||
}
|
||||
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
|
||||
title = element.selectFirst("h1")!!.text()
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "nav.gridlove-pagination > span.current + a"
|
||||
|
||||
// =============================== 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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
override fun latestUpdatesNextPageSelector(): String = throw Exception("Not used")
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
@ -113,23 +98,8 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<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 oldAnime = anime
|
||||
|
||||
oldAnime.description = document.selectFirst("div.entry-content > p")?.text()
|
||||
|
||||
return oldAnime
|
||||
override fun animeDetailsParse(document: Document): SAnime = SAnime.create().apply {
|
||||
description = document.selectFirst("div.entry-content > p")?.text()
|
||||
}
|
||||
|
||||
// ============================== Episodes ==============================
|
||||
@ -150,13 +120,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
|
||||
|
||||
val episode = SEpisode.create()
|
||||
episode.name = link.text()
|
||||
episode.episode_number = 1F
|
||||
episode.date_upload = -1L
|
||||
episode.url = link.attr("href")
|
||||
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = link.text()
|
||||
episode_number = 1F
|
||||
date_upload = -1L
|
||||
url = link.attr("href")
|
||||
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 episode = SEpisode.create()
|
||||
episode.name = "Ep. $episodeNumber - ${link.text()}"
|
||||
episode.episode_number = episodeNumber
|
||||
episode.date_upload = -1L
|
||||
episode.url = link.attr("href")
|
||||
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = "Ep. $episodeNumber - ${link.text()}"
|
||||
episode_number = episodeNumber
|
||||
date_upload = -1L
|
||||
url = link.attr("href")
|
||||
scanlator = "${if (size == null) "" else "$size • "}$info"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,13 +170,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val size = sizeRegex.find(title)?.groupValues?.get(1)
|
||||
?: sizeRegex.find(link.text())?.groupValues?.get(1)
|
||||
|
||||
val episode = SEpisode.create()
|
||||
episode.name = "$title - ${link.text()}"
|
||||
episode.episode_number = 1F
|
||||
episode.date_upload = -1L
|
||||
episode.scanlator = size
|
||||
episode.url = link.attr("href")
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = "$title - ${link.text()}"
|
||||
episode_number = 1F
|
||||
date_upload = -1L
|
||||
scanlator = size
|
||||
url = link.attr("href")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -218,13 +194,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
|
||||
|
||||
val episode = SEpisode.create()
|
||||
episode.name = "$title - ${link.text()}"
|
||||
episode.episode_number = 1F
|
||||
episode.date_upload = -1L
|
||||
episode.scanlator = size
|
||||
episode.url = link.attr("href")
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = "$title - ${link.text()}"
|
||||
episode_number = 1F
|
||||
date_upload = -1L
|
||||
scanlator = size
|
||||
url = link.attr("href")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -239,13 +217,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
|
||||
|
||||
val episode = SEpisode.create()
|
||||
episode.name = link.text()
|
||||
episode.episode_number = 1F
|
||||
episode.date_upload = -1L
|
||||
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
|
||||
episode.url = link.attr("href")
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = link.text()
|
||||
episode_number = 1F
|
||||
date_upload = -1L
|
||||
scanlator = "${if (size == null) "" else "$size • "}$info"
|
||||
url = link.attr("href")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -260,13 +240,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
|
||||
|
||||
val episode = SEpisode.create()
|
||||
episode.name = link.text()
|
||||
episode.episode_number = 1F
|
||||
episode.date_upload = -1L
|
||||
episode.scanlator = "${if (size == null) "" else "$size • "}$info"
|
||||
episode.url = link.attr("href")
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = link.text()
|
||||
episode_number = 1F
|
||||
date_upload = -1L
|
||||
scanlator = "${if (size == null) "" else "$size • "}$info"
|
||||
url = link.attr("href")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -277,13 +259,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
if (zipRegex.find(link.text()) != null) return@forEach
|
||||
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
|
||||
|
||||
val episode = SEpisode.create()
|
||||
episode.name = link.text()
|
||||
episode.episode_number = 1F
|
||||
episode.date_upload = -1L
|
||||
episode.scanlator = size
|
||||
episode.url = link.attr("href")
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = link.text()
|
||||
episode_number = 1F
|
||||
date_upload = -1L
|
||||
scanlator = size
|
||||
url = link.attr("href")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -294,13 +278,15 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
if (zipRegex.find(link.text()) != null) return@forEach
|
||||
val size = sizeRegex.find(link.text())?.groupValues?.get(1)
|
||||
|
||||
val episode = SEpisode.create()
|
||||
episode.name = link.text()
|
||||
episode.episode_number = 1F
|
||||
episode.date_upload = -1L
|
||||
episode.scanlator = size
|
||||
episode.url = link.attr("href")
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = link.text()
|
||||
episode_number = 1F
|
||||
date_upload = -1L
|
||||
scanlator = size
|
||||
url = link.attr("href")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -326,16 +312,20 @@ class PobMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
else -> { throw Exception("Unsupported url: ${episode.url}") }
|
||||
}
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
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 videoFromElement(element: Element): Video = throw Exception("Not Used")
|
||||
|
||||
override fun videoUrlParse(document: Document): String = throw Exception("Not Used")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
// ============================== Settings ==============================
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'Kawaiifu'
|
||||
pkgNameSuffix = 'en.kawaiifu'
|
||||
extClass = '.Kawaiifu'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '13'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@ -54,7 +53,7 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun popularAnimeSelector(): String = "ul.list-film li"
|
||||
|
||||
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()
|
||||
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])")!!
|
||||
|
||||
return SAnime.create().apply {
|
||||
setUrlWithoutDomain(a.relative())
|
||||
setUrlWithoutDomain(a.attr("abs:href"))
|
||||
thumbnail_url = element.select("a.thumb img").attr("src")
|
||||
title = a.text()
|
||||
}
|
||||
@ -196,6 +195,8 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
)
|
||||
}
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return Observable.just(videoList)
|
||||
}
|
||||
|
||||
@ -218,10 +219,6 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
).reversed()
|
||||
}
|
||||
|
||||
private fun Element.relative(): String {
|
||||
return this.attr("abs:href").toHttpUrl().encodedPath
|
||||
}
|
||||
|
||||
private fun Int.toPage(): String {
|
||||
return if (this == 1) {
|
||||
""
|
||||
@ -244,9 +241,6 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
companion object {
|
||||
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"
|
||||
}
|
||||
|
||||
@ -255,9 +249,9 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p", "240p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360", "240")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
@ -267,6 +261,6 @@ class Kawaiifu : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'Kayoanime'
|
||||
pkgNameSuffix = 'en.kayoanime'
|
||||
extClass = '.Kayoanime'
|
||||
extVersionCode = 5
|
||||
extVersionCode = 6
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import android.util.Base64
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
@ -18,7 +17,11 @@ class DriveIndexExtractor(private val client: OkHttpClient, private val headers:
|
||||
|
||||
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 basePathCounter = indexUrl.toHttpUrl().pathSegments.size
|
||||
@ -52,11 +55,21 @@ class DriveIndexExtractor(private val client: OkHttpClient, private val headers:
|
||||
traverseDirectory(newUrl)
|
||||
}
|
||||
if (item.mimeType.startsWith("video/")) {
|
||||
val episode = SEpisode.create()
|
||||
val epUrl = joinUrl(url, item.name)
|
||||
val paths = epUrl.toHttpUrl().pathSegments
|
||||
|
||||
// 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) {
|
||||
"/$path/" + paths.subList(basePathCounter - 1, paths.size - 1).joinToString("/") { it.trimInfo() }
|
||||
} else {
|
||||
@ -64,18 +77,16 @@ class DriveIndexExtractor(private val client: OkHttpClient, private val headers:
|
||||
}
|
||||
val size = item.size?.toLongOrNull()?.let { formatFileSize(it) }
|
||||
|
||||
episode.name = item.name.trimInfo()
|
||||
episode.url = epUrl
|
||||
episode.scanlator = if (flipOrder) {
|
||||
"$extraInfo • ${size ?: "N/A"}"
|
||||
} else {
|
||||
"${size ?: "N/A"} • $extraInfo"
|
||||
}
|
||||
episode.episode_number = counter.toFloat()
|
||||
episode.date_upload = -1L
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = if (trimName) item.name.trimInfo() else item.name
|
||||
this.url = epUrl
|
||||
scanlator = "${if (size == null) "" else "$size"} • $seasonInfo$extraInfo"
|
||||
date_upload = -1L
|
||||
episode_number = counter.toFloat()
|
||||
},
|
||||
)
|
||||
counter++
|
||||
|
||||
episodeList.add(episode)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -34,10 +33,6 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.MessageDigest
|
||||
import java.text.CharacterIterator
|
||||
import java.text.SimpleDateFormat
|
||||
import java.text.StringCharacterIterator
|
||||
import java.util.Locale
|
||||
|
||||
class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
@ -61,20 +56,12 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val maxRecursionDepth = 2
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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 popularAnimeNextPageSelector(): String = "div.pages-nav > a[data-text=load more]"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
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 popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
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 ===============================
|
||||
|
||||
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 latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
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 latestUpdatesFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
|
||||
title = element.selectFirst("a.post-title")!!.text().substringBefore(" Episode")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
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 searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
@ -280,26 +261,13 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<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()
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
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 {
|
||||
it.selectFirst("div.toggle > div.toggle-content")!!.text() + "\n\n"
|
||||
} ?: ""
|
||||
|
||||
return SAnime.create().apply {
|
||||
title = anime.title
|
||||
thumbnail_url = anime.thumbnail_url
|
||||
status = document.selectFirst("div.toggle-content > ul > li:contains(Status)")?.let {
|
||||
parseStatus(it.text())
|
||||
} ?: SAnime.UNKNOWN
|
||||
@ -320,13 +288,8 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val document = response.asJsoup()
|
||||
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) {
|
||||
if (recursionDepth == maxRecursionDepth) return
|
||||
if (recursionDepth == MAX_RECURSION_DEPTH) return
|
||||
|
||||
val folderId = url.substringAfter("/folders/")
|
||||
val driveHeaders = headers.newBuilder()
|
||||
@ -342,14 +305,14 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return
|
||||
|
||||
val keyScript = driveDocument.select("script").first { script ->
|
||||
keyRegex.find(script.data()) != null
|
||||
KEY_REGEX.find(script.data()) != null
|
||||
}.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 ->
|
||||
keyRegex.find(script.data()) != null
|
||||
KEY_REGEX.find(script.data()) != null
|
||||
}.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 {
|
||||
it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
|
||||
}?.value ?: ""
|
||||
@ -357,7 +320,7 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
var pageToken: String? = ""
|
||||
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 body = """--$boundary
|
||||
val body = """--$BOUNDARY
|
||||
|content-type: application/http
|
||||
|content-transfer-encoding: binary
|
||||
|
|
||||
@ -366,12 +329,12 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|authorization: ${generateSapisidhashHeader(sapisid)}
|
||||
|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()
|
||||
.addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"")
|
||||
.addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$BOUNDARY\"")
|
||||
.addQueryParameter("key", key)
|
||||
.build()
|
||||
.toString()
|
||||
@ -386,34 +349,23 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
POST(postUrl, body = body, headers = postHeaders),
|
||||
).execute()
|
||||
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")
|
||||
parsed.items.forEachIndexed { index, it ->
|
||||
if (it.mimeType.startsWith("video")) {
|
||||
val episode = SEpisode.create()
|
||||
val size = formatBytes(it.fileSize?.toLongOrNull())
|
||||
val pathName = if (preferences.getBoolean("trim_info", false)) {
|
||||
path.trimInfo()
|
||||
} else {
|
||||
path
|
||||
}
|
||||
val pathName = path.trimInfo()
|
||||
|
||||
val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex()
|
||||
episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) {
|
||||
"/$pathName • $size"
|
||||
} else {
|
||||
"$size • /$pathName"
|
||||
}
|
||||
episode.name = if (preferences.getBoolean("trim_episode", false)) {
|
||||
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)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = if (preferences.trimEpisodeName) it.title.trimInfo() else it.title
|
||||
this.url = "https://drive.google.com/uc?id=${it.id}"
|
||||
episode_number = ITEM_NUMBER_REGEX.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
|
||||
date_upload = -1L
|
||||
scanlator = "$size • /$pathName"
|
||||
},
|
||||
)
|
||||
}
|
||||
if (it.mimeType.endsWith(".folder")) {
|
||||
traverseFolder(
|
||||
@ -449,7 +401,7 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
indexExtractor.getEpisodesFromIndex(
|
||||
location,
|
||||
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")) {
|
||||
getIndexVideoUrl(episode.url)
|
||||
} else {
|
||||
emptyList()
|
||||
throw Exception("Unsupported url: ${episode.url}")
|
||||
}
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return Observable.just(videoList)
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = 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")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
@ -519,7 +473,7 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
while (regex.containsMatchIn(newString)) {
|
||||
@ -570,21 +524,14 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
)
|
||||
|
||||
private fun formatBytes(bytes: Long?): String? {
|
||||
if (bytes == null) return null
|
||||
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes)
|
||||
if (absB < 1024) {
|
||||
return "$bytes B"
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
|
||||
var value = bytes?.toDouble() ?: return null
|
||||
var i = 0
|
||||
while (value >= 1024 && i < units.size - 1) {
|
||||
value /= 1024
|
||||
i++
|
||||
}
|
||||
var value = absB
|
||||
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())
|
||||
return String.format("%.1f %s", value, units[i])
|
||||
}
|
||||
|
||||
private fun getCookie(url: String): String {
|
||||
@ -604,34 +551,32 @@ class Kayoanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val scanlatorOrder = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "scanlator_order"
|
||||
title = "Switch order of file path and size"
|
||||
setDefaultValue(false)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private val ITEM_NUMBER_REGEX = """ - (?:S\d+E)?(\d+)""".toRegex()
|
||||
private val KEY_REGEX = """"(\w{39})"""".toRegex()
|
||||
private val VERSION_REGEX = """"([^"]+web-frontend[^"]+)"""".toRegex()
|
||||
private val JSON_REGEX = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
private const val BOUNDARY = "=====vc17a3rwnndj====="
|
||||
|
||||
screen.addPreference(scanlatorOrder)
|
||||
screen.addPreference(trimEpisodeName)
|
||||
screen.addPreference(trimEpisodeInfo)
|
||||
private const val MAX_RECURSION_DEPTH = 2
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'KissAnime'
|
||||
pkgNameSuffix = 'en.kissanime'
|
||||
extClass = '.KissAnime'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,6 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
@ -46,7 +45,7 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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"
|
||||
|
||||
@ -60,38 +59,30 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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 popularAnimeNextPageSelector(): String = "div.pagination > ul > li.current ~ li"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
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 popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
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 ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/AnimeListOnline/LatestUpdate?page=$page")
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
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 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 {
|
||||
title = it.selectFirst("h2 > a > span.jtitle")!!.text()
|
||||
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 {
|
||||
super.searchAnimeParse(response)
|
||||
}
|
||||
@ -134,10 +125,10 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== FILTERS ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = KissAnimeFilters.FILTER_LIST
|
||||
@ -146,6 +137,7 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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")}%" } ?: ""
|
||||
|
||||
return SAnime.create().apply {
|
||||
title = document.selectFirst("div.barContent > div.full > h2")!!.text()
|
||||
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 episodeFromElement(element: Element): SEpisode {
|
||||
return SEpisode.create().apply {
|
||||
name = element.selectFirst("a")!!.text()
|
||||
episode_number = element.selectFirst("a")!!.text().substringAfter("Episode ").toFloatOrNull() ?: 0F
|
||||
date_upload = parseDate(element.selectFirst("div:not(:has(a))")!!.text())
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href").substringAfter(baseUrl))
|
||||
}
|
||||
override fun episodeFromElement(element: Element): SEpisode = SEpisode.create().apply {
|
||||
name = element.selectFirst("a")!!.text()
|
||||
episode_number = element.selectFirst("a")!!.text().substringAfter("Episode ").toFloatOrNull() ?: 0F
|
||||
date_upload = parseDate(element.selectFirst("div:not(:has(a))")!!.text())
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
}
|
||||
|
||||
// ============================ Video Links =============================
|
||||
@ -251,19 +241,21 @@ class KissAnime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}.filterNotNull().flatten(),
|
||||
)
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
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 videoUrlParse(document: Document): String = throw Exception("Not Used")
|
||||
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
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(
|
||||
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
|
||||
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
|
||||
runBlocking {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -113,21 +113,23 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
val dataPage = document.select("div#app").attr("data-page").replace(""", "\"")
|
||||
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 {
|
||||
title = details.title
|
||||
thumbnail_url = details.cover
|
||||
genre = details.genre_list.joinToString(", ") { it.name }
|
||||
author = details.production_list.joinToString(", ") { it.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 {
|
||||
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_GROUP_KEY = "preferred_group"
|
||||
private const val PREF_GROUP_TITLE = "Preferred group"
|
||||
private const val PREF_GROUP_DEFAULT = "site_default"
|
||||
|
||||
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_ENTRIES = arrayOf("Subs", "Dubs")
|
||||
private const val PREF_SUBS_DEFAULT = "sub"
|
||||
|
||||
private const val PREF_SPECIAL_KEY = "preferred_special"
|
||||
private const val PREF_SPECIAL_TITLE = "Include Special Episodes"
|
||||
private const val PREF_SPECIAL_DEFAULT = true
|
||||
}
|
||||
|
||||
@ -336,7 +334,7 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = PREF_QUALITY_TITLE
|
||||
title = "Preferred quality"
|
||||
entries = PREF_QUALITY_ENTRIES
|
||||
entryValues = PREF_QUALITY_ENTRY_VALUES
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
@ -348,11 +346,11 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_GROUP_KEY
|
||||
title = PREF_GROUP_TITLE
|
||||
title = "Preferred group"
|
||||
entries = MarinMoeConstants.GROUP_ENTRIES
|
||||
entryValues = MarinMoeConstants.GROUP_ENTRY_VALUES
|
||||
setDefaultValue(PREF_GROUP_DEFAULT)
|
||||
@ -364,11 +362,11 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SUBS_KEY
|
||||
title = PREF_SUBS_TITLE
|
||||
title = "Prefer subs or dubs?"
|
||||
entries = PREF_SUBS_ENTRIES
|
||||
entryValues = PREF_SUBS_ENTRY_VALUES
|
||||
setDefaultValue(PREF_SUBS_DEFAULT)
|
||||
@ -380,17 +378,17 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_SPECIAL_KEY
|
||||
title = PREF_SPECIAL_TITLE
|
||||
title = "Include Special Episodes"
|
||||
setDefaultValue(PREF_SPECIAL_DEFAULT)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val new = newValue as Boolean
|
||||
preferences.edit().putBoolean(key, new).commit()
|
||||
}
|
||||
}.let { screen.addPreference(it) }
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'Myanime'
|
||||
pkgNameSuffix = 'en.myanime'
|
||||
extClass = '.Myanime'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -21,8 +21,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@ -31,9 +29,6 @@ import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
@ -47,58 +42,40 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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 {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("d MMMM yyyy", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
|
||||
override fun popularAnimeRequest(page: Int): Request {
|
||||
return GET("$baseUrl/category/donghua-list/page/$page/")
|
||||
}
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/category/donghua-list/page/$page/")
|
||||
|
||||
override fun popularAnimeSelector(): String = "main#main > article.post"
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "script:containsData(infiniteScroll)"
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
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 popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("h2.entry-header-title > a")!!.attr("href"))
|
||||
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 ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/page/$page/")
|
||||
|
||||
override fun latestUpdatesSelector(): String = popularAnimeSelector()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
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()
|
||||
.substringBefore(" Episode")
|
||||
.substringBefore(" episode")
|
||||
}
|
||||
override fun latestUpdatesFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("h2.entry-header-title > a")!!.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
|
||||
title = element.selectFirst("h2.entry-header-title > a")!!.text()
|
||||
.substringBefore(" Episode")
|
||||
.substringBefore(" episode")
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
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 searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
@ -141,9 +118,7 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||
return Observable.just(anime)
|
||||
}
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = Observable.just(anime)
|
||||
|
||||
override fun animeDetailsParse(document: Document): SAnime = throw Exception("Not used")
|
||||
|
||||
@ -164,7 +139,7 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
SEpisode.create().apply {
|
||||
name = a.text()
|
||||
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 {
|
||||
val a = it.selectFirst("h2.entry-header-title > a")!!
|
||||
val episode = SEpisode.create()
|
||||
|
||||
episode.name = a.text()
|
||||
episode.episode_number = a.text().substringAfter("pisode ").substringBefore(" ").toFloatOrNull() ?: 0F
|
||||
episode.setUrlWithoutDomain(a.attr("href").toHttpUrl().encodedPath)
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = a.text()
|
||||
episode_number = a.text().substringAfter("pisode ").substringBefore(" ").toFloatOrNull() ?: 0F
|
||||
setUrlWithoutDomain(a.attr("href"))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
infiniteScroll = epDocument.selectFirst("script:containsData(infiniteScroll)") != null
|
||||
page++
|
||||
}
|
||||
} else if (document.selectFirst("iframe.youtube-player[src]") != null) {
|
||||
val episode = SEpisode.create()
|
||||
|
||||
episode.name = document.selectFirst("title")!!.text()
|
||||
episode.episode_number = 0F
|
||||
episode.setUrlWithoutDomain(response.request.url.encodedPath)
|
||||
episodeList.add(episode)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = document.selectFirst("title")!!.text()
|
||||
episode_number = 0F
|
||||
setUrlWithoutDomain(response.request.url.toString())
|
||||
},
|
||||
)
|
||||
} else if (document.selectFirst("span > a[href*=/tag/]") != null) {
|
||||
val url = document.selectFirst("span > a[href*=/tag/]")!!.attr("href")
|
||||
episodeList.addAll(
|
||||
@ -247,20 +224,22 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}.filterNotNull().flatten(),
|
||||
)
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return videoList
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
|
||||
|
||||
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")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
override fun List<Video>.sort(): List<Video> {
|
||||
val quality = preferences.getString("preferred_quality", "1080")!!
|
||||
val server = preferences.getString("preferred_server", "dailymotion")!!
|
||||
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
|
||||
val server = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_DEFAULT)!!
|
||||
|
||||
return this.sortedWith(
|
||||
compareBy(
|
||||
@ -276,13 +255,23 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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) {
|
||||
val videoQualityPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_quality"
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_QUALITY_KEY
|
||||
title = "Preferred quality"
|
||||
entries = arrayOf("1080p", "720p", "480p", "360p")
|
||||
entryValues = arrayOf("1080", "720", "480", "360")
|
||||
setDefaultValue("1080")
|
||||
setDefaultValue(PREF_QUALITY_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@ -291,13 +280,14 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
val videoServerPref = ListPreference(screen.context).apply {
|
||||
key = "preferred_server"
|
||||
}.also(screen::addPreference)
|
||||
|
||||
ListPreference(screen.context).apply {
|
||||
key = PREF_SERVER_KEY
|
||||
title = "Preferred server"
|
||||
entries = arrayOf("YouTube", "Dailymotion", "ok.ru")
|
||||
entryValues = arrayOf("youtube", "dailymotion", "okru")
|
||||
setDefaultValue("dailymotion")
|
||||
setDefaultValue(PREF_SERVER_DEFAULT)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
@ -306,8 +296,6 @@ class Myanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString(key, entry).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(videoQualityPref)
|
||||
screen.addPreference(videoServerPref)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ ext {
|
||||
extName = 'NollyVerse'
|
||||
pkgNameSuffix = 'en.nollyverse'
|
||||
extClass = '.NollyVerse'
|
||||
extVersionCode = 1
|
||||
extVersionCode = 2
|
||||
libVersion = '13'
|
||||
}
|
||||
|
||||
|
@ -42,21 +42,9 @@ class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
// Popular Anime
|
||||
// ============================== Popular ===============================
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
|
||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/category/trending-movies/page/$page/")
|
||||
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
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 popularAnimeRequest(page: Int): Request {
|
||||
return GET("$baseUrl/category/trending-movies/page/$page/")
|
||||
override fun popularAnimeFromElement(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")
|
||||
setUrlWithoutDomain(element.select("a.post-img").attr("href"))
|
||||
}
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
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
|
||||
}
|
||||
override fun popularAnimeNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
|
||||
|
||||
// Episodes
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun episodeListRequest(anime: SAnime): Request {
|
||||
return if (anime.url.startsWith("/movie/")) {
|
||||
GET(baseUrl + anime.url + "/download/", headers)
|
||||
} else {
|
||||
GET(baseUrl + anime.url + "/seasons/", headers)
|
||||
}
|
||||
}
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/category/new-series/page/$page/")
|
||||
|
||||
override fun episodeListSelector() = throw Exception("not used")
|
||||
|
||||
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>()
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val fragment = response.request.url.fragment!!
|
||||
if (fragment == "movie") {
|
||||
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()
|
||||
videoList.add(Video(url, name, url))
|
||||
val animes = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||
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 {
|
||||
val episodeIndex = fragment.toInt() - 1
|
||||
|
||||
val episodeList = document.select("table.table.table-striped tbody tr").toList()
|
||||
|
||||
for (res in episodeList[episodeIndex].select("td").reversed()) {
|
||||
val url = res.select("a").attr("href")
|
||||
if (url.isNotEmpty()) {
|
||||
videoList.add(
|
||||
Video(url, res.text().trim(), url),
|
||||
)
|
||||
var searchPath = ""
|
||||
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"
|
||||
is SeriesGenreFilter -> searchPath = "/series/genre/${filter.toUriPart()}/page/$page"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
val document = response.asJsoup()
|
||||
val path = response.request.url.encodedPath
|
||||
@ -317,141 +221,250 @@ class NollyVerse : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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 movieGenreFromElement(element: Element): SAnime {
|
||||
return latestUpdatesFromElement(element)
|
||||
}
|
||||
private fun movieGenreFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
|
||||
|
||||
private fun seriesGenreSelector(): String = "div.row div.col-md-8 div.col-md-6"
|
||||
|
||||
private fun seriesGenreFromElement(element: Element): SAnime {
|
||||
return latestUpdatesFromElement(element)
|
||||
}
|
||||
private fun seriesGenreFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
|
||||
|
||||
private fun koreanSelector(): String = "div.col-md-8 div.row div.col-md-6"
|
||||
|
||||
private fun koreanFromElement(element: Element): SAnime {
|
||||
return latestUpdatesFromElement(element)
|
||||
}
|
||||
private fun koreanFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
|
||||
|
||||
private fun latestSelector(): String = latestUpdatesSelector()
|
||||
|
||||
private fun latestFromElement(element: Element): SAnime {
|
||||
return latestUpdatesFromElement(element)
|
||||
}
|
||||
private fun latestFromElement(element: Element): SAnime = latestUpdatesFromElement(element)
|
||||
|
||||
private fun seriesSelector(): String = "div.section-row ul.list-style li"
|
||||
|
||||
private fun seriesFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = element.select("a").text()
|
||||
anime.thumbnail_url = toImgUrl(element.select("a").attr("href"))
|
||||
anime.setUrlWithoutDomain(element.select("a").attr("href").toHttpUrl().encodedPath)
|
||||
return anime
|
||||
private fun seriesFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
title = element.select("a").text()
|
||||
thumbnail_url = toImgUrl(element.select("a").attr("href"))
|
||||
setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
}
|
||||
|
||||
private fun movieSelector(): String = "div.container div.row div.col-md-12 div.col-md-4"
|
||||
|
||||
private fun movieFromElement(element: Element): SAnime {
|
||||
val anime = SAnime.create()
|
||||
anime.title = element.select("h3 a").text()
|
||||
anime.thumbnail_url = element.select("a img").attr("src")
|
||||
anime.setUrlWithoutDomain(element.select("a.post-img").attr("href").toHttpUrl().encodedPath)
|
||||
return anime
|
||||
private fun movieFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
title = element.select("h3 a").text()
|
||||
thumbnail_url = element.select("a img").attr("src")
|
||||
setUrlWithoutDomain(element.select("a.post-img").attr("href"))
|
||||
}
|
||||
|
||||
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 searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
if (query.isNotBlank()) {
|
||||
val body = FormBody.Builder()
|
||||
.add("name", "$query")
|
||||
.build()
|
||||
return POST("$baseUrl/livesearch.php", body = body)
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
title = element.text()
|
||||
thumbnail_url = toImgUrl(element.attr("href"))
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
|
||||
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 {
|
||||
var searchPath = ""
|
||||
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"
|
||||
is SeriesGenreFilter -> searchPath = "/series/genre/${filter.toUriPart()}/page/$page"
|
||||
else -> ""
|
||||
GET(baseUrl + anime.url + "/seasons/", headers)
|
||||
}
|
||||
}
|
||||
|
||||
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/")) {
|
||||
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 GET(baseUrl + searchPath)
|
||||
}
|
||||
|
||||
return episodeList.reversed()
|
||||
}
|
||||
|
||||
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 {
|
||||
val anime = SAnime.create()
|
||||
anime.title = document.select("div.page-header div.container div.row div.text-center h1").text()
|
||||
anime.description = document.select("blockquote.blockquote small").text()
|
||||
document.select("div.col-md-8 ul.list-style li").forEach {
|
||||
if (it.text().startsWith("Genre: ")) {
|
||||
anime.genre = it.text().substringAfter("Genre: ").replace(",", ", ")
|
||||
}
|
||||
// ============================ Video Links =============================
|
||||
|
||||
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)
|
||||
}
|
||||
return anime
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String = "div.loadmore ul.pagination.pagination-md li:nth-last-child(2)"
|
||||
|
||||
override fun latestUpdatesParse(response: Response): AnimesPage {
|
||||
override fun videoListParse(response: Response): List<Video> {
|
||||
val videoList = mutableListOf<Video>()
|
||||
val document = response.asJsoup()
|
||||
|
||||
val animes = document.select(latestUpdatesSelector()).map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
|
||||
if (document.select(selector).text() != ">") {
|
||||
return AnimesPage(animes, false)
|
||||
val fragment = response.request.url.fragment!!
|
||||
if (fragment == "movie") {
|
||||
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()
|
||||
videoList.add(Video(url, name, url))
|
||||
}
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
} else {
|
||||
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 {
|
||||
val anime = SAnime.create()
|
||||
|
||||
anime.setUrlWithoutDomain(element.select("a.post-img").attr("href").toHttpUrl().encodedPath)
|
||||
anime.title = element.select("div.post-body h3 a").text()
|
||||
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")
|
||||
for (res in episodeList[episodeIndex].select("td").reversed()) {
|
||||
val url = res.select("a").attr("href")
|
||||
if (url.isNotEmpty()) {
|
||||
videoList.add(
|
||||
Video(url, res.text().trim(), url),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return anime
|
||||
|
||||
require(videoList.isNotEmpty()) { "Failed to fetch videos" }
|
||||
|
||||
return videoList.sort()
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/category/new-series/page/$page/")
|
||||
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 ==============================
|
||||
|
||||
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(
|
||||
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()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -49,17 +49,17 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
override fun popularAnimeParse(response: Response): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val badNames = arrayOf("../", "gifs/")
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
|
||||
document.select(popularAnimeSelector()).forEach {
|
||||
val animeList = document.select(popularAnimeSelector()).mapNotNull {
|
||||
val a = it.selectFirst("a")!!
|
||||
val name = a.text()
|
||||
if (name in badNames) return@forEach
|
||||
if (name in badNames) return@mapNotNull null
|
||||
|
||||
val anime = SAnime.create()
|
||||
anime.title = name.removeSuffix("/")
|
||||
anime.setUrlWithoutDomain(a.attr("href"))
|
||||
animeList.add(anime)
|
||||
SAnime.create().apply {
|
||||
title = name.removeSuffix("/")
|
||||
setUrlWithoutDomain(a.attr("href"))
|
||||
thumbnail_url = ""
|
||||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, false)
|
||||
@ -67,20 +67,20 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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 popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = 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 latestUpdatesNextPageSelector(): String = throw Exception("Not used")
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun fetchSearchAnime(
|
||||
@ -107,17 +107,17 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
private fun searchAnimeParse(response: Response, query: String): AnimesPage {
|
||||
val document = response.asJsoup()
|
||||
val badNames = arrayOf("../", "gifs/")
|
||||
val animeList = mutableListOf<SAnime>()
|
||||
|
||||
document.select(popularAnimeSelector()).forEach {
|
||||
val animeList = document.select(popularAnimeSelector()).mapNotNull {
|
||||
val name = it.text()
|
||||
if (name in badNames || !name.contains(query, ignoreCase = true)) return@forEach
|
||||
if (it.selectFirst("span.size")?.text()?.contains(" KiB") == 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@mapNotNull null
|
||||
|
||||
val anime = SAnime.create()
|
||||
anime.title = name.removeSuffix("/")
|
||||
anime.setUrlWithoutDomain(it.selectFirst("a")!!.attr("href"))
|
||||
animeList.add(anime)
|
||||
SAnime.create().apply {
|
||||
title = name.removeSuffix("/")
|
||||
setUrlWithoutDomain(it.selectFirst("a")!!.attr("href"))
|
||||
thumbnail_url = ""
|
||||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animeList, false)
|
||||
@ -125,15 +125,13 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
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 searchAnimeNextPageSelector(): String = throw Exception("Not used")
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||
return Observable.just(anime)
|
||||
}
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = Observable.just(anime)
|
||||
|
||||
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 text = link.selectFirst("a")!!.text()
|
||||
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 != "..") {
|
||||
val fullUrl = baseUrl + href
|
||||
@ -158,7 +156,6 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
traverseDirectory(fullUrl)
|
||||
}
|
||||
if (videoFormats.any { t -> fullUrl.endsWith(t) }) {
|
||||
val episode = SEpisode.create()
|
||||
val paths = fullUrl.toHttpUrl().pathSegments
|
||||
|
||||
val seasonInfoRegex = """(\([\s\w-]+\))(?: ?\[[\s\w-]+\])?${'$'}""".toRegex()
|
||||
@ -181,13 +178,15 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
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"}"
|
||||
episode.url = fullUrl
|
||||
episode.scanlator = seasonInfo + extraInfo
|
||||
episode.episode_number = counter.toFloat()
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = "${season}${videoFormats.fold(paths.last()) { acc, suffix -> acc.removeSuffix(suffix).trimInfo() }}"
|
||||
this.url = fullUrl
|
||||
scanlator = "${if (size == null) "" else "$size • "}$seasonInfo$extraInfo"
|
||||
episode_number = counter.toFloat()
|
||||
},
|
||||
)
|
||||
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 episodeListSelector(): String = throw Exception("Not Used")
|
||||
|
||||
override fun episodeFromElement(element: Element): SEpisode = throw Exception("Not used")
|
||||
|
||||
override fun episodeListSelector(): String = throw Exception("Not Used")
|
||||
|
||||
// ============================ Video Links =============================
|
||||
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||
return Observable.just(listOf(Video(episode.url, "Video", episode.url)))
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = throw Exception("Not Used")
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> =
|
||||
Observable.just(listOf(Video(episode.url, "Video", episode.url)))
|
||||
|
||||
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")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
@ -231,15 +229,21 @@ class NoobSubs : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
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) {
|
||||
val ignoreExtras = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "ignore_extras"
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = PREF_IGNORE_EXTRA_KEY
|
||||
title = "Ignore \"Extras\" folder"
|
||||
setDefaultValue(true)
|
||||
setDefaultValue(PREF_IGNORE_EXTRA_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
preferences.edit().putBoolean(key, newValue as Boolean).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(ignoreExtras)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -32,8 +31,6 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.security.MessageDigest
|
||||
import java.text.CharacterIterator
|
||||
import java.text.StringCharacterIterator
|
||||
|
||||
class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
@ -49,8 +46,6 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
private val maxRecursionDepth = 2
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
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 popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
||||
return SAnime.create().apply {
|
||||
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 popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
|
||||
thumbnail_url = element.selectFirst("img[src]")?.attr("src") ?: ""
|
||||
title = element.selectFirst("a:matches(.)")!!.text().substringBefore(" | Episode").trimEnd()
|
||||
}
|
||||
|
||||
override fun popularAnimeNextPageSelector(): String? = null
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = 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 latestUpdatesNextPageSelector(): String = throw Exception("Not Used")
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||
@ -115,20 +108,20 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
} else {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val animes = document.select(searchAnimeSelector()).map { element ->
|
||||
val animeList = document.select(searchAnimeSelector()).map { element ->
|
||||
popularAnimeFromElement(element)
|
||||
}
|
||||
|
||||
return AnimesPage(animes, animes.size == 40)
|
||||
return AnimesPage(animeList, animeList.size == 40)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchAnimeSelector(): String = "div#infinite-list"
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
|
||||
|
||||
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
|
||||
|
||||
override fun searchAnimeNextPageSelector(): String? = popularAnimeNextPageSelector()
|
||||
|
||||
// ============================== Filters ===============================
|
||||
|
||||
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
|
||||
@ -179,24 +172,11 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|
||||
// =========================== Anime Details ============================
|
||||
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<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()
|
||||
override fun animeDetailsParse(document: Document): SAnime {
|
||||
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" } ?: ""
|
||||
|
||||
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 {
|
||||
parseStatus(it.text().substringAfter("Status: "))
|
||||
} ?: SAnime.UNKNOWN
|
||||
@ -217,13 +197,8 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
val document = response.asJsoup()
|
||||
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) {
|
||||
if (recursionDepth == maxRecursionDepth) return
|
||||
if (recursionDepth == MAX_RECURSION_DEPTH) return
|
||||
|
||||
val folderId = url.substringAfter("/folders/")
|
||||
val driveHeaders = headers.newBuilder()
|
||||
@ -239,14 +214,14 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
if (driveDocument.selectFirst("title:contains(Error 404 \\(Not found\\))") != null) return
|
||||
|
||||
val keyScript = driveDocument.select("script").first { script ->
|
||||
keyRegex.find(script.data()) != null
|
||||
KEY_REGEX.find(script.data()) != null
|
||||
}.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 ->
|
||||
keyRegex.find(script.data()) != null
|
||||
KEY_REGEX.find(script.data()) != null
|
||||
}.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 {
|
||||
it.name == "SAPISID" || it.name == "__Secure-3PAPISID"
|
||||
}?.value ?: ""
|
||||
@ -254,7 +229,7 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
var pageToken: String? = ""
|
||||
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 body = """--$boundary
|
||||
val body = """--$BOUNDARY
|
||||
|content-type: application/http
|
||||
|content-transfer-encoding: binary
|
||||
|
|
||||
@ -263,12 +238,12 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
|authorization: ${generateSapisidhashHeader(sapisid)}
|
||||
|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()
|
||||
.addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$boundary\"")
|
||||
.addQueryParameter("${'$'}ct", "multipart/mixed;boundary=\"$BOUNDARY\"")
|
||||
.addQueryParameter("key", key)
|
||||
.build()
|
||||
.toString()
|
||||
@ -283,34 +258,23 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
POST(postUrl, body = body, headers = postHeaders),
|
||||
).execute()
|
||||
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")
|
||||
parsed.items.forEachIndexed { index, it ->
|
||||
if (it.mimeType.startsWith("video")) {
|
||||
val episode = SEpisode.create()
|
||||
val size = formatBytes(it.fileSize?.toLongOrNull())
|
||||
val pathName = if (preferences.getBoolean("trim_info", false)) {
|
||||
path.trimInfo()
|
||||
} else {
|
||||
path
|
||||
}
|
||||
val pathName = path.trimInfo()
|
||||
|
||||
val itemNumberRegex = """ - (?:S\d+E)?(\d+)""".toRegex()
|
||||
episode.scanlator = if (preferences.getBoolean("scanlator_order", false)) {
|
||||
"/$pathName • $size"
|
||||
} else {
|
||||
"$size • /$pathName"
|
||||
}
|
||||
episode.name = if (preferences.getBoolean("trim_episode", false)) {
|
||||
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)
|
||||
episodeList.add(
|
||||
SEpisode.create().apply {
|
||||
name = if (preferences.trimEpisodeName) it.title.trimInfo() else it.title
|
||||
this.url = "https://drive.google.com/uc?id=${it.id}"
|
||||
episode_number = ITEM_NUMBER_REGEX.find(it.title.trimInfo())?.groupValues?.get(1)?.toFloatOrNull() ?: index.toFloat()
|
||||
date_upload = -1L
|
||||
scanlator = "$size • /$pathName"
|
||||
},
|
||||
)
|
||||
}
|
||||
if (it.mimeType.endsWith(".folder")) {
|
||||
traverseFolder(
|
||||
@ -358,10 +322,10 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
return Observable.just(videoList)
|
||||
}
|
||||
|
||||
override fun videoFromElement(element: Element): Video = 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")
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
@ -405,21 +369,14 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
|
||||
private fun formatBytes(bytes: Long?): String? {
|
||||
if (bytes == null) return null
|
||||
val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes)
|
||||
if (absB < 1024) {
|
||||
return "$bytes B"
|
||||
val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB")
|
||||
var value = bytes?.toDouble() ?: return null
|
||||
var i = 0
|
||||
while (value >= 1024 && i < units.size - 1) {
|
||||
value /= 1024
|
||||
i++
|
||||
}
|
||||
var value = absB
|
||||
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())
|
||||
return String.format("%.1f %s", value, units[i])
|
||||
}
|
||||
|
||||
private fun getCookie(url: String): String {
|
||||
@ -439,34 +396,32 @@ class Ripcrabbyanime : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val scanlatorOrder = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = "scanlator_order"
|
||||
title = "Switch order of file path and size"
|
||||
setDefaultValue(false)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
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()
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
private val ITEM_NUMBER_REGEX = """ - (?:S\d+E)?(\d+)""".toRegex()
|
||||
private val KEY_REGEX = """"(\w{39})"""".toRegex()
|
||||
private val VERSION_REGEX = """"([^"]+web-frontend[^"]+)"""".toRegex()
|
||||
private val JSON_REGEX = """(?:)\s*(\{(.+)\})\s*(?:)""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
private const val BOUNDARY = "=====vc17a3rwnndj====="
|
||||
|
||||
screen.addPreference(scanlatorOrder)
|
||||
screen.addPreference(trimEpisodeName)
|
||||
screen.addPreference(trimEpisodeInfo)
|
||||
private const val MAX_RECURSION_DEPTH = 2
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user