diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 859c1bad..e29e99d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.animeproviders.* +import com.lagradost.cloudstream3.liveproviders.EjaTv import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider import com.lagradost.cloudstream3.movieproviders.* import com.lagradost.cloudstream3.mvvm.logError @@ -65,6 +66,7 @@ object APIHolder { FrenchStreamProvider(), AsianLoadProvider(), AsiaFlixProvider(), // This should be removed in favor of asianembed.io, same source + EjaTv(), BflixProvider(), FmoviesToProvider(), SflixProProvider(), @@ -600,11 +602,16 @@ enum class TvType { Torrent, Documentary, AsianDrama, + Live, } // IN CASE OF FUTURE ANIME MOVIE OR SMTH fun TvType.isMovieType(): Boolean { - return this == TvType.Movie || this == TvType.AnimeMovie || this == TvType.Torrent + return this == TvType.Movie || this == TvType.AnimeMovie || this == TvType.Torrent || this == TvType.Live +} + +fun TvType.isLiveStream(): Boolean { + return this == TvType.Live } // returns if the type has an anime opening @@ -1117,6 +1124,28 @@ suspend fun MainAPI.newAnimeLoadResponse( return builder } +data class LiveStreamLoadResponse( + override var name: String, + override var url: String, + override var apiName: String, + var dataUrl: String, + + override var posterUrl: String? = null, + override var year: Int? = null, + override var plot: String? = null, + + override var type: TvType = TvType.Live, + override var rating: Int? = null, + override var tags: List? = null, + override var duration: Int? = null, + override var trailers: List? = null, + override var recommendations: List? = null, + override var actors: List? = null, + override var comingSoon: Boolean = false, + override var syncData: MutableMap = mutableMapOf(), + override var posterHeaders: Map? = null, +) : LoadResponse + data class MovieLoadResponse( override var name: String, override var url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/liveproviders/EjaTv.kt b/app/src/main/java/com/lagradost/cloudstream3/liveproviders/EjaTv.kt new file mode 100644 index 00000000..513b3303 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/liveproviders/EjaTv.kt @@ -0,0 +1,119 @@ +package com.lagradost.cloudstream3.liveproviders + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.* +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.Qualities +import org.jsoup.nodes.Element +import org.jsoup.select.Elements + +class EjaTv : MainAPI() { + override var mainUrl = "https://eja.tv/" + override var name = "Eja.tv" + + // Universal language? + override var lang = "en" + override val hasDownloadSupport = false + + override val hasMainPage = true + override val supportedTypes = setOf( + TvType.Live + ) + + private fun Element.toSearchResponse(): MovieSearchResponse? { + val link = this.select("div.alternative a").last() ?: return null + val href = fixUrl(link.attr("href")) + val img = this.select("div.thumb img") + + return MovieSearchResponse( + // Kinda hack way to get the title + img.attr("alt").replaceFirst("Watch ", ""), + href, + this@EjaTv.name, + TvType.Live, + fixUrl(img.attr("src")) + ) + } + + override suspend fun getMainPage(): HomePageResponse { + // Maybe this based on app language or as setting? + val language = "English" + val dataMap = mapOf( + "News" to mapOf("language" to language, "category" to "News"), + "Sports" to mapOf("language" to language, "category" to "Sports"), + "Entertainment" to mapOf("language" to language, "category" to "Entertainment") + ) + return HomePageResponse(dataMap.apmap { (title, data) -> + val document = app.post(mainUrl, data = data).document + val shows = document.select("div.card-body").mapNotNull { + it.toSearchResponse() + } + HomePageList( + title, + shows + ) + }) + } + + override suspend fun search(query: String): List { + return app.post( + mainUrl, data = mapOf("search" to query) + ).document.select("div.card-body").mapNotNull { + it.toSearchResponse() + } + } + + override suspend fun load(url: String): LoadResponse { + val doc = app.get(url).document + val sections = + doc.select("li.list-group-item.d-flex.justify-content-between.align-items-center") + + val link = fixUrl(sections.last()!!.select("a").attr("href")) + + val title = doc.select("h5.text-center").text() + val poster = fixUrl(doc.select("p.text-center img").attr("src")) + + val summary = sections.subList(0, 3).joinToString("
") { + val innerText = it.ownText().trim() + val outerText = it.select("a").text().trim() + "$innerText: $outerText" + } + + return LiveStreamLoadResponse( + title, + url, + this.name, + LoadData(link, title).toJson(), + poster, + plot = summary + ) + } + + data class LoadData( + val url: String, + val title: String + ) + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + val loadData = parseJson(data) + + callback.invoke( + ExtractorLink( + this.name, + loadData.title, + loadData.url, + "", + Qualities.Unknown.value, + isM3u8 = true + ) + ) + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 332bb4f6..3d7e5d72 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -164,6 +164,7 @@ class HomeFragment : Fragment() { docs: MaterialButton?, movies: MaterialButton?, asian: MaterialButton?, + livestream: MaterialButton?, ): List>> { return listOf( Pair(anime, listOf(TvType.Anime, TvType.OVA, TvType.AnimeMovie)), @@ -172,6 +173,7 @@ class HomeFragment : Fragment() { Pair(docs, listOf(TvType.Documentary)), Pair(movies, listOf(TvType.Movie, TvType.Torrent)), Pair(asian, listOf(TvType.AsianDrama)), + Pair(livestream, listOf(TvType.Live)), ) } @@ -205,10 +207,11 @@ class HomeFragment : Fragment() { val docs = dialog.findViewById(R.id.home_select_documentaries) val movies = dialog.findViewById(R.id.home_select_movies) val asian = dialog.findViewById(R.id.home_select_asian) + val livestream = dialog.findViewById(R.id.home_select_livestreams) val cancelBtt = dialog.findViewById(R.id.cancel_btt) val applyBtt = dialog.findViewById(R.id.apply_btt) - val pairList = getPairList(anime, cartoons, tvs, docs, movies, asian) + val pairList = getPairList(anime, cartoons, tvs, docs, movies, asian, livestream) cancelBtt?.setOnClickListener { dialog.dismissSafe() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 276904f7..2052a370 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -817,6 +817,7 @@ class CS3IPlayer : IPlayer { // Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period // If you can get the total time that'd be better, but this is already niche. && exoPlayer?.currentTimeline?.periodCount == 1 + && exoPlayer?.isCurrentMediaItemLive != true } ?: false if (invalid) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index b56e6f79..c9406631 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -364,7 +364,8 @@ class GeneratorPlayer : FullScreenPlayer() { activity?.showDialog( languages.map { it.languageName }, lang639_1.indexOf(currentLanguageTwoLetters), - view?.context?.getString(R.string.subs_subtitle_languages) ?: return@setOnClickListener, + view?.context?.getString(R.string.subs_subtitle_languages) + ?: return@setOnClickListener, true, { } ) { index -> @@ -721,6 +722,9 @@ class GeneratorPlayer : FullScreenPlayer() { var maxEpisodeSet: Int? = null override fun playerPositionChanged(posDur: Pair) { + // Don't save livestream data + if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return + val (position, duration) = posDur viewModel.getId()?.let { DataStoreHelper.setViewPos(it, position, duration) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index c4e31f63..3a69925d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -303,6 +303,7 @@ class ResultFragment : ResultTrailerPlayer() { TvType.Torrent -> "Torrent" TvType.Documentary -> "Documentaries" TvType.AsianDrama -> "AsianDrama" + TvType.Live -> "LiveStreams" } } @@ -1665,10 +1666,15 @@ class ResultFragment : ResultTrailerPlayer() { } result_resume_progress_holder?.isVisible = isProgressVisible - context?.getString(if (isProgressVisible) R.string.resume else R.string.play_movie_button) - ?.let { - result_play_movie?.text = it + context?.getString( + when { + currentType?.isLiveStream() == true -> R.string.play_livestream_button + isProgressVisible -> R.string.resume + else -> R.string.play_movie_button } + )?.let { + result_play_movie?.text = it + } //println("startAction = $startAction") when (startAction) { @@ -1943,10 +1949,10 @@ class ResultFragment : ResultTrailerPlayer() { result_poster_holder?.visibility = VISIBLE - /*result_play_movie?.text = - if (d.type == TvType.Torrent) getString(R.string.play_torrent_button) else getString( + result_play_movie?.text = + if (d.type == TvType.Live) getString(R.string.play_livestream_button) else getString( R.string.play_movie_button - )*/ + ) //result_plot_header?.text = // if (d.type == TvType.Torrent) getString(R.string.torrent_plot) else getString(R.string.result_plot) val syno = d.plot @@ -2130,6 +2136,7 @@ class ResultFragment : ResultTrailerPlayer() { TvType.Movie -> R.string.movies_singular TvType.Torrent -> R.string.torrent_singular TvType.AsianDrama -> R.string.asian_drama_singular + TvType.Live -> R.string.live_singular } )?.let { result_meta_type?.text = it diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index ec4f6fab..1cdd5e24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -498,6 +498,26 @@ class ResultViewModel : ViewModel() { updateEpisodes(mainId, listOf(it), -1) } } + is LiveStreamLoadResponse -> { + buildResultEpisode( + loadResponse.name, + loadResponse.name, + null, + 0, + null, + loadResponse.dataUrl, + loadResponse.apiName, + (mainId), // HAS SAME ID + 0, + null, + null, + null, + loadResponse.type, + mainId + ).let { + updateEpisodes(mainId, listOf(it), -1) + } + } is TorrentLoadResponse -> { updateEpisodes( mainId, listOf( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 0c1680b6..fc2d1064 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -167,11 +167,21 @@ class SearchFragment : Fragment() { val docs = dialog.findViewById(R.id.home_select_documentaries) val movies = dialog.findViewById(R.id.home_select_movies) val asian = dialog.findViewById(R.id.home_select_asian) + val livestream = + dialog.findViewById(R.id.home_select_livestreams) val cancelBtt = dialog.findViewById(R.id.cancel_btt) val applyBtt = dialog.findViewById(R.id.apply_btt) val pairList = - HomeFragment.getPairList(anime, cartoons, tvs, docs, movies, asian) + HomeFragment.getPairList( + anime, + cartoons, + tvs, + docs, + movies, + asian, + livestream + ) cancelBtt?.setOnClickListener { dialog.dismissSafe() @@ -287,6 +297,7 @@ class SearchFragment : Fragment() { search_select_documentaries, search_select_movies, search_select_asian, + search_select_livestreams ) val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 62c02f36..fb1d6e41 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -1,61 +1,61 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/searchRoot" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="@dimen/navbar_height" + android:background="?attr/primaryGrayBackground" + android:orientation="vertical" + tools:context=".ui.search.SearchFragment"> + android:layout_width="match_parent" + android:layout_height="40dp" + android:layout_margin="10dp" + android:background="@drawable/search_background" + android:visibility="visible"> + android:layout_width="match_parent" + android:layout_height="30dp" + android:layout_gravity="center_vertical" + android:layout_marginEnd="30dp"> + android:nextFocusRight="@id/search_filter" + android:nextFocusUp="@id/nav_rail_view" + android:nextFocusDown="@id/search_autofit_results" + android:paddingStart="-10dp" + app:iconifiedByDefault="false" + app:queryBackground="@color/transparent" + app:queryHint="@string/search_hint" + app:searchIcon="@drawable/search_icon" + tools:ignore="RtlSymmetry"> + android:id="@+id/search_loading_bar" + style="@style/Widget.AppCompat.ProgressBar" + android:layout_width="20dp" + android:layout_height="20dp" + android:layout_gravity="center" + android:layout_marginStart="-35dp" + android:foregroundTint="@color/white" + android:progressTint="@color/white"> + style="@style/RoundedSelectableButton" + android:nextFocusRight="@id/home_select_tv_series" + android:text="@string/movies" /> + android:nextFocusLeft="@id/home_select_movies" + android:nextFocusRight="@id/home_select_anime" + android:text="@string/tv_series" /> + android:nextFocusLeft="@id/home_select_tv_series" + android:nextFocusRight="@id/home_select_asian" + android:text="@string/anime" /> + android:nextFocusLeft="@id/home_select_anime" + android:nextFocusRight="@id/home_select_cartoons" + android:text="@string/asian_drama" /> + android:nextFocusLeft="@id/home_select_asian" + android:nextFocusRight="@id/home_select_documentaries" + android:text="@string/cartoons" /> + + - android:id="@+id/home_select_documentaries" - android:text="@string/documentaries" - style="@style/RoundedSelectableButton" /> + android:id="@+id/apply_btt_holder" + android:layout_width="match_parent" + android:layout_height="60dp" + android:layout_gravity="bottom" + android:gravity="bottom|end" + android:orientation="horizontal" + android:visibility="gone"> + android:id="@+id/apply_btt" + style="@style/WhiteButton" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_apply" /> + android:id="@+id/cancel_btt" + style="@style/BlackButton" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_cancel" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e10bb4d..7c841e0f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,6 +119,7 @@ Rewatching Play Movie + Play Livestream Stream Torrent Sources Subtitles @@ -327,6 +328,7 @@ Documentaries OVA Asian Dramas + Livestreams Movie @@ -337,6 +339,7 @@ Torrent Documentary Asian Drama + Livestream Source error Remote error