fix(en/fmovies): Update enimax's vrf helper & add subtitle support for streamtape (#2227)
This commit is contained in:
parent
53bd7d1ec3
commit
1b51325344
@ -1,12 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.lib.streamtapeextractor
|
package eu.kanade.tachiyomi.lib.streamtapeextractor
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
||||||
class StreamTapeExtractor(private val client: OkHttpClient) {
|
class StreamTapeExtractor(private val client: OkHttpClient) {
|
||||||
fun videoFromUrl(url: String, quality: String = "StreamTape"): Video? {
|
fun videoFromUrl(url: String, quality: String = "StreamTape", subtitleList: List<Track> = emptyList()): Video? {
|
||||||
val baseUrl = "https://streamtape.com/e/"
|
val baseUrl = "https://streamtape.com/e/"
|
||||||
val newUrl = if (!url.startsWith(baseUrl)) {
|
val newUrl = if (!url.startsWith(baseUrl)) {
|
||||||
// ["https", "", "<domain>", "<???>", "<id>", ...]
|
// ["https", "", "<domain>", "<???>", "<id>", ...]
|
||||||
@ -21,6 +22,6 @@ class StreamTapeExtractor(private val client: OkHttpClient) {
|
|||||||
?: return null
|
?: return null
|
||||||
val videoUrl = "https:" + script.substringBefore("'") +
|
val videoUrl = "https:" + script.substringBefore("'") +
|
||||||
script.substringAfter("+ ('xcd").substringBefore("'")
|
script.substringAfter("+ ('xcd").substringBefore("'")
|
||||||
return Video(videoUrl, quality, videoUrl)
|
return Video(videoUrl, quality, videoUrl, subtitleTracks = subtitleList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
apply plugin: 'com.android.application'
|
plugins {
|
||||||
apply plugin: 'kotlin-android'
|
alias(libs.plugins.android.application)
|
||||||
apply plugin: 'kotlinx-serialization'
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
extName = 'FMovies'
|
extName = 'FMovies'
|
||||||
pkgNameSuffix = 'en.fmovies'
|
pkgNameSuffix = 'en.fmovies'
|
||||||
extClass = '.FMovies'
|
extClass = '.FMovies'
|
||||||
extVersionCode = 5
|
extVersionCode = 6
|
||||||
libVersion = '13'
|
libVersion = '13'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(':lib-filemoon-extractor'))
|
implementation(project(':lib-filemoon-extractor'))
|
||||||
implementation "dev.datlag.jsunpacker:jsunpacker:1.0.1"
|
implementation(project(':lib-streamtape-extractor'))
|
||||||
|
implementation(project(':lib-playlist-utils'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,16 +5,16 @@ import android.content.SharedPreferences
|
|||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.MultiSelectListPreference
|
import androidx.preference.MultiSelectListPreference
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.animeextension.en.fmovies.extractors.StreamtapeExtractor
|
import eu.kanade.tachiyomi.animeextension.en.fmovies.extractors.VidsrcExtractor
|
||||||
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
|
||||||
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
|
||||||
|
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
@ -24,8 +24,6 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@ -42,7 +40,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
override val name = "FMovies"
|
override val name = "FMovies"
|
||||||
|
|
||||||
override val baseUrl by lazy { preferences.getString(PREF_DOMAIN_KEY, PREF_DOMAIN_DEFAULT)!! }
|
override val baseUrl = "https://fmoviesz.to"
|
||||||
|
|
||||||
override val lang = "en"
|
override val lang = "en"
|
||||||
|
|
||||||
@ -56,20 +54,21 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val vrfHelper by lazy { FMoviesHelper(client, headers) }
|
||||||
|
|
||||||
// ============================== Popular ===============================
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending${page.toPageQuery()}", headers)
|
override fun popularAnimeRequest(page: Int): Request = GET("$baseUrl/trending${page.toPageQuery()}", headers)
|
||||||
|
|
||||||
override fun popularAnimeSelector(): String = "div.items > div.item"
|
override fun popularAnimeSelector(): String = "div.items > div.item"
|
||||||
|
|
||||||
override fun popularAnimeFromElement(element: Element): SAnime {
|
override fun popularAnimeFromElement(element: Element): SAnime = SAnime.create().apply {
|
||||||
val a = element.selectFirst("div.meta a")!!
|
element.selectFirst("div.meta a")!!.let { a ->
|
||||||
|
|
||||||
return SAnime.create().apply {
|
|
||||||
setUrlWithoutDomain(a.attr("abs:href"))
|
|
||||||
thumbnail_url = element.select("div.poster img").attr("data-src")
|
|
||||||
title = a.text()
|
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"
|
override fun popularAnimeNextPageSelector(): String = "ul.pagination > li.active + li"
|
||||||
@ -87,19 +86,11 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
// =============================== Search ===============================
|
// =============================== Search ===============================
|
||||||
|
|
||||||
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("Not used")
|
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
|
||||||
|
|
||||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
|
||||||
val params = FMoviesFilters.getSearchParameters(filters)
|
val params = FMoviesFilters.getSearchParameters(filters)
|
||||||
return client.newCall(searchAnimeRequest(page, query, params))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
searchAnimeParse(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchAnimeRequest(page: Int, query: String, filters: FMoviesFilters.FilterSearchParams): Request =
|
return GET("$baseUrl/filter?keyword=$query${params.filter}${page.toPageQuery(false)}", headers)
|
||||||
GET("$baseUrl/filter?keyword=$query${filters.filter}${page.toPageQuery(false)}", headers)
|
}
|
||||||
|
|
||||||
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
override fun searchAnimeSelector(): String = popularAnimeSelector()
|
||||||
|
|
||||||
@ -139,16 +130,17 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
override fun episodeListRequest(anime: SAnime): Request {
|
override fun episodeListRequest(anime: SAnime): Request {
|
||||||
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
val id = client.newCall(GET(baseUrl + anime.url)).execute().asJsoup()
|
||||||
.selectFirst("div[data-id]")!!.attr("data-id")
|
.selectFirst("div[data-id]")!!.attr("data-id")
|
||||||
val vrf = callConsumet(id, "fmovies-vrf")
|
|
||||||
|
|
||||||
val vrfHeaders = headers.newBuilder()
|
val vrf = vrfHelper.getVrf(id)
|
||||||
.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", headers = vrfHeaders)
|
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> {
|
override fun episodeListParse(response: Response): List<SEpisode> {
|
||||||
@ -165,10 +157,11 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
season.select("li").forEach { ep ->
|
season.select("li").forEach { ep ->
|
||||||
val a = ep.selectFirst("a")!!
|
|
||||||
episodeList.add(
|
episodeList.add(
|
||||||
SEpisode.create().apply {
|
SEpisode.create().apply {
|
||||||
name = "$seasonPrefix${ep.text().trim()}".replace("Episode ", "Ep. ")
|
name = "$seasonPrefix${ep.text().trim()}".replace("Episode ", "Ep. ")
|
||||||
|
|
||||||
|
ep.selectFirst("a")!!.let { a ->
|
||||||
episode_number = a.attr("data-num").toFloatOrNull() ?: 0F
|
episode_number = a.attr("data-num").toFloatOrNull() ?: 0F
|
||||||
url = json.encodeToString(
|
url = json.encodeToString(
|
||||||
EpisodeInfo(
|
EpisodeInfo(
|
||||||
@ -176,6 +169,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
url = "$baseUrl${a.attr("href")}",
|
url = "$baseUrl${a.attr("href")}",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -200,8 +194,7 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
override fun videoListRequest(episode: SEpisode): Request {
|
override fun videoListRequest(episode: SEpisode): Request {
|
||||||
val data = json.decodeFromString<EpisodeInfo>(episode.url)
|
val data = json.decodeFromString<EpisodeInfo>(episode.url)
|
||||||
|
val vrf = vrfHelper.getVrf(data.id)
|
||||||
val vrf = callConsumet(data.id, "fmovies-vrf")
|
|
||||||
|
|
||||||
val vrfHeaders = headers.newBuilder()
|
val vrfHeaders = headers.newBuilder()
|
||||||
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
@ -210,9 +203,13 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
.add("X-Requested-With", "XMLHttpRequest")
|
.add("X-Requested-With", "XMLHttpRequest")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return GET("$baseUrl/ajax/server/list/${data.id}?$vrf", headers = vrfHeaders)
|
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 fun videoListParse(response: Response, episode: SEpisode): List<Video> {
|
private fun videoListParse(response: Response, episode: SEpisode): List<Video> {
|
||||||
val data = json.decodeFromString<EpisodeInfo>(episode.url)
|
val data = json.decodeFromString<EpisodeInfo>(episode.url)
|
||||||
val document = Jsoup.parse(
|
val document = Jsoup.parse(
|
||||||
@ -228,7 +225,8 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
if (!hosterSelection.contains(name)) return@runCatching null
|
if (!hosterSelection.contains(name)) return@runCatching null
|
||||||
|
|
||||||
// Get decrypted url
|
// Get decrypted url
|
||||||
val vrf = callConsumet(server.attr("data-link-id"), "fmovies-vrf")
|
val vrf = vrfHelper.getVrf(server.attr("data-link-id"))
|
||||||
|
|
||||||
val vrfHeaders = headers.newBuilder()
|
val vrfHeaders = headers.newBuilder()
|
||||||
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
.add("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
.add("Host", baseUrl.toHttpUrl().host)
|
.add("Host", baseUrl.toHttpUrl().host)
|
||||||
@ -236,40 +234,22 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
.add("X-Requested-With", "XMLHttpRequest")
|
.add("X-Requested-With", "XMLHttpRequest")
|
||||||
.build()
|
.build()
|
||||||
val encrypted = client.newCall(
|
val encrypted = client.newCall(
|
||||||
GET("$baseUrl/ajax/server/${server.attr("data-link-id")}?$vrf", headers = vrfHeaders),
|
GET("$baseUrl/ajax/server/${server.attr("data-link-id")}?vrf=$vrf", headers = vrfHeaders),
|
||||||
).execute().parseAs<AjaxServerResponse>().result.url
|
).execute().parseAs<AjaxServerResponse>().result.url
|
||||||
val decrypted = callConsumet(encrypted, "fmovies-decrypt")
|
|
||||||
|
val decrypted = vrfHelper.decrypt(encrypted)
|
||||||
|
|
||||||
when (name) {
|
when (name) {
|
||||||
// Stolen from 9anime extension
|
"Vidplay", "MyCloud" -> vidsrcExtractor.videosFromUrl(decrypted, name)
|
||||||
"Vidstream", "MyCloud" -> {
|
"Filemoon" -> filemoonExtractor.videosFromUrl(decrypted, headers = headers)
|
||||||
val embedReferer = Headers.headersOf(
|
|
||||||
"referer",
|
|
||||||
"https://" + decrypted.toHttpUrl().host + "/",
|
|
||||||
)
|
|
||||||
val vidId = decrypted.substringAfterLast("/").substringBefore("?")
|
|
||||||
val (serverName, action) = when (name) {
|
|
||||||
"Vidstream" -> Pair("Vidstream", "rawVizcloud")
|
|
||||||
"MyCloud" -> Pair("MyCloud", "rawMcloud")
|
|
||||||
else -> return@parallelMap null
|
|
||||||
}
|
|
||||||
|
|
||||||
val playlistUrl = callConsumet(vidId, action)
|
|
||||||
val playlist = client.newCall(GET(playlistUrl, embedReferer)).execute()
|
|
||||||
|
|
||||||
parseVizPlaylist(
|
|
||||||
playlist.body.string(),
|
|
||||||
playlist.request.url,
|
|
||||||
serverName,
|
|
||||||
embedReferer,
|
|
||||||
decrypted.toHttpUrl().queryParameter("sub.info"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"Filemoon" -> {
|
|
||||||
FilemoonExtractor(client).videosFromUrl(decrypted, headers = headers)
|
|
||||||
}
|
|
||||||
"Streamtape" -> {
|
"Streamtape" -> {
|
||||||
StreamtapeExtractor(client, headers).videosFromUrl(decrypted)
|
val subtitleList = decrypted.toHttpUrl().queryParameter("sub.info")?.let {
|
||||||
|
client.newCall(GET(it, headers)).execute().parseAs<List<FMoviesSubs>>().map { t ->
|
||||||
|
Track(t.file, t.label)
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
streamtapeExtractor.videoFromUrl(decrypted, subtitleList = subtitleList)?.let(::listOf) ?: emptyList()
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@ -288,71 +268,6 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
override fun videoUrlParse(document: Document) = throw Exception("not used")
|
override fun videoUrlParse(document: Document) = throw Exception("not used")
|
||||||
|
|
||||||
// ============== Utilities stolen from 9anime extension ================
|
|
||||||
|
|
||||||
private fun callConsumet(query: String, action: String): String {
|
|
||||||
return client.newCall(
|
|
||||||
GET("https://9anime.eltik.net/$action?query=$query&apikey=aniyomi"),
|
|
||||||
).execute().body.string().let {
|
|
||||||
when (action) {
|
|
||||||
"rawVizcloud", "rawMcloud" -> {
|
|
||||||
val rawURL = json.decodeFromString<RawResponse>(it).rawURL
|
|
||||||
val referer = if (action == "rawVizcloud") "https://vidstream.pro/" else "https://mcloud.to/"
|
|
||||||
val apiResponse = client.newCall(
|
|
||||||
GET(
|
|
||||||
url = rawURL,
|
|
||||||
headers = Headers.headersOf("Referer", referer),
|
|
||||||
),
|
|
||||||
).execute().body.string()
|
|
||||||
apiResponse.substringAfter("file\":\"").substringBefore("\"")
|
|
||||||
}
|
|
||||||
"fmovies-decrypt" -> {
|
|
||||||
json.decodeFromString<VrfResponse>(it).url
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
json.decodeFromString<VrfResponse>(it).let { vrf ->
|
|
||||||
"vrf=${java.net.URLEncoder.encode(vrf.url, "utf-8")}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseVizPlaylist(
|
|
||||||
masterPlaylist: String,
|
|
||||||
masterUrl: HttpUrl,
|
|
||||||
prefix: String,
|
|
||||||
embedReferer: Headers,
|
|
||||||
subtitlesUrl: String?,
|
|
||||||
): List<Video> {
|
|
||||||
val playlistHeaders = embedReferer.newBuilder()
|
|
||||||
.add("accept", "*/*")
|
|
||||||
// .add("host", masterUrl.host)
|
|
||||||
.add("connection", "keep-alive")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val subtitleList = mutableListOf<Track>()
|
|
||||||
runCatching {
|
|
||||||
if (subtitlesUrl != null) {
|
|
||||||
val subData = client.newCall(GET(subtitlesUrl, headers)).execute().parseAs<List<FMoviesSubs>>()
|
|
||||||
subtitleList.addAll(
|
|
||||||
subData.map {
|
|
||||||
Track(it.file, it.label)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return masterPlaylist.substringAfter("#EXT-X-STREAM-INF:")
|
|
||||||
.split("#EXT-X-STREAM-INF:").map {
|
|
||||||
val quality = "$prefix - " + it.substringAfter("RESOLUTION=")
|
|
||||||
.substringAfter("x").substringBefore("\n") + "p"
|
|
||||||
val videoUrl = masterUrl.toString().substringBeforeLast("/") + "/" +
|
|
||||||
it.substringAfter("\n").substringBefore("\n")
|
|
||||||
Video(videoUrl, quality, videoUrl, playlistHeaders, subtitleTracks = subtitleList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================= Utilities ==============================
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
override fun List<Video>.sort(): List<Video> {
|
override fun List<Video>.sort(): List<Video> {
|
||||||
@ -385,57 +300,24 @@ class FMovies : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val HOSTERS = arrayOf(
|
private val HOSTERS = arrayOf(
|
||||||
"Vidstream",
|
"Vidplay",
|
||||||
"MyCloud",
|
"MyCloud",
|
||||||
"Filemoon",
|
"Filemoon",
|
||||||
"Streamtape",
|
"Streamtape",
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val PREF_DOMAIN_KEY = "preferred_domain"
|
|
||||||
private val PREF_DOMAIN_DEFAULT = "https://fmovies.to"
|
|
||||||
|
|
||||||
private const val PREF_QUALITY_KEY = "preferred_quality"
|
private const val PREF_QUALITY_KEY = "preferred_quality"
|
||||||
private const val PREF_QUALITY_DEFAULT = "1080"
|
private const val PREF_QUALITY_DEFAULT = "1080"
|
||||||
|
|
||||||
private const val PREF_SERVER_KEY = "preferred_server"
|
private const val PREF_SERVER_KEY = "preferred_server"
|
||||||
private const val PREF_SERVER_DEFAULT = "Vidstream"
|
private const val PREF_SERVER_DEFAULT = "Vidplay"
|
||||||
|
|
||||||
private const val PREF_HOSTER_KEY = "hoster_selection"
|
private const val PREF_HOSTER_KEY = "hoster_selection"
|
||||||
private val PREF_HOSTER_DEFAULT = setOf("Vidstream", "Filemoon")
|
private val PREF_HOSTER_DEFAULT = setOf("Vidplay", "Filemoon")
|
||||||
}
|
}
|
||||||
// ============================== Settings ==============================
|
// ============================== Settings ==============================
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_DOMAIN_KEY
|
|
||||||
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"
|
|
||||||
|
|
||||||
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 {
|
ListPreference(screen.context).apply {
|
||||||
key = PREF_QUALITY_KEY
|
key = PREF_QUALITY_KEY
|
||||||
title = "Preferred quality"
|
title = "Preferred quality"
|
||||||
|
@ -39,3 +39,18 @@ data class FMoviesSubs(
|
|||||||
data class RawResponse(
|
data class RawResponse(
|
||||||
val rawURL: String,
|
val rawURL: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VidsrcResponse(
|
||||||
|
val result: ResultObject,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class ResultObject(
|
||||||
|
val sources: List<SourceObject>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SourceObject(
|
||||||
|
val file: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.fmovies
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
class FMoviesHelper(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
fun getVrf(id: String): String {
|
||||||
|
val url = API_URL.newBuilder().apply {
|
||||||
|
addPathSegment("fmovies-vrf")
|
||||||
|
addQueryParameter("query", id)
|
||||||
|
addQueryParameter("apikey", API_KEY)
|
||||||
|
}.build().toString()
|
||||||
|
|
||||||
|
return client.newCall(GET(url)).execute().parseAs<VrfResponse>().let {
|
||||||
|
URLEncoder.encode(it.url, "utf-8")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(encrypted: String): String {
|
||||||
|
val url = API_URL.newBuilder().apply {
|
||||||
|
addPathSegment("fmovies-decrypt")
|
||||||
|
addQueryParameter("query", encrypted)
|
||||||
|
addQueryParameter("apikey", API_KEY)
|
||||||
|
}.build().toString()
|
||||||
|
|
||||||
|
return client.newCall(GET(url)).execute().parseAs<VrfResponse>().url
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getVidSrc(query: String, host: String): String {
|
||||||
|
val url = API_URL.newBuilder().apply {
|
||||||
|
addPathSegment("rawVizcloud")
|
||||||
|
addQueryParameter("apikey", API_KEY)
|
||||||
|
}.build().toString()
|
||||||
|
|
||||||
|
val futoken = client.newCall(
|
||||||
|
GET("https://$host/futoken", headers),
|
||||||
|
).execute().use { it.body.string() }
|
||||||
|
|
||||||
|
val body = FormBody.Builder().apply {
|
||||||
|
add("query", query)
|
||||||
|
add("futoken", futoken)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val rawURL = client.newCall(
|
||||||
|
POST(url, body = body),
|
||||||
|
).execute().parseAs<RawResponse>().rawURL
|
||||||
|
|
||||||
|
return rawURL.toHttpUrl().newBuilder().apply {
|
||||||
|
host(host)
|
||||||
|
}.build().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val API_KEY = "aniyomi"
|
||||||
|
val API_URL = "https://9anime.eltik.net".toHttpUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(transform: (String) -> String = { it }): T {
|
||||||
|
val responseBody = use { transform(it.body.string()) }
|
||||||
|
return json.decodeFromString(responseBody)
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.animeextension.en.fmovies.extractors
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.animeextension.en.fmovies.FMoviesSubs
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Track
|
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.Headers
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Response
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class StreamtapeExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
fun videosFromUrl(url: String, quality: String = "Streamtape"): List<Video> {
|
|
||||||
val subtitleList = mutableListOf<Track>()
|
|
||||||
val subInfoUrl = url.toHttpUrl().queryParameter("sub.info")
|
|
||||||
runCatching {
|
|
||||||
if (subInfoUrl != null) {
|
|
||||||
val subData = client.newCall(GET(subInfoUrl, headers)).execute().parseAs<List<FMoviesSubs>>()
|
|
||||||
subtitleList.addAll(
|
|
||||||
subData.map {
|
|
||||||
Track(it.file, it.label)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val baseUrl = "https://streamtape.com/e/"
|
|
||||||
val newUrl = if (!url.startsWith(baseUrl)) {
|
|
||||||
// ["https", "", "<domain>", "<???>", "<id>", ...]
|
|
||||||
val id = url.split("/").getOrNull(4) ?: return emptyList()
|
|
||||||
baseUrl + id
|
|
||||||
} else { url }
|
|
||||||
val document = client.newCall(GET(newUrl)).execute().asJsoup()
|
|
||||||
val targetLine = "document.getElementById('robotlink')"
|
|
||||||
val script = document.selectFirst("script:containsData($targetLine)")
|
|
||||||
?.data()
|
|
||||||
?.substringAfter("$targetLine.innerHTML = '")
|
|
||||||
?: return emptyList()
|
|
||||||
val videoUrl = "https:" + script.substringBefore("'") +
|
|
||||||
script.substringAfter("+ ('xcd").substringBefore("'")
|
|
||||||
return listOf(Video(videoUrl, quality, videoUrl, subtitleTracks = subtitleList))
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
val responseBody = use { it.body.string() }
|
|
||||||
return json.decodeFromString(responseBody)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,60 @@
|
|||||||
|
package eu.kanade.tachiyomi.animeextension.en.fmovies.extractors
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.fmovies.FMoviesHelper
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.fmovies.FMoviesSubs
|
||||||
|
import eu.kanade.tachiyomi.animeextension.en.fmovies.VidsrcResponse
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Track
|
||||||
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
|
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Headers
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class VidsrcExtractor(private val client: OkHttpClient, private val headers: Headers) {
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
private val vrfHelper by lazy { FMoviesHelper(client, headers) }
|
||||||
|
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||||
|
|
||||||
|
fun videosFromUrl(url: String, name: String): List<Video> {
|
||||||
|
val host = when (name) {
|
||||||
|
"Vidplay" -> "vidstream.pro"
|
||||||
|
else -> "mcloud.to"
|
||||||
|
}
|
||||||
|
val referer = "https://$host/"
|
||||||
|
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
|
||||||
|
val query = "${httpUrl.pathSegments.last()}?t=${httpUrl.queryParameter("t")!!}"
|
||||||
|
val rawUrl = vrfHelper.getVidSrc(query, host)
|
||||||
|
|
||||||
|
val refererHeaders = headers.newBuilder().apply {
|
||||||
|
add("Referer", referer)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val infoJson = client.newCall(
|
||||||
|
GET(rawUrl, headers = refererHeaders),
|
||||||
|
).execute().parseAs<VidsrcResponse>()
|
||||||
|
|
||||||
|
val subtitleList = httpUrl.queryParameter("sub.info")?.let {
|
||||||
|
client.newCall(
|
||||||
|
GET(it, headers = refererHeaders),
|
||||||
|
).execute().parseAs<List<FMoviesSubs>>().map {
|
||||||
|
Track(it.file, it.label)
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
return infoJson.result.sources.distinctBy { it.file }.flatMap {
|
||||||
|
playlistUtils.extractFromHls(it.file, subtitleList = subtitleList, referer = referer, videoNameGen = { q -> "$name - $q" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(transform: (String) -> String = { it }): T {
|
||||||
|
val responseBody = use { transform(it.body.string()) }
|
||||||
|
return json.decodeFromString(responseBody)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user