fix(en/fmovies): Update enimax's vrf helper & add subtitle support for streamtape (#2227)

This commit is contained in:
Secozzi 2023-09-21 10:49:27 +00:00 committed by GitHub
parent 53bd7d1ec3
commit 1b51325344
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 214 additions and 234 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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