diff --git a/Moviehab/build.gradle.kts b/Moviehab/build.gradle.kts new file mode 100644 index 00000000..9676a397 --- /dev/null +++ b/Moviehab/build.gradle.kts @@ -0,0 +1,26 @@ +// use an integer for version numbers +version = 1 + + +cloudstream { + language = "tl" + // 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=moviehab.com&sz=%size%" +} \ No newline at end of file diff --git a/Moviehab/src/main/AndroidManifest.xml b/Moviehab/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c98063f8 --- /dev/null +++ b/Moviehab/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Moviehab/src/main/kotlin/com/hexated/Moviehab.kt b/Moviehab/src/main/kotlin/com/hexated/Moviehab.kt new file mode 100644 index 00000000..8c37067b --- /dev/null +++ b/Moviehab/src/main/kotlin/com/hexated/Moviehab.kt @@ -0,0 +1,205 @@ +package com.hexated + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.loadExtractor +import org.jsoup.nodes.Element + +class Moviehab : MainAPI() { + override var mainUrl = "https://moviehab.com" + override var name = "Moviehab" + override val hasMainPage = true + override var lang = "tl" + override val hasDownloadSupport = true + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + ) + + companion object { + private const val mainServer = "https://play.moviehab.com" + } + + override val mainPage = mainPageOf( + "$mainUrl/library/movies?sort_by=imdb_rate&page=" to "Movies by IMDB Rating", + "$mainUrl/library/shows?&sort_by=imdb_rate&page=" to "TV Shows by IMDB Rating", + "$mainUrl/library/movies?&sort_by=year&page=" to "New Movies", + "$mainUrl/library/shows?&sort_by=year&page=" to "New TV Shows", + "$mainUrl/library/movies?country=Philippines&sort_by=year&page=" to "New Philippines Movies", + "$mainUrl/library/shows?&country=Philippines&sort_by=year&page=" to "New Philippines TV Shows", + ) + + override suspend fun getMainPage( + page: Int, + request: MainPageRequest + ): HomePageResponse { + val document = app.get(request.data + page).document + val home = document.select("div.row div.col-6.col-md-4.col-lg-3").mapNotNull { + it.toSearchResult() + } + return newHomePageResponse(request.name, home) + } + + private fun Element.toSearchResult(): SearchResponse? { + val title = this.selectFirst("p.title")?.text() ?: return null + val href = this.selectFirst("div.btn-list a")!!.attr("href") + val posterUrl = fixUrlNull( + this.select("div.poster-img").attr("data-bg-multi").substringAfter("url(") + .substringBefore(")") + ) + val quality = getQualityFromString(this.select("span.badge.bg-dark-dm").text()) + return newMovieSearchResponse(title, href, TvType.Movie) { + this.posterUrl = posterUrl + this.quality = quality + } + } + + override suspend fun search(query: String): List { + val document = app.get( + "$mainUrl/search?term=$query", + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).document + return document.select("div.col-6.col-md-4.col-lg-3").mapNotNull { + it.toSearchResult() + } + } + + override suspend fun load(url: String): LoadResponse? { + val document = app.get(url).document + + val title = document.selectFirst("h1.content-title")?.text() ?: return null + val poster = fixUrlNull(document.selectFirst("img.img-fluid.w-full")?.attr("src")) + val tags = document.select("div.content div:nth-child(2) a").map { it.text() } + + val year = document.select("div.content div:nth-child(3) a").text().trim().toIntOrNull() + val tvType = if (document.select("div.card.seasons-list") + .isNullOrEmpty() + ) TvType.Movie else TvType.TvSeries + val description = document.select("div.card:contains(Storyline) p").text().trim() + val trailer = document.selectFirst("div#trailer-modal iframe")?.attr("data-src") + val rating = document.select("div.content div:nth-child(1) span").text().toRatingInt() + + return if (tvType == TvType.TvSeries) { + val episodes = + document.select("div.card.seasons-list select.episodes-select option").map { ele -> + val id = ele.attr("data-id") + val name = ele.text() + val episode = ele.attr("value").toIntOrNull() + val season = ele.parent()?.attr("id")?.filter { it.isDigit() }?.toIntOrNull() + Episode( + Episodes("$episode", "$season", id).toJson(), + name, + season, + episode, + ) + } + newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodes) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addTrailer(trailer) + } + } else { + val link = + document.select("div#direct-links-content input#link-1").attr("value").split("/") + .last() + newMovieLoadResponse(title, url, TvType.Movie, Episodes(id = link).toJson()) { + this.posterUrl = poster + this.year = year + this.plot = description + this.tags = tags + this.rating = rating + addTrailer(trailer) + } + } + } + + private suspend fun invokeLokalSource( + url: String, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + res.document.select("video#player source").attr("src").let { + val link = app.get("$mainServer/$it", referer = url).url + M3u8Helper.generateM3u8( + this.name, + link, + url + ).forEach(callback) + } + + Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub -> + subtitleCallback.invoke( + SubtitleFile( + "English", + "$mainServer/$sub" + ) + ) + } + + } + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + + val res = parseJson(data) + val url = if (res.season.isNullOrBlank()) { + "$mainUrl/embed/${res.id}" + } else { + "$mainUrl/embed/series?id=${res.id}&sea=${res.season}&epi=${res.episode}" + } + + app.get(url).document.select("div.dropdown-menu a.server").apmap { iframe -> + safeApiCall { + app.get( + "$mainUrl/ajax/get_stream_link?id=${iframe.attr("data-id")}&movie=${res.id}&is_init=false&captcha=&ref=", + referer = url, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ).parsedSafe()?.data?.link?.let { link -> + if (link.startsWith(mainServer)) { + invokeLokalSource(link, subtitleCallback, callback) + } else { + loadExtractor( + link, + "$mainUrl/", + subtitleCallback, + callback + ) + } + } + } + } + + return true + } + + private data class Source( + @JsonProperty("link") val link: String? = null, + @JsonProperty("_played") val _played: String? = null, + @JsonProperty("token") val token: String? = null, + ) + + private data class Data( + @JsonProperty("data") val data: Source? = null, + ) + + data class Episodes( + val episode: String? = null, + val season: String? = null, + val id: String? = null, + ) + +} \ No newline at end of file diff --git a/Moviehab/src/main/kotlin/com/hexated/MoviehabPlugin.kt b/Moviehab/src/main/kotlin/com/hexated/MoviehabPlugin.kt new file mode 100644 index 00000000..35ca2e25 --- /dev/null +++ b/Moviehab/src/main/kotlin/com/hexated/MoviehabPlugin.kt @@ -0,0 +1,14 @@ + +package com.hexated + +import com.lagradost.cloudstream3.plugins.CloudstreamPlugin +import com.lagradost.cloudstream3.plugins.Plugin +import android.content.Context + +@CloudstreamPlugin +class MoviehabPlugin: Plugin() { + override fun load(context: Context) { + // All providers should be added in this manner. Please don't edit the providers list directly. + registerMainAPI(Moviehab()) + } +} \ No newline at end of file