diff --git a/Moflix/build.gradle.kts b/Moflix/build.gradle.kts
new file mode 100644
index 00000000..e0197813
--- /dev/null
+++ b/Moflix/build.gradle.kts
@@ -0,0 +1,26 @@
+// use an integer for version numbers
+version = 1
+
+
+cloudstream {
+ language = "de"
+ // All of these properties are optional, you can safely remove them
+
+ // description = "Lorem Ipsum"
+ authors = listOf("Hexated")
+
+ /**
+ * Status int as the following:
+ * 0: Down
+ * 1: Ok
+ * 2: Slow
+ * 3: Beta only
+ * */
+ status = 1 // will be 3 if unspecified
+ tvTypes = listOf(
+ "TvSeries",
+ "Movie",
+ )
+
+ iconUrl = "https://www.google.com/s2/favicons?domain=moflix-stream.xyz&sz=%size%"
+}
\ No newline at end of file
diff --git a/Moflix/src/main/AndroidManifest.xml b/Moflix/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..c98063f8
--- /dev/null
+++ b/Moflix/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/Moflix/src/main/kotlin/com/hexated/Extractors.kt b/Moflix/src/main/kotlin/com/hexated/Extractors.kt
new file mode 100644
index 00000000..7100f24f
--- /dev/null
+++ b/Moflix/src/main/kotlin/com/hexated/Extractors.kt
@@ -0,0 +1,52 @@
+package com.hexated
+
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+
+class MoflixLink : MoflixClick() {
+ override val name = "MoflixLink"
+ override val mainUrl = "https://moflix-stream.link"
+}
+
+class MoflixFans : MoflixClick() {
+ override val name = "MoflixFans"
+ override val mainUrl = "https://moflix-stream.fans"
+}
+
+class Highstream : MoflixClick() {
+ override val name = "Highstream"
+ override val mainUrl = "https://highstream.tv"
+}
+
+open class MoflixClick : ExtractorApi() {
+ override val name = "MoflixClick"
+ override val mainUrl = "https://moflix-stream.click"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val response = app.get(url, referer = referer)
+ val script = if (!getPacked(response.text).isNullOrEmpty()) {
+ getAndUnpack(response.text)
+ } else {
+ response.document.selectFirst("script:containsData(sources:)")?.data()
+ }
+ val m3u8 = Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1)
+ callback.invoke(
+ ExtractorLink(
+ name,
+ name,
+ m3u8 ?: return,
+ "$mainUrl/",
+ Qualities.Unknown.value,
+ INFER_TYPE
+ )
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/Moflix/src/main/kotlin/com/hexated/Moflix.kt b/Moflix/src/main/kotlin/com/hexated/Moflix.kt
new file mode 100644
index 00000000..4d569d08
--- /dev/null
+++ b/Moflix/src/main/kotlin/com/hexated/Moflix.kt
@@ -0,0 +1,326 @@
+package com.hexated
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
+import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
+import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.AppUtils.toJson
+import kotlin.math.roundToInt
+
+class Moflix : MainAPI() {
+ override var name = "Moflix"
+ override var mainUrl = "https://moflix-stream.xyz"
+ override var lang = "de"
+ override val hasMainPage = true
+ override val hasQuickSearch = true
+ override val supportedTypes = setOf(TvType.TvSeries, TvType.Movie)
+
+ companion object {
+ fun getType(isSeries: Boolean?): TvType {
+ return when (isSeries) {
+ true -> TvType.TvSeries
+ else -> TvType.Movie
+ }
+ }
+
+ fun getStatus(t: String?): ShowStatus {
+ return when (t) {
+ "ongoing" -> ShowStatus.Ongoing
+ else -> ShowStatus.Completed
+ }
+ }
+ }
+
+ override val mainPage = mainPageOf(
+ "351" to "Kürzlich hinzugefügt",
+ "345" to "Movie-Datenbank",
+ "352" to "Angesagte Serien",
+ "358" to "Kinder & Familien",
+ )
+
+ override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
+ val order = if (request.data == "345") "popularity:desc" else "channelables.order:asc"
+ val home = app.get(
+ "$mainUrl/api/v1/channel/${request.data}?returnContentOnly=true&restriction=&order=$order&paginate=simple&perPage=50&query=&page=$page",
+ referer = "$mainUrl/"
+ ).parsedSafe()?.pagination?.data?.mapNotNull { it.toSearchResponse() }
+ ?: emptyList()
+
+ return newHomePageResponse(request.name, home)
+ }
+
+ private fun Data.toSearchResponse(): SearchResponse? {
+ return newTvSeriesSearchResponse(
+ this.name ?: return null,
+ "${this.id}",
+ TvType.TvSeries,
+ false
+ ) {
+ posterUrl = this@toSearchResponse.poster?.compress()
+ }
+ }
+
+ override suspend fun quickSearch(query: String): List? = search(query)
+
+ override suspend fun search(query: String): List? {
+ return app.get("$mainUrl/api/v1/search/$query?loader=searchPage", referer = "$mainUrl/")
+ .parsedSafe()?.results?.mapNotNull { it.toSearchResponse() }
+ }
+
+ override suspend fun load(url: String): LoadResponse {
+ val res = app.get(
+ "$mainUrl/api/v1/titles/${url.removePrefix("$mainUrl/")}?loader=titlePage",
+ referer = "$mainUrl/"
+ )
+ .parsedSafe()
+
+ val id = res?.title?.id
+ val title = res?.title?.name ?: ""
+ val poster = res?.title?.poster
+ val backdrop = res?.title?.backdrop
+ val tags = res?.title?.keywords?.mapNotNull { it.displayName }
+ val year = res?.title?.year
+ val isSeries = res?.title?.isSeries
+ val type = getType(isSeries)
+ val description = res?.title?.description
+ val trailers = res?.title?.videos?.filter { it.category.equals("trailer", true) }
+ ?.mapNotNull { it.src }
+ val rating = "${res?.title?.rating}".toRatingInt()
+ val actors = res?.credits?.actors?.mapNotNull {
+ ActorData(
+ Actor(it.name ?: return@mapNotNull null, it.poster),
+ roleString = it.pivot?.character
+ )
+ }
+ val recommendations = app.get("$mainUrl/api/v1/titles/$id/related", referer = "$mainUrl/")
+ .parsedSafe()?.titles?.mapNotNull { it.toSearchResponse() }
+
+ return if (type == TvType.TvSeries) {
+ val episodes = res?.seasons?.data?.mapNotNull { season ->
+ app.get(
+ "$mainUrl/api/v1/titles/${res.title?.id}/seasons/${season.number}?loader=seasonPage",
+ referer = "$mainUrl/"
+ ).parsedSafe()?.episodes?.data?.map { episode ->
+ val status =
+ if (episode.status.equals("upcoming", true)) " • [UPCOMING]" else ""
+ Episode(
+ LoadData(
+ id,
+ episode.seasonNumber,
+ episode.episodeNumber,
+ res.title?.isSeries
+ ).toJson(),
+ episode.name + status,
+ episode.seasonNumber,
+ episode.episodeNumber,
+ episode.poster,
+ episode.rating?.times(10)?.roundToInt(),
+ episode.description,
+ ).apply {
+ this.addDate(episode.releaseDate?.substringBefore("T"))
+ }
+ }
+ }?.flatten() ?: emptyList()
+ newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) {
+ this.posterUrl = poster
+ this.backgroundPosterUrl = backdrop
+ this.year = year
+ this.showStatus = getStatus(res?.title?.status)
+ this.plot = description
+ this.tags = tags
+ this.rating = rating
+ this.actors = actors
+ this.recommendations = recommendations
+ addTrailer(trailers)
+ addImdbId(res?.title?.imdbId)
+ addTMDbId(res?.title?.tmdbId)
+ }
+ } else {
+ val urls = res?.title?.videos?.filter { it.category.equals("full", true) }
+
+ newMovieLoadResponse(
+ title,
+ url,
+ TvType.Movie,
+ LoadData(isSeries = isSeries, urls = urls)
+ ) {
+ this.posterUrl = poster
+ this.backgroundPosterUrl = backdrop
+ this.year = year
+ this.comingSoon = res?.title?.status.equals("upcoming", true)
+ this.plot = description
+ this.tags = tags
+ this.rating = rating
+ this.actors = actors
+ this.recommendations = recommendations
+ addTrailer(trailers)
+ addImdbId(res?.title?.imdbId)
+ addTMDbId(res?.title?.tmdbId)
+ }
+ }
+ }
+
+ override suspend fun loadLinks(
+ data: String,
+ isCasting: Boolean,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ): Boolean {
+
+ val json = parseJson(data)
+
+ val iframes = if (json.isSeries == true) {
+ app.get(
+ "$mainUrl/api/v1/titles/${json.id}/seasons/${json.season}/episodes/${json.episode}?loader=episodePage",
+ referer = "$mainUrl/"
+ ).parsedSafe()?.episode?.videos?.filter { it.category.equals("full", true) }
+ } else {
+ json.urls
+ }
+
+ iframes?.apmap { iframe ->
+ loadCustomExtractor(
+ iframe.src ?: return@apmap,
+ "$mainUrl/",
+ subtitleCallback,
+ callback,
+ iframe.quality?.filter { it.isDigit() }?.toIntOrNull()
+ )
+ }
+
+ return true
+ }
+
+ private suspend fun loadCustomExtractor(
+ url: String,
+ referer: String? = null,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit,
+ quality: Int? = null,
+ ) {
+ loadExtractor(url, referer, subtitleCallback) { link ->
+ if (link.quality == Qualities.Unknown.value) {
+ callback.invoke(
+ ExtractorLink(
+ link.source,
+ link.name,
+ link.url,
+ link.referer,
+ quality ?: link.quality,
+ link.type,
+ link.headers,
+ link.extractorData
+ )
+ )
+ }
+ }
+ }
+
+ private fun String.compress(): String {
+ return this.replace("/original/", "/w500/")
+ }
+
+ data class LoadData(
+ val id: Int? = null,
+ val season: Int? = null,
+ val episode: Int? = null,
+ val isSeries: Boolean? = null,
+ val urls: List? = listOf(),
+ )
+
+ data class Responses(
+ @JsonProperty("pagination") val pagination: Pagination? = null,
+ @JsonProperty("title") val title: Title? = null,
+ @JsonProperty("credits") val credits: Credits? = null,
+ @JsonProperty("seasons") val seasons: Seasons? = null,
+ @JsonProperty("episodes") val episodes: Episodes? = null,
+ @JsonProperty("titles") val titles: ArrayList? = arrayListOf(),
+ @JsonProperty("results") val results: ArrayList? = arrayListOf(),
+ )
+
+ data class Seasons(
+ @JsonProperty("data") val data: ArrayList? = arrayListOf(),
+ ) {
+ data class Data(
+ @JsonProperty("id") val id: Int? = null,
+ @JsonProperty("number") val number: Int? = null,
+ @JsonProperty("poster") val poster: String? = null,
+ @JsonProperty("release_date") val releaseDate: String? = null,
+ )
+ }
+
+ data class Episodes(
+ @JsonProperty("data") val data: ArrayList? = arrayListOf(),
+ @JsonProperty("episode") val episode: Data? = null,
+ ) {
+ data class Data(
+ @JsonProperty("id") val id: Int? = null,
+ @JsonProperty("name") val name: String? = null,
+ @JsonProperty("description") val description: String? = null,
+ @JsonProperty("season_number") val seasonNumber: Int? = null,
+ @JsonProperty("episode_number") val episodeNumber: Int? = null,
+ @JsonProperty("rating") val rating: Float? = null,
+ @JsonProperty("poster") val poster: String? = null,
+ @JsonProperty("release_date") val releaseDate: String? = null,
+ @JsonProperty("status") val status: String? = null,
+ @JsonProperty("videos") val videos: ArrayList? = arrayListOf(),
+ )
+ }
+
+ data class Pagination(
+ @JsonProperty("data") val data: ArrayList? = arrayListOf(),
+ )
+
+ data class Data(
+ @JsonProperty("id") val id: Any? = null,
+ @JsonProperty("name") val name: String? = null,
+ @JsonProperty("poster") val poster: String? = null,
+ @JsonProperty("backdrop") val backdrop: String? = null,
+ )
+
+ data class Credits(
+ @JsonProperty("actors") val actors: ArrayList? = arrayListOf(),
+ ) {
+ data class Actors(
+ @JsonProperty("name") val name: String? = null,
+ @JsonProperty("poster") val poster: String? = null,
+ @JsonProperty("pivot") val pivot: Pivot? = null,
+ ) {
+ data class Pivot(
+ @JsonProperty("character") val character: String? = null,
+ )
+ }
+ }
+
+ data class Videos(
+ @JsonProperty("category") val category: String? = null,
+ @JsonProperty("src") val src: String? = null,
+ @JsonProperty("quality") val quality: String? = null,
+ )
+
+ data class Title(
+ @JsonProperty("id") val id: Int? = null,
+ @JsonProperty("name") val name: String? = null,
+ @JsonProperty("release_date") val releaseDate: String? = null,
+ @JsonProperty("year") val year: Int? = null,
+ @JsonProperty("poster") val poster: String? = null,
+ @JsonProperty("backdrop") val backdrop: String? = null,
+ @JsonProperty("description") val description: String? = null,
+ @JsonProperty("certification") val certification: String? = null,
+ @JsonProperty("rating") val rating: Float? = null,
+ @JsonProperty("imdb_id") val imdbId: String? = null,
+ @JsonProperty("tmdb_id") val tmdbId: String? = null,
+ @JsonProperty("status") val status: String? = null,
+ @JsonProperty("is_series") val isSeries: Boolean? = null,
+ @JsonProperty("videos") val videos: ArrayList? = arrayListOf(),
+ @JsonProperty("keywords") val keywords: ArrayList? = arrayListOf(),
+ ) {
+ data class Keywords(
+ @JsonProperty("display_name") val displayName: String? = null,
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/Moflix/src/main/kotlin/com/hexated/MoflixPlugin.kt b/Moflix/src/main/kotlin/com/hexated/MoflixPlugin.kt
new file mode 100644
index 00000000..7e15db0a
--- /dev/null
+++ b/Moflix/src/main/kotlin/com/hexated/MoflixPlugin.kt
@@ -0,0 +1,18 @@
+
+package com.hexated
+
+import com.lagradost.cloudstream3.plugins.CloudstreamPlugin
+import com.lagradost.cloudstream3.plugins.Plugin
+import android.content.Context
+
+@CloudstreamPlugin
+class MoflixPlugin: Plugin() {
+ override fun load(context: Context) {
+ // All providers should be added in this manner. Please don't edit the providers list directly.
+ registerMainAPI(Moflix())
+ registerExtractorAPI(MoflixClick())
+ registerExtractorAPI(Highstream())
+ registerExtractorAPI(MoflixFans())
+ registerExtractorAPI(MoflixLink())
+ }
+}
\ No newline at end of file