2024-01-15 06:24:24 +00:00
|
|
|
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
|
2024-01-16 10:04:44 +00:00
|
|
|
import org.jsoup.Jsoup
|
2024-01-15 06:24:24 +00:00
|
|
|
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<Responses>()?.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<SearchResponse>? = search(query)
|
|
|
|
|
|
|
|
override suspend fun search(query: String): List<SearchResponse>? {
|
|
|
|
return app.get("$mainUrl/api/v1/search/$query?loader=searchPage", referer = "$mainUrl/")
|
|
|
|
.parsedSafe<Responses>()?.results?.mapNotNull { it.toSearchResponse() }
|
|
|
|
}
|
|
|
|
|
|
|
|
override suspend fun load(url: String): LoadResponse {
|
|
|
|
val res = app.get(
|
2024-01-16 10:04:44 +00:00
|
|
|
"$mainUrl/api/v1/titles/${url.fixId()}?loader=titlePage",
|
2024-01-15 06:24:24 +00:00
|
|
|
referer = "$mainUrl/"
|
2024-01-15 07:36:56 +00:00
|
|
|
).parsedSafe<Responses>()
|
2024-01-15 06:24:24 +00:00
|
|
|
|
2024-01-16 10:04:44 +00:00
|
|
|
val uri = Jsoup.parse(res?.seo.toString()).selectFirst("link[rel=canonical]")?.attr("href")
|
2024-01-15 06:24:24 +00:00
|
|
|
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
|
2024-01-16 10:04:44 +00:00
|
|
|
val certification = res?.title?.certification
|
|
|
|
val duration = res?.title?.runtime
|
2024-01-15 06:24:24 +00:00
|
|
|
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<Responses>()?.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<Responses>()?.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()
|
2024-01-16 10:04:44 +00:00
|
|
|
newTvSeriesLoadResponse(title, uri ?: url, TvType.TvSeries, episodes) {
|
2024-01-15 06:24:24 +00:00
|
|
|
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
|
2024-01-16 10:04:44 +00:00
|
|
|
this.duration = duration
|
2024-01-15 06:24:24 +00:00
|
|
|
this.recommendations = recommendations
|
2024-01-16 10:04:44 +00:00
|
|
|
this.contentRating = certification
|
2024-01-15 06:24:24 +00:00
|
|
|
addTrailer(trailers)
|
|
|
|
addImdbId(res?.title?.imdbId)
|
|
|
|
addTMDbId(res?.title?.tmdbId)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
val urls = res?.title?.videos?.filter { it.category.equals("full", true) }
|
|
|
|
|
|
|
|
newMovieLoadResponse(
|
|
|
|
title,
|
2024-01-16 10:04:44 +00:00
|
|
|
uri ?: url,
|
2024-01-15 06:24:24 +00:00
|
|
|
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
|
2024-01-16 10:04:44 +00:00
|
|
|
this.duration = duration
|
2024-01-15 06:24:24 +00:00
|
|
|
this.recommendations = recommendations
|
2024-01-16 10:04:44 +00:00
|
|
|
this.contentRating = certification
|
2024-01-15 06:24:24 +00:00
|
|
|
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<LoadData>(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<Episodes>()?.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
|
|
|
|
}
|
|
|
|
|
2024-01-16 10:04:44 +00:00
|
|
|
private fun String.fixId(): String {
|
|
|
|
val chunk = "/titles/"
|
|
|
|
return if (this.contains(chunk)) this.substringAfter(chunk)
|
|
|
|
.substringBefore("/") else this.substringAfterLast("/")
|
|
|
|
}
|
|
|
|
|
2024-01-15 06:24:24 +00:00
|
|
|
private suspend fun loadCustomExtractor(
|
|
|
|
url: String,
|
|
|
|
referer: String? = null,
|
|
|
|
subtitleCallback: (SubtitleFile) -> Unit,
|
|
|
|
callback: (ExtractorLink) -> Unit,
|
|
|
|
quality: Int? = null,
|
|
|
|
) {
|
|
|
|
loadExtractor(url, referer, subtitleCallback) { link ->
|
2024-01-15 07:36:56 +00:00
|
|
|
if (link.quality == Qualities.Unknown.value || !link.isM3u8) {
|
2024-01-15 06:24:24 +00:00
|
|
|
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<Videos>? = listOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Responses(
|
|
|
|
@JsonProperty("pagination") val pagination: Pagination? = null,
|
|
|
|
@JsonProperty("title") val title: Title? = null,
|
2024-01-16 10:04:44 +00:00
|
|
|
@JsonProperty("seo") val seo: String? = null,
|
2024-01-15 06:24:24 +00:00
|
|
|
@JsonProperty("credits") val credits: Credits? = null,
|
|
|
|
@JsonProperty("seasons") val seasons: Seasons? = null,
|
|
|
|
@JsonProperty("episodes") val episodes: Episodes? = null,
|
|
|
|
@JsonProperty("titles") val titles: ArrayList<Data>? = arrayListOf(),
|
|
|
|
@JsonProperty("results") val results: ArrayList<Data>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
|
|
|
|
data class Seasons(
|
|
|
|
@JsonProperty("data") val data: ArrayList<Data>? = 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<Data>? = 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<Videos>? = arrayListOf(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
data class Pagination(
|
|
|
|
@JsonProperty("data") val data: ArrayList<Data>? = 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<Actors>? = 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,
|
2024-01-16 10:04:44 +00:00
|
|
|
@JsonProperty("runtime") val runtime: Int? = null,
|
2024-01-15 06:24:24 +00:00
|
|
|
@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<Videos>? = arrayListOf(),
|
|
|
|
@JsonProperty("keywords") val keywords: ArrayList<Keywords>? = arrayListOf(),
|
|
|
|
) {
|
|
|
|
data class Keywords(
|
|
|
|
@JsonProperty("display_name") val displayName: String? = null,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|