krysanify 2025-04-17 11:03:24 +08:00 committed by GitHub
parent fa67b1d427
commit 974df97f50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 0 additions and 879 deletions

View file

@ -1,13 +0,0 @@
ext {
extName = 'FMovies'
extClass = '.FMovies'
extVersionCode = 26
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib:vidsrc-extractor'))
implementation(project(':lib:filemoon-extractor'))
implementation(project(':lib:streamtape-extractor'))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1,368 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.vidsrcextractor.VidsrcExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
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 FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "FMovies"
override val baseUrl = "https://fmovies24.to"
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 utils by lazy { FmoviesUtils(client, headers) }
// ============================== Popular ===============================
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending${page.toPageQuery()}", headers)
override fun popularAnimeSelector(): String = "div.items > div.item"
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
element.selectFirst("div.meta a")!!.let { a ->
title = a.text()
setUrlWithoutDomain(a.attr("abs:href"))
}
thumbnail_url = element.select("div.poster img").attr("data-src")
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/filter?keyword=&sort=recently_updated${page.toPageQuery(false)}", headers)
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 params = FMoviesFilters.getSearchParameters(filters)
return GET("$baseUrl/filter?keyword=$query${params.filter}${page.toPageQuery(false)}", headers)
}
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun searchAnimeFromElement(element: Element): SAnime = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
// ============================== Filters ===============================
override fun getFilterList(): AnimeFilterList = FMoviesFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val info = document.selectFirst("section#w-info > div.info")!!
val detail = info.selectFirst("div.detail")
val descElement = info.selectFirst("div.description")
val desc = descElement?.selectFirst("div[data-name=full]")?.ownText() ?: descElement?.ownText() ?: ""
val extraInfo = detail?.select("> div")?.joinToString("\n") { it.text() } ?: ""
val mediaTitle = info.selectFirst("h1.name")!!.text()
val mediaDetail = utils.getDetail(mediaTitle)
return SAnime.create().apply {
title = mediaTitle
status = when (mediaDetail?.status) {
"Ended", "Released" -> SAnime.COMPLETED
"In Production" -> SAnime.LICENSED
"Canceled" -> SAnime.CANCELLED
"Returning Series" -> {
mediaDetail.nextEpisode?.let { SAnime.ONGOING } ?: SAnime.ON_HIATUS
}
else -> SAnime.UNKNOWN
}
thumbnail_url = document.selectFirst("section#w-info > div.poster img")!!.attr("src")
description = buildString {
appendLine(desc.ifBlank { mediaDetail?.overview })
appendLine()
mediaDetail?.nextEpisode?.let {
appendLine("Next: Ep ${it.epNumber} - ${it.name}")
appendLine("Air Date: ${it.airDate}")
appendLine()
}
appendLine(extraInfo)
}
genre = detail?.let { dtl ->
dtl.select("> div:has(> div:contains(Genre:)) span").joinToString { it.text() }
}
author = detail?.let { dtl ->
dtl.select("> div:has(> div:contains(Production:)) span").joinToString { it.text() }
}
}
}
// ============================== Episodes ==============================
override fun episodeListRequest(anime: SAnime): Request {
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
.selectFirst("div[data-id]")!!.attr("data-id")
val vrf = utils.vrfEncrypt(id)
val vrfHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", baseUrl.toHttpUrl().host)
add("Referer", baseUrl + anime.url)
add("X-Requested-With", "XMLHttpRequest")
}.build()
return GET("$baseUrl/ajax/episode/list/$id?vrf=$vrf", headers = vrfHeaders)
}
override fun episodeListParse(response: Response): List<SEpisode> {
val document = Jsoup.parse(
response.parseAs<AjaxResponse>().result,
)
val episodeList = mutableListOf<SEpisode>()
val seasons = document.select("div.body > ul.episodes")
seasons.forEach { season ->
val seasonPrefix = if (seasons.size > 1) {
"Season ${season.attr("data-season")} "
} else {
""
}
season.select("li").forEach { ep ->
episodeList.add(
SEpisode.create().apply {
name = "$seasonPrefix${ep.text().trim()}".replace("Episode ", "Ep. ")
ep.selectFirst("a")!!.let { a ->
episode_number = a.attr("data-num").toFloatOrNull() ?: 0F
url = json.encodeToString(
EpisodeInfo(
id = a.attr("data-id"),
url = "$baseUrl${a.attr("href")}",
),
)
}
},
)
}
}
return episodeList.reversed()
}
override fun episodeListSelector() = throw UnsupportedOperationException()
override fun episodeFromElement(element: Element): SEpisode = throw UnsupportedOperationException()
// ============================ Video Links =============================
override suspend fun getVideoList(episode: SEpisode): List<Video> {
return client.newCall(videoListRequest(episode))
.awaitSuccess()
.let { response ->
videoListParse(response, episode).sort()
}
}
override fun videoListRequest(episode: SEpisode): Request {
val data = json.decodeFromString<EpisodeInfo>(episode.url)
val vrf = utils.vrfEncrypt(data.id)
val vrfHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", data.url)
.add("X-Requested-With", "XMLHttpRequest")
.build()
return GET("$baseUrl/ajax/server/list/${data.id}?vrf=$vrf", headers = vrfHeaders)
}
private val vidsrcExtractor by lazy { VidsrcExtractor(client, headers) }
private val filemoonExtractor by lazy { FilemoonExtractor(client) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private suspend fun videoListParse(response: Response, episode: SEpisode): List<Video> {
val data = json.decodeFromString<EpisodeInfo>(episode.url)
val document = Jsoup.parse(
response.parseAs<AjaxResponse>().result,
)
val hosterSelection = preferences.getStringSet(PREF_HOSTER_KEY, PREF_HOSTER_DEFAULT)!!
return document.select("ul.servers > li.server").parallelCatchingFlatMap { server ->
val name = server.text().trim()
if (!hosterSelection.contains(name)) return@parallelCatchingFlatMap emptyList()
// Get decrypted url
val vrf = utils.vrfEncrypt(server.attr("data-link-id"))
val vrfHeaders = headers.newBuilder()
.add("Accept", "application/json, text/javascript, */*; q=0.01")
.add("Host", baseUrl.toHttpUrl().host)
.add("Referer", data.url)
.add("X-Requested-With", "XMLHttpRequest")
.build()
val encrypted = client.newCall(
GET("$baseUrl/ajax/server/${server.attr("data-link-id")}?vrf=$vrf", headers = vrfHeaders),
).await().parseAs<AjaxServerResponse>().result.url
val decrypted = utils.vrfDecrypt(encrypted)
when (name) {
"Vidplay", "MyCloud" -> {
val subs = client.newCall(
GET("$baseUrl/ajax/episode/subtitles/${data.id}"),
).execute().toTracks()
vidsrcExtractor.videosFromUrl(decrypted, name, subtitleList = subs)
}
"Filemoon" -> filemoonExtractor.videosFromUrl(decrypted, headers = headers)
"Streamtape" -> {
val subtitleList = decrypted.toHttpUrl().queryParameter("sub.info")?.let {
client.newCall(GET(it, headers)).await().toTracks()
} ?: emptyList()
streamtapeExtractor.videoFromUrl(decrypted, subtitleList = subtitleList)?.let(::listOf) ?: emptyList()
}
else -> emptyList()
}
}
}
override fun videoListSelector() = throw UnsupportedOperationException()
override fun videoFromElement(element: Element) = throw UnsupportedOperationException()
override fun videoUrlParse(document: Document) = throw UnsupportedOperationException()
// ============================= Utilities ==============================
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_DEFAULT)!!
return this.sortedWith(
compareBy(
{ it.quality.contains(server) },
{ it.quality.contains(quality) },
{ Regex("""(\d+)p""").find(it.quality)?.groupValues?.get(1)?.toIntOrNull() ?: 0 },
),
).reversed()
}
private fun Int.toPageQuery(first: Boolean = true): String {
return if (this == 1) "" else "${if (first) "?" else "&"}page=$this"
}
private fun Response.toTracks(): List<Track> = parseAs<List<FMoviesSubs>>()
.map { t ->
Track(t.file, t.label)
}
companion object {
private val HOSTERS = arrayOf(
"Vidplay",
"MyCloud",
"Filemoon",
"Streamtape",
)
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 = "Vidplay"
private const val PREF_HOSTER_KEY = "hoster_selection"
private val PREF_HOSTER_DEFAULT = setOf("Vidplay", "Filemoon")
}
// ============================== 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)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = HOSTERS
entryValues = HOSTERS
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 = HOSTERS
entryValues = HOSTERS
setDefaultValue(PREF_HOSTER_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference)
}
}

