diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 72091157..d9cfc62b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -19,6 +19,7 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.ExtractorLink +import okhttp3.Headers import okhttp3.Interceptor import java.text.SimpleDateFormat import java.util.* @@ -107,6 +108,7 @@ object APIHolder { MonoschinosProvider(), KawaiifuProvider(), // disabled due to cloudflare //MultiAnimeProvider(), + NginxProvider(), ) } @@ -303,6 +305,7 @@ const val PROVIDER_STATUS_DOWN = 0 data class ProvidersInfoJson( @JsonProperty("name") var name: String, @JsonProperty("url") var url: String, + @JsonProperty("credentials") var credentials: String? = null, @JsonProperty("status") var status: Int, ) @@ -315,6 +318,7 @@ abstract class MainAPI { fun overrideWithNewData(data: ProvidersInfoJson) { this.name = data.name this.mainUrl = data.url + this.storedCredentials = data.credentials } init { @@ -325,6 +329,7 @@ abstract class MainAPI { open var name = "NONE" open var mainUrl = "NONE" + open var storedCredentials: String? = null //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a43d4741..015a5004 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -64,12 +64,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.requestRW +import com.lagradost.cloudstream3.movieproviders.NginxProvider import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File +import kotlin.collections.HashMap import kotlin.concurrent.thread @@ -360,6 +362,57 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { e.printStackTrace() false } + fun addNginxToJson(data: java.util.HashMap): java.util.HashMap? { + try { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val nginxUrl = + settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key").toString() + val nginxCredentials = + settingsManager.getString(getString(R.string.nginx_credentials), "nginx_credentials") + .toString() + val StoredNginxProvider = NginxProvider() + if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value, or empty: + data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson( + url = nginxUrl, + name = StoredNginxProvider.name, + status = PROVIDER_STATUS_DOWN, // the provider will not be display + credentials = nginxCredentials + ) + } else { // valid url + data[StoredNginxProvider.javaClass.simpleName] = ProvidersInfoJson( + url = nginxUrl, + name = StoredNginxProvider.name, + status = PROVIDER_STATUS_OK, + credentials = nginxCredentials + ) + } + + return data + } catch (e: Exception) { + logError(e) + return data + } + } + fun createNginxJson() : ProvidersInfoJson? { //java.util.HashMap + return try { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val nginxUrl = settingsManager.getString(getString(R.string.nginx_url_key), "nginx_url_key").toString() + val nginxCredentials = settingsManager.getString(getString(R.string.nginx_credentials), "nginx_credentials").toString() + if (nginxUrl == "nginx_url_key" || nginxUrl == "") { // if key is default value or empty: + null // don't overwrite anything + } else { + ProvidersInfoJson( + url = nginxUrl, + name = NginxProvider().name, + status = PROVIDER_STATUS_OK, + credentials = nginxCredentials + ) + } + } catch (e: Exception) { + logError(e) + null + } + } // this pulls the latest data so ppl don't have to update to simply change provider url if (downloadFromGithub) { @@ -379,8 +432,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { tryParseJson>(txt) setKey(PROVIDER_STATUS_KEY, txt) MainAPI.overrideData = newCache // update all new providers - for (api in apis) { // update current providers - newCache?.get(api.javaClass.simpleName)?.let { data -> + + val newUpdatedCache = newCache?.let { addNginxToJson(it) ?: it } + + for (api in apis) { // update current providers + newUpdatedCache?.get(api.javaClass.simpleName)?.let { data -> api.overrideWithNewData(data) } } @@ -397,12 +453,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newCache }?.let { providersJsonMap -> MainAPI.overrideData = providersJsonMap + val providersJsonMapUpdated = addNginxToJson(providersJsonMap)?: providersJsonMap // if return null, use unchanged one val acceptableProviders = - providersJsonMap.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } + providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } .map { it.key }.toSet() val restrictedApis = - if (hasBenene) providersJsonMap.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } + if (hasBenene) providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } .map { it.key }.toSet() else emptySet() apis = allProviders.filter { api -> @@ -425,6 +482,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } else { apis = allProviders + try { + val nginxProviderName = NginxProvider().name + val nginxProviderIndex = apis.indexOf(APIHolder.getApiFromName(nginxProviderName)) + val createdJsonProvider = createNginxJson() + if (createdJsonProvider != null) { + apis[nginxProviderIndex].overrideWithNewData(createdJsonProvider) // people will have access to it if they disable metadata check (they are not filtered) + } + } catch (e: Exception) { + logError(e) + } + } loadThemes(this) @@ -619,4 +687,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val output = src?.doMath() println("MASTER OUTPUT = $output")*/ } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/NginxProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/NginxProvider.kt new file mode 100644 index 00000000..ce62f013 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/NginxProvider.kt @@ -0,0 +1,268 @@ +package com.lagradost.cloudstream3.movieproviders + +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.addRating +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.Qualities +import java.lang.Exception + +class NginxProvider : MainAPI() { + override var name = "Nginx" + override val hasQuickSearch = false + override val hasMainPage = true + override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) + + + + fun getAuthHeader(storedCredentials: String?): Map { + if (storedCredentials == null) { + return mapOf(Pair("Authorization", "Basic ")) // no Authorization headers + } + val basicAuthToken = base64Encode(storedCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load + return mapOf(Pair("Authorization", "Basic $basicAuthToken")) + } + + override suspend fun load(url: String): LoadResponse { + val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after + // url can be tvshow.nfo for series or mediaRootUrl for movies + + val mediaRootDocument = app.get(url, authHeader).document + + val nfoUrl = url + mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href") // metadata url file + + val metadataDocument = app.get(nfoUrl, authHeader).document // get the metadata nfo file + + val isMovie = !nfoUrl.contains("tvshow.nfo") + + val title = metadataDocument.selectFirst("title").text() + + val description = metadataDocument.selectFirst("plot").text() + + if (isMovie) { + val poster = metadataDocument.selectFirst("thumb").text() + val trailer = metadataDocument.select("trailer")?.mapNotNull { + it?.text()?.replace( + "plugin://plugin.video.youtube/play/?video_id=", + "https://www.youtube.com/watch?v=" + ) + } + val partialUrl = mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href").replace(".nfo", ".") + val date = metadataDocument.selectFirst("year")?.text()?.toIntOrNull() + val ratingAverage = metadataDocument.selectFirst("value")?.text()?.toIntOrNull() + val tagsList = metadataDocument.select("genre") + ?.mapNotNull { // all the tags like action, thriller ... + it?.text() + + } + + + val dataList = mediaRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage + "href", + partialUrl + ) + + val data = url + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file + + + return MovieLoadResponse( + title, + data, + this.name, + TvType.Movie, + data, + poster, + date, + description, + ratingAverage, + tagsList, + null, + trailer, + null, + null, + ) + } else // a tv serie + { + + val list = ArrayList>() + val mediaRootUrl = url.replace("tvshow.nfo", "") + val posterUrl = mediaRootUrl + "poster.jpg" + val mediaRootDocument = app.get(mediaRootUrl, authHeader).document + val seasons = + mediaRootDocument.getElementsByAttributeValueContaining("href", "Season%20") + + + val tagsList = metadataDocument.select("genre") + ?.mapNotNull { // all the tags like action, thriller ...; unused variable + it?.text() + } + + //val actorsList = document.select("actor") + // ?.mapNotNull { // all the tags like action, thriller ...; unused variable + // it?.text() + // } + + seasons.forEach { element -> + val season = + element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull() + val href = mediaRootUrl + element.attr("href") + if (season != null && season > 0 && href.isNotBlank()) { + list.add(Pair(season, href)) + } + } + + if (list.isEmpty()) throw ErrorLoadingException("No Seasons Found") + + val episodeList = ArrayList() + + + list.apmap { (seasonInt, seasonString) -> + val seasonDocument = app.get(seasonString, authHeader).document + val episodes = seasonDocument.getElementsByAttributeValueContaining( + "href", + ".nfo" + ) // get metadata + episodes.forEach { episode -> + val nfoDocument = app.get(seasonString + episode.attr("href"), authHeader).document // get episode metadata file + val epNum = nfoDocument.selectFirst("episode")?.text()?.toIntOrNull() + val poster = + seasonString + episode.attr("href").replace(".nfo", "-thumb.jpg") + val name = nfoDocument.selectFirst("title").text() + // val seasonInt = nfoDocument.selectFirst("season").text().toIntOrNull() + val date = nfoDocument.selectFirst("aired")?.text() + val plot = nfoDocument.selectFirst("plot")?.text() + + val dataList = seasonDocument.getElementsByAttributeValueContaining( + "href", + episode.attr("href").replace(".nfo", "") + ) + val data = seasonString + dataList.firstNotNullOf { item -> item.takeIf { (!it.attr("href").contains(".nfo") && !it.attr("href").contains(".jpg"))} }.attr("href").toString() // exclude poster and nfo (metadata) file + + episodeList.add( + newEpisode(data) { + this.name = name + this.season = seasonInt + this.episode = epNum + this.posterUrl = poster + addDate(date) + this.description = plot + } + ) + } + } + return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) { + this.name = title + this.url = url + this.posterUrl = posterUrl + this.episodes = episodeList + this.plot = description + this.tags = tagsList + } + } + + } + + + override suspend fun loadLinks( + data: String, + isCasting: Boolean, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ): Boolean { + // loadExtractor(data, null) { callback(it.copy(headers=authHeader)) } + val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after + callback.invoke ( + ExtractorLink( + name, + name, + data, + data, // referer not needed + Qualities.Unknown.value, + false, + authHeader, + ) + ) + + return true + } + + + + override suspend fun getMainPage(): HomePageResponse? { + val authHeader = getAuthHeader(storedCredentials) // call again because it isn't reloaded if in main class and storedCredentials loads after + if (mainUrl == "NONE"){ + throw ErrorLoadingException("No nginx url specified in the settings: Nginx Settigns > Nginx server url, try again in a few seconds") + } + val document = app.get(mainUrl, authHeader).document + val categories = document.select("a") + val returnList = categories.mapNotNull { + val categoryPath = mainUrl + it.attr("href") ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/ + val categoryTitle = it.text() // get the category title like Movies or Series + if (categoryTitle != "../" && categoryTitle != "Music/") { // exclude parent dir and Music dir + val categoryDocument = app.get(categoryPath, authHeader).document // queries the page http://192.168.1.10/media/Movies/ + val contentLinks = categoryDocument.select("a") + val currentList = contentLinks.mapNotNull { head -> + if (head.attr("href") != "../") { + try { + val mediaRootUrl = + categoryPath + head.attr("href")// like http://192.168.1.10/media/Series/Chernobyl/ + val mediaDocument = app.get(mediaRootUrl, authHeader).document + val nfoFilename = mediaDocument.getElementsByAttributeValueContaining( + "href", + ".nfo" + )[0].attr("href") + val isMovieType = nfoFilename != "tvshow.nfo" + val nfoPath = + mediaRootUrl + nfoFilename // must exist or will raise errors, only the first one is taken + val nfoContent = + app.get(nfoPath, authHeader).document // all the metadata + + + if (isMovieType) { + val movieName = nfoContent.select("title").text() + + val posterUrl = mediaRootUrl + "poster.jpg" + + return@mapNotNull MovieSearchResponse( + movieName, + mediaRootUrl, + this.name, + TvType.Movie, + posterUrl, + null, + ) + } else { // tv serie + val serieName = nfoContent.select("title").text() + + val posterUrl = mediaRootUrl + "poster.jpg" + + TvSeriesSearchResponse( + serieName, + nfoPath, + this.name, + TvType.TvSeries, + posterUrl, + null, + null, + ) + + + } + } catch (e: Exception) { // can cause issues invisible errors + null + //logError(e) // not working because it changes the return type of currentList to Any + } + + + } else null + } + if (currentList.isNotEmpty() && categoryTitle != "../") { // exclude upper dir + HomePageList(categoryTitle, currentList) + } else null + } else null // the path is ../ which is parent directory + } + // if (returnList.isEmpty()) return null // maybe doing nothing idk + return HomePageResponse(returnList) + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 4bad5c57..cb7f1138 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -47,6 +47,7 @@ import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso @@ -485,6 +486,32 @@ class SettingsFragment : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.nginx_url_key)?.setOnPreferenceClickListener { + + activity?.showNginxTextInputDialog( + settingsManager.getString(getString(R.string.nginx_url_pref), "Nginx server url").toString(), + settingsManager.getString(getString(R.string.nginx_url_key), "").toString(), // key: the actual you use rn + android.text.InputType.TYPE_TEXT_VARIATION_URI, // uri + {}) { + settingsManager.edit() + .putString(getString(R.string.nginx_url_key), it).apply() // change the stored url in nginx_url_key to it + } + return@setOnPreferenceClickListener true + } + + getPref(R.string.nginx_credentials)?.setOnPreferenceClickListener { + + activity?.showNginxTextInputDialog( + settingsManager.getString(getString(R.string.nginx_credentials_title), "Nginx Credentials").toString(), + settingsManager.getString(getString(R.string.nginx_credentials), "").toString(), // key: the actual you use rn + android.text.InputType.TYPE_TEXT_VARIATION_URI, + {}) { + settingsManager.edit() + .putString(getString(R.string.nginx_credentials), it).apply() // change the stored url in nginx_url_key to it + } + return@setOnPreferenceClickListener true + } + getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.media_type_pref) val prefValues = resources.getIntArray(R.array.media_type_pref_values) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index ce49bac1..0a069cec 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -144,6 +144,46 @@ object SingleSelectionHelper { } } + + + private fun Activity.showInputDialog( + dialog: Dialog, + value: String, + name: String, + textInputType: Int?, + callback: (String) -> Unit, + dismissCallback: () -> Unit + ) { + val inputView = dialog.findViewById(R.id.nginx_text_input)!! + val textView = dialog.findViewById(R.id.text1)!! + val applyButton = dialog.findViewById(R.id.apply_btt)!! + val cancelButton = dialog.findViewById(R.id.cancel_btt)!! + val applyHolder = dialog.findViewById(R.id.apply_btt_holder)!! + + applyHolder.isVisible = true + textView.text = name + + if (textInputType != null) { + inputView.inputType = textInputType // 16 for website url input type + } + inputView.setText(value, TextView.BufferType.EDITABLE) + + + applyButton.setOnClickListener { + callback.invoke(inputView.text.toString()) // try to save the setting, using callback + dialog.dismissSafe(this) + } + + cancelButton.setOnClickListener { // just dismiss + dialog.dismissSafe(this) + } + + dialog.setOnDismissListener { + dismissCallback.invoke() + } + + } + fun Activity.showMultiDialog( items: List, selectedIndex: List, @@ -192,7 +232,7 @@ object SingleSelectionHelper { selectedIndex: Int, name: String, showApply: Boolean, - dismissCallback: () -> Unit, + dismissCallback: () -> Unit, callback: (Int) -> Unit, ) { val builder = @@ -211,4 +251,25 @@ object SingleSelectionHelper { dismissCallback ) } -} \ No newline at end of file + + fun Activity.showNginxTextInputDialog( + name: String, + value: String, + textInputType: Int?, + dismissCallback: () -> Unit, + callback: (String) -> Unit, + ) { + val builder = BottomSheetDialog(this) // probably the stuff at the bottom + builder.setContentView(R.layout.bottom_input_dialog) // input layout + + builder.show() + showInputDialog( + builder, + value, + name, + textInputType, // type is a uri + callback, + dismissCallback + ) + } +} diff --git a/app/src/main/res/layout/bottom_input_dialog.xml b/app/src/main/res/layout/bottom_input_dialog.xml new file mode 100644 index 00000000..c7755b9e --- /dev/null +++ b/app/src/main/res/layout/bottom_input_dialog.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d026710..eb644776 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,9 @@ provider_lang_key dns_key download_path_key + nginx_url_key + nginx_credentials + nginx_info Cloudstream app_layout_key primary_color_key @@ -227,6 +230,12 @@ Error backing up %s Search + Nginx Settings + Nginx Credential + You have to use the following format mycoolusername:mysecurepassword123 + What is Nginx ? + Nginx is a software that can be used to display files from a server that you own. Click to see a Nginx setup guide + Info Advanced Search Gives you the search results separated by provider @@ -352,6 +361,8 @@ Download path + Nginx server url + Display Dubbed/Subbed Anime Fit to screen diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 5552b587..c73765a6 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -162,6 +162,31 @@ app:defaultValue="true" /> + + + + + + + + +