Fix(src/es): Pelisforte, HomeCine and MetroSeries fixes (#419)

* MetroSeries fixes

Closes #370

* HomeCine fixes

Closes #384

* PelisForte fixes

Closes #411
This commit is contained in:
imper1aldev 2024-12-09 15:48:19 -06:00 committed by GitHub
parent f3f1a64fda
commit 440917b8ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 311 additions and 317 deletions

View file

@ -1,11 +1,18 @@
ext { ext {
extName = 'HomeCine' extName = 'HomeCine'
extClass = '.HomeCine' extClass = '.HomeCine'
extVersionCode = 1 extVersionCode = 2
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
dependencies { dependencies {
implementation(project(':lib:burstcloud-extractor'))
implementation(project(':lib:mp4upload-extractor'))
implementation(project(':lib:streamwish-extractor'))
implementation(project(':lib:voe-extractor'))
implementation(project(':lib:yourupload-extractor'))
implementation(project(':lib:fastream-extractor')) implementation(project(':lib:fastream-extractor'))
implementation(project(':lib:upstream-extractor'))
implementation(project(':lib:filemoon-extractor'))
} }

View file

@ -5,19 +5,27 @@ import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource 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.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage 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.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.lib.burstcloudextractor.BurstCloudExtractor
import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor import eu.kanade.tachiyomi.lib.fastreamextractor.FastreamExtractor
import eu.kanade.tachiyomi.lib.filemoonextractor.FilemoonExtractor
import eu.kanade.tachiyomi.lib.mp4uploadextractor.Mp4uploadExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
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 eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.FormBody
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -25,7 +33,7 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "HomeCine" override val name = "HomeCine"
override val baseUrl = "https://www3.homecine.tv" override val baseUrl = "https://homecine.cc"
override val lang = "es" override val lang = "es"
@ -45,116 +53,175 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
private val QUALITY_LIST = arrayOf("1080", "720", "480", "360") private val QUALITY_LIST = arrayOf("1080", "720", "480", "360")
private const val PREF_SERVER_KEY = "preferred_server" private const val PREF_SERVER_KEY = "preferred_server"
private const val PREF_SERVER_DEFAULT = "Fastream" private const val PREF_SERVER_DEFAULT = "YourUpload"
private val SERVER_LIST = arrayOf("Fastream") private val SERVER_LIST = arrayOf(
"YourUpload",
"BurstCloud",
"Voe",
"StreamWish",
"Mp4Upload",
"Fastream",
"Upstream",
"Filemoon",
)
} }
override fun animeDetailsParse(response: Response): SAnime { override fun popularAnimeRequest(page: Int) = GET("$baseUrl/cartelera-series/page/$page", headers)
val document = response.asJsoup()
val animeDetails = SAnime.create().apply {
title = document.selectFirst(".mvic-desc h1")?.text()?.trim() ?: ""
status = if (document.location().contains("/series/")) SAnime.UNKNOWN else SAnime.COMPLETED
description = document.selectFirst(".mvic-desc .f-desc")?.ownText()
genre = document.select(".mvic-info [rel='category tag']").joinToString { it.text() }
thumbnail_url = document.selectFirst("[itemprop=image]")?.attr("abs:src")?.replace("/w185/", "/w500/")
document.select(".mvici-left p").map { it.text() }.map { textContent ->
when {
"Director" in textContent -> author = textContent.substringAfter("Director:").trim().split(", ").firstOrNull()
"Actors" in textContent -> artist = textContent.substringAfter("Actors:").trim().split(", ").firstOrNull()
}
}
}
return animeDetails
}
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/peliculas/page/$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val elements = document.select("[data-movie-id] > a") val elements = document.select(".post")
val nextPage = document.select(".pagination li a:contains(Last)").any() val nextPage = document.select(".nav-links .current ~ a").any()
val animeList = elements.map { element -> val animeList = elements.map { element ->
SAnime.create().apply { SAnime.create().apply {
title = element.selectFirst(".mli-info")?.text()?.trim() ?: "" setUrlWithoutDomain(element.selectFirst(".lnk-blk")?.attr("abs:href") ?: "")
thumbnail_url = element.selectFirst("img")!!.attr("abs:data-original") title = element.selectFirst(".entry-header .entry-title")?.text() ?: ""
setUrlWithoutDomain(element.attr("abs:href")) description = element.select(".entry-content p").text() ?: ""
thumbnail_url = element.selectFirst(".post-thumbnail figure img")?.let { getImageUrl(it) }
} }
} }
return AnimesPage(animeList, nextPage) return AnimesPage(animeList, nextPage)
} }
override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
override fun latestUpdatesRequest(page: Int) = popularAnimeRequest(page) override fun latestUpdatesRequest(page: Int) = popularAnimeRequest(page)
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request { override fun latestUpdatesParse(response: Response) = popularAnimeParse(response)
val filterList = if (filters.isEmpty()) getFilterList() else filters
val genreFilter = filterList.find { it is GenreFilter } as GenreFilter
return when { override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList) = GET("$baseUrl/?s=$query", headers)
query.isNotBlank() -> GET("$baseUrl/page/$page/?s=$query", headers)
genreFilter.state != 0 -> GET("$baseUrl/${genreFilter.toUriPart()}/page/$page", headers)
else -> popularAnimeRequest(page)
}
}
override fun searchAnimeParse(response: Response) = popularAnimeParse(response) override fun searchAnimeParse(response: Response) = popularAnimeParse(response)
override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup()
return SAnime.create().apply {
title = document.selectFirst("aside .entry-header .entry-title")?.text() ?: ""
description = document.select("aside .description p:not([class])").joinToString { it.text() }
thumbnail_url = document.selectFirst(".post-thumbnail img")?.let { getImageUrl(it)?.replace("/w185/", "/w500/") }
genre = document.select(".genres a").joinToString { it.text() }
status = if (document.location().contains("pelicula")) SAnime.COMPLETED else SAnime.UNKNOWN
}
}
private fun getImageUrl(element: Element): String? {
return when {
element.hasAttr("data-src") -> element.attr("abs:data-src")
element.hasAttr("src") -> element.attr("abs:src")
else -> null
}
}
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.asJsoup()
return if (document.location().contains("/series/")) { val referer = response.request.url.toString()
var episodeCounter = 1F return if (referer.contains("pelicula")) {
document.select(".tvseason").flatMap { season ->
val noSeason = season.select(".les-title strong").text().substringAfter("Temporada").trim()
season.select(".les-content a").map { ep ->
SEpisode.create().apply {
episode_number = episodeCounter++
name = "T$noSeason - ${ep.text().trim()}"
setUrlWithoutDomain(ep.attr("abs:href"))
}
}
}.reversed()
} else {
listOf( listOf(
SEpisode.create().apply { SEpisode.create().apply {
episode_number = 1f episode_number = 1f
name = "PELÍCULA" name = "Película"
setUrlWithoutDomain(document.location()) setUrlWithoutDomain(referer)
}, },
) )
} else {
val chunkSize = Runtime.getRuntime().availableProcessors()
document.select(".sel-temp a")
.sortedByDescending { it.attr("data-season") }
.chunked(chunkSize).flatMap { chunk ->
chunk.parallelCatchingFlatMapBlocking { season ->
getDetailSeason(season, referer)
}
}.sortedByDescending {
it.name.substringBeforeLast("-")
}
}
}
private fun getDetailSeason(element: Element, referer: String): List<SEpisode> {
return try {
val post = element.attr("data-post")
val season = element.attr("data-season")
val formBody = FormBody.Builder()
.add("action", "action_select_season")
.add("season", season)
.add("post", post)
.build()
val request = Request.Builder()
.url("$baseUrl/wp-admin/admin-ajax.php")
.post(formBody)
.header("Origin", baseUrl)
.header("Referer", referer)
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
val detail = client.newCall(request).execute().asJsoup()
detail.select(".post").reversed().mapIndexed { idx, it ->
val epNumber = try {
it.select(".entry-header .num-epi").text().substringAfter("x").substringBefore("").trim()
} catch (_: Exception) { "${idx + 1}" }
SEpisode.create().apply {
setUrlWithoutDomain(it.select("a").attr("abs:href"))
name = "T$season - Episodio $epNumber"
episode_number = epNumber.toFloat()
}
}
} catch (_: Exception) {
emptyList()
} }
} }
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.asJsoup()
val videoList = mutableListOf<Video>()
return document.select(".movieplay iframe").parallelCatchingFlatMapBlocking { document.select(".aa-tbs-video a").forEach {
val link = it.attr("abs:src")
val prefix = runCatching { val prefix = runCatching {
val tabElement = it.closest("[id*=tab]") ?: return@runCatching "" val lang = it.select(".server").text().lowercase()
val tabName = tabElement.attr("id")
val lang = document.selectFirst("[href=\"#$tabName\"]")?.ownText()?.trim() ?: ""
when { when {
lang.lowercase().contains("latino") -> "[LAT]" lang.contains("latino") -> "[LAT]"
lang.lowercase().contains("castellano") -> "[CAST]" lang.contains("castellano") -> "[CAST]"
lang.lowercase().contains("subtitulado") -> "[SUB]" lang.contains("sub") || lang.contains("vose") -> "[SUB]"
else -> "" else -> ""
} }
}.getOrDefault("") }.getOrDefault("")
serverVideoResolver(link, prefix) val ide = it.attr("href")
} var src = document.select("$ide iframe").attr("data-src").replace("#038;", "&").replace("&amp;", "")
} try {
if (src.contains("home")) {
src = client.newCall(GET(src)).execute().asJsoup().selectFirst("iframe")?.attr("src") ?: ""
}
private fun serverVideoResolver(url: String, prefix: String = ""): List<Video> { if (src.contains("fastream")) {
val embedUrl = url.lowercase() if (src.contains("emb.html")) {
return when { val key = src.split("/").last()
embedUrl.contains("fastream") -> { src = "https://fastream.to/embed-$key.html"
val link = if (url.contains("emb.html")) "https://fastream.to/embed-${url.split("/").last()}.html" else url }
FastreamExtractor(client, headers).videosFromUrl(link, prefix = "$prefix Fastream:") FastreamExtractor(client, headers).videosFromUrl(src, needsSleep = false, prefix = "$prefix Fastream:").also(videoList::addAll)
} }
else -> emptyList() if (src.contains("upstream")) {
UpstreamExtractor(client).videosFromUrl(src, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("yourupload")) {
YourUploadExtractor(client).videoFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("voe")) {
VoeExtractor(client).videosFromUrl(src, prefix = "$prefix ").also(videoList::addAll)
}
if (src.contains("wishembed") || src.contains("streamwish") || src.contains("wish")) {
StreamWishExtractor(client, headers).videosFromUrl(src) { "$prefix StreamWish:$it" }.also(videoList::addAll)
}
if (src.contains("mp4upload")) {
Mp4uploadExtractor(client).videosFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(src, headers = headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("filemoon") || src.contains("moonplayer")) {
FilemoonExtractor(client).videosFromUrl(src, headers = headers, prefix = "$prefix Filemoon:").let { videoList.addAll(it) }
}
} catch (_: Exception) {}
} }
return videoList
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
@ -171,25 +238,6 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
).reversed() ).reversed()
} }
override fun getFilterList(): AnimeFilterList = AnimeFilterList(
AnimeFilter.Header("La busqueda por texto ignora el filtro"),
GenreFilter(),
)
private class GenreFilter : UriPartFilter(
"Género",
arrayOf(
Pair("<Seleccionar>", ""),
Pair("Películas", "peliculas"),
Pair("Series", "series"),
),
)
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
}
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY key = PREF_LANGUAGE_KEY
@ -207,22 +255,6 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
} }
}.also(screen::addPreference) }.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
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)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = "Preferred quality" title = "Preferred quality"
@ -238,5 +270,21 @@ class HomeCine : ConfigurableAnimeSource, AnimeHttpSource() {
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
}.also(screen::addPreference) }.also(screen::addPreference)
ListPreference(screen.context).apply {
key = PREF_SERVER_KEY
title = "Preferred server"
entries = SERVER_LIST
entryValues = SERVER_LIST
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)
} }
} }