View file

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import kotlinx.serialization.Serializable
@Serializable
data class AjaxResponse(
val result: String,
)
@Serializable
data class AjaxServerResponse(
val result: UrlObject,
) {
@Serializable
data class UrlObject(
val url: String,
)
}
@Serializable
data class EpisodeInfo(
val id: String,
val url: String,
)
@Serializable
data class FMoviesSubs(
val file: String,
val label: String,
)
@Serializable
data class MediaResponseBody(
val status: Int,
val result: Result,
) {
@Serializable
data class Result(
val sources: ArrayList<Source>,
val tracks: ArrayList<SubTrack> = ArrayList(),
) {
@Serializable
data class Source(
val file: String,
)
@Serializable
data class SubTrack(
val file: String,
val label: String = "",
val kind: String,
)
}
}

View file

@ -1,304 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object FMoviesFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart(name: String) = "&$name=${vals[state].second}"
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
open class TriStateFilterList(name: String, values: List<TriFilter>) : AnimeFilter.Group<AnimeFilter.TriState>(name, values)
class TriFilter(name: String, val value: String) : AnimeFilter.TriState(name)
private inline fun <reified R> AnimeFilterList.asQueryPart(name: String): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart(name)
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name[]=").let {
if (it.isBlank()) {
""
} else {
"&$name[]=$it"
}
}
}
private inline fun <reified R> AnimeFilterList.parseTriFilter(
options: Array<Pair<String, String>>,
name: String,
): String {
return (this.getFirst<R>() as TriStateFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state != AnimeFilter.TriState.STATE_IGNORE) {
(if (checkbox.state == AnimeFilter.TriState.STATE_EXCLUDE) "-" else "") + options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("&$name[]=").let {
if (it.isBlank()) {
""
} else {
"&$name[]=$it"
}
}
}
class TypesFilter : CheckBoxFilterList(
"Type",
FMoviesFiltersData.TYPES.map { CheckBoxVal(it.first, false) },
)
class GenresFilter : TriStateFilterList(
"Genre",
FMoviesFiltersData.GENRES.map { TriFilter(it.first, it.second) },
)
class CountriesFilter : CheckBoxFilterList(
"Country",
FMoviesFiltersData.COUNTRIES.map { CheckBoxVal(it.first, false) },
)
class YearsFilter : CheckBoxFilterList(
"Year",
FMoviesFiltersData.YEARS.map { CheckBoxVal(it.first, false) },
)
class RatingsFilter : CheckBoxFilterList(
"Rating",
FMoviesFiltersData.RATINGS.map { CheckBoxVal(it.first, false) },
)
class QualitiesFilter : CheckBoxFilterList(
"Quality",
FMoviesFiltersData.QUALITIES.map { CheckBoxVal(it.first, false) },
)
class SortFilter : QueryPartFilter("Sort", FMoviesFiltersData.SORT)
val FILTER_LIST get() = AnimeFilterList(
TypesFilter(),
GenresFilter(),
CountriesFilter(),
YearsFilter(),
RatingsFilter(),
QualitiesFilter(),
SortFilter(),
)
data class FilterSearchParams(
val filter: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<TypesFilter>(FMoviesFiltersData.TYPES, "type") +
filters.parseTriFilter<GenresFilter>(FMoviesFiltersData.GENRES, "genre") +
filters.parseCheckbox<CountriesFilter>(FMoviesFiltersData.COUNTRIES, "country") +
filters.parseCheckbox<YearsFilter>(FMoviesFiltersData.YEARS, "year") +
filters.parseCheckbox<RatingsFilter>(FMoviesFiltersData.RATINGS, "rating") +
filters.parseCheckbox<QualitiesFilter>(FMoviesFiltersData.QUALITIES, "quality") +
filters.asQueryPart<SortFilter>("sort"),
)
}
private object FMoviesFiltersData {
val TYPES = arrayOf(
Pair("Movie", "movie"),
Pair("TV-Shows", "tv"),
)
val GENRES = arrayOf(
Pair("Action", "25"),
Pair("Adult", "1068691"),
Pair("Adventure", "17"),
Pair("Animation", "10"),
Pair("Biography", "215"),
Pair("Comedy", "14"),
Pair("Costume", "1693"),
Pair("Crime", "26"),
Pair("Documentary", "131"),
Pair("Drama", "1"),
Pair("Family", "43"),
Pair("Fantasy", "31"),
Pair("Film-Noir", "1068395"),
Pair("Game-Show", "212"),
Pair("History", "47"),
Pair("Horror", "74"),
Pair("Kungfu", "248"),
Pair("Music", "199"),
Pair("Musical", "1066604"),
Pair("Mystery", "64"),
Pair("News", "1066549"),
Pair("Reality", "1123750"),
Pair("Reality-TV", "4"),
Pair("Romance", "23"),
Pair("Sci-Fi", "15"),
Pair("Short", "1066916"),
Pair("Sport", "44"),
Pair("Talk", "1124002"),
Pair("Talk-Show", "1067786"),
Pair("Thriller", "7"),
Pair("TV Movie", "1123752"),
Pair("TV Show", "139"),
Pair("War", "58"),
Pair("Western", "28"),
)
val COUNTRIES = arrayOf(
Pair("Argentina", "181863"),
Pair("Australia", "181851"),
Pair("Austria", "181882"),
Pair("Belgium", "181849"),
Pair("Brazil", "181867"),
Pair("Canada", "181861"),
Pair("China", "108"),
Pair("Czech Republic", "181859"),
Pair("Denmark", "181855"),
Pair("Finland", "181877"),
Pair("France", "11"),
Pair("Germany", "1025332"),
Pair("Hong Kong", "2630"),
Pair("Hungary", "181876"),
Pair("India", "34"),
Pair("Ireland", "181862"),
Pair("Israel", "181887"),
Pair("Italy", "181857"),
Pair("Japan", "36"),
Pair("Luxembourg", "181878"),
Pair("Mexico", "181852"),
Pair("Netherlands", "181848"),
Pair("New Zealand", "181847"),
Pair("Norway", "181901"),
Pair("Philippines", "1025339"),
Pair("Poland", "181880"),
Pair("Romania", "181895"),
Pair("Russia", "181860"),
Pair("South Africa", "181850"),
Pair("South Korea", "1025429"),
Pair("Spain", "181871"),
Pair("Sweden", "181883"),
Pair("Switzerland", "181869"),
Pair("Thailand", "94"),
Pair("Turkey", "1025379"),
Pair("United Kingdom", "8"),
Pair("United States", "2"),
)
val YEARS = arrayOf(
Pair("2024", "2024"),
Pair("2023", "2023"),
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("2017", "2017"),
Pair("2016", "2016"),
Pair("2015", "2015"),
Pair("2014", "2014"),
Pair("2013", "2013"),
Pair("2012", "2012"),
Pair("2011", "2011"),
Pair("2010", "2010"),
Pair("2009", "2009"),
Pair("2008", "2008"),
Pair("2007", "2007"),
Pair("2006", "2006"),
Pair("2005", "2005"),
Pair("2004", "2004"),
Pair("2003", "2003"),
Pair("2000s", "2000s"),
Pair("1990s", "1990s"),
Pair("1980s", "1980s"),
Pair("1970s", "1970s"),
Pair("1960s", "1960s"),
Pair("1950s", "1950s"),
Pair("1940s", "1940s"),
Pair("1930s", "1930s"),
Pair("1920s", "1920s"),
Pair("1910s", "1910s"),
)
val RATINGS = arrayOf(
Pair("12", "12"),
Pair("13+", "13+"),
Pair("16+", "16+"),
Pair("18", "18"),
Pair("18+", "18+"),
Pair("AO", "AO"),
Pair("C", "C"),
Pair("E", "E"),
Pair("G", "G"),
Pair("GP", "GP"),
Pair("M", "M"),
Pair("M/PG", "M/PG"),
Pair("MA-13", "MA-13"),
Pair("MA-17", "MA-17"),
Pair("NC-17", "NC-17"),
Pair("PG", "PG"),
Pair("PG-13", "PG-13"),
Pair("R", "R"),
Pair("TV_MA", "TV_MA"),
Pair("TV-13", "TV-13"),
Pair("TV-14", "TV-14"),
Pair("TV-G", "TV-G"),
Pair("TV-MA", "TV-MA"),
Pair("TV-PG", "TV-PG"),
Pair("TV-Y", "TV-Y"),
Pair("TV-Y7", "TV-Y7"),
Pair("TV-Y7-FV", "TV-Y7-FV"),
Pair("X", "X"),
)
val QUALITIES = arrayOf(
Pair("HD", "HD"),
Pair("HDRip", "HDRip"),
Pair("SD", "SD"),
Pair("TS", "TS"),
Pair("CAM", "CAM"),
)
val SORT = arrayOf(
Pair("Most relevance", "most_relevance"),
Pair("Recently updated", "recently_updated"),
Pair("Recently added", "recently_added"),
Pair("Release date", "release_date"),
Pair("Trending", "trending"),
Pair("Name A-Z", "title_az"),
Pair("Scores", "scores"),
Pair("IMDb", "imdb"),
Pair("Most watched", "most_watched"),
Pair("Most favourited", "most_favourited"),
)
}
}

