From b351a9e62d858544c7e4ec49b073ae094da52c8d Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 15 Jan 2024 13:24:24 +0700 Subject: [PATCH] added Moflix #347 --- Moflix/build.gradle.kts | 26 ++ Moflix/src/main/AndroidManifest.xml | 2 + .../src/main/kotlin/com/hexated/Extractors.kt | 52 +++ Moflix/src/main/kotlin/com/hexated/Moflix.kt | 326 ++++++++++++++++++ .../main/kotlin/com/hexated/MoflixPlugin.kt | 18 + 5 files changed, 424 insertions(+) create mode 100644 Moflix/build.gradle.kts create mode 100644 Moflix/src/main/AndroidManifest.xml create mode 100644 Moflix/src/main/kotlin/com/hexated/Extractors.kt create mode 100644 Moflix/src/main/kotlin/com/hexated/Moflix.kt create mode 100644 Moflix/src/main/kotlin/com/hexated/MoflixPlugin.kt 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