View file

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

View file

@ -20,25 +20,20 @@ import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking import eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import eu.kanade.tachiyomi.util.parallelMapBlocking
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() { class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
override val name = "MetroSeries" override val name = "MetroSeries"
override val baseUrl = "https://metroseries.net" override val baseUrl = "https://www3.seriesmetro.net"
override val lang = "es" override val lang = "es"
@ -71,11 +66,11 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
) )
} }
override fun popularAnimeRequest(page: Int) = GET("$baseUrl/series/page/$page", headers) override fun popularAnimeRequest(page: Int) = GET("$baseUrl/cartelera-series/page/$page", headers)
override fun popularAnimeParse(response: Response): AnimesPage { override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup() val document = response.asJsoup()
val elements = document.select(".post-list, .results-post > .post") val elements = document.select(".post")
val nextPage = document.select(".nav-links .current ~ a").any() val nextPage = document.select(".nav-links .current ~ a").any()
val animeList = elements.map { element -> val animeList = elements.map { element ->
SAnime.create().apply { SAnime.create().apply {
@ -99,11 +94,11 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
override fun animeDetailsParse(response: Response): SAnime { override fun animeDetailsParse(response: Response): SAnime {
val document = response.asJsoup() val document = response.asJsoup()
return SAnime.create().apply { return SAnime.create().apply {
title = document.selectFirst("main .entry-header .entry-title")?.text() ?: "" title = document.selectFirst("aside .entry-header .entry-title")?.text() ?: ""
description = document.select("main .entry-content p").joinToString { it.text() } description = document.select("aside .description p:not([class])").joinToString { it.text() }
thumbnail_url = document.selectFirst("main .post-thumbnail img")?.let { getImageUrl(it) } thumbnail_url = document.selectFirst(".post-thumbnail img")?.let { getImageUrl(it)?.replace("/w185/", "/w500/") }
genre = document.select("main .entry-content .tagcloud a").joinToString { it.text() } genre = document.select(".genres a").joinToString { it.text() }
status = SAnime.UNKNOWN status = if (document.location().contains("pelicula")) SAnime.COMPLETED else SAnime.UNKNOWN
} }
} }
@ -118,34 +113,40 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup() val document = response.asJsoup()
val referer = response.request.url.toString() val referer = response.request.url.toString()
val chunkSize = Runtime.getRuntime().availableProcessors() return if (referer.contains("pelicula")) {
val objectNumber = document.select("#aa-season").attr("data-object") listOf(
val episodes = document.select(".season-list li a") SEpisode.create().apply {
.sortedByDescending { it.attr("data-season") } episode_number = 1f
.chunked(chunkSize).flatMap { chunk -> name = "Película"
chunk.parallelCatchingFlatMapBlocking { season -> setUrlWithoutDomain(referer)
val pages = getDetailSeason(season, objectNumber, referer) },
getPageEpisodeList(pages, referer, objectNumber, season.attr("data-season")) )
} else {
val chunkSize = Runtime.getRuntime().availableProcessors()
document.select(".sel-temp a")
.sortedByDescending { it.attr("data-season") }
.chunked(chunkSize).flatMap { chunk ->
chunk.parallelCatchingFlatMapBlocking { season ->
getDetailSeason(season, referer)
}
}.sortedByDescending {
it.name.substringBeforeLast("-")
} }
}.sortedByDescending { }
it.name.substringBeforeLast("-")
}
return episodes
} }
private fun getDetailSeason(element: Element, objectNumber: String, referer: String): IntRange { private fun getDetailSeason(element: Element, referer: String): List<SEpisode> {
try { return try {
val post = element.attr("data-post") val post = element.attr("data-post")
val season = element.attr("data-season") val season = element.attr("data-season")
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
.add("action", "action_select_season") .add("action", "action_select_season")
.add("season", season) .add("season", season)
.add("post", post) .add("post", post)
.add("object", objectNumber)
.build() .build()
val request = Request.Builder() val request = Request.Builder()
.url("https://metroseries.net/wp-admin/admin-ajax.php") .url("$baseUrl/wp-admin/admin-ajax.php")
.post(formBody) .post(formBody)
.header("Origin", baseUrl) .header("Origin", baseUrl)
.header("Referer", referer) .header("Referer", referer)
@ -153,110 +154,72 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
.build() .build()
val detail = client.newCall(request).execute().asJsoup() val detail = client.newCall(request).execute().asJsoup()
val firstPage = try { detail.selectFirst("#aa-season > nav > span.page-numbers")?.text()?.toInt() ?: 1 } catch (_: Exception) { 1 } detail.select(".post").reversed().mapIndexed { idx, it ->
val lastPage = try { detail.select(".pagination a.page-numbers:not(.next)").last()?.text()?.toInt() ?: firstPage } catch (_: Exception) { firstPage } val epNumber = try {
it.select(".entry-header .num-epi").text().substringAfter("x").substringBefore("").trim()
} catch (_: Exception) { "${idx + 1}" }
return firstPage.rangeTo(lastPage) SEpisode.create().apply {
} catch (_: Exception) { setUrlWithoutDomain(it.select("a").attr("abs:href"))
return 1..1 name = "T$season - Episodio $epNumber"
} episode_number = epNumber.toFloat()
} }
private fun getPageEpisodeList(pages: IntRange, referer: String, objectNumber: String, season: String): List<SEpisode> {
val episodes = mutableListOf<SEpisode>()
try {
pages.parallelMapBlocking {
val formBody = FormBody.Builder()
.add("action", "action_pagination_ep")
.add("page", "$it")
.add("object", objectNumber)
.add("season", season)
.build()
val requestPage = Request.Builder()
.url("https://metroseries.net/wp-admin/admin-ajax.php")
.post(formBody)
.header("authority", baseUrl.toHttpUrl().host)
.header("Origin", "https://${baseUrl.toHttpUrl().host}")
.header("Referer", referer)
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
client.newCall(requestPage).await().parseAsEpisodeList().also(episodes::addAll)
} }
} catch (_: Exception) { } } catch (_: Exception) {
return episodes emptyList()
}
} }
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.asJsoup()
val videoList = mutableListOf<Video>() val videoList = mutableListOf<Video>()
val termId = document.select("#option-players").attr("data-term") document.select(".aa-tbs-video a").forEach {
document.select(".player-options-list li a").forEach {
val prefix = runCatching { val prefix = runCatching {
val lang = it.select(".option").text().lowercase() val lang = it.select(".server").text().lowercase()
when { when {
lang.contains("latino") -> "[LAT]" lang.contains("latino") -> "[LAT]"
lang.contains("castellano") -> "[CAST]" lang.contains("castellano") -> "[CAST]"
lang.contains("sub") -> "[SUB]" lang.contains("sub") || lang.contains("vose") -> "[SUB]"
else -> "" else -> ""
} }
}.getOrDefault("") }.getOrDefault("")
val ide = it.attr("data-opt") val ide = it.attr("href")
val formBody = FormBody.Builder() var src = document.select("$ide iframe").attr("data-src").replace("#038;", "&").replace("&amp;", "")
.add("action", "action_player_series") try {
.add("ide", ide) if (src.contains("metro")) {
.add("term_id", termId) src = client.newCall(GET(src)).execute().asJsoup().selectFirst("iframe")?.attr("src") ?: ""
.build() }
val postRequest = Request.Builder() if (src.contains("fastream")) {
.url("https://metroseries.net/wp-admin/admin-ajax.php") if (src.contains("emb.html")) {
.post(formBody) val key = src.split("/").last()
.header("Origin", baseUrl) src = "https://fastream.to/embed-$key.html"
.header("Referer", response.request.url.toString())
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
val playerDocument = client.newCall(postRequest).execute().asJsoup()
playerDocument.select("iframe").forEach {
var src = it.attr("src").replace("#038;", "&").replace("&amp;", "")
try {
if (src.contains("metroseries")) {
src = client.newCall(GET(src)).execute().asJsoup().selectFirst("iframe")?.attr("src") ?: ""
} }
FastreamExtractor(client, headers).videosFromUrl(src, needsSleep = false, prefix = "$prefix Fastream:").also(videoList::addAll)
if (src.contains("fastream")) { }
if (src.contains("emb.html")) { if (src.contains("upstream")) {
val key = src.split("/").last() UpstreamExtractor(client).videosFromUrl(src, prefix = "$prefix ").let { videoList.addAll(it) }
src = "https://fastream.to/embed-$key.html" }
} if (src.contains("yourupload")) {
FastreamExtractor(client, headers).videosFromUrl(src, needsSleep = false, prefix = "$prefix Fastream:").also(videoList::addAll) YourUploadExtractor(client).videoFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
} }
if (src.contains("voe")) {
if (src.contains("upstream")) { VoeExtractor(client).videosFromUrl(src, prefix = "$prefix ").also(videoList::addAll)
UpstreamExtractor(client).videosFromUrl(src, prefix = "$prefix ").let { videoList.addAll(it) } }
} if (src.contains("wishembed") || src.contains("streamwish") || src.contains("wish")) {
if (src.contains("yourupload")) { StreamWishExtractor(client, headers).videosFromUrl(src) { "$prefix StreamWish:$it" }.also(videoList::addAll)
YourUploadExtractor(client).videoFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) } }
} if (src.contains("mp4upload")) {
if (src.contains("voe")) { Mp4uploadExtractor(client).videosFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) }
VoeExtractor(client).videosFromUrl(src, prefix = "$prefix ").also(videoList::addAll) }
} if (src.contains("burst")) {
if (src.contains("wishembed") || src.contains("streamwish") || src.contains("wish")) { BurstCloudExtractor(client).videoFromUrl(src, headers = headers, prefix = "$prefix ").let { videoList.addAll(it) }
StreamWishExtractor(client, headers).videosFromUrl(src) { "$prefix StreamWish:$it" }.also(videoList::addAll) }
} if (src.contains("filemoon") || src.contains("moonplayer")) {
if (src.contains("mp4upload")) { FilemoonExtractor(client).videosFromUrl(src, headers = headers, prefix = "$prefix Filemoon:").let { videoList.addAll(it) }
Mp4uploadExtractor(client).videosFromUrl(src, headers, prefix = "$prefix ").let { videoList.addAll(it) } }
} } catch (_: Exception) {}
if (src.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(src, headers = headers, prefix = "$prefix ").let { videoList.addAll(it) }
}
if (src.contains("filemoon") || src.contains("moonplayer")) {
FilemoonExtractor(client).videosFromUrl(src, headers = headers, prefix = "$prefix Filemoon:").let { videoList.addAll(it) }
}
} catch (_: Exception) {}
}
} }
return videoList return videoList
} }
@ -324,21 +287,4 @@ class MetroSeries : ConfigurableAnimeSource, AnimeHttpSource() {
} }
}.also(screen::addPreference) }.also(screen::addPreference)
} }
private fun Response.parseAsEpisodeList(): List<SEpisode> {
return asJsoup().select(".episodes-list li a").reversed().mapIndexed { idx, it ->
val epNumber = try { it.ownText().substringAfter("x").substringBefore("").trim() } catch (_: Exception) { "${idx + 1}" }
val season = it.ownText().substringBefore("x").trim()
SEpisode.create().apply {
setUrlWithoutDomain(it.attr("abs:href"))
name = "T$season - E$epNumber - ${it.ownText().substringAfter("").trim()}"
episode_number = epNumber.toFloat()
date_upload = try {
SimpleDateFormat("dd-MM-yyyy", Locale.ENGLISH).parse(it.select("span").text()).time
} catch (_: Exception) {
System.currentTimeMillis()
}
}
}
}
} }

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'PelisForte' extName = 'PelisForte'
extClass = '.PelisForte' extClass = '.PelisForte'
extVersionCode = 16 extVersionCode = 17
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"
@ -21,4 +21,5 @@ dependencies {
implementation(project(':lib:playlist-utils')) implementation(project(':lib:playlist-utils'))
implementation(project(':lib:streamlare-extractor')) implementation(project(':lib:streamlare-extractor'))
implementation(project(':lib:okru-extractor')) implementation(project(':lib:okru-extractor'))
implementation(project(':lib:vidguard-extractor'))
} }

