diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 6ea4dd39..7d8be59f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -14,8 +14,8 @@ import com.lagradost.cloudstream3.animeproviders.* import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider import com.lagradost.cloudstream3.movieproviders.* import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.ExtractorLink @@ -352,7 +352,8 @@ abstract class MainAPI { fun overrideWithNewData(data: ProvidersInfoJson) { this.name = data.name - this.mainUrl = data.url + if (data.url.isNotBlank() && data.url != "NONE") + this.mainUrl = data.url this.storedCredentials = data.credentials } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index f5e5d781..c8bf72bf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -35,13 +35,13 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale -import com.lagradost.cloudstream3.movieproviders.NginxProvider import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.result.ResultFragment @@ -132,7 +132,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_download_child, R.id.navigation_subtitles, R.id.navigation_chrome_subtitles, - R.id.navigation_settings_nginx, R.id.navigation_settings_player, R.id.navigation_settings_updates, R.id.navigation_settings_ui, @@ -368,10 +367,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onCreate(savedInstanceState: Bundle?) { // init accounts - for (api in OAuth2accountApis) { + for (api in accountManagers) { api.init() } + ioSafe { + inAppAuths.apmap { api -> + try { + api.initialize() + } catch (e: Exception) { + logError(e) + } + } + } + SearchResultBuilder.updateCache(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -391,68 +400,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { 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) { try { @@ -472,11 +419,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { setKey(PROVIDER_STATUS_KEY, txt) MainAPI.overrideData = newCache // update all new providers - val newUpdatedCache = - newCache?.let { addNginxToJson(it) } initAll() for (api in apis) { // update current providers - newUpdatedCache?.get(api.javaClass.simpleName) + newCache?.get(api.javaClass.simpleName) ?.let { data -> api.overrideWithNewData(data) } @@ -494,15 +439,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newCache }?.let { providersJsonMap -> MainAPI.overrideData = providersJsonMap - val providersJsonMapUpdated = - addNginxToJson(providersJsonMap) // if return null, use unchanged one initAll() val acceptableProviders = - providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } + providersJsonMap.filter { it.value.status == PROVIDER_STATUS_OK || it.value.status == PROVIDER_STATUS_SLOW } .map { it.key }.toSet() val restrictedApis = - if (hasBenene) providersJsonMapUpdated.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } + if (hasBenene) providersJsonMap.filter { it.value.status == PROVIDER_STATUS_BETA_ONLY } .map { it.key }.toSet() else emptySet() apis = allProviders.filter { api -> @@ -527,16 +470,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } else { initAll() 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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt index 2813aa7c..208db14b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/AnilistRedirector.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.metaproviders import com.lagradost.cloudstream3.ErrorLoadingException -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.utils.SyncUtil object SyncRedirector { diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt index 50f2af20..38c2abea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt @@ -3,7 +3,7 @@ package com.lagradost.cloudstream3.metaproviders import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer -import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.providers.AniListApi import com.lagradost.cloudstream3.syncproviders.providers.MALApi @@ -15,7 +15,7 @@ class MultiAnimeProvider : MainAPI() { override val lang = "en" override val usesWebView = true override val supportedTypes = setOf(TvType.Anime) - private val syncApi: SyncAPI = OAuth2API.aniListApi + private val syncApi: SyncAPI = aniListApi private val syncUtilType by lazy { when (syncApi) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/EgyBestProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/EgyBestProvider.kt index de0d0d72..7fa54ad6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/EgyBestProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/EgyBestProvider.kt @@ -161,7 +161,6 @@ class EgyBestProvider : MainAPI() { @JsonProperty("link") val link: String ) - override suspend fun loadLinks( data: String, isCasting: Boolean, diff --git a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/NginxProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/NginxProvider.kt index fd2a27c8..b9472f60 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/movieproviders/NginxProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/movieproviders/NginxProvider.kt @@ -1,13 +1,8 @@ package com.lagradost.cloudstream3.movieproviders import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.LoadResponse.Companion.addDuration -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" @@ -15,23 +10,40 @@ class NginxProvider : MainAPI() { override val hasMainPage = true override val supportedTypes = setOf(TvType.AnimeMovie, TvType.TvSeries, TvType.Movie) + companion object { + var loginCredentials: String? = null + var overrideUrl: String? = null + const val ERROR_STRING = "No nginx url specified in the settings" + } - - fun getAuthHeader(storedCredentials: String?): Map { - if (storedCredentials == null) { - return mapOf(Pair("Authorization", "Basic ")) // no Authorization headers + private fun getAuthHeader(): Map { + val url = overrideUrl ?: throw ErrorLoadingException(ERROR_STRING) + mainUrl = url + println("OVERRIDING URL TO $overrideUrl") + if (mainUrl == "NONE" || mainUrl.isBlank()) { + throw ErrorLoadingException(ERROR_STRING) } - val basicAuthToken = base64Encode(storedCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load - return mapOf(Pair("Authorization", "Basic $basicAuthToken")) + + val localCredentials = loginCredentials + if (localCredentials == null || localCredentials.trim() == ":") { + return mapOf("Authorization" to "Basic ") // no Authorization headers + } + + val basicAuthToken = + base64Encode(localCredentials.toByteArray()) // will this be loaded when not using the provider ??? can increase load + + return mapOf("Authorization" to "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 + val authHeader = + getAuthHeader() // 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 mainRootDocument = app.get(url, authHeader).document - val nfoUrl = url + mediaRootDocument.getElementsByAttributeValueContaining("href", ".nfo").attr("href") // metadata url file + val nfoUrl = url + mainRootDocument.getElementsByAttributeValueContaining("href", ".nfo") + .attr("href") // metadata url file val metadataDocument = app.get(nfoUrl, authHeader).document // get the metadata nfo file @@ -44,27 +56,34 @@ class NginxProvider : MainAPI() { 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=" - ) + 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 partialUrl = + mainRootDocument.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 ... + .mapNotNull { // all the tags like action, thriller ... it?.text() } - val dataList = mediaRootDocument.getElementsByAttributeValueContaining( // list of all urls of the webpage - "href", - partialUrl - ) + val dataList = + mainRootDocument.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 + 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 newMovieLoadResponse( title, @@ -81,7 +100,6 @@ class NginxProvider : MainAPI() { } } else // a tv serie { - val list = ArrayList>() val mediaRootUrl = url.replace("tvshow.nfo", "") val posterUrl = mediaRootUrl + "poster.jpg" @@ -91,7 +109,7 @@ class NginxProvider : MainAPI() { val tagsList = metadataDocument.select("genre") - ?.mapNotNull { // all the tags like action, thriller ...; unused variable + .mapNotNull { // all the tags like action, thriller ...; unused variable it?.text() } @@ -102,7 +120,7 @@ class NginxProvider : MainAPI() { seasons.forEach { element -> val season = - element.attr("href")?.replace("Season%20", "")?.replace("/", "")?.toIntOrNull() + 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)) @@ -120,33 +138,40 @@ class NginxProvider : MainAPI() { "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() + 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 + 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 // will require headers too - this.description = plot - addDate(date) - } - ) - } + episodeList.add( + newEpisode(data) { + this.name = name + this.season = seasonInt + this.episode = epNum + this.posterUrl = poster // will require headers too + this.description = plot + addDate(date) + } + ) + } } return newTvSeriesLoadResponse(title, url, TvType.TvSeries, episodeList) { this.name = title @@ -168,8 +193,9 @@ class NginxProvider : MainAPI() { 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 ( + val authHeader = + getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after + callback.invoke( ExtractorLink( name, name, @@ -185,19 +211,23 @@ class NginxProvider : MainAPI() { } - 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 authHeader = + getAuthHeader() // call again because it isn't reloaded if in main class and storedCredentials loads after + 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 href = it?.attr("href") + val categoryPath = fixUrlNull(href?.trim()) + ?: return@mapNotNull null // get the url of the category; like http://192.168.1.10/media/Movies/ + + 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") != "../") { @@ -215,7 +245,6 @@ class NginxProvider : MainAPI() { val nfoContent = app.get(nfoPath, authHeader).document // all the metadata - if (isMovieType) { val movieName = nfoContent.select("title").text() val posterUrl = mediaRootUrl + "poster.jpg" @@ -238,15 +267,11 @@ class NginxProvider : MainAPI() { ) { addPoster(posterUrl, authHeader) } - - } } 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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt new file mode 100644 index 00000000..7b96f89a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt @@ -0,0 +1,17 @@ +package com.lagradost.cloudstream3.subtitles + +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch + +interface AbstractSubProvider { + @WorkerThread + suspend fun search(query: SubtitleSearch): List? { + throw NotImplementedError() + } + + @WorkerThread + suspend fun load(data: SubtitleEntity): String? { + throw NotImplementedError() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt new file mode 100644 index 00000000..561cc4f8 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -0,0 +1,25 @@ +package com.lagradost.cloudstream3.subtitles + +import com.lagradost.cloudstream3.TvType + +class AbstractSubtitleEntities { + data class SubtitleEntity( + var idPrefix : String, + var name: String = "", //Title of movie/series. This is the one to be displayed when choosing. + var lang: String = "en", + var data: String = "", //Id or link, depends on provider how to process + var type: TvType = TvType.Movie, //Movie, TV series, etc.. + var epNumber: Int? = null, + var seasonNumber: Int? = null, + var year: Int? = null + ) + + data class SubtitleSearch( + var query: String = "", + var imdb: Long? = null, + var lang: String? = null, + var epNumber: Int? = null, + var seasonNumber: Int? = null, + var year: Int? = null + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index bfae9cf4..d8c7c869 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,9 +3,77 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.syncproviders.providers.AniListApi +import com.lagradost.cloudstream3.syncproviders.providers.MALApi +import com.lagradost.cloudstream3.syncproviders.providers.NginxApi +import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi +import java.util.concurrent.TimeUnit + +abstract class AccountManager(private val defIndex: Int) : AuthAPI { + companion object { + val malApi = MALApi(0) + val aniListApi = AniListApi(0) + val openSubtitlesApi = OpenSubtitlesApi(0) + val nginxApi = NginxApi(0) + + // used to login via app intent + val OAuth2Apis + get() = listOf( + malApi, aniListApi + ) + + // this needs init with context and can be accessed in settings + val accountManagers + get() = listOf( + malApi, aniListApi, openSubtitlesApi, nginxApi + ) + + // used for active syncing + val SyncApis + get() = listOf( + SyncRepo(malApi), SyncRepo(aniListApi) + ) + + val inAppAuths + get() = listOf(openSubtitlesApi, nginxApi) + + val subtitleProviders + get() = listOf( + openSubtitlesApi + ) + + const val appString = "cloudstreamapp" + + val unixTime: Long + get() = System.currentTimeMillis() / 1000L + val unixTimeMs: Long + get() = System.currentTimeMillis() + + const val maxStale = 60 * 10 + + fun secondsToReadable(seconds: Int, completedValue: String): String { + var secondsLong = seconds.toLong() + val days = TimeUnit.SECONDS + .toDays(secondsLong) + secondsLong -= TimeUnit.DAYS.toSeconds(days) + + val hours = TimeUnit.SECONDS + .toHours(secondsLong) + secondsLong -= TimeUnit.HOURS.toSeconds(hours) + + val minutes = TimeUnit.SECONDS + .toMinutes(secondsLong) + secondsLong -= TimeUnit.MINUTES.toSeconds(minutes) + if (minutes < 0) { + return completedValue + } + //println("$days $hours $minutes") + return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m" + } + } -abstract class AccountManager(private val defIndex: Int) : OAuth2API { var accountIndex = defIndex + private var lastAccountIndex = defIndex protected val accountId get() = "${idPrefix}_account_$accountIndex" private val accountActiveKey get() = "${idPrefix}_active" @@ -35,8 +103,12 @@ abstract class AccountManager(private val defIndex: Int) : OAuth2API { protected fun switchToNewAccount() { val accounts = getAccounts() + lastAccountIndex = accountIndex accountIndex = (accounts?.maxOrNull() ?: 0) + 1 } + protected fun switchToOldAccount() { + accountIndex = lastAccountIndex + } protected fun registerAccount() { setKey(accountActiveKey, accountIndex) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt new file mode 100644 index 00000000..8b085bc0 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt @@ -0,0 +1,23 @@ +package com.lagradost.cloudstream3.syncproviders + +interface AuthAPI { + val name: String + val icon: Int? + + val requiresLogin: Boolean + + val createAccountUrl : String? + + // don't change this as all keys depend on it + val idPrefix: String + + // if this returns null then you are not logged in + fun loginInfo(): LoginInfo? + fun logOut() + + class LoginInfo( + val profilePicture: String? = null, + val name: String?, + val accountIndex: Int, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/InAppAuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/InAppAuthAPI.kt new file mode 100644 index 00000000..8b6fdf46 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/InAppAuthAPI.kt @@ -0,0 +1,66 @@ +package com.lagradost.cloudstream3.syncproviders + +import androidx.annotation.WorkerThread + +interface InAppAuthAPI : AuthAPI { + data class LoginData( + val username: String? = null, + val password: String? = null, + val server: String? = null, + val email: String? = null, + ) + + // this is for displaying the UI + val requiresPassword: Boolean + val requiresUsername: Boolean + val requiresServer: Boolean + val requiresEmail: Boolean + + // if this is false we can assume that getLatestLoginData returns null and wont be called + // this is used in case for some reason it is not preferred to store any login data besides the "token" or encrypted data + val storesPasswordInPlainText: Boolean + + // return true if logged in successfully + suspend fun login(data: LoginData): Boolean + + // used to fill the UI if you want to edit any data about your login info + fun getLatestLoginData(): LoginData? +} + +abstract class InAppAuthAPIManager(defIndex: Int) : AccountManager(defIndex), InAppAuthAPI { + override val requiresPassword = false + override val requiresUsername = false + override val requiresEmail = false + override val requiresServer = false + override val storesPasswordInPlainText = true + override val requiresLogin = true + + // runs on startup + @WorkerThread + open suspend fun initialize() { + } + + override fun logOut() { + throw NotImplementedError() + } + + override val idPrefix: String + get() = throw NotImplementedError() + + override val name: String + get() = throw NotImplementedError() + + override val icon: Int? = null + + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + throw NotImplementedError() + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + throw NotImplementedError() + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + throw NotImplementedError() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt index b961153f..0f882f3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -1,77 +1,9 @@ package com.lagradost.cloudstream3.syncproviders -import com.lagradost.cloudstream3.syncproviders.providers.AniListApi -import com.lagradost.cloudstream3.syncproviders.providers.MALApi -import java.util.concurrent.TimeUnit - -interface OAuth2API { +interface OAuth2API : AuthAPI { val key: String - val name: String val redirectUrl: String - // don't change this as all keys depend on it - val idPrefix: String - suspend fun handleRedirect(url: String) : Boolean fun authenticate() - - fun loginInfo(): LoginInfo? - fun logOut() - - class LoginInfo( - val profilePicture: String?, - val name: String?, - - val accountIndex: Int, - ) - - companion object { - val malApi = MALApi(0) - val aniListApi = AniListApi(0) - - // used to login via app intent - val OAuth2Apis - get() = listOf( - malApi, aniListApi - ) - - // this needs init with context and can be accessed in settings - val OAuth2accountApis - get() = listOf( - malApi, aniListApi - ) - - // used for active syncing - val SyncApis - get() = listOf( - SyncRepo(malApi), SyncRepo(aniListApi) - ) - - const val appString = "cloudstreamapp" - - val unixTime: Long - get() = System.currentTimeMillis() / 1000L - - const val maxStale = 60 * 10 - - fun secondsToReadable(seconds: Int, completedValue: String): String { - var secondsLong = seconds.toLong() - val days = TimeUnit.SECONDS - .toDays(secondsLong) - secondsLong -= TimeUnit.DAYS.toSeconds(days) - - val hours = TimeUnit.SECONDS - .toHours(secondsLong) - secondsLong -= TimeUnit.HOURS.toSeconds(hours) - - val minutes = TimeUnit.SECONDS - .toMinutes(secondsLong) - secondsLong -= TimeUnit.MINUTES.toSeconds(minutes) - if (minutes < 0) { - return completedValue - } - //println("$days $hours $minutes") - return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m" - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 40d91dcd..3b16cb7f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.* interface SyncAPI : OAuth2API { - val icon: Int val mainUrl: String /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 94aabe21..81c14979 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -12,10 +12,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager -import com.lagradost.cloudstream3.syncproviders.OAuth2API -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.maxStale -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime +import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -32,11 +29,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override val idPrefix = "anilist" override var mainUrl = "https://anilist.co" override val icon = R.drawable.ic_anilist_icon + override val requiresLogin = true + override val createAccountUrl = "$mainUrl/signup" - override fun loginInfo(): OAuth2API.LoginInfo? { + override fun loginInfo(): AuthAPI.LoginInfo? { // context.getUser(true)?. getKey(accountId, ANILIST_USER_KEY)?.let { user -> - return OAuth2API.LoginInfo( + return AuthAPI.LoginInfo( profilePicture = user.picture, name = user.name, accountIndex = accountIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt index 41a338f9..f847e0b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.syncproviders.providers +import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API //TODO dropbox sync @@ -8,6 +9,11 @@ class Dropbox : OAuth2API { override var name = "Dropbox" override val key = "zlqsamadlwydvb2" override val redirectUrl = "dropboxlogin" + override val requiresLogin = true + override val createAccountUrl: String? = null + + override val icon: Int + get() = TODO("Not yet implemented") override fun authenticate() { TODO("Not yet implemented") @@ -21,7 +27,7 @@ class Dropbox : OAuth2API { TODO("Not yet implemented") } - override fun loginInfo(): OAuth2API.LoginInfo? { + override fun loginInfo(): AuthAPI.LoginInfo? { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index e00ef53a..9dd5b214 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -14,10 +14,7 @@ import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager -import com.lagradost.cloudstream3.syncproviders.OAuth2API -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime +import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject @@ -37,15 +34,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override val idPrefix = "mal" override var mainUrl = "https://myanimelist.net" override val icon = R.drawable.mal_logo + override val requiresLogin = true + + override val createAccountUrl = "$mainUrl/register.php" override fun logOut() { removeAccountKeys() } - override fun loginInfo(): OAuth2API.LoginInfo? { + override fun loginInfo(): AuthAPI.LoginInfo? { //getMalUser(true)? getKey(accountId, MAL_USER_KEY)?.let { user -> - return OAuth2API.LoginInfo( + return AuthAPI.LoginInfo( profilePicture = user.picture, name = user.name, accountIndex = accountIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/NginxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/NginxApi.kt new file mode 100644 index 00000000..b9f46006 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/NginxApi.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.movieproviders.NginxProvider +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager + +class NginxApi(index: Int) : InAppAuthAPIManager(index) { + override val name = "Nginx" + override val idPrefix = "nginx" + override val icon = R.drawable.nginx + override val requiresUsername = true + override val requiresPassword = true + override val requiresServer = true + override val createAccountUrl = "https://www.sarlays.com/use-nginx-with-cloudstream/" + + companion object { + const val NGINX_USER_KEY: String = "nginx_user" + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + return getKey(accountId, NGINX_USER_KEY) + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + val data = getLatestLoginData() ?: return null + return AuthAPI.LoginInfo(name = data.username ?: data.server, accountIndex = accountIndex) + } + + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + if (data.server.isNullOrBlank()) return false // we require a server + switchToNewAccount() + setKey(accountId, NGINX_USER_KEY, data) + registerAccount() + initialize() + return true + } + + override fun logOut() { + removeAccountKeys() + initializeData() + } + + private fun initializeData() { + val data = getLatestLoginData() ?: run { + NginxProvider.overrideUrl = null + NginxProvider.loginCredentials = null + return + } + NginxProvider.overrideUrl = data.server?.removeSuffix("/") + NginxProvider.loginCredentials = "${data.username ?: ""}:${data.password ?: ""}" + } + + override suspend fun initialize() { + initializeData() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt new file mode 100644 index 00000000..9b3fadb1 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -0,0 +1,311 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager +import com.lagradost.cloudstream3.utils.AppUtils + +class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubProvider { + override val idPrefix = "opensubtitles" + override val name = "OpenSubtitles" + override val icon = R.drawable.open_subtitles_icon + override val requiresPassword = true + override val requiresUsername = true + override val createAccountUrl = "https://www.opensubtitles.com/" + + companion object { + const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile + const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" + const val host = "https://api.opensubtitles.com/api/v1" + const val TAG = "OPENSUBS" + const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms + var currentCoolDown: Long = 0L + var currentSession: SubtitleOAuthEntity? = null + } + + private fun canDoRequest(): Boolean { + return unixTimeMs > currentCoolDown + } + + private fun throwIfCantDoRequest() { + if (!canDoRequest()) { + throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s") + } + } + + private fun throwGotTooManyRequests() { + currentCoolDown = unixTimeMs + coolDownDuration + throw ErrorLoadingException("Too many requests") + } + + private fun getAuthKey(): SubtitleOAuthEntity? { + return getKey(accountId, OPEN_SUBTITLES_USER_KEY) + } + + private fun setAuthKey(data: SubtitleOAuthEntity?) { + if (data == null) removeKey(accountId, OPEN_SUBTITLES_USER_KEY) + currentSession = data + setKey(accountId, OPEN_SUBTITLES_USER_KEY, data) + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + getAuthKey()?.let { user -> + return AuthAPI.LoginInfo( + profilePicture = null, + name = user.user, + accountIndex = accountIndex + ) + } + return null + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + val current = getAuthKey() ?: return null + return InAppAuthAPI.LoginData(username = current.user, current.pass) + } + + /* + Authorize app to connect to API, using username/password. + Required to run at startup. + Returns OAuth entity with valid access token. + */ + override suspend fun initialize() { + currentSession = getAuthKey() ?: return // just in case the following fails + initLogin(currentSession?.user ?: return, currentSession?.pass ?: return) + } + + override fun logOut() { + setAuthKey(null) + removeAccountKeys() + currentSession = getAuthKey() + } + + private suspend fun initLogin(username: String, password: String): Boolean { + Log.i(TAG, "DATA = [$username] [$password]") + val response = app.post( + url = "$host/login", + headers = mapOf( + "Api-Key" to apiKey, + "Content-Type" to "application/json" + ), + data = mapOf( + "username" to username, + "password" to password + ) + ) + Log.i(TAG, "Responsecode = ${response.code}") + Log.i(TAG, "Result => ${response.text}") + + if (response.isSuccessful) { + AppUtils.tryParseJson(response.text)?.let { token -> + setAuthKey( + SubtitleOAuthEntity( + user = username, + pass = password, + access_token = token.token ?: run { + return false + }) + ) + } + return true + } + return false + } + + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + val username = data.username ?: throw ErrorLoadingException("Requires Username") + val password = data.password ?: throw ErrorLoadingException("Requires Password") + switchToNewAccount() + try { + if (initLogin(username, password)) { + registerAccount() + return true + } + } catch (e: Exception) { + logError(e) + switchToOldAccount() + } + switchToOldAccount() + return false + } + + /* + Fetch subtitles using token authenticated on previous method (see authorize). + Returns list of Subtitles which user can select to download (see load). + */ + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + throwIfCantDoRequest() + val imdbId = query.imdb ?: 0 + val queryText = query.query.replace(" ", "+") + val epNum = query.epNumber ?: 0 + val seasonNum = query.seasonNumber ?: 0 + val yearNum = query.year ?: 0 + val epQuery = if (epNum > 0) "&episode_number=$epNum" else "" + val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else "" + val yearQuery = if (yearNum > 0) "&year=$yearNum" else "" + + val searchQueryUrl = when (imdbId > 0) { + //Use imdb_id to search if its valid + true -> "$host/subtitles?imdb_id=$imdbId&languages=${query.lang}$yearQuery$epQuery$seasonQuery" + false -> "$host/subtitles?query=$queryText&languages=${query.lang}$yearQuery$epQuery$seasonQuery" + } + + val req = app.get( + url = searchQueryUrl, + headers = mapOf( + Pair("Api-Key", apiKey), + Pair("Content-Type", "application/json") + ) + ) + Log.i(TAG, "Search Req => ${req.text}") + if (!req.isSuccessful) { + if (req.code == 429) + throwGotTooManyRequests() + return null + } + + val results = mutableListOf() + + AppUtils.tryParseJson(req.text)?.let { + it.data?.forEach { item -> + val attr = item.attributes ?: return@forEach + val featureDetails = attr.featDetails + //Use any valid name/title in hierarchy + val name = featureDetails?.movieName ?: featureDetails?.title + ?: featureDetails?.parentTitle ?: attr.release ?: "" + val lang = attr.language ?: "" + val resEpNum = featureDetails?.episodeNumber ?: query.epNumber + val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber + val year = featureDetails?.year ?: query.year + val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie + //Log.i(TAG, "Result id/name => ${item.id} / $name") + item.attributes?.files?.forEach { file -> + val resultData = file.fileId?.toString() ?: "" + //Log.i(TAG, "Result file => ${file.fileId} / ${file.fileName}") + results.add( + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = name, + lang = lang, + data = resultData, + type = type, + epNumber = resEpNum, + seasonNumber = resSeasonNum, + year = year + ) + ) + } + } + } + return results + } + + /* + Process data returned from search. + Returns string url for the subtitle file. + */ + override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { + throwIfCantDoRequest() + + val req = app.post( + url = "$host/download", + headers = mapOf( + Pair( + "Authorization", + "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" + ), + Pair("Api-Key", apiKey), + Pair("Content-Type", "application/json"), + Pair("Accept", "*/*") + ), + data = mapOf( + Pair("file_id", data.data) + ) + ) + Log.i(TAG, "Request result => (${req.code}) ${req.text}") + //Log.i(TAG, "Request headers => ${req.headers}") + if (req.isSuccessful) { + AppUtils.tryParseJson(req.text)?.let { + val link = it.link ?: "" + Log.i(TAG, "Request load link => $link") + return link + } + } else { + if (req.code == 429) + throwGotTooManyRequests() + } + return null + } + + + data class SubtitleOAuthEntity( + var user: String, + var pass: String, + var access_token: String, + ) + + data class OAuthToken( + @JsonProperty("token") var token: String? = null, + @JsonProperty("status") var status: Int? = null + ) + + data class Results( + @JsonProperty("data") var data: List? = listOf() + ) + + data class ResultData( + @JsonProperty("id") var id: String? = null, + @JsonProperty("type") var type: String? = null, + @JsonProperty("attributes") var attributes: ResultAttributes? = ResultAttributes() + ) + + data class ResultAttributes( + @JsonProperty("subtitle_id") var subtitleId: String? = null, + @JsonProperty("language") var language: String? = null, + @JsonProperty("release") var release: String? = null, + @JsonProperty("url") var url: String? = null, + @JsonProperty("files") var files: List? = listOf(), + @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails() + ) + + data class ResultFiles( + @JsonProperty("file_id") var fileId: Int? = null, + @JsonProperty("file_name") var fileName: String? = null + ) + + data class ResultDownloadLink( + @JsonProperty("link") var link: String? = null, + @JsonProperty("file_name") var fileName: String? = null, + @JsonProperty("requests") var requests: Int? = null, + @JsonProperty("remaining") var remaining: Int? = null, + @JsonProperty("message") var message: String? = null, + @JsonProperty("reset_time") var resetTime: String? = null, + @JsonProperty("reset_time_utc") var resetTimeUtc: String? = null + ) + + data class ResultFeatureDetails( + @JsonProperty("year") var year: Int? = null, + @JsonProperty("title") var title: String? = null, + @JsonProperty("movie_name") var movieName: String? = null, + @JsonProperty("imdb_id") var imdbId: Int? = null, + @JsonProperty("tmdb_id") var tmdbId: Int? = null, + @JsonProperty("season_number") var seasonNumber: Int? = null, + @JsonProperty("episode_number") var episodeNumber: Int? = null, + @JsonProperty("parent_imdb_id") var parentImdbId: Int? = null, + @JsonProperty("parent_title") var parentTitle: String? = null, + @JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null, + @JsonProperty("parent_feature_id") var parentFeatureId: Int? = null + ) +} \ 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 00727624..332bb4f6 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 @@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe -import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.AutofitRecyclerView @@ -882,7 +882,7 @@ class HomeFragment : Fragment() { home_change_api_loading?.isVisible = false } - for (syncApi in OAuth2API.OAuth2Apis) { + for (syncApi in OAuth2Apis) { val login = syncApi.loginInfo() val pic = login?.profilePicture if (home_profile_picture?.setImage( 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 0fdba535..e11ea630 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 @@ -815,10 +815,6 @@ class CS3IPlayer : IPlayer { null } } - SubtitleOrigin.OPEN_SUBTITLES -> { - // TODO - throw NotImplementedError() - } SubtitleOrigin.EMBEDDED_IN_VIDEO -> { if (offlineSourceFactory != null) { activeSubtitles.add(sub) 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 5c3e7fdb..ba4532e7 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 @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint +import android.app.Dialog +import android.content.Context import android.content.Intent +import android.content.res.ColorStateList import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -23,6 +26,8 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -30,12 +35,20 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment +import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog +import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage +import com.lagradost.cloudstream3.utils.SubtitleHelper.languages +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage +import kotlinx.android.synthetic.main.dialog_online_subtitles.* +import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt +import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt import kotlinx.android.synthetic.main.fragment_player.* import kotlinx.android.synthetic.main.player_custom_layout.* import kotlinx.android.synthetic.main.player_select_source_and_subs.* @@ -54,6 +67,11 @@ class GeneratorPlayer : FullScreenPlayer() { } } + private val subsProviders + get() = subtitleProviders.filter { !it.requiresLogin || it.loginInfo() != null } + private val subsProvidersIsActive + get() = subsProviders.isNotEmpty() + private var titleRez = 3 private var limitTitle = 0 @@ -163,6 +181,174 @@ class GeneratorPlayer : FullScreenPlayer() { } } + data class TempMetaData( + var episode: Int? = null, + var season: Int? = null, + var name: String? = null, + ) + + private fun getMetaData(): TempMetaData { + val meta = TempMetaData() + + when (val newMeta = currentMeta) { + is ResultEpisode -> { + if (!newMeta.tvType.isMovieType()) { + meta.episode = newMeta.episode + meta.season = newMeta.season + } + meta.name = newMeta.headerName + } + is ExtractorUri -> { + if (newMeta.tvType?.isMovieType() == false) { + meta.episode = newMeta.episode + meta.season = newMeta.season + } + meta.name = newMeta.headerName + } + } + return meta + } + + private fun openOnlineSubPicker( + context: Context, + imdbId: Long?, + dismissCallback: (() -> Unit) + ) { + val providers = subsProviders + + val dialog = Dialog(context, R.style.AlertDialogCustomBlack) + dialog.setContentView(R.layout.dialog_online_subtitles) + + val arrayAdapter = + ArrayAdapter(dialog.context, R.layout.sort_bottom_single_choice) + + dialog.show() + + dialog.cancel_btt.setOnClickListener { + dialog.dismissSafe() + } + + dialog.subtitle_adapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE + dialog.subtitle_adapter.adapter = arrayAdapter + val adapter = dialog.subtitle_adapter.adapter as? ArrayAdapter + + var currentSubtitles: List = emptyList() + var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null + + dialog.subtitle_adapter.setOnItemClickListener { _, _, position, _ -> + currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener + } + + var currentLanguageTwoLetters: String = getAutoSelectLanguageISO639_1() + + fun getName(entry: AbstractSubtitleEntities.SubtitleEntity): String { + return if (entry.lang.isBlank()) { + entry.name + } else { + val language = fromTwoLettersToLanguage(entry.lang.trim()) ?: entry.lang + return "$language ${entry.name}" + } + } + + fun setSubtitlesList(list: List) { + currentSubtitles = list + adapter?.clear() + adapter?.addAll(currentSubtitles.map { getName(it) }) + } + + val currentTempMeta = getMetaData() + // bruh idk why it is not correct + val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) + dialog.search_loading_bar.progressTintList = color + dialog.search_loading_bar.indeterminateTintList = color + + dialog.subtitles_search.setOnQueryTextListener(object : + androidx.appcompat.widget.SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + dialog.search_loading_bar?.show() + ioSafe { + val search = AbstractSubtitleEntities.SubtitleSearch( + query = query ?: return@ioSafe, + imdb = imdbId, + epNumber = currentTempMeta.episode, + seasonNumber = currentTempMeta.season, + lang = currentLanguageTwoLetters.ifBlank { null } + ) + val results = providers.apmap { + try { + it.search(search) + } catch (e: Exception) { + null + } + }.filterNotNull() + val max = results.map { it.size }.maxOrNull() ?: return@ioSafe + + // very ugly + val items = ArrayList() + val arrays = results.size + for (index in 0 until max) { + for (i in 0 until arrays) { + items.add(results[i].getOrNull(index) ?: continue) + } + } + + // ugly ik + activity?.runOnUiThread { + setSubtitlesList(items) + dialog.search_loading_bar?.hide() + } + } + + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + return true + } + }) + + dialog.search_filter.setOnClickListener { + val lang639_1 = languages.map { it.ISO_639_1 } + activity?.showDialog( + languages.map { it.languageName }, + lang639_1.indexOf(currentLanguageTwoLetters), + context.getString(R.string.subs_subtitle_languages), + true, + { } + ) { index -> + currentLanguageTwoLetters = lang639_1[index] + dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true) + } + } + + dialog.apply_btt.setOnClickListener { + currentSubtitle?.let { currentSubtitle -> + providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api -> + ioSafe { + val url = api.load(currentSubtitle) ?: return@ioSafe + val subtitle = SubtitleData( + name = getName(currentSubtitle), + url = url, + origin = SubtitleOrigin.URL, + mimeType = url.toSubtitleMimeType() + ) + runOnMainThread { + addAndSelectSubtitles(subtitle) + } + } + } + } + dialog.dismissSafe() + } + + dialog.setOnDismissListener { + dismissCallback.invoke() + } + + dialog.show() + dialog.subtitles_search.setQuery(currentTempMeta.name, true) + } + private fun openSubPicker() { try { subsPathPicker.launch( @@ -183,6 +369,27 @@ class GeneratorPlayer : FullScreenPlayer() { } } + private fun addAndSelectSubtitles(subtitleData: SubtitleData) { + val ctx = context ?: return + setSubtitles(subtitleData) + + // this is used instead of observe, because observe is too slow + val subs = currentSubs.toMutableSet() + subs.add(subtitleData) + player.setActiveSubtitles(subs) + player.reloadPlayer(ctx) + + viewModel.addSubtitles(setOf(subtitleData)) + + selectSourceDialog?.dismissSafe() + + showToast( + activity, + String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name), + Toast.LENGTH_LONG + ) + } + // Open file picker private val subsPathPicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> @@ -208,23 +415,7 @@ class GeneratorPlayer : FullScreenPlayer() { name.toSubtitleMimeType() ) - setSubtitles(subtitleData) - - // this is used instead of observe, because observe is too slow - val subs = currentSubs.toMutableSet() - subs.add(subtitleData) - player.setActiveSubtitles(subs) - player.reloadPlayer(ctx) - - viewModel.addSubtitles(setOf(subtitleData)) - - selectSourceDialog?.dismissSafe() - - showToast( - activity, - String.format(ctx.getString(R.string.player_loaded_subtitles), name), - Toast.LENGTH_LONG - ) + addAndSelectSubtitles(subtitleData) } } @@ -232,6 +423,7 @@ class GeneratorPlayer : FullScreenPlayer() { override fun showMirrorsDialogue() { try { currentSelectedSubtitles = player.getCurrentPreferredSubtitle() + println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs") context?.let { ctx -> val isPlaying = player.getIsPlaying() player.handleEvent(CSPlayerEvent.Pause) @@ -246,13 +438,43 @@ class GeneratorPlayer : FullScreenPlayer() { val providerList = sourceDialog.sort_providers val subtitleList = sourceDialog.sort_subtitles - val footer: TextView = + val loadFromFileFooter: TextView = layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView - footer.text = ctx.getString(R.string.player_load_subtitles) - footer.setOnClickListener { + + loadFromFileFooter.text = ctx.getString(R.string.player_load_subtitles) + loadFromFileFooter.setOnClickListener { openSubPicker() } - subtitleList.addFooterView(footer) + subtitleList.addFooterView(loadFromFileFooter) + + var shouldDismiss = true + + fun dismiss() { + if (isPlaying) { + player.handleEvent(CSPlayerEvent.Play) + } + activity?.hideSystemUI() + } + + if (subsProvidersIsActive) { + val loadFromOpenSubsFooter: TextView = + layoutInflater.inflate( + R.layout.sort_bottom_footer_add_choice, + null + ) as TextView + + loadFromOpenSubsFooter.text = + ctx.getString(R.string.player_load_subtitles_online) + + loadFromOpenSubsFooter.setOnClickListener { + shouldDismiss = false + sourceDialog.dismissSafe(activity) + openOnlineSubPicker(it.context, null) { + dismiss() + } + } + subtitleList.addFooterView(loadFromOpenSubsFooter) + } var sourceIndex = 0 var startSource = 0 @@ -283,15 +505,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } - var shouldDismiss = true - - fun dismiss() { - if (isPlaying) { - player.handleEvent(CSPlayerEvent.Play) - } - activity?.hideSystemUI() - } - sourceDialog.setOnDismissListener { if (shouldDismiss) dismiss() selectSourceDialog = null @@ -598,18 +811,15 @@ class GeneratorPlayer : FullScreenPlayer() { } } - @SuppressLint("SetTextI18n") - fun setTitle() { + private fun getPlayerVideoTitle(): String { var headerName: String? = null var subName: String? = null var episode: Int? = null var season: Int? = null var tvType: TvType? = null - var isFiller: Boolean? = null when (val meta = currentMeta) { is ResultEpisode -> { - isFiller = meta.isFiller headerName = meta.headerName subName = meta.name episode = meta.episode @@ -626,39 +836,40 @@ class GeneratorPlayer : FullScreenPlayer() { } //Generate video title - context?.let { ctx -> - var playerVideoTitle = if (headerName != null) { - (headerName + - if (tvType.isEpisodeBased() && episode != null) - if (season == null) - " - ${ctx.getString(R.string.episode)} $episode" - else - " \"${ctx.getString(R.string.season_short)}${season}:${ - ctx.getString( - R.string.episode_short - ) - }${episode}\"" - else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName" - } else { - "" - } - - //Hide title, if set in setting - if (limitTitle < 0) { - player_video_title?.visibility = View.GONE - } else { - //Truncate video title if it exceeds limit - val differenceInLength = playerVideoTitle.length - limitTitle - val margin = 3 //If the difference is smaller than or equal to this value, ignore it - if (limitTitle > 0 && differenceInLength > margin) { - playerVideoTitle = playerVideoTitle.substring(0, limitTitle - 1) + "..." - } - } - - player_episode_filler_holder?.isVisible = isFiller ?: false - player_video_title?.text = playerVideoTitle + val playerVideoTitle = if (headerName != null) { + (headerName + + if (tvType.isEpisodeBased() && episode != null) + if (season == null) + " - ${getString(R.string.episode)} $episode" + else + " \"${getString(R.string.season_short)}${season}:${getString(R.string.episode_short)}${episode}\"" + else "") + if (subName.isNullOrBlank() || subName == headerName) "" else " - $subName" + } else { + "" } + return playerVideoTitle + } + + @SuppressLint("SetTextI18n") + fun setTitle() { + var playerVideoTitle = getPlayerVideoTitle() + + //Hide title, if set in setting + if (limitTitle < 0) { + player_video_title?.visibility = View.GONE + } else { + //Truncate video title if it exceeds limit + val differenceInLength = playerVideoTitle.length - limitTitle + val margin = 3 //If the difference is smaller than or equal to this value, ignore it + if (limitTitle > 0 && differenceInLength > margin) { + playerVideoTitle = playerVideoTitle.substring(0, limitTitle - 1) + "..." + } + } + val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller + + player_episode_filler_holder?.isVisible = isFiller ?: false + player_video_title?.text = playerVideoTitle } @SuppressLint("SetTextI18n") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 5e443bfa..11fd76b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -1,13 +1,10 @@ package com.lagradost.cloudstream3.ui.player -import android.content.Context -import android.net.Uri import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout import com.google.android.exoplayer2.ui.SubtitleView import com.google.android.exoplayer2.util.MimeTypes -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions @@ -24,7 +21,6 @@ enum class SubtitleStatus { enum class SubtitleOrigin { URL, DOWNLOADED_FILE, - OPEN_SUBTITLES, EMBEDDED_IN_VIDEO } @@ -68,28 +64,6 @@ class PlayerSubtitleHelper { } } - private fun getSubtitleMimeType(context: Context, url: String, origin: SubtitleOrigin): String { - return when (origin) { - // The url can look like .../document/4294 when the name is EnglishSDH.srt - SubtitleOrigin.DOWNLOADED_FILE -> { - UniFile.fromUri( - context, - Uri.parse(url) - ).name?.toSubtitleMimeType() ?: MimeTypes.APPLICATION_SUBRIP - } - SubtitleOrigin.URL -> { - return url.toSubtitleMimeType() - } - SubtitleOrigin.OPEN_SUBTITLES -> { - // TODO - throw NotImplementedError() - } - SubtitleOrigin.EMBEDDED_IN_VIDEO -> { - throw NotImplementedError() - } - } - } - fun getSubtitleData(subtitleFile: SubtitleFile): SubtitleData { return SubtitleData( name = subtitleFile.lang, 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 39506e71..247d4b6f 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 @@ -24,7 +24,6 @@ import android.widget.* import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.core.content.FileProvider -import androidx.core.graphics.drawable.toBitmap import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView @@ -41,7 +40,6 @@ import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -1353,7 +1351,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio val newList = list.filter { it.isSynced && it.hasAccount } result_mini_sync?.isVisible = newList.isNotEmpty() - (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.map { it.icon }) + (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon }) } observe(syncModel.syncIds) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index a14061e5..81b1e1d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -7,9 +7,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.SyncUtil import kotlinx.coroutines.launch @@ -20,7 +20,7 @@ data class CurrentSynced( val idPrefix: String, val isSynced: Boolean, val hasAccount: Boolean, - val icon: Int, + val icon: Int?, ) class SyncViewModel : ViewModel() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt index 7f6587c1..9e03079f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt @@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.ui.search import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis class SyncSearchViewModel { - private val repos = OAuth2API.SyncApis + private val repos = SyncApis data class SyncSearchResultSearchResponse( override val name: String, @@ -18,5 +18,4 @@ class SyncSearchViewModel { override var quality: SearchQuality? = null, override var posterHeaders: Map? = null, ) : SearchResponse - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index 8eb27e4c..e879f0df 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -8,13 +8,13 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.utils.UIHelper.setImage -class AccountClickCallback(val action: Int, val view : View, val card: OAuth2API.LoginInfo) +class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo) class AccountAdapter( - val cardList: List, + val cardList: List, val layout: Int = R.layout.account_single, private val clickCallback: (AccountClickCallback) -> Unit ) : @@ -48,15 +48,13 @@ class AccountAdapter( private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! private val accountName: TextView = itemView.findViewById(R.id.account_name)!! - fun bind(card: OAuth2API.LoginInfo) { + fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened - accountName.text = card.name ?: "%s %d".format(accountName.context.getString(R.string.account), card.accountIndex) - if(card.profilePicture.isNullOrEmpty()) { - pfp.isVisible = false - } else { - pfp.isVisible = true - pfp.setImage(card.profilePicture) - } + accountName.text = card.name ?: "%s %d".format( + accountName.context.getString(R.string.account), + card.accountIndex + ) + pfp.isVisible = pfp.setImage(card.profilePicture) itemView.setOnClickListener { clickCallback.invoke(AccountClickCallback(0, itemView, card)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 167bdfcb..c31c9e0e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -1,38 +1,56 @@ package com.lagradost.cloudstream3.ui.settings import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.View -import android.widget.ImageView import android.widget.TextView +import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog +import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.nginxApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.beneneCount import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage +import kotlinx.android.synthetic.main.account_managment.* +import kotlinx.android.synthetic.main.account_switch.* +import kotlinx.android.synthetic.main.add_account_input.* class SettingsAccount : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_credits) } - private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) { + + private fun showLoginInfo(api: AccountManager, info: AuthAPI.LoginInfo) { val builder = AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) .setView(R.layout.account_managment) val dialog = builder.show() - dialog.findViewById(R.id.account_profile_picture)?.setImage(info.profilePicture) - dialog.findViewById(R.id.account_logout)?.setOnClickListener { + dialog.account_main_profile_picture_holder?.isVisible = + dialog.account_main_profile_picture?.setImage(info.profilePicture) == true + + dialog.account_logout?.setOnClickListener { api.logOut() dialog.dismissSafe(activity) } @@ -40,13 +58,93 @@ class SettingsAccount : PreferenceFragmentCompat() { (info.name ?: context?.getString(R.string.no_data))?.let { dialog.findViewById(R.id.account_name)?.text = it } - dialog.findViewById(R.id.account_site)?.text = api.name - dialog.findViewById(R.id.account_switch_account)?.setOnClickListener { + + dialog.account_site?.text = api.name + dialog.account_switch_account?.setOnClickListener { dialog.dismissSafe(activity) showAccountSwitch(it.context, api) } } + @UiThread + private fun addAccount(api: AccountManager) { + try { + when (api) { + is OAuth2API -> { + api.authenticate() + } + is InAppAuthAPI -> { + val builder = + AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) + .setView(R.layout.add_account_input) + val dialog = builder.show() + dialog.login_email_input?.isVisible = api.requiresEmail + dialog.login_password_input?.isVisible = api.requiresPassword + dialog.login_server_input?.isVisible = api.requiresServer + dialog.login_username_input?.isVisible = api.requiresUsername + dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() + dialog.create_account?.setOnClickListener { + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(api.createAccountUrl) + try { + startActivity(i) + } catch (e: Exception) { + logError(e) + } + } + dialog.text1?.text = api.name + + if (api.storesPasswordInPlainText) { + api.getLatestLoginData()?.let { data -> + dialog.login_email_input?.setText(data.email ?: "") + dialog.login_server_input?.setText(data.server ?: "") + dialog.login_username_input?.setText(data.username ?: "") + dialog.login_password_input?.setText(data.password ?: "") + } + } + + dialog.apply_btt?.setOnClickListener { + val loginData = InAppAuthAPI.LoginData( + username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null, + password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null, + email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null, + server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null, + ) + ioSafe { + val isSuccessful = try { + api.login(loginData) + } catch (e: Exception) { + logError(e) + false + } + activity?.runOnUiThread { + try { + showToast( + activity, + getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( + api.name + ) + ) + } catch (e: Exception) { + logError(e) // format might fail + } + } + } + dialog.dismissSafe(activity) + } + dialog.cancel_btt?.setOnClickListener { + dialog.dismissSafe(activity) + } + } + else -> { + throw NotImplementedError("You are trying to add an account that has an unknown login method") + } + } + } catch (e: Exception) { + logError(e) + } + } + private fun showAccountSwitch(context: Context, api: AccountManager) { val accounts = api.getAccounts() ?: return @@ -54,17 +152,14 @@ class SettingsAccount : PreferenceFragmentCompat() { AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch) val dialog = builder.show() - dialog.findViewById(R.id.account_add)?.setOnClickListener { - try { - api.authenticate() - } catch (e: Exception) { - logError(e) - } + dialog.account_add?.setOnClickListener { + addAccount(api) + dialog?.dismissSafe(activity) } val ogIndex = api.accountIndex - val items = ArrayList() + val items = ArrayList() for (index in accounts) { api.accountIndex = index @@ -97,33 +192,28 @@ class SettingsAccount : PreferenceFragmentCompat() { val syncApis = listOf( - Pair(R.string.mal_key, OAuth2API.malApi), Pair( - R.string.anilist_key, - OAuth2API.aniListApi - ) + R.string.mal_key to malApi, + R.string.anilist_key to aniListApi, + R.string.opensubtitles_key to openSubtitlesApi, + R.string.nginx_key to nginxApi, ) + for ((key, api) in syncApis) { getPref(key)?.apply { title = getString(R.string.login_format).format(api.name, getString(R.string.account)) - setOnPreferenceClickListener { _ -> + setOnPreferenceClickListener { val info = api.loginInfo() if (info != null) { showLoginInfo(api, info) } else { - try { - api.authenticate() - } catch (e: Exception) { - logError(e) - } + addAccount(api) } return@setOnPreferenceClickListener true } } } - - try { beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0) getPref(R.string.benene_count)?.let { pref -> 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 74e8213d..884feaba 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 @@ -16,7 +16,7 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -116,7 +116,7 @@ class SettingsFragment : Fragment() { val isTrueTv = context?.isTrueTvSettings() == true - for (syncApi in OAuth2API.OAuth2Apis) { + for (syncApi in accountManagers) { val login = syncApi.loginInfo() val pic = login?.profilePicture ?: continue if (settings_profile_pic?.setImage( @@ -135,7 +135,6 @@ class SettingsFragment : Fragment() { Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account), Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui), Pair(settings_lang, R.id.action_navigation_settings_to_navigation_settings_lang), - Pair(settings_nginx, R.id.action_navigation_settings_to_navigation_settings_nginx), Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates), ).forEach { (view, navigationId) -> view?.apply { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsLang.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsLang.kt index 6da97df8..ad51a0f7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsLang.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsLang.kt @@ -116,7 +116,7 @@ class SettingsLang : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref -> + getPref(R.string.locale_key)?.setOnPreferenceClickListener { val tempLangs = languages.toMutableList() //if (beneneCount > 100) { // tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo")) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsNginx.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsNginx.kt deleted file mode 100644 index cb6d28c7..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsNginx.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.lagradost.cloudstream3.ui.settings - -import android.os.Bundle -import android.view.View -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showNginxTextInputDialog -import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard - -class SettingsNginx : PreferenceFragmentCompat() { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setUpToolbar(R.string.category_nginx) - } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - hideKeyboard() - setPreferencesFromResource(R.xml.settings_nginx, rootKey) - val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) - - 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.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 - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt index a34f30c6..4b5f5264 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleHelper.kt @@ -45,7 +45,6 @@ object SubtitleHelper { * @param looseCheck will use .contains in addition to .equals * */ fun fromLanguageToTwoLetters(input: String, looseCheck: Boolean): String? { - languages.forEach { if (it.languageName.equals(input, ignoreCase = true) || it.nativeName.equals(input, ignoreCase = true) diff --git a/app/src/main/res/drawable/nginx.xml b/app/src/main/res/drawable/nginx.xml new file mode 100644 index 00000000..931c8b0b --- /dev/null +++ b/app/src/main/res/drawable/nginx.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/nginx_question.xml b/app/src/main/res/drawable/nginx_question.xml new file mode 100644 index 00000000..7e110e55 --- /dev/null +++ b/app/src/main/res/drawable/nginx_question.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/open_subtitles_icon.xml b/app/src/main/res/drawable/open_subtitles_icon.xml new file mode 100644 index 00000000..d2c8e368 --- /dev/null +++ b/app/src/main/res/drawable/open_subtitles_icon.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/question_mark_24.xml b/app/src/main/res/drawable/question_mark_24.xml new file mode 100644 index 00000000..a62e44f9 --- /dev/null +++ b/app/src/main/res/drawable/question_mark_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index 18d1d986..389a3406 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -16,13 +16,14 @@ android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/account_single.xml b/app/src/main/res/layout/account_single.xml index 1e74b299..cbfb9f18 100644 --- a/app/src/main/res/layout/account_single.xml +++ b/app/src/main/res/layout/account_single.xml @@ -7,6 +7,7 @@ android:layout_width="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_input_dialog.xml b/app/src/main/res/layout/bottom_input_dialog.xml index c7755b9e..aacc4024 100644 --- a/app/src/main/res/layout/bottom_input_dialog.xml +++ b/app/src/main/res/layout/bottom_input_dialog.xml @@ -32,7 +32,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_rowWeight="1" - android:autofillHints="Autofill Hint" android:inputType="text" tools:ignore="LabelFor" /> diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml new file mode 100644 index 00000000..824d8089 --- /dev/null +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index 1bd64915..1df1b1d1 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -70,7 +70,7 @@ - - - - - Auto-Select Language Download Languages + Subtitle Language Hold to reset to default Import fonts by placing them in %s Continue Watching @@ -437,6 +438,13 @@ anilist_key mal_key + opensubtitles_key + nginx_key + password123 + MyCoolUsername + hello@world.com + 127.0.0.1 + true false diff --git a/app/src/main/res/xml/settings_credits_account.xml b/app/src/main/res/xml/settings_credits_account.xml index 57ce24de..aeb38e4b 100644 --- a/app/src/main/res/xml/settings_credits_account.xml +++ b/app/src/main/res/xml/settings_credits_account.xml @@ -8,6 +8,23 @@ + + + + + + + - - - - - - - \ No newline at end of file