New source: Megaflix(pt) (#1493)

* feat: Create megaflix base

* feat: Implement popular series page

* feat: Implement latest updates page

* feat: Implement search page

* feat: Implement anime details page

* feat: Implement episode list

* fix: Fix URL intent handler

* feat: Add search filters

* feat: Implement video list extraction

* feat: Add MegaFlix extractor

* feat: Add more preferences

* chore: Add containsNsfw property
This commit is contained in:
Claudemirovsky
2023-04-15 06:07:51 -03:00
committed by GitHub
parent ccd6307247
commit 8220525295
12 changed files with 497 additions and 0 deletions

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.animeextension">
<application>
<activity
android:name=".pt.megaflix.MegaflixUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="megaflix.co"
android:pathPattern="/..*/..*/"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,22 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
ext {
extName = 'Megaflix'
pkgNameSuffix = 'pt.megaflix'
extClass = '.Megaflix'
extVersionCode = 1
containsNsfw = true
}
dependencies {
implementation(project(":lib-fembed-extractor"))
implementation(project(":lib-streamsb-extractor"))
implementation(project(":lib-streamtape-extractor"))
// for mixdrop and megaflix
implementation("dev.datlag.jsunpacker:jsunpacker:1.0.1")
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -0,0 +1,290 @@
package eu.kanade.tachiyomi.animeextension.pt.megaflix
import android.app.Application
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.pt.megaflix.extractors.MegaflixExtractor
import eu.kanade.tachiyomi.animeextension.pt.megaflix.extractors.MixDropExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
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.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.fembedextractor.FembedExtractor
import eu.kanade.tachiyomi.lib.streamsbextractor.StreamSBExtractor
import eu.kanade.tachiyomi.lib.streamtapeextractor.StreamTapeExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class Megaflix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "Megaflix"
override val baseUrl = "https://megaflix.co"
override val lang = "pt-BR"
override val supportsLatest = true
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// ============================== Popular ===============================
override fun popularAnimeFromElement(element: Element): SAnime {
return SAnime.create().apply {
title = element.selectFirst("h2.entry-title")!!.text()
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
thumbnail_url = "https:" + element.selectFirst("img")!!.attr("src")
}
}
override fun popularAnimeNextPageSelector() = null
override fun popularAnimeRequest(page: Int) = GET(baseUrl)
override fun popularAnimeSelector() = "section#widget_list_movies_series-5 li > article"
// ============================== Episodes ==============================
override fun episodeFromElement(element: Element): SEpisode {
return SEpisode.create().apply {
name = element.selectFirst("h2.entry-title")!!.text()
setUrlWithoutDomain(element.selectFirst("a.lnk-blk")!!.attr("href"))
episode_number = element.selectFirst("span.num-epi")
?.text()
?.substringAfter("x")
?.toFloatOrNull()
?: 0F
}
}
override fun episodeListSelector() = "li > article.episodes"
override fun episodeListParse(response: Response): List<SEpisode> {
val items = response.asJsoup().select(episodeListSelector())
return when {
items.isEmpty() -> listOf(
SEpisode.create().apply {
name = "Filme"
setUrlWithoutDomain(response.request.url.toString())
episode_number = 1F
},
)
else -> items.map(::episodeFromElement)
}
}
// =========================== 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 =============================
override fun videoListParse(response: Response): List<Video> {
val items = response.asJsoup().select(videoListSelector())
return items
.parallelMap { element ->
val language = element.text().substringAfter("-")
val id = element.attr("href")
val url = element.parents().get(5)
?.selectFirst("div$id a")
?.attr("href")
?.substringAfter("token=")
?.let { Base64.decode(it, Base64.DEFAULT).let(::String) }
?: return@parallelMap null
runCatching { getVideoList(url, language) }.getOrNull()
}.filterNotNull().flatten()
}
private fun getVideoList(url: String, language: String): List<Video>? {
return when {
"mixdrop.co" in url ->
MixDropExtractor(client).videoFromUrl(url, language)?.let(::listOf)
"fembed.com" in url ->
FembedExtractor(client).videosFromUrl(url, language)
"streamtape.com" in url ->
StreamTapeExtractor(client).videoFromUrl(url, "StreamTape - $language")?.let(::listOf)
"watchsb.com" in url ->
StreamSBExtractor(client).videosFromUrl(url, headers, suffix = language)
"mflix.vip" in url ->
MegaflixExtractor(client).videosFromUrl(url, language)
else -> null
}
}
override fun videoListSelector() = "aside.video-options li a"
override fun videoFromElement(element: Element): Video {
TODO("Not yet implemented")
}
override fun videoUrlParse(document: Document): String {
TODO("Not yet implemented")
}
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector() = latestUpdatesNextPageSelector()
override fun getFilterList() = MegaflixFilters.filterList
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 ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val preferredQuality = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_VALUES
entryValues = PREF_QUALITY_VALUES
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val preferredLanguage = ListPreference(screen.context).apply {
key = PREF_LANGUAGE_KEY
title = PREF_LANGUAGE_TITLE
entries = PREF_LANGUAGE_VALUES
entryValues = PREF_LANGUAGE_VALUES
setDefaultValue(PREF_LANGUAGE_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()
}
}
val preferredLatestPage = ListPreference(screen.context).apply {
key = PREF_LATEST_PAGE_KEY
title = PREF_LATEST_PAGE_TITLE
entries = PREF_LATEST_PAGE_ENTRIES
entryValues = PREF_LATEST_PAGE_VALUES
setDefaultValue(PREF_LATEST_PAGE_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()
}
}
screen.addPreference(preferredQuality)
screen.addPreference(preferredLanguage)
screen.addPreference(preferredLatestPage)
}
// ============================= Utilities ==============================
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
val lang = preferences.getString(PREF_LANGUAGE_KEY, PREF_LANGUAGE_DEFAULT)!!
return sortedWith(
compareBy(
{ it.quality.contains(quality) },
{ it.quality.contains(lang) },
),
).reversed()
}
companion object {
const val PREFIX_SEARCH = "path:"
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Qualidade preferida"
private const val PREF_QUALITY_DEFAULT = "720p"
private val PREF_QUALITY_VALUES = arrayOf("360p", "480p", "720p", "1080p")
private const val PREF_LANGUAGE_KEY = "pref_language"
private const val PREF_LANGUAGE_DEFAULT = "Legendado"
private const val PREF_LANGUAGE_TITLE = "Língua/tipo preferido"
private val PREF_LANGUAGE_VALUES = arrayOf("Legendado", "Dublado")
private const val PREF_LATEST_PAGE_KEY = "pref_latest_page"
private const val PREF_LATEST_PAGE_DEFAULT = "series"
private const val PREF_LATEST_PAGE_TITLE = "Página de últimos adicionados"
private val PREF_LATEST_PAGE_ENTRIES = arrayOf(
"Filmes",
"Séries",
)
private val PREF_LATEST_PAGE_VALUES = arrayOf(
"filmes",
"series",
)
}
}

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.animeextension.pt.megaflix
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object MegaflixFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return this.first { it is R }.let {
(it as QueryPartFilter).toQueryPart()
}
}
class GenreFilter : QueryPartFilter("Gênero", MegaflixFiltersData.genres)
val filterList = AnimeFilterList(
AnimeFilter.Header(MegaflixFiltersData.IGNORE_SEARCH_MSG),
GenreFilter(),
)
fun getGenre(filters: AnimeFilterList) = filters.asQueryPart<GenreFilter>()
private object MegaflixFiltersData {
const val IGNORE_SEARCH_MSG = "NOTA: O filtro é IGNORADO ao usar a pesquisa."
val genres = arrayOf(
Pair("Animação", "animacao"),
Pair("Aventura", "aventura"),
Pair("Ação", "acao"),
Pair("Biografia", "biografia"),
Pair("Comédia", "comedia"),
Pair("Crime", "crime"),
Pair("Documentário", "documentario"),
Pair("Drama", "drama"),
Pair("Esporte", "esporte"),
Pair("Família", "familia"),
Pair("Fantasia", "fantasia"),
Pair("Faroeste", "faroeste"),
Pair("Ficção científica", "ficcao-cientifica"),
Pair("Guerra", "guerra"),
Pair("História", "historia"),
Pair("Mistério", "misterio"),
Pair("Musical", "musical"),
Pair("Música", "musica"),
Pair("Romance", "romance"),
Pair("Show", "show"),
Pair("Terror", "terror"),
Pair("Thriller", "thriller"),
)
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.animeextension.pt.megaflix
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://megaflix.co/<type>/<item> intents
* and redirects them to the main Aniyomi process.
*/
class MegaflixUrlActivity : Activity() {
private val TAG = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val path = "${pathSegments[0]}/${pathSegments[1]}"
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.ANIMESEARCH"
putExtra("query", "${Megaflix.PREFIX_SEARCH}$path")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.toString())
}
} else {
Log.e(TAG, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -0,0 +1,31 @@
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.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.OkHttpClient
class MixDropExtractor(private val client: OkHttpClient) {
fun videoFromUrl(url: String, lang: String = ""): Video? {
val doc = client.newCall(GET(url)).execute().asJsoup()
val unpacked = doc.selectFirst("script:containsData(eval):containsData(MDCore)")
?.data()
?.let(JsUnpacker::unpackAndCombine)
?: return null
val videoUrl = "https:" + unpacked.substringAfter("Core.wurl=\"")
.substringBefore("\"")
val quality = ("MixDrop").let {
when {
lang.isNotBlank() -> "$it($lang)"
else -> it
}
}
return Video(videoUrl, quality, videoUrl)
}
}