en/animeflix dead (#660)

Co-authored-by: Your Name <you@example.com>
This commit is contained in:
krysanify 2025-02-13 03:58:15 +08:00 committed by GitHub
parent 26da2ef585
commit 259281f542
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 0 additions and 1124 deletions

View file

@ -1,7 +0,0 @@
ext {
extName = 'AnimeFlix'
extClass = '.AnimeFlix'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,398 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeflix
import android.app.Application
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MultipartBody
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class AnimeFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "AnimeFlix"
override val baseUrl = "https://animeflix.mobi"
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/page/$page/")
override fun popularAnimeSelector() = "div#content_box > div.post-cards > article"
override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
// prevent base64 images
thumbnail_url = element.selectFirst("img")!!.run {
attr("data-pagespeed-high-res-src").ifEmpty { attr("src") }
}
title = element.selectFirst("header")!!.text()
}
override fun popularAnimeNextPageSelector() = "div.nav-links > a.next"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-release/page/$page/")
override fun latestUpdatesSelector(): String = popularAnimeSelector()
override fun latestUpdatesFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularAnimeNextPageSelector()
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val cleanQuery = query.replace(" ", "+").lowercase()
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
val subpageFilter = filterList.find { it is SubPageFilter } as SubPageFilter
return when {
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$cleanQuery", headers = headers)
genreFilter.state != 0 -> GET("$baseUrl/genre/${genreFilter.toUriPart()}/page/$page/", headers = headers)
subpageFilter.state != 0 -> GET("$baseUrl/${subpageFilter.toUriPart()}/page/$page/", headers = headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("Text search ignores filters"),
GenreFilter(),
SubPageFilter(),
)
private class GenreFilter : UriPartFilter(
"Genres",
arrayOf(
Pair("<select>", ""),
Pair("Action", "action"),
Pair("Adventure", "adventure"),
Pair("Isekai", "isekai"),
Pair("Drama", "drama"),
Pair("Psychological", "psychological"),
Pair("Ecchi", "ecchi"),
Pair("Sci-Fi", "sci-fi"),
Pair("Magic", "magic"),
Pair("Slice Of Life", "slice-of-life"),
Pair("Sports", "sports"),
Pair("Comedy", "comedy"),
Pair("Fantasy", "fantasy"),
Pair("Horror", "horror"),
Pair("Yaoi", "yaoi"),
),
)
private class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Ongoing", "ongoing"),
Pair("Latest Release", "latest-release"),
Pair("Movies", "movies"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
AnimeFilter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
title = document.selectFirst("div.single_post > header > h1")!!.text()
thumbnail_url = document.selectFirst("img.imdbwp__img")?.attr("src")
val infosDiv = document.selectFirst("div.thecontent h3:contains(Anime Info) ~ ul")!!
status = when (infosDiv.getInfo("Status").toString()) {
"Completed" -> SAnime.COMPLETED
"Currently Airing" -> SAnime.ONGOING
else -> SAnime.UNKNOWN
}
artist = infosDiv.getInfo("Studios")
author = infosDiv.getInfo("Producers")
genre = infosDiv.getInfo("Genres")
val animeInfo = infosDiv.select("li").joinToString("\n") { it.text() }
description = document.select("div.thecontent h3:contains(Summary) ~ p:not(:has(*)):not(:empty)")
.joinToString("\n\n") { it.ownText() } + "\n\n$animeInfo"
}
private fun Element.getInfo(info: String) =
selectFirst("li:contains($info)")?.ownText()?.trim()
// ============================== Episodes ==============================
val seasonRegex by lazy { Regex("""season (\d+)""", RegexOption.IGNORE_CASE) }
val qualityRegex by lazy { """(\d+)p""".toRegex() }
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
val document = client.newCall(GET(baseUrl + anime.url)).execute()
.asJsoup()
val seasonList = document.select("div.inline > h3:contains(Season),div.thecontent > h3:contains(Season)")
val episodeList = if (seasonList.distinctBy { seasonRegex.find(it.text())!!.groupValues[1] }.size > 1) {
val seasonsLinks = document.select("div.thecontent p:has(span:contains(Gdrive))").groupBy {
seasonRegex.find(it.previousElementSibling()!!.text())!!.groupValues[1]
}
seasonsLinks.flatMap { (seasonNumber, season) ->
val serverListSeason = season.map {
val previousText = it.previousElementSibling()!!.text()
val quality = qualityRegex.find(previousText)?.groupValues?.get(1) ?: "Unknown quality"
val url = it.selectFirst("a")!!.attr("href")
val episodesDocument = client.newCall(GET(url)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(quality, it.attr("href"), "Season $seasonNumber ${it.text()}")
}
}
transposeEpisodes(serverListSeason)
}
} else {
val driveList = document.select("div.thecontent p:has(span:contains(Gdrive))").map {
val quality = qualityRegex.find(it.previousElementSibling()!!.text())?.groupValues?.get(1) ?: "Unknown quality"
Pair(it.selectFirst("a")!!.attr("href"), quality)
}
// Load episodes
val serversList = driveList.map { drive ->
val episodesDocument = client.newCall(GET(drive.first)).execute()
.asJsoup()
episodesDocument.select("div.entry-content > h3 > a").map {
EpUrl(drive.second, it.attr("href"), it.text())
}
}
transposeEpisodes(serversList)
}
return episodeList.reversed()
}
private fun transposeEpisodes(serversList: List<List<EpUrl>>) =
transpose(serversList).mapIndexed { index, serverList ->
SEpisode.create().apply {
name = serverList.first().name
episode_number = (index + 1).toFloat()
setUrlWithoutDomain(json.encodeToString(serverList))
}
}
override fun episodeListSelector(): String = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
val urls = json.decodeFromString<List<EpUrl>>(episode.url)
val leechUrls = urls.map {
val firstLeech = client.newCall(GET(it.url)).execute()
.asJsoup()
.selectFirst("script:containsData(downlaod_button)")!!
.data()
.substringAfter("<a href=\"")
.substringBefore("\">")
val path = client.newCall(GET(firstLeech)).execute()
.body.string()
.substringAfter("replace(\"")
.substringBefore("\"")
val link = "https://" + firstLeech.toHttpUrl().host + path
EpUrl(it.quality, link, it.name)
}
val videoList = leechUrls.parallelCatchingFlatMap { url ->
if (url.url.toHttpUrl().encodedPath == "/404") return@parallelCatchingFlatMap emptyList()
val (videos, mediaUrl) = extractVideo(url)
when {
videos.isEmpty() -> {
extractGDriveLink(mediaUrl, url.quality).ifEmpty {
getDirectLink(mediaUrl, "instant", "/mfile/")?.let {
listOf(Video(it, "${url.quality}p - GDrive Instant link", it))
} ?: emptyList()
}
}
else -> videos
}
}
return videoList.sort()
}
override fun videoFromElement(element: Element): Video = throw UnsupportedOperationException()
override fun videoListSelector(): String = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document): String = throw UnsupportedOperationException()
// ============================= Utilities ==============================
// https://github.com/aniyomiorg/aniyomi-extensions/blob/master/src/en/uhdmovies/src/eu/kanade/tachiyomi/animeextension/en/uhdmovies/UHDMovies.kt
private fun extractVideo(epUrl: EpUrl): Pair<List<Video>, String> {
val matchResult = qualityRegex.find(epUrl.name)
val quality = matchResult?.groupValues?.get(1) ?: epUrl.quality
return (1..3).toList().flatMap { type ->
extractWorkerLinks(epUrl.url, quality, type)
}.let { Pair(it, epUrl.url) }
}
private fun extractWorkerLinks(mediaUrl: String, quality: String, type: Int): List<Video> {
val reqLink = mediaUrl.replace("/file/", "/wfile/") + "?type=$type"
val resp = client.newCall(GET(reqLink)).execute().asJsoup()
val sizeMatch = SIZE_REGEX.find(resp.select("div.card-header").text().trim())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
return resp.select("div.card-body div.mb-4 > a").mapIndexed { index, linkElement ->
val link = linkElement.attr("href")
val decodedLink = if (link.contains("workers.dev")) {
link
} else {
String(Base64.decode(link.substringAfter("download?url="), Base64.DEFAULT))
}
Video(
url = decodedLink,
quality = "${quality}p - CF $type Worker ${index + 1}$size",
videoUrl = decodedLink,
)
}
}
private fun getDirectLink(url: String, action: String = "direct", newPath: String = "/file/"): String? {
val doc = client.newCall(GET(url, headers)).execute().asJsoup()
val script = doc.selectFirst("script:containsData(async function taskaction)")
?.data()
?: return url
val key = script.substringAfter("key\", \"").substringBefore('"')
val form = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("action", action)
.addFormDataPart("key", key)
.addFormDataPart("action_token", "")
.build()
val headers = headersBuilder().set("x-token", url.toHttpUrl().host).build()
val req = client.newCall(POST(url.replace("/file/", newPath), headers, form)).execute()
return runCatching {
json.decodeFromString<DriveLeechDirect>(req.body.string()).url
}.getOrNull()
}
private fun extractGDriveLink(mediaUrl: String, quality: String): List<Video> {
val neoUrl = getDirectLink(mediaUrl) ?: mediaUrl
val response = client.newCall(GET(neoUrl)).execute().asJsoup()
val gdBtn = response.selectFirst("div.card-body a.btn")!!
val gdLink = gdBtn.attr("href")
val sizeMatch = SIZE_REGEX.find(gdBtn.text())
val size = sizeMatch?.groups?.get(1)?.value?.let { " - $it" } ?: ""
val gdResponse = client.newCall(GET(gdLink)).execute().asJsoup()
val link = gdResponse.select("form#download-form")
return if (link.isNullOrEmpty()) {
emptyList()
} else {
val realLink = link.attr("action")
listOf(Video(realLink, "$quality - Gdrive$size", realLink))
}
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun <E> transpose(xs: List<List<E>>): List<List<E>> {
// Helpers
fun <E> List<E>.head(): E = this.first()
fun <E> List<E>.tail(): List<E> = this.takeLast(this.size - 1)
fun <E> E.append(xs: List<E>): List<E> = listOf(this).plus(xs)
xs.filter { it.isNotEmpty() }.let { ys ->
return when (ys.isNotEmpty()) {
true -> ys.map { it.head() }.append(transpose(ys.map { it.tail() }))
else -> emptyList()
}
}
}
@Serializable
data class EpUrl(
val quality: String,
val url: String,
val name: String,
)
@Serializable
data class DriveLeechDirect(val url: String? = null)
companion object {
private val SIZE_REGEX = "\\[((?:.(?!\\[))+)][ ]*\$".toRegex(RegexOption.IGNORE_CASE)
private const val PREF_QUALITY_KEY = "pref_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRIES = arrayOf("1080p", "720p", "480p", "360p")
private val PREF_QUALITY_VALUES = arrayOf("1080", "720", "480", "360")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
}

View file

@ -1,12 +0,0 @@
ext {
extName = 'Animeflix.live'
extClass = '.AnimeflixLive'
extVersionCode = 7
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:gogostream-extractor'))
implementation(project(':lib:playlist-utils'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,503 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import GenreFilter
import SortFilter
import SubPageFilter
import TypeFilter
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URLDecoder
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import kotlin.math.min
class AnimeflixLive : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "Animeflix.live"
override val baseUrl by lazy { preferences.baseUrl }
private val apiUrl by lazy { preferences.apiUrl }
override val lang = "en"
override val supportsLatest = true
private val json: Json by injectLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val apiHeaders = headersBuilder().apply {
add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("Referer", "$baseUrl/")
}.build()
private val docHeaders = headersBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
add("Host", apiUrl.toHttpUrl().host)
add("Referer", "$baseUrl/")
}.build()
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request =
GET("$apiUrl/popular?page=${page - 1}", apiHeaders)
override fun popularAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$apiUrl/trending?page=${page - 1}", apiHeaders)
override fun latestUpdatesParse(response: Response): AnimesPage {
val parsed = response.parseAs<TrendingDto>()
val titlePref = preferences.titleType
val animeList = parsed.trending.map {
it.toSAnime(titlePref)
}
return AnimesPage(animeList, animeList.size == PAGE_SIZE)
}
// =============================== Search ===============================
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val sort = filters.filterIsInstance<SortFilter>().first().getValue()
val type = filters.filterIsInstance<TypeFilter>().first().getValues()
val genre = filters.filterIsInstance<GenreFilter>().first().getValues()
val subPage = filters.filterIsInstance<SubPageFilter>().first().getValue()
if (subPage.isNotBlank()) {
return GET("$apiUrl/$subPage?page=${page - 1}", apiHeaders)
}
if (query.isEmpty()) {
throw Exception("Search must not be empty")
}
val filtersObj = buildJsonObject {
put("sort", sort)
if (type.isNotEmpty()) {
put("type", json.encodeToString(type))
}
if (genre.isNotEmpty()) {
put("genre", json.encodeToString(genre))
}
}.toJsonString()
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("info")
addPathSegment("")
addQueryParameter("query", query)
addQueryParameter("limit", "15")
addQueryParameter("filters", filtersObj)
addQueryParameter("k", query.substr(0, 3).sk())
}.build()
return GET(url, apiHeaders)
}
override fun searchAnimeParse(response: Response): AnimesPage {
val parsed = response.parseAs<List<AnimeDto>>()
val titlePref = preferences.titleType
val animeList = parsed.map {
it.toSAnime(titlePref)
}
val hasNextPage = if (response.request.url.queryParameter("limit") == null) {
animeList.size == 44
} else {
animeList.size == 15
}
return AnimesPage(animeList, hasNextPage)
}
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
SortFilter(),
TypeFilter(),
GenreFilter(),
AnimeFilter.Separator(),
AnimeFilter.Header("NOTE: Subpage overrides search and other filters!"),
SubPageFilter(),
)
// =========================== Anime Details ============================
override fun animeDetailsRequest(anime: SAnime): Request {
return GET("$apiUrl/getslug/${anime.url}", apiHeaders)
}
override fun getAnimeUrl(anime: SAnime): String {
return "$baseUrl/search/${anime.title}?anime=${anime.url}"
}
override fun animeDetailsParse(response: Response): SAnime {
val titlePref = preferences.titleType
return response.parseAs<DetailsDto>().toSAnime(titlePref)
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val lang = preferences.lang
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("episodes")
addQueryParameter("id", anime.url)
addQueryParameter("dub", (lang == "Dub").toString())
addQueryParameter("c", anime.url.sk())
}.build()
return GET(url, apiHeaders)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val slug = response.request.url.queryParameter("id")!!
return response.parseAs<EpisodeResponseDto>().episodes.map {
it.toSEpisode(slug)
}.sortedByDescending { it.episode_number }
}
// ============================ Video Links =============================
override fun videoListRequest(episode: SEpisode): Request {
val url = "$apiUrl${episode.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("server", "")
addQueryParameter("c", episode.url.substringAfter("/watch/").sk())
}.build()
return GET(url, apiHeaders)
}
override fun videoListParse(response: Response): List<Video> {
val videoList = mutableListOf<Video>()
val initialPlayerUrl = apiUrl + response.parseAs<ServerDto>().source
val initialServer = initialPlayerUrl.toHttpUrl().queryParameter("server")!!
val initialPlayerDocument = client.newCall(
GET(initialPlayerUrl, docHeaders),
).execute().asJsoup().unescape()
videoList.addAll(
videosFromPlayer(
initialPlayerDocument,
initialServer.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
),
)
// Go through rest of servers
val servers = initialPlayerDocument.selectFirst("script:containsData(server-settings)")!!.data()
val serversHtml = SERVER_REGEX.findAll(servers).map {
Jsoup.parseBodyFragment(it.groupValues[1])
}.toList()
videoList.addAll(
serversHtml.parallelCatchingFlatMapBlocking {
val server = serverMapping[
it.selectFirst("button")!!
.attr("onclick")
.substringAfter("postMessage('")
.substringBefore("'"),
]
if (server == initialServer) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val serverUrl = response.request.url.newBuilder()
.setQueryParameter("server", server)
.build()
val playerUrl = apiUrl + client.newCall(
GET(serverUrl, apiHeaders),
).execute().parseAs<ServerDto>().source
if (server != playerUrl.toHttpUrl().queryParameter("server")!!) {
return@parallelCatchingFlatMapBlocking emptyList()
}
val playerDocument = client.newCall(
GET(playerUrl, docHeaders),
).execute().asJsoup().unescape()
videosFromPlayer(
playerDocument,
server.replaceFirstChar { c -> c.titlecase(Locale.ROOT) },
)
},
)
return videoList
}
private val serverMapping = mapOf(
"settings-0" to "moon",
"settings-1" to "sun",
"settings-2" to "zoro",
"settings-3" to "gogo",
)
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
private fun getVideoHeaders(baseHeaders: Headers, referer: String, videoUrl: String): Headers {
return baseHeaders.newBuilder().apply {
add("Accept", "*/*")
add("Accept-Language", "en-US,en;q=0.5")
add("Host", videoUrl.toHttpUrl().host)
add("Origin", "https://${apiUrl.toHttpUrl().host}")
add("Referer", "$apiUrl/")
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "cross-site")
}.build()
}
private fun Document.unescape(): Document {
val unescapeScript = this.selectFirst("script:containsData(unescape)")
return if (unescapeScript == null) {
this
} else {
val data = URLDecoder.decode(unescapeScript.data(), "UTF-8")
Jsoup.parse(data, this.location())
}
}
private fun videosFromPlayer(document: Document, name: String): List<Video> {
val dataScript = document.selectFirst("script:containsData(m3u8)")
?.data() ?: return emptyList()
val subtitleList = document.select("video > track[kind=captions]").map {
Track(it.attr("id"), it.attr("label"))
}
var masterPlaylist = M3U8_REGEX.find(dataScript)?.groupValues?.get(1)
?: return emptyList()
if (name.equals("moon", true)) {
masterPlaylist += dataScript.substringAfter("`${'$'}{url}")
.substringBefore("`")
}
return playlistUtils.extractFromHls(
masterPlaylist,
videoHeadersGen = ::getVideoHeaders,
videoNameGen = { q -> "$name - $q" },
subtitleList = subtitleList,
)
}
// ============================= Utilities ==============================
override fun List<Video>.sort(): List<Video> {
val quality = preferences.quality
val server = preferences.server
return this.sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(server, true) },
),
).reversed()
}
private fun JsonObject.toJsonString(): String {
return json.encodeToString(this)
}
private fun String.sk(): String {
val t = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
val n = 17 + (t.get(Calendar.DAY_OF_MONTH) - t.get(Calendar.MONTH)) / 2
return this.toCharArray().fold("") { acc, c ->
acc + c.code.toString(n).padStart(2, '0')
}
}
private fun String.substr(start: Int, end: Int): String {
val stop = min(end, this.length)
return this.substring(start, stop)
}
companion object {
private val SERVER_REGEX = Regex("""'1' === '1'.*?(<button.*?</button>)""", RegexOption.DOT_MATCHES_ALL)
private val M3U8_REGEX = Regex("""const ?\w*? ?= ?`(.*?)`""")
private const val PAGE_SIZE = 24
private const val PREF_DOMAIN_KEY = "pref_domain_key"
private const val PREF_DOMAIN_DEFAULT = "https://animeflix.live,https://api.animeflix.dev"
private val PREF_DOMAIN_ENTRIES = arrayOf("animeflix.live", "animeflix.ro")
private val PREF_DOMAIN_ENTRY_VALUES = arrayOf(
"https://animeflix.live,https://api.animeflix.dev",
"https://animeflix.ro,https://api.animeflixtv.to",
)
private const val PREF_TITLE_KEY = "pref_title_type_key"
private const val PREF_TITLE_DEFAULT = "English"
private val PREF_TITLE_ENTRIES = arrayOf("English", "Native", "Romaji")
private const val PREF_LANG_KEY = "pref_lang_key"
private const val PREF_LANG_DEFAULT = "Sub"
private val PREF_LANG_ENTRIES = arrayOf("Sub", "Dub")
private const val PREF_QUALITY_KEY = "pref_quality_key"
private const val PREF_QUALITY_DEFAULT = "1080"
private val PREF_QUALITY_ENTRY_VALUES = arrayOf("1080", "720", "480", "360")
private val PREF_QUALITY_ENTRIES = PREF_QUALITY_ENTRY_VALUES.map { "${it}p" }.toTypedArray()
private const val PREF_SERVER_KEY = "pref_server_key"
private const val PREF_SERVER_DEFAULT = "Moon"
private val PREF_SERVER_ENTRIES = arrayOf("Moon", "Sun", "Zoro", "Gogo")
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = "Preferred domain (requires app restart)"
entries = PREF_DOMAIN_ENTRIES
entryValues = PREF_DOMAIN_ENTRY_VALUES
setDefaultValue(PREF_DOMAIN_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_TITLE_KEY
title = "Preferred Title Type"
entries = PREF_TITLE_ENTRIES
entryValues = PREF_TITLE_ENTRIES
setDefaultValue(PREF_TITLE_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_LANG_KEY
title = "Preferred Language"
entries = PREF_LANG_ENTRIES
entryValues = PREF_LANG_ENTRIES
setDefaultValue(PREF_LANG_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = "Preferred quality"
entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = PREF_SERVER_ENTRIES
entryValues = PREF_SERVER_ENTRIES
setDefaultValue(PREF_SERVER_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}.also(screen::addPreference)
}
private val SharedPreferences.baseUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").first()
private val SharedPreferences.apiUrl
get() = getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!!
.split(",").last()
private val SharedPreferences.titleType
get() = getString(PREF_TITLE_KEY, PREF_TITLE_DEFAULT)!!
private val SharedPreferences.lang
get() = getString(PREF_LANG_KEY, PREF_LANG_DEFAULT)!!
private val SharedPreferences.quality
get() = getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
private val SharedPreferences.server
get() = getString(PREF_SERVER_KEY, PREF_QUALITY_DEFAULT)!!
}

View file

@ -1,123 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.animeflixlive
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import kotlin.math.ceil
import kotlin.math.floor
@Serializable
class TrendingDto(
val trending: List<AnimeDto>,
)
@Serializable
class AnimeDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
}
}
@Serializable
class DetailsDto(
val slug: String,
@SerialName("title") val titleObj: TitleObject,
val description: String,
val genres: List<String>,
val status: String? = null,
val images: ImageObject,
) {
@Serializable
class TitleObject(
val english: String? = null,
val native: String? = null,
val romaji: String? = null,
)
@Serializable
class ImageObject(
val large: String? = null,
val medium: String? = null,
val small: String? = null,
)
fun toSAnime(titlePref: String): SAnime = SAnime.create().apply {
title = when (titlePref) {
"English" -> titleObj.english ?: titleObj.romaji ?: titleObj.native ?: "Title N/A"
"Romaji" -> titleObj.romaji ?: titleObj.english ?: titleObj.native ?: "Title N/A"
else -> titleObj.native ?: titleObj.romaji ?: titleObj.english ?: "Title N/A"
}
thumbnail_url = images.large ?: images.medium ?: images.small ?: ""
url = slug
genre = genres.joinToString()
status = this@DetailsDto.status.parseStatus()
description = Jsoup.parseBodyFragment(
this@DetailsDto.description.replace("<br>", "br2n"),
).text().replace("br2n", "\n")
}
private fun String?.parseStatus(): Int = when (this?.lowercase()) {
"releasing" -> SAnime.ONGOING
"finished" -> SAnime.COMPLETED
"cancelled" -> SAnime.CANCELLED
else -> SAnime.UNKNOWN
}
}
@Serializable
class EpisodeResponseDto(
val episodes: List<EpisodeDto>,
) {
@Serializable
class EpisodeDto(
val number: Float,
val title: String? = null,
) {
fun toSEpisode(slug: String): SEpisode = SEpisode.create().apply {
val epNum = if (floor(number) == ceil(number)) {
number.toInt().toString()
} else {
number.toString()
}
url = "/watch/$slug-episode-$epNum"
episode_number = number
name = if (title == null) {
"Episode $epNum"
} else {
"Ep. $epNum - $title"
}
}
}
}
@Serializable
class ServerDto(
val source: String,
)