View file

@ -23,10 +23,12 @@ import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor import eu.kanade.tachiyomi.lib.streamwishextractor.StreamWishExtractor
import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor import eu.kanade.tachiyomi.lib.upstreamextractor.UpstreamExtractor
import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor import eu.kanade.tachiyomi.lib.uqloadextractor.UqloadExtractor
import eu.kanade.tachiyomi.lib.vidguardextractor.VidGuardExtractor
import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor import eu.kanade.tachiyomi.lib.voeextractor.VoeExtractor
import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor import eu.kanade.tachiyomi.lib.youruploadextractor.YourUploadExtractor
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 eu.kanade.tachiyomi.util.parallelCatchingFlatMapBlocking
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@ -62,6 +64,7 @@ open class PelisForte : ConfigurableAnimeSource, AnimeHttpSource() {
"YourUpload", "Voe", "Mp4Upload", "Doodstream", "YourUpload", "Voe", "Mp4Upload", "Doodstream",
"Upload", "BurstCloud", "Upstream", "StreamTape", "Upload", "BurstCloud", "Upstream", "StreamTape",
"Fastream", "Filemoon", "StreamWish", "Okru", "Fastream", "Filemoon", "StreamWish", "Okru",
"VidGuard",
) )
} }
@ -149,83 +152,70 @@ open class PelisForte : ConfigurableAnimeSource, AnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val document = response.asJsoup() val document = response.asJsoup()
val videoList = mutableListOf<Video>() return document.select(".video-player iframe").parallelCatchingFlatMapBlocking {
document.select(".video-player iframe").forEach { val id = it.parent()?.attr("id")
try { val idTab = document.selectFirst("[href=\"#$id\"]")?.closest(".lrt")?.attr("id")
val id = it.parent()?.attr("id") val lang = document.select("[tab=$idTab]").text()
val idTab = document.selectFirst("[href=\"#$id\"]")?.closest(".lrt")?.attr("id") val src = it.attr("src").ifEmpty { it.attr("data-src") }
val lang = document.select("[tab=$idTab]").text() val key = src.substringAfter("/?h=")
val src = it.attr("src").ifEmpty { it.attr("data-src") } val player = "https://${src.toHttpUrl().host}/r.php?h=$key"
val key = src.substringAfter("/?h=")
val player = "https://${src.toHttpUrl().host}/r.php?h=$key"
val prefix = when {
lang.contains("Latino", true) -> "[LAT]"
lang.contains("Subtitulado", true) -> "[SUB]"
lang.contains("Castellano", true) -> "[CAST]"
else -> ""
}
val locationsDdh = client.newCall(GET(player, headers = headers.newBuilder().add("referer", src).build()))
.execute().networkResponse.toString()
fetchUrls(locationsDdh).forEach { val prefix = when {
serverVideoResolver(it, prefix, src).also(videoList::addAll) lang.contains("Latino", true) -> "[LAT]"
} lang.contains("Subtitulado", true) -> "[SUB]"
} catch (_: Exception) {} lang.contains("Castellano", true) -> "[CAST]"
else -> ""
}
val locationsDdh = client.newCall(
GET(player, headers = headers.newBuilder().add("referer", src).build()),
).execute().networkResponse.toString()
fetchUrls(locationsDdh).flatMap { serverVideoResolver(it, prefix) }
} }
return videoList
} }
private fun serverVideoResolver(url: String, prefix: String = "", referer: String = ""): List<Video> { /*--------------------------------Video extractors------------------------------------*/
val videoList = mutableListOf<Video>() private val voeExtractor by lazy { VoeExtractor(client) }
val embedUrl = url.lowercase() private val okruExtractor by lazy { OkruExtractor(client) }
try { private val filemoonExtractor by lazy { FilemoonExtractor(client) }
if (embedUrl.contains("voe")) { private val uqloadExtractor by lazy { UqloadExtractor(client) }
VoeExtractor(client).videosFromUrl(url, prefix).also(videoList::addAll) private val mp4uploadExtractor by lazy { Mp4uploadExtractor(client) }
private val streamWishExtractor by lazy { StreamWishExtractor(client, headers) }
private val doodExtractor by lazy { DoodExtractor(client) }
private val streamlareExtractor by lazy { StreamlareExtractor(client) }
private val yourUploadExtractor by lazy { YourUploadExtractor(client) }
private val burstCloudExtractor by lazy { BurstCloudExtractor(client) }
private val fastreamExtractor by lazy { FastreamExtractor(client, headers) }
private val upstreamExtractor by lazy { UpstreamExtractor(client) }
private val streamTapeExtractor by lazy { StreamTapeExtractor(client) }
private val vidGuardExtractor by lazy { VidGuardExtractor(client) }
private fun serverVideoResolver(url: String, prefix: String = ""): List<Video> {
return runCatching {
when {
arrayOf("voe").any(url) -> voeExtractor.videosFromUrl(url, "$prefix ")
arrayOf("ok.ru", "okru").any(url) -> okruExtractor.videosFromUrl(url, prefix)
arrayOf("filemoon", "moonplayer").any(url) -> filemoonExtractor.videosFromUrl(url, prefix = "$prefix Filemoon:")
arrayOf("uqload").any(url) -> uqloadExtractor.videosFromUrl(url, prefix)
arrayOf("mp4upload").any(url) -> mp4uploadExtractor.videosFromUrl(url, headers, prefix = "$prefix ")
arrayOf("wishembed", "streamwish", "strwish", "wish").any(url) -> {
streamWishExtractor.videosFromUrl(url, videoNameGen = { "$prefix StreamWish:$it" })
}
arrayOf("doodstream", "dood.", "ds2play", "doods.").any(url) -> {
val url2 = url.replace("https://doodstream.com/e/", "https://d0000d.com/e/")
doodExtractor.videosFromUrl(url2, "$prefix DoodStream")
}
arrayOf("streamlare").any(url) -> streamlareExtractor.videosFromUrl(url, prefix)
arrayOf("yourupload", "upload").any(url) -> yourUploadExtractor.videoFromUrl(url, headers = headers, prefix = "$prefix ")
arrayOf("burstcloud", "burst").any(url) -> burstCloudExtractor.videoFromUrl(url, headers = headers, prefix = "$prefix ")
arrayOf("fastream").any(url) -> fastreamExtractor.videosFromUrl(url, prefix = "$prefix Fastream:")
arrayOf("upstream").any(url) -> upstreamExtractor.videosFromUrl(url, prefix = "$prefix ")
arrayOf("streamtape", "stp", "stape").any(url) -> streamTapeExtractor.videosFromUrl(url, quality = "$prefix StreamTape")
arrayOf("vembed", "guard", "listeamed", "bembed", "vgfplay").any(url) -> vidGuardExtractor.videosFromUrl(url, prefix = "$prefix ")
else -> emptyList()
} }
if (embedUrl.contains("ok.ru") || embedUrl.contains("okru")) { }.getOrNull() ?: emptyList()
OkruExtractor(client).videosFromUrl(url, prefix).also(videoList::addAll)
}
if (embedUrl.contains("filemoon") || embedUrl.contains("moonplayer")) {
FilemoonExtractor(client).videosFromUrl(url, prefix = "${prefix}Filemoon").also(videoList::addAll)
}
if (embedUrl.contains("uqload")) {
UqloadExtractor(client).videosFromUrl(url, prefix = prefix).also(videoList::addAll)
}
if (embedUrl.contains("mp4upload")) {
Mp4uploadExtractor(client).videosFromUrl(url, headers, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("wishembed") || embedUrl.contains("streamwish") ||
embedUrl.contains("strwish") || embedUrl.contains("wish")
) {
val docHeaders = headers.newBuilder()
.add("Referer", referer)
.build()
StreamWishExtractor(client, docHeaders).videosFromUrl(url, videoNameGen = { "${prefix}StreamWish:$it" }).also(videoList::addAll)
}
if (embedUrl.contains("doodstream") || embedUrl.contains("dood.")) {
DoodExtractor(client).videoFromUrl(url, "${prefix}DoodStream", false)?.let { videoList.add(it) }
}
if (embedUrl.contains("streamlare")) {
StreamlareExtractor(client).videosFromUrl(url, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("yourupload")) {
YourUploadExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("burstcloud") || embedUrl.contains("burst")) {
BurstCloudExtractor(client).videoFromUrl(url, headers = headers, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("fastream")) {
FastreamExtractor(client, headers).videosFromUrl(url, prefix = "${prefix}Fastream:").also(videoList::addAll)
}
if (embedUrl.contains("upstream")) {
UpstreamExtractor(client).videosFromUrl(url, prefix = prefix).let { videoList.addAll(it) }
}
if (embedUrl.contains("streamtape")) {
StreamTapeExtractor(client).videoFromUrl(url, quality = "${prefix}StreamTape")?.let { videoList.add(it) }
}
} catch (_: Exception) {
}
return videoList
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
@ -278,6 +268,8 @@ open class PelisForte : ConfigurableAnimeSource, AnimeHttpSource() {
fun toUriPart() = vals[state].second fun toUriPart() = vals[state].second
} }
private fun Array<String>.any(url: String): Boolean = this.any { url.contains(it, ignoreCase = true) }
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY key = PREF_LANGUAGE_KEY