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

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

View File

@ -26,6 +26,10 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.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())!!
}

View File

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

View File

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

View File

@ -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 {

View File

@ -12,7 +12,7 @@ class Mp4uploadExtractor(private val client: OkHttpClient) {
val packed = body.substringAfter("<script type='text/javascript'>eval(function(p,a,c,k,e,d)")
.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/")),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,9 +13,7 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.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) {
""

View File

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

View File

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

View File

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

View File

@ -25,8 +25,6 @@ import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.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) { }
}

View File

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

View File

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

View File

@ -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 ===============================

View File

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.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) {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -113,21 +113,23 @@ class MarinMoe : ConfigurableAnimeSource, AnimeHttpSource() {
val dataPage = document.select("div#app").attr("data-page").replace("&quot;", "\"")
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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