View file

@ -1,140 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.fmovies
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import java.net.URLDecoder
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
class FmoviesUtils(private val client: OkHttpClient, private val headers: Headers) {
// ===================== Media Detail ================================
private val tmdbURL = "https://api.themoviedb.org/3".toHttpUrl()
private val seez = "https://seez.su"
private val apiKey by lazy {
val jsUrl = client.newCall(GET(seez, headers)).execute().asJsoup()
.select("script[defer][src]")[1].attr("abs:src")
val jsBody = client.newCall(GET(jsUrl, headers)).execute().use { it.body.string() }
Regex("""f="(\w{20,})"""").find(jsBody)!!.groupValues[1]
}
private val apiHeaders = headers.newBuilder().apply {
add("Accept", "application/json, text/javascript, */*; q=0.01")
add("Host", "api.themoviedb.org")
add("Origin", seez)
add("Referer", "$seez/")
}.build()
fun getDetail(mediaTitle: String): TmdbDetailsResponse? =
runCatching {
val searchUrl = tmdbURL.newBuilder().apply {
addPathSegment("search")
addPathSegment("multi")
addQueryParameter("query", mediaTitle)
addQueryParameter("api_key", apiKey)
}.build().toString()
val searchResp = client.newCall(GET(searchUrl, headers = apiHeaders))
.execute()
.parseAs<TmdbResponse>()
val media = searchResp.results.first()
val detailUrl = tmdbURL.newBuilder().apply {
addPathSegment(media.mediaType)
addPathSegment(media.id.toString())
addQueryParameter("api_key", apiKey)
}.build().toString()
client.newCall(GET(detailUrl, headers = apiHeaders))
.execute()
.parseAs<TmdbDetailsResponse>()
}.getOrNull()
// ===================== Encryption ================================
fun vrfEncrypt(input: String): String {
val rc4Key = SecretKeySpec("Ij4aiaQXgluXQRs6".toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.ENCRYPT_MODE, rc4Key, cipher.parameters)
var vrf = cipher.doFinal(input.toByteArray())
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
// vrf = rot13(vrf)
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
vrf.reverse()
vrf = Base64.encode(vrf, Base64.URL_SAFE or Base64.NO_WRAP)
vrf = vrfShift(vrf)
val stringVrf = vrf.toString(Charsets.UTF_8)
return java.net.URLEncoder.encode(stringVrf, "utf-8")
}
fun vrfDecrypt(input: String): String {
var vrf = input.toByteArray()
vrf = Base64.decode(vrf, Base64.URL_SAFE)
val rc4Key = SecretKeySpec("8z5Ag5wgagfsOuhz".toByteArray(), "RC4")
val cipher = Cipher.getInstance("RC4")
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
vrf = cipher.doFinal(vrf)
return URLDecoder.decode(vrf.toString(Charsets.UTF_8), "utf-8")
}
private fun rot13(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val byte = vrf[i]
if (byte in 'A'.code..'Z'.code) {
vrf[i] = ((byte - 'A'.code + 13) % 26 + 'A'.code).toByte()
} else if (byte in 'a'.code..'z'.code) {
vrf[i] = ((byte - 'a'.code + 13) % 26 + 'a'.code).toByte()
}
}
return vrf
}
private fun vrfShift(vrf: ByteArray): ByteArray {
for (i in vrf.indices) {
val shift = arrayOf(4, 3, -2, 5, 2, -4, -4, 2)[i % 8]
vrf[i] = vrf[i].plus(shift).toByte()
}
return vrf
}
}
@Serializable
data class TmdbResponse(
val results: List<TmdbResult>,
) {
@Serializable
data class TmdbResult(
val id: Int,
@SerialName("media_type")
val mediaType: String = "tv",
)
}
@Serializable
data class TmdbDetailsResponse(
val status: String,
val overview: String? = null,
@SerialName("next_episode_to_air")
val nextEpisode: NextEpisode? = null,
) {
@Serializable
data class NextEpisode(
val name: String? = "",
@SerialName("episode_number")
val epNumber: Int,
@SerialName("air_date")
val airDate: String,
)
}