View file

@ -1,81 +0,0 @@
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
open class UriPartFilter(
name: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : AnimeFilter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
) {
fun getValue(): String {
return vals[state].second
}
}
open class UriMultiSelectOption(name: String, val value: String) : AnimeFilter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val vals: Array<Pair<String, String>>,
) : AnimeFilter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }) {
fun getValues(): List<String> {
return state.filter { it.state }.map { it.value }
}
}
class SortFilter : UriPartFilter(
"Sort",
arrayOf(
Pair("Recently Updated", "recently_updated"),
Pair("Recently Added", "recently_added"),
Pair("Release Date ↓", "release_date_down"),
Pair("Release Date ↑", "release_date_up"),
Pair("Name A-Z", "title_az"),
Pair("Best Rating", "scores"),
Pair("Most Watched", "most_watched"),
Pair("Anime Length", "number_of_episodes"),
),
)
class TypeFilter : UriMultiSelectFilter(
"Type",
arrayOf(
Pair("TV", "TV"),
Pair("Movie", "MOVIE"),
Pair("OVA", "OVA"),
Pair("ONA", "ONA"),
Pair("Special", "SPECIAL"),
),
)
class GenreFilter : UriMultiSelectFilter(
"Genre",
arrayOf(
Pair("Action", "Action"),
Pair("Adventure", "Adventure"),
Pair("Comedy", "Comedy"),
Pair("Drama", "Drama"),
Pair("Ecchi", "Ecchi"),
Pair("Fantasy", "Fantasy"),
Pair("Horror", "Horror"),
Pair("Mecha", "Mecha"),
Pair("Mystery", "Mystery"),
Pair("Psychological", "Psychological"),
Pair("Romance", "Romance"),
Pair("Sci-Fi", "Sci-Fi"),
Pair("Sports", "Sports"),
Pair("Supernatural", "Supernatural"),
Pair("Thriller", "Thriller"),
),
)
class SubPageFilter : UriPartFilter(
"Sub-page",
arrayOf(
Pair("<select>", ""),
Pair("Movies", "movies"),
Pair("Series", "series"),
),
)