fix(pt/megaflix): Fix video extractors + refactor (#2133)

This commit is contained in:
Claudemirovsky
2023-09-03 07:45:51 -03:00
committed by GitHub
parent c15316f7c6
commit 4dcb0ec9da
4 changed files with 132 additions and 137 deletions

View File

@ -7,7 +7,7 @@ ext {
extName = 'Megaflix' extName = 'Megaflix'
pkgNameSuffix = 'pt.megaflix' pkgNameSuffix = 'pt.megaflix'
extClass = '.Megaflix' extClass = '.Megaflix'
extVersionCode = 6 extVersionCode = 7
libVersion = '13' libVersion = '13'
containsNsfw = true containsNsfw = true
} }
@ -15,6 +15,7 @@ ext {
dependencies { dependencies {
implementation(project(':lib-mixdrop-extractor')) implementation(project(':lib-mixdrop-extractor'))
implementation(project(":lib-streamtape-extractor")) implementation(project(":lib-streamtape-extractor"))
implementation(project(":lib-playlist-utils"))
// for mixdrop and megaflix // for mixdrop and megaflix
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1") implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
} }

View File

@ -40,36 +40,93 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val supportsLatest = true override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
private val preferences: SharedPreferences by lazy { private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
} }
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularAnimeFromElement(element: Element): SAnime { override fun popularAnimeRequest(page: Int) = GET(baseUrl)
return SAnime.create().apply {
title = element.selectFirst("h2.entry-title")!!.text() override fun popularAnimeSelector() = "section#widget_list_movies_series-5 li > article"
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
thumbnail_url = "https:" + element.selectFirst("img")!!.attr("src") override fun popularAnimeFromElement(element: Element) = SAnime.create().apply {
} title = element.selectFirst("h2.entry-title")!!.text()
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
thumbnail_url = element.selectFirst("img")!!.attr("abs:src")
} }
override fun popularAnimeNextPageSelector() = null override fun popularAnimeNextPageSelector() = null
override fun popularAnimeRequest(page: Int) = GET(baseUrl) // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val pageType = preferences.getString(PREF_LATEST_PAGE_KEY, PREF_LATEST_PAGE_DEFAULT)!!
return GET("$baseUrl/$pageType/page/$page")
}
override fun popularAnimeSelector() = "section#widget_list_movies_series-5 li > article" override fun latestUpdatesSelector() = "li > article"
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "div.nav-links > a:containsOwn(PRÓXIMO)"
// =============================== Search ===============================
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path"))
.asObservableSuccess()
.map(::searchAnimeByPathParse)
} else {
super.fetchSearchAnime(page, query, filters)
}
}
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
return AnimesPage(listOf(details), false)
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val genre = MegaflixFilters.getGenre(filters)
GET("$baseUrl/categoria/$genre/page/$page")
}
}
override fun searchAnimeSelector() = latestUpdatesSelector()
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun getFilterList() = MegaflixFilters.FILTER_LIST
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document) = SAnime.create().apply {
setUrlWithoutDomain(document.location())
val infos = document.selectFirst("div.bd > article.post.single")!!
title = infos.selectFirst("h1.entry-title")!!.text()
thumbnail_url = "https:" + infos.selectFirst("img")!!.attr("src")
genre = infos.select("span.genres > a").eachText().joinToString()
description = infos.selectFirst("div.description")?.text()
}
// ============================== Episodes ============================== // ============================== Episodes ==============================
override fun episodeListSelector() = "li > article.episodes" override fun episodeListSelector() = "li > article.episodes"
private fun seasonListSelector() = "section.episodes div.choose-season > a" private fun seasonListSelector() = "section.episodes div.choose-season > a"
override fun episodeListParse(response: Response): List<SEpisode> { override fun episodeListParse(response: Response): List<SEpisode> {
val seasons = response.asJsoup().select(seasonListSelector()) val doc = response.use { it.asJsoup() }
val seasons = doc.select(seasonListSelector())
return when { return when {
seasons.isEmpty() -> listOf( seasons.isEmpty() -> listOf(
SEpisode.create().apply { SEpisode.create().apply {
name = "Filme" name = "Filme"
setUrlWithoutDomain(response.request.url.toString()) setUrlWithoutDomain(doc.location())
episode_number = 1F episode_number = 1F
}, },
) )
@ -79,38 +136,25 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun episodesFromSeason(seasonElement: Element): List<SEpisode> { private fun episodesFromSeason(seasonElement: Element): List<SEpisode> {
return seasonElement.attr("href").let { url -> return seasonElement.attr("href").let { url ->
client.newCall(GET(url)).execute() client.newCall(GET(url, headers)).execute()
.asJsoup() .use { it.asJsoup() }
.select(episodeListSelector()) .select(episodeListSelector())
.map(::episodeFromElement) .map(::episodeFromElement)
} }
} }
override fun episodeFromElement(element: Element): SEpisode { override fun episodeFromElement(element: Element) = SEpisode.create().apply {
return SEpisode.create().apply { name = element.selectFirst("h2.entry-title")!!.text()
name = element.selectFirst("h2.entry-title")!!.text() setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href")) episode_number = element.selectFirst("span.num-epi")
episode_number = element.selectFirst("span.num-epi") ?.text()
?.text() ?.split("x")
?.split("x") ?.let {
?.let { val season = it.first().toFloatOrNull() ?: 0F
val season = it.first().toFloatOrNull() ?: 0F val episode = it.last().toFloatOrNull() ?: 0F
val episode = it.last().toFloatOrNull() ?: 0F (season * 100F) + episode
(season * 100F) + episode }
} ?: 0F
?: 0F
}
}
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
return SAnime.create().apply {
val infos = document.selectFirst("div.bd > article.post.single")!!
title = infos.selectFirst("h1.entry-title")!!.text()
thumbnail_url = "https:" + infos.selectFirst("img")!!.attr("src")
genre = infos.select("span.genres > a").eachText().joinToString()
description = infos.selectFirst("div.description")?.text()
}
} }
// ============================ Video Links ============================= // ============================ Video Links =============================
@ -125,20 +169,22 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
?.attr("href") ?.attr("href")
?.substringAfter("token=") ?.substringAfter("token=")
?.let { Base64.decode(it, Base64.DEFAULT).let(::String) } ?.let { Base64.decode(it, Base64.DEFAULT).let(::String) }
?: return@parallelMap null ?.substringAfter("||")
?: return@parallelMap emptyList()
runCatching { getVideoList(url, language) }.getOrNull() runCatching { getVideoList(url, language) }.getOrNull() ?: emptyList()
}.filterNotNull().flatten() }.flatten()
} }
private val mixdropExtractor by lazy { MixDropExtractor(client) }
private val streamtapeExtractor by lazy { StreamTapeExtractor(client) }
private val megaflixExtractor by lazy { MegaflixExtractor(client, headers) }
private fun getVideoList(url: String, language: String): List<Video>? { private fun getVideoList(url: String, language: String): List<Video>? {
return when { return when {
"mixdrop.co" in url -> "mixdrop.co" in url -> mixdropExtractor.videoFromUrl(url, language)
MixDropExtractor(client).videoFromUrl(url, language) "streamtape.com" in url -> streamtapeExtractor.videoFromUrl(url, "StreamTape - $language")?.let(::listOf)
"streamtape.com" in url -> "mflix.vip" in url -> megaflixExtractor.videosFromUrl(url, language)
StreamTapeExtractor(client).videoFromUrl(url, "StreamTape - $language")?.let(::listOf)
"mflix.vip" in url ->
MegaflixExtractor(client).videosFromUrl(url, language)
else -> null else -> null
} }
} }
@ -153,108 +199,59 @@ class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun getFilterList() = MegaflixFilters.FILTER_LIST
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/page/$page/?s=$query")
} else {
val genre = MegaflixFilters.getGenre(filters)
GET("$baseUrl/categoria/$genre/page/$page")
}
}
override fun searchAnimeSelector() = latestUpdatesSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val path = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$baseUrl/$path"))
.asObservableSuccess()
.map(::searchAnimeByPathParse)
} else {
super.fetchSearchAnime(page, query, filters)
}
}
private fun searchAnimeByPathParse(response: Response): AnimesPage {
val details = animeDetailsParse(response.asJsoup())
details.setUrlWithoutDomain(response.request.url.toString())
return AnimesPage(listOf(details), false)
}
// =============================== Latest ===============================
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesNextPageSelector() = "div.nav-links > a:containsOwn(PRÓXIMO)"
override fun latestUpdatesRequest(page: Int): Request {
val pageType = preferences.getString(PREF_LATEST_PAGE_KEY, PREF_LATEST_PAGE_DEFAULT)!!
return GET("$baseUrl/$pageType/page/$page")
}
override fun latestUpdatesSelector() = "li > article"
// ============================== Settings ============================== // ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) { override fun setupPreferenceScreen(screen: PreferenceScreen) {
val preferredQuality = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_ENTRIES entries = PREF_QUALITY_ENTRIES
entryValues = PREF_QUALITY_ENTRIES entryValues = PREF_QUALITY_ENTRIES
setDefaultValue(PREF_QUALITY_DEFAULT) setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val selected = newValue as String
val index = findIndexOfValue(selected) val index = findIndexOfValue(selected)
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val preferredLanguage = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY key = PREF_LANGUAGE_KEY
title = PREF_LANGUAGE_TITLE title = PREF_LANGUAGE_TITLE
entries = PREF_LANGUAGE_VALUES entries = PREF_LANGUAGE_VALUES
entryValues = PREF_LANGUAGE_VALUES entryValues = PREF_LANGUAGE_VALUES
setDefaultValue(PREF_LANGUAGE_DEFAULT) setDefaultValue(PREF_LANGUAGE_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val selected = newValue as String
val index = findIndexOfValue(selected) val index = findIndexOfValue(selected)
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
val preferredLatestPage = ListPreference(screen.context).apply { ListPreference(screen.context).apply {
key = PREF_LATEST_PAGE_KEY key = PREF_LATEST_PAGE_KEY
title = PREF_LATEST_PAGE_TITLE title = PREF_LATEST_PAGE_TITLE
entries = PREF_LATEST_PAGE_ENTRIES entries = PREF_LATEST_PAGE_ENTRIES
entryValues = PREF_LATEST_PAGE_VALUES entryValues = PREF_LATEST_PAGE_VALUES
setDefaultValue(PREF_LATEST_PAGE_DEFAULT) setDefaultValue(PREF_LATEST_PAGE_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String val selected = newValue as String
val index = findIndexOfValue(selected) val index = findIndexOfValue(selected)
val entry = entryValues[index] as String val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit() preferences.edit().putString(key, entry).commit()
} }
} }.also(screen::addPreference)
screen.addPreference(preferredQuality)
screen.addPreference(preferredLanguage)
screen.addPreference(preferredLatestPage)
} }
// ============================= Utilities ============================== // ============================= Utilities ==============================
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> = private inline fun <A, B> Iterable<A>.parallelMap(crossinline f: suspend (A) -> B): List<B> =
runBlocking { runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll() map { async(Dispatchers.Default) { f(it) } }.awaitAll()
} }

View File

@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.animeextension.pt.megaflix.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET
import okhttp3.OkHttpClient
class MegaflixExtractor(private val client: OkHttpClient) {
fun videosFromUrl(url: String, lang: String = ""): List<Video> {
val unpacked = client.newCall(GET(url)).execute()
.body.string()
.let(JsUnpacker::unpackAndCombine)
?.replace("\\", "")
?: return emptyList()
val playlistUrl = unpacked.substringAfter("file':'").substringBefore("'")
val playlistBody = client.newCall(GET(playlistUrl)).execute().body.string()
val separator = "#EXT-X-STREAM-INF"
return playlistBody.substringAfter(separator).split(separator).map {
val resolution = it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore("\n") + "p"
val quality = "Megaflix($lang) - $resolution"
val path = it.substringAfter("\n").substringBefore("\n")
val videoUrl = playlistUrl.substringBeforeLast("/") + "/$path"
Video(videoUrl, quality, videoUrl)
}
}
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.animeextension.pt.megaflix.extractors
import dev.datlag.jsunpacker.JsUnpacker
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class MegaflixExtractor(private val client: OkHttpClient, private val headers: Headers) {
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
fun videosFromUrl(url: String, lang: String = ""): List<Video> {
val unpacked = client.newCall(GET(url, headers)).execute()
.body.string()
.let(JsUnpacker::unpackAndCombine)
?.replace("\\", "")
?: return emptyList()
val playlistUrl = unpacked.substringAfter("file':'").substringBefore("'")
return playlistUtils.extractFromHls(
playlistUrl,
"https://megaflix.co",
videoNameGen = { "Megaflix($lang) - $it" },
)
}
}