From 2a27c0360dc8022adbae7bd5b7aa8e5bcd99c081 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 1 Apr 2022 22:05:34 +0200 Subject: [PATCH] sync stuff --- .../lagradost/cloudstream3/MainActivity.kt | 6 +- .../cloudstream3/syncproviders/OAuth2API.kt | 6 +- .../cloudstream3/syncproviders/SyncAPI.kt | 42 +- .../cloudstream3/syncproviders/SyncRepo.kt | 25 + .../syncproviders/providers/AniListApi.kt | 236 ++++----- .../syncproviders/providers/MALApi.kt | 478 +++++++++++------- .../ui/quicksearch/QuickSearchFragment.kt | 71 +-- .../cloudstream3/ui/result/ResultFragment.kt | 122 +++-- .../cloudstream3/ui/result/ResultViewModel.kt | 15 - .../cloudstream3/ui/result/SyncViewModel.kt | 79 +++ .../cloudstream3/ui/search/SearchViewModel.kt | 55 +- .../ui/search/SyncSearchViewModel.kt | 30 ++ .../ui/settings/SettingsFragment.kt | 19 +- .../lagradost/cloudstream3/utils/AppUtils.kt | 18 +- app/src/main/res/layout/result_sync.xml | 64 ++- .../lagradost/cloudstream3/ProviderTests.kt | 5 +- 16 files changed, 729 insertions(+), 542 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 053c41f6..904a63ac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -266,7 +266,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (str.contains(appString)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { - api.handleRedirect(str) + try { + api.handleRedirect(str) + } catch (e : Exception) { + logError(e) + } } } } else { 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 1fef3849..e6dfd94c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -10,7 +10,7 @@ interface OAuth2API { val redirectUrl: String // don't change this as all keys depend on it - val idPrefix : String + val idPrefix: String fun handleRedirect(url: String) fun authenticate() @@ -43,8 +43,8 @@ interface OAuth2API { // used for active syncing val SyncApis - get() = listOf( - malApi, aniListApi + get() = listOf( + SyncRepo(malApi), SyncRepo(aniListApi) ) const val appString = "cloudstreamapp" 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 3366384c..5680529e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -3,6 +3,26 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.ShowStatus interface SyncAPI : OAuth2API { + val icon: Int + val mainUrl: String + + /** + -1 -> None + 0 -> Watching + 1 -> Completed + 2 -> OnHold + 3 -> Dropped + 4 -> PlanToWatch + 5 -> ReWatching + */ + suspend fun score(id: String, status: SyncStatus): Boolean + + suspend fun getStatus(id: String): SyncStatus? + + suspend fun getResult(id: String): SyncResult? + + suspend fun search(name: String): List? + data class SyncSearchResult( val name: String, val syncApiName: String, @@ -48,7 +68,7 @@ interface SyncAPI : OAuth2API { var synopsis: String? = null, var airStatus: ShowStatus? = null, var nextAiring: SyncNextAiring? = null, - var studio: String? = null, + var studio: List? = null, var genres: List? = null, var trailerUrl: String? = null, @@ -62,24 +82,4 @@ interface SyncAPI : OAuth2API { var actors: List? = null, var characters: List? = null, ) - - val icon: Int - - val mainUrl: String - suspend fun search(name: String): List? - - /** - -1 -> None - 0 -> Watching - 1 -> Completed - 2 -> OnHold - 3 -> Dropped - 4 -> PlanToWatch - 5 -> ReWatching - */ - suspend fun score(id: String, status: SyncStatus): Boolean - - suspend fun getStatus(id: String): SyncStatus? - - suspend fun getResult(id: String): SyncResult? } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt new file mode 100644 index 00000000..1b8394bb --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -0,0 +1,25 @@ +package com.lagradost.cloudstream3.syncproviders + +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.safeApiCall + +class SyncRepo(private val repo: SyncAPI) { + val idPrefix get() = repo.idPrefix + + suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource { + return safeApiCall { repo.score(id, status) } + } + + suspend fun getStatus(id : String) : Resource { + return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } + } + + suspend fun getResult(id : String) : Resource { + return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } + } + + suspend fun search(query : String) : Resource> { + return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } + } +} \ No newline at end of file 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 6efd2204..b1d8deea 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 @@ -55,23 +55,19 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } override fun handleRedirect(url: String) { - try { - val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR - val token = sanitizer["access_token"]!! - val expiresIn = sanitizer["expires_in"]!! + val sanitizer = + splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + val token = sanitizer["access_token"]!! + val expiresIn = sanitizer["expires_in"]!! - val endTime = unixTime + expiresIn.toLong() + val endTime = unixTime + expiresIn.toLong() - switchToNewAccount() - setKey(accountId, ANILIST_UNIXTIME_KEY, endTime) - setKey(accountId, ANILIST_TOKEN_KEY, token) - setKey(ANILIST_SHOULD_UPDATE_LIST, true) - ioSafe { - getUser() - } - } catch (e: Exception) { - e.printStackTrace() + switchToNewAccount() + setKey(accountId, ANILIST_UNIXTIME_KEY, endTime) + setKey(accountId, ANILIST_TOKEN_KEY, token) + setKey(ANILIST_SHOULD_UPDATE_LIST, true) + ioSafe { + getUser() } } @@ -338,7 +334,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } fun initGetUser() { - if (getKey(accountId, ANILIST_TOKEN_KEY, null) == null) return + if (getAuth() == null) return ioSafe { getUser() } @@ -351,7 +347,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { )!! } - suspend fun getDataAboutId(id: Int): AniListTitleHolder? { + private suspend fun getDataAboutId(id: Int): AniListTitleHolder? { val q = """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) @@ -369,71 +365,56 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } } }""" - try { - val data = postApi("https://graphql.anilist.co", q, true) - var d: GetDataRoot? = null - try { - d = mapper.readValue(data) - } catch (e: Exception) { - logError(e) - println("AniList json failed") - } - if (d == null) { - return null - } - val main = d.data.Media - if (main.mediaListEntry != null) { - return AniListTitleHolder( - title = main.title, - id = id, - isFavourite = main.isFavourite, - progress = main.mediaListEntry.progress, - episodes = main.episodes, - score = main.mediaListEntry.score, - type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)), - ) - } else { - return AniListTitleHolder( - title = main.title, - id = id, - isFavourite = main.isFavourite, - progress = 0, - episodes = main.episodes, - score = 0, - type = AniListStatusType.None, - ) - } - } catch (e: Exception) { - logError(e) - return null + val data = postApi(q, true) + val d = mapper.readValue(data ?: return null) + + val main = d.data.Media + if (main.mediaListEntry != null) { + return AniListTitleHolder( + title = main.title, + id = id, + isFavourite = main.isFavourite, + progress = main.mediaListEntry.progress, + episodes = main.episodes, + score = main.mediaListEntry.score, + type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)), + ) + } else { + return AniListTitleHolder( + title = main.title, + id = id, + isFavourite = main.isFavourite, + progress = 0, + episodes = main.episodes, + score = 0, + type = AniListStatusType.None, + ) } + } - private suspend fun postApi(url: String, q: String, cache: Boolean = false): String { - return try { - if (!checkToken()) { - // println("VARS_ " + vars) - app.post( - "https://graphql.anilist.co/", - headers = mapOf( - "Authorization" to "Bearer " + getKey( - accountId, - ANILIST_TOKEN_KEY, - "" - )!!, - if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" - ), - cacheTime = 0, - data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) - timeout = 5 // REASONABLE TIMEOUT - ).text.replace("\\/", "/") - } else { - "" - } - } catch (e: Exception) { - logError(e) - "" + private fun getAuth(): String? { + return getKey( + accountId, + ANILIST_TOKEN_KEY + ) + } + + private suspend fun postApi(q: String, cache: Boolean = false): String? { + return if (!checkToken()) { + app.post( + "https://graphql.anilist.co/", + headers = mapOf( + "Authorization" to "Bearer " + (getAuth() ?: return null), + if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" + ), + cacheTime = 0, + data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) + timeout = 5 // REASONABLE TIMEOUT + ).text.replace("\\/", "/") + } else { + null } } @@ -515,12 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } suspend fun getAnilistAnimeListSmart(): Array? { - if (getKey( - accountId, - ANILIST_TOKEN_KEY, - null - ) == null - ) return null + if (getAuth() == null) return null if (checkToken()) return null return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { @@ -536,19 +512,18 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } private suspend fun getFullAnilistList(): FullAnilistList? { - try { - var userID: Int? = null - /** WARNING ASSUMES ONE USER! **/ - getKeys(ANILIST_USER_KEY)?.forEach { key -> - getKey(key, null)?.let { - userID = it.id - } + var userID: Int? = null + /** WARNING ASSUMES ONE USER! **/ + getKeys(ANILIST_USER_KEY)?.forEach { key -> + getKey(key, null)?.let { + userID = it.id } + } - val fixedUserID = userID ?: return null - val mediaType = "ANIME" + val fixedUserID = userID ?: return null + val mediaType = "ANIME" - val query = """ + val query = """ query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { lists { @@ -588,13 +563,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } } """ - val text = postApi("https://graphql.anilist.co", query) - return text.toKotlinObject() - - } catch (e: Exception) { - logError(e) - return null - } + val text = postApi(query) + return text?.toKotlinObject() } suspend fun toggleLike(id: Int): Boolean { @@ -610,7 +580,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } } }""" - val data = postApi("https://graphql.anilist.co", q) + val data = postApi(q) return data != "" } @@ -620,14 +590,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { score: Int?, progress: Int? ): Boolean { - try { - val q = - """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ - aniListStatusString[maxOf( - 0, - type.value - )] - }, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) { + val q = + """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ + aniListStatusString[maxOf( + 0, + type.value + )] + }, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) { SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) { id status @@ -635,12 +604,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { score } }""" - val data = postApi("https://graphql.anilist.co", q) - return data != "" - } catch (e: Exception) { - logError(e) - return false - } + val data = postApi(q) + return data != "" } private suspend fun getUser(setSettings: Boolean = true): AniListUser? { @@ -661,29 +626,24 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { } } }""" - try { - val data = postApi("https://graphql.anilist.co", q) - if (data == "") return null - val userData = mapper.readValue(data) - val u = userData.data.Viewer - val user = AniListUser( - u.id, - u.name, - u.avatar.large, - ) - if (setSettings) { - setKey(accountId, ANILIST_USER_KEY, user) - registerAccount() - } - /* // TODO FIX FAVS - for(i in u.favourites.anime.nodes) { - println("FFAV:" + i.id) - }*/ - return user - } catch (e: java.lang.Exception) { - logError(e) - return null + val data = postApi(q) + if (data == "") return null + val userData = mapper.readValue(data ?: return null) + val u = userData.data.Viewer + val user = AniListUser( + u.id, + u.name, + u.avatar.large, + ) + if (setSettings) { + setKey(accountId, ANILIST_USER_KEY, user) + registerAccount() } + /* // TODO FIX FAVS + for(i in u.favourites.anime.nodes) { + println("FFAV:" + i.id) + }*/ + return user } suspend fun getAllSeasons(id: Int): List { 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 40f5ac17..f2241446 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 @@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager @@ -45,20 +46,28 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override fun loginInfo(): OAuth2API.LoginInfo? { //getMalUser(true)? getKey(accountId, MAL_USER_KEY)?.let { user -> - return OAuth2API.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex) + return OAuth2API.LoginInfo( + profilePicture = user.picture, + name = user.name, + accountIndex = accountIndex + ) } return null } - override suspend fun search(name: String): List { - val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" - val auth = getKey( + private fun getAuth(): String? { + return getKey( accountId, MAL_TOKEN_KEY - ) ?: return emptyList() + ) + } + + override suspend fun search(name: String): List { + val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" + val auth = getAuth() ?: return emptyList() val res = app.get( url, headers = mapOf( - "Authorization" to "Bearer " + auth, + "Authorization" to "Bearer $auth", ), cacheTime = 0 ).text return mapper.readValue(res).data.map { @@ -73,7 +82,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - override suspend fun score(id: String, status : SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { return setScoreRequest( id.toIntOrNull() ?: return false, fromIntToAnimeStatus(status.status), @@ -82,25 +91,156 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ) } + data class MalAnime( + @JsonProperty("id") val id: Int?, + @JsonProperty("title") val title: String?, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("synopsis") val synopsis: String?, + @JsonProperty("mean") val mean: Double?, + @JsonProperty("rank") val rank: Int?, + @JsonProperty("popularity") val popularity: Int?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("nsfw") val nsfw: String?, + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("status") val status: String?, + @JsonProperty("genres") val genres: ArrayList, + @JsonProperty("my_list_status") val myListStatus: MyListStatus?, + @JsonProperty("num_episodes") val numEpisodes: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, + @JsonProperty("broadcast") val broadcast: Broadcast?, + @JsonProperty("source") val source: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, + @JsonProperty("rating") val rating: String?, + @JsonProperty("pictures") val pictures: ArrayList, + @JsonProperty("background") val background: String?, + @JsonProperty("related_anime") val relatedAnime: ArrayList, + @JsonProperty("related_manga") val relatedManga: ArrayList, + @JsonProperty("recommendations") val recommendations: ArrayList, + @JsonProperty("studios") val studios: ArrayList, + @JsonProperty("statistics") val statistics: Statistics?, + ) + + data class Recommendations( + @JsonProperty("node") val node: Node? = null, + @JsonProperty("num_recommendations") val numRecommendations: Int? = null + ) + + data class Studios( + @JsonProperty("id") val id: Int? = null, + @JsonProperty("name") val name: String? = null + ) + + + data class MyListStatus( + @JsonProperty("status") val status: String? = null, + @JsonProperty("score") val score: Int? = null, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int? = null, + @JsonProperty("is_rewatching") val isRewatching: Boolean? = null, + @JsonProperty("updated_at") val updatedAt: String? = null + ) + + data class RelatedAnime( + @JsonProperty("node") val node: Node? = null, + @JsonProperty("relation_type") val relationType: String? = null, + @JsonProperty("relation_type_formatted") val relationTypeFormatted: String? = null + ) + + data class Status( + @JsonProperty("watching") val watching: String? = null, + @JsonProperty("completed") val completed: String? = null, + @JsonProperty("on_hold") val onHold: String? = null, + @JsonProperty("dropped") val dropped: String? = null, + @JsonProperty("plan_to_watch") val planToWatch: String? = null + ) + + data class Statistics( + @JsonProperty("status") val status: Status? = null, + @JsonProperty("num_list_users") val numListUsers: Int? = null + ) + + private fun parseDate(string: String?): Long? { + return try { + SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time + } catch (e: Exception) { + null + } + } + + private fun toSearchResult(node : Node?) : SyncAPI.SyncSearchResult? { + return SyncAPI.SyncSearchResult( + name = node?.title ?: return null, + syncApiName = this.name, + id = node.id.toString(), + url = "https://myanimelist.net/anime/${node.id}", + posterUrl = node.main_picture?.large + ) + } + override suspend fun getResult(id: String): SyncAPI.SyncResult? { val internalId = id.toIntOrNull() ?: return null - TODO("Not yet implemented") + val url = + "https://api.myanimelist.net/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics" + val res = app.get( + url, headers = mapOf( + "Authorization" to "Bearer " + (getAuth() ?: return null) + ) + ).text + return mapper.readValue(res).let { malAnime -> + SyncAPI.SyncResult( + id = malAnime.id?.toString()!!, + totalEpisodes = malAnime.numEpisodes, + title = malAnime.title, + publicScore = malAnime.mean?.toFloat()?.times(100)?.toInt(), + duration = malAnime.averageEpisodeDuration, + synopsis = malAnime.synopsis, + airStatus = when (malAnime.status) { + "finished_airing" -> ShowStatus.Completed + "airing" -> ShowStatus.Ongoing + else -> null + }, + nextAiring = null, + studio = malAnime.studios.mapNotNull { it.name }, + genres = malAnime.genres.map { it.name }, + trailerUrl = null, + startDate = parseDate(malAnime.startDate), + endDate = parseDate(malAnime.endDate), + recommendations = malAnime.recommendations.mapNotNull { rec -> + val node = rec.node ?: return@mapNotNull null + toSearchResult(node) + }, + nextSeason = malAnime.relatedAnime.firstOrNull { + return@firstOrNull it.relationType == "sequel" + }?.let { toSearchResult(it.node) }, + prevSeason = malAnime.relatedAnime.firstOrNull { + return@firstOrNull it.relationType == "prequel" + }?.let { toSearchResult(it.node) }, + actors = null, + characters = null, + ) + } } override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { val internalId = id.toIntOrNull() ?: return null - val data = getDataAboutMalId(internalId)?.my_list_status ?: return null + val data = getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status") return SyncAPI.SyncStatus( - score = data.score, - status = malStatusAsString.indexOf(data.status), + score = data?.score, + status = malStatusAsString.indexOf(data?.status), isFavorite = null, - watchedEpisodes = data.num_episodes_watched, + watchedEpisodes = data?.num_episodes_watched, ) } companion object { - private val malStatusAsString = arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch") + private val malStatusAsString = + arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch") const val MAL_USER_KEY: String = "mal_user" // user data like profile const val MAL_CACHED_LIST: String = "mal_cached_list" @@ -111,39 +251,35 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } override fun handleRedirect(url: String) { - try { - val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR - val state = sanitizer["state"]!! - if (state == "RequestID$requestId") { - val currentCode = sanitizer["code"]!! - ioSafe { - var res = "" - try { - //println("cc::::: " + codeVerifier) - res = app.post( - "https://myanimelist.net/v1/oauth2/token", - data = mapOf( - "client_id" to key, - "code" to currentCode, - "code_verifier" to codeVerifier, - "grant_type" to "authorization_code" - ) - ).text - } catch (e: Exception) { - e.printStackTrace() - } + val sanitizer = + splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + val state = sanitizer["state"]!! + if (state == "RequestID$requestId") { + val currentCode = sanitizer["code"]!! + ioSafe { + var res = "" + try { + //println("cc::::: " + codeVerifier) + res = app.post( + "https://myanimelist.net/v1/oauth2/token", + data = mapOf( + "client_id" to key, + "code" to currentCode, + "code_verifier" to codeVerifier, + "grant_type" to "authorization_code" + ) + ).text + } catch (e: Exception) { + e.printStackTrace() + } - if (res != "") { - switchToNewAccount() - storeToken(res) - getMalUser() - setKey(MAL_SHOULD_UPDATE_LIST, true) - } + if (res != "") { + switchToNewAccount() + storeToken(res) + getMalUser() + setKey(MAL_SHOULD_UPDATE_LIST, true) } } - } catch (e: Exception) { - e.printStackTrace() } } @@ -277,44 +413,35 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("start_time") val start_time: String? ) - fun getMalAnimeListCached(): Array? { + private fun getMalAnimeListCached(): Array? { return getKey(MAL_CACHED_LIST) as? Array } suspend fun getMalAnimeListSmart(): Array? { - if (getKey( - accountId, - MAL_TOKEN_KEY - ) == null - ) return null + if (getAuth() == null) return null return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { val list = getMalAnimeList() - if (list != null) { - setKey(MAL_CACHED_LIST, list) - setKey(MAL_SHOULD_UPDATE_LIST, false) - } + setKey(MAL_CACHED_LIST, list) + setKey(MAL_SHOULD_UPDATE_LIST, false) list } else { getMalAnimeListCached() } } - private suspend fun getMalAnimeList(): Array? { - return try { - checkMalToken() - var offset = 0 - val fullList = mutableListOf() - val offsetRegex = Regex("""offset=(\d+)""") - while (true) { - val data: MalList = getMalAnimeListSlice(offset) ?: break - fullList.addAll(data.data) - offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break - } - fullList.toTypedArray() - //mapper.readValue(res) - } catch (e: Exception) { - null + private suspend fun getMalAnimeList(): Array { + checkMalToken() + var offset = 0 + val fullList = mutableListOf() + val offsetRegex = Regex("""offset=(\d+)""") + while (true) { + val data: MalList = getMalAnimeListSlice(offset) ?: break + fullList.addAll(data.data) + offset = + data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } + ?: break } + return fullList.toTypedArray() } fun convertToStatus(string: String): MalStatusType { @@ -323,43 +450,30 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { val user = "@me" - val auth = getKey( - accountId, - MAL_TOKEN_KEY - ) ?: return null - return try { - // Very lackluster docs - // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get - val url = - "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" - val res = app.get( - url, headers = mapOf( - "Authorization" to "Bearer $auth", - ), cacheTime = 0 - ).text - res.toKotlinObject() - } catch (e: Exception) { - logError(e) - null - } + val auth = getAuth() ?: return null + // Very lackluster docs + // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get + val url = + "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" + val res = app.get( + url, headers = mapOf( + "Authorization" to "Bearer $auth", + ), cacheTime = 0 + ).text + return res.toKotlinObject() } - private suspend fun getDataAboutMalId(id: Int): MalAnime? { - return try { - // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get - val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" - val res = app.get( - url, headers = mapOf( - "Authorization" to "Bearer " + getKey( - accountId, - MAL_TOKEN_KEY - )!! - ), cacheTime = 0 - ).text - mapper.readValue(res) - } catch (e: Exception) { - null - } + private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? { + // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get + val url = + "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" + val res = app.get( + url, headers = mapOf( + "Authorization" to "Bearer " + (getAuth() ?: return null) + ), cacheTime = 0 + ).text + + return mapper.readValue(res) } suspend fun setAllMalData() { @@ -372,14 +486,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val res = app.get( "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}", headers = mapOf( - "Authorization" to "Bearer " + getKey( - accountId, - MAL_TOKEN_KEY - )!! + "Authorization" to "Bearer " + (getAuth() ?: return) ), cacheTime = 0 ).text val values = mapper.readValue(res) - val titles = values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } + val titles = + values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } for (t in titles) { allTitles[t.id] = t } @@ -389,41 +501,37 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { + // No time remaining if the show has already ended try { - // No time remaining if the show has already ended - try { - endDate?.let { - if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null - } - } catch (e: ParseException) { - logError(e) + endDate?.let { + if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null } - - // Unparseable date: "2021 7 4 other null" - // Weekday: other, date: null - if (date.contains("null") || date.contains("other")) { - return null - } - - val currentDate = Calendar.getInstance() - val currentMonth = currentDate.get(Calendar.MONTH) + 1 - val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) - val currentYear = currentDate.get(Calendar.YEAR) - - val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") - dateFormat.timeZone = TimeZone.getTimeZone("Japan") - val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null - val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000 - - // if it has already aired this week add a week to the timer - val updatedTimeDiff = - if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff - return secondsToReadable(updatedTimeDiff.toInt(), "Now") - - } catch (e: Exception) { + } catch (e: ParseException) { logError(e) } - return null + + // Unparseable date: "2021 7 4 other null" + // Weekday: other, date: null + if (date.contains("null") || date.contains("other")) { + return null + } + + val currentDate = Calendar.getInstance() + val currentMonth = currentDate.get(Calendar.MONTH) + 1 + val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) + val currentYear = currentDate.get(Calendar.YEAR) + + val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") + dateFormat.timeZone = TimeZone.getTimeZone("Japan") + val parsedDate = + dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null + val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000 + + // if it has already aired this week add a week to the timer + val updatedTimeDiff = + if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff + return secondsToReadable(updatedTimeDiff.toInt(), "Now") + } private suspend fun checkMalToken() { @@ -438,27 +546,19 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private suspend fun getMalUser(setSettings: Boolean = true): MalUser? { checkMalToken() - return try { - val res = app.get( - "https://api.myanimelist.net/v2/users/@me", - headers = mapOf( - "Authorization" to "Bearer " + getKey( - accountId, - MAL_TOKEN_KEY - )!! - ), cacheTime = 0 - ).text + val res = app.get( + "https://api.myanimelist.net/v2/users/@me", + headers = mapOf( + "Authorization" to "Bearer " + (getAuth() ?: return null) + ), cacheTime = 0 + ).text - val user = mapper.readValue(res) - if (setSettings) { - setKey(accountId, MAL_USER_KEY, user) - registerAccount() - } - user - } catch (e: Exception) { - e.printStackTrace() - null + val user = mapper.readValue(res) + if (setSettings) { + setKey(accountId, MAL_USER_KEY, user) + registerAccount() } + return user } enum class MalStatusType(var value: Int) { @@ -483,7 +583,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - suspend fun setScoreRequest( + private suspend fun setScoreRequest( id: Int, status: MalStatusType? = null, score: Int? = null, @@ -495,22 +595,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { score, num_watched_episodes ) - if (res != "") { - return try { - val malStatus = mapper.readValue(res) - if (allTitles.containsKey(id)) { - val currentTitle = allTitles[id]!! - allTitles[id] = MalTitleHolder(malStatus, id, currentTitle.name) - } else { - allTitles[id] = MalTitleHolder(malStatus, id, "") - } - true - } catch (e: Exception) { - logError(e) - false - } + + return if (res.isNullOrBlank()) { + false } else { - return false + val malStatus = mapper.readValue(res) + if (allTitles.containsKey(id)) { + val currentTitle = allTitles[id]!! + allTitles[id] = MalTitleHolder(malStatus, id, currentTitle.name) + } else { + allTitles[id] = MalTitleHolder(malStatus, id, "") + } + true } } @@ -519,26 +615,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { status: String? = null, score: Int? = null, num_watched_episodes: Int? = null, - ): String { - return try { - app.put( - "https://api.myanimelist.net/v2/anime/$id/my_list_status", - headers = mapOf( - "Authorization" to "Bearer " + getKey( - accountId, - MAL_TOKEN_KEY - )!! - ), - data = mapOf( - "status" to status, - "score" to score?.toString(), - "num_watched_episodes" to num_watched_episodes?.toString() - ) - ).text - } catch (e: Exception) { - e.printStackTrace() - return "" - } + ): String? { + return app.put( + "https://api.myanimelist.net/v2/anime/$id/my_list_status", + headers = mapOf( + "Authorization" to "Bearer " + (getAuth() ?: return null) + ), + data = mapOf( + "status" to status, + "score" to score?.toString(), + "num_watched_episodes" to num_watched_episodes?.toString() + ) + ).text } @@ -591,7 +679,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ) // Used for getDataAboutId() - data class MalAnime( + data class SmallMalAnime( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String?, @JsonProperty("num_episodes") val num_episodes: Int, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 07314883..3efcc1a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -12,8 +12,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource @@ -21,8 +20,11 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.home.ParentItemAdapter -import com.lagradost.cloudstream3.ui.search.* +import com.lagradost.cloudstream3.ui.search.SearchAdapter +import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse +import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.search.SearchViewModel import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -31,11 +33,10 @@ import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.quick_search.* import java.util.concurrent.locks.ReentrantLock -class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { +class QuickSearchFragment : Fragment() { companion object { fun pushSearch(activity: Activity?, autoSearch: String? = null) { activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { - putBoolean("mainapi", true) autoSearch?.let { putString( "autosearch", @@ -49,18 +50,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { }) } - fun pushSync( - activity: Activity?, - autoSearch: String? = null, - callback: (SearchClickCallback) -> Unit - ) { - clickCallback = callback - activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { - putBoolean("mainapi", false) - putString("autosearch", autoSearch) - }) - } - var clickCallback: ((SearchClickCallback) -> Unit)? = null } @@ -87,10 +76,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { super.onViewCreated(view, savedInstanceState) context?.fixPaddingStatusbar(quick_search_root) - arguments?.getBoolean("mainapi", true)?.let { - isMainApis = it - } - val listLock = ReentrantLock() observe(searchViewModel.currentSearch) { list -> try { @@ -114,44 +99,38 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { val masterAdapter: RecyclerView.Adapter = ParentItemAdapter(mutableListOf(), { callback -> - when (callback.action) { - SEARCH_ACTION_LOAD -> { - if (isMainApis) { - activity?.popCurrentPage() - - SearchHelper.handleSearchClickCallback(activity, callback) - } else { - clickCallback?.invoke(callback) - } - } - else -> SearchHelper.handleSearchClickCallback(activity, callback) - } + SearchHelper.handleSearchClickCallback(activity, callback) + //when (callback.action) { + //SEARCH_ACTION_LOAD -> { + // clickCallback?.invoke(callback) + //} + // else -> SearchHelper.handleSearchClickCallback(activity, callback) + //} }, { item -> activity?.loadHomepageList(item) }) val searchExitIcon = quick_search?.findViewById(androidx.appcompat.R.id.search_close_btn) + val searchMagIcon = quick_search?.findViewById(androidx.appcompat.R.id.search_mag_icon) searchMagIcon?.scaleX = 0.65f searchMagIcon?.scaleY = 0.65f + quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { - val active = if (isMainApis) { - val langs = context?.getApiProviderLangSettings() - apis.filter { langs?.contains(it.lang) == true }.map { it.name }.toSet() - } else emptySet() - - searchViewModel.searchAndCancel( - query = query, - isMainApis = isMainApis, - ignoreSettings = false, - providersActive = active - ) - quick_search?.let { - UIHelper.hideKeyboard(it) + context?.filterProviderByPreferredMedia(hasHomePageIsRequired = false) + ?.map { it.name }?.toSet()?.let { active -> + searchViewModel.searchAndCancel( + query = query, + ignoreSettings = false, + providersActive = active + ) + quick_search?.let { + UIHelper.hideKeyboard(it) + } } return true 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 48b65a58..c4e84cd4 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 @@ -47,8 +47,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.mvvm.* -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis -import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO @@ -60,6 +58,7 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 @@ -73,8 +72,6 @@ import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.setKey -import com.lagradost.cloudstream3.utils.DataStoreHelper.addSync -import com.lagradost.cloudstream3.utils.DataStoreHelper.getSync import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.checkWrite @@ -93,6 +90,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result_swipe.* +import kotlinx.android.synthetic.main.result_sync.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext @@ -372,6 +370,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio private var currentLoadingCount = 0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED private lateinit var viewModel: ResultViewModel //by activityViewModels() + private lateinit var syncModel: SyncViewModel private var currentHeaderName: String? = null private var currentType: TvType? = null private var currentEpisodes: List? = null @@ -384,6 +383,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio ): View? { viewModel = ViewModelProvider(this)[ResultViewModel::class.java] + syncModel = + ViewModelProvider(this)[SyncViewModel::class.java] + return inflater.inflate(R.layout.fragment_result_swipe, container, false) } @@ -469,16 +471,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio var startAction: Int? = null private var startValue: Int? = null - private fun updateSync(id: Int) { - val syncList = getSync(id, SyncApis.map { it.idPrefix }) ?: return - val list = ArrayList>() - for (i in 0 until SyncApis.count()) { - val res = syncList[i] ?: continue - list.add(Pair(SyncApis[i], res)) - } - viewModel.updateSync(context, list) - } - private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { // java.util.IllegalFormatConversionException: f != java.lang.Integer // This can fail with malformed formatting @@ -525,6 +517,16 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) } + private fun setMalSync(id: String?): Boolean { + syncModel.setMalId(id ?: return false) + return true + } + + private fun setAniListSync(id: String?): Boolean { + syncModel.setAniListId(id ?: return false) + return true + } + private fun setActors(actors: List?) { if (actors.isNullOrEmpty()) { result_cast_text?.isVisible = false @@ -1147,6 +1149,45 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio } } + observe(syncModel.status) { status -> + var closed = false + when (status) { + is Resource.Failure -> { + result_sync_loading_shimmer?.stopShimmer() + result_sync_loading_shimmer?.isVisible = false + result_sync_holder?.isVisible = false + } + is Resource.Loading -> { + result_sync_loading_shimmer?.startShimmer() + result_sync_loading_shimmer?.isVisible = true + result_sync_holder?.isVisible = false + } + is Resource.Success -> { + result_sync_loading_shimmer?.stopShimmer() + result_sync_loading_shimmer?.isVisible = false + result_sync_holder?.isVisible = true + + val d = status.value + result_sync_rating?.value = d.score?.toFloat() ?: 0.0f + + /*when(d.status) { + -1 -> None + 0 -> Watching + 1 -> Completed + 2 -> OnHold + 3 -> Dropped + 4 -> PlanToWatch + 5 -> ReWatching + }*/ + //d.status + } + null -> { + closed = false + } + } + result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) + } + observe(viewModel.episodes) { episodeList -> lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this var isSeriesVisible = false @@ -1262,7 +1303,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio observe(viewModel.dubSubSelections) { range -> dubRange = range - if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true){ + if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true) { viewModel.changeDubStatus(DubStatus.Dubbed) } @@ -1297,7 +1338,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio } } - result_episode_select.setOnClickListener { + result_episode_select?.setOnClickListener { val ranges = episodeRanges if (ranges != null) { it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) } @@ -1307,6 +1348,13 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio } } + result_sync_set_score?.setOnClickListener { + // TODO set score + //syncModel.setScore(SyncAPI.SyncStatus( + // status = + //)) + } + observe(viewModel.publicEpisodesCount) { count -> if (count < 0) { result_episodes_text?.isVisible = false @@ -1321,19 +1369,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio currentId = it } - observe(viewModel.sync) { sync -> - for (s in sync) { - when (s) { - is Resource.Success -> { - val d = s.value ?: continue - setDuration(d.duration) - setRating(d.publicScore) - } - else -> Unit - } - } - } - observe(viewModel.resultResponse) { data -> when (data) { is Resource.Success -> { @@ -1399,27 +1434,12 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio } } - updateSync(d.getId()) - result_add_sync?.setOnClickListener { - QuickSearchFragment.pushSync(activity, d.name) { click -> - addSync(d.getId(), click.card.apiName, click.card.url) - - showToast( - activity, - context?.getString(R.string.added_sync_format) - ?.format(click.card.name), - Toast.LENGTH_SHORT - ) - - updateSync(d.getId()) - } - } - val showStatus = when (d) { is TvSeriesLoadResponse -> d.showStatus is AnimeLoadResponse -> d.showStatus else -> null } + setShow(showStatus) setDuration(d.duration) setYear(d.year) @@ -1427,6 +1447,18 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio setRecommendations(d.recommendations) setActors(d.actors) + if (SettingsFragment.accountEnabled) + if (d is AnimeLoadResponse) { + if ( + setMalSync(d.malId?.toString()) + || + setAniListSync(d.anilistId?.toString()) + ) { + syncModel.updateMetadata() + syncModel.updateStatus() + } + } + result_meta_site?.text = d.apiName val posterImageLink = d.posterUrl diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index fc346256..3948c2eb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.player.IGenerator @@ -79,9 +78,6 @@ class ResultViewModel : ViewModel() { private val _watchStatus: MutableLiveData = MutableLiveData() val watchStatus: LiveData get() = _watchStatus - private val _sync: MutableLiveData>> = MutableLiveData() - val sync: LiveData>> get() = _sync - fun updateWatchStatus(status: WatchType) = viewModelScope.launch { val currentId = id.value ?: return@launch _watchStatus.postValue(status) @@ -233,17 +229,6 @@ class ResultViewModel : ViewModel() { return generator } - fun updateSync(context: Context?, sync: List>) = viewModelScope.launch { - if (context == null) return@launch - - val list = ArrayList>() - for (s in sync) { - val result = safeApiCall { s.first.getResult(s.second) } - list.add(result) - _sync.postValue(list) - } - } - private fun updateEpisodes(localId: Int?, list: List, selection: Int?) { _episodes.postValue(list) generator = RepoLinkGenerator(list) 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 new file mode 100644 index 00000000..3b4cabde --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -0,0 +1,79 @@ +package com.lagradost.cloudstream3.ui.result + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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.SyncAPI +import kotlinx.coroutines.launch + +class SyncViewModel : ViewModel() { + private val repos = SyncApis + + private val _metaResponse: MutableLiveData> = + MutableLiveData() + + val metadata: LiveData> get() = _metaResponse + + private val _statusResponse: MutableLiveData?> = + MutableLiveData(null) + + val status: LiveData?> get() = _statusResponse + + // prefix, id + private val syncIds = hashMapOf() + + fun setMalId(id: String) { + syncIds[malApi.idPrefix] = id + } + + fun setAniListId(id: String) { + syncIds[aniListApi.idPrefix] = id + } + + fun setScore(status: SyncAPI.SyncStatus) = viewModelScope.launch { + for ((prefix, id) in syncIds) { + repos.firstOrNull { it.idPrefix == prefix }?.score(id, status) + } + + updateStatus() + } + + fun updateStatus() = viewModelScope.launch { + _statusResponse.postValue(Resource.Loading()) + var lastError: Resource = Resource.Failure(false, null, null, "No data") + for ((prefix, id) in syncIds) { + repos.firstOrNull { it.idPrefix == prefix }?.let { + val result = it.getStatus(id) + if (result is Resource.Success) { + _statusResponse.postValue(result) + return@launch + } else if (result is Resource.Failure) { + lastError = result + } + } + } + _statusResponse.postValue(lastError) + } + + fun updateMetadata() = viewModelScope.launch { + _metaResponse.postValue(Resource.Loading()) + var lastError: Resource = Resource.Failure(false, null, null, "No data") + for ((prefix, id) in syncIds) { + repos.firstOrNull { it.idPrefix == prefix }?.let { + val result = it.getResult(id) + if (result is Resource.Success) { + _metaResponse.postValue(result) + return@launch + } else if (result is Resource.Failure) { + lastError = result + } + } + } + _metaResponse.postValue(lastError) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index 551f0919..132a6307 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -4,15 +4,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis -import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import kotlinx.coroutines.Dispatchers @@ -39,7 +37,6 @@ class SearchViewModel : ViewModel() { val currentHistory: LiveData> get() = _currentHistory private val repos = apis.map { APIRepository(it) } - private val syncApis = SyncApis fun clearSearch() { _searchResponse.postValue(Resource.Success(ArrayList())) @@ -49,33 +46,11 @@ class SearchViewModel : ViewModel() { private var onGoingSearch: Job? = null fun searchAndCancel( query: String, - isMainApis: Boolean = true, providersActive: Set = setOf(), ignoreSettings: Boolean = false ) { onGoingSearch?.cancel() - onGoingSearch = search(query, isMainApis, providersActive, ignoreSettings) - } - - data class SyncSearchResultSearchResponse( - override val name: String, - override val url: String, - override val apiName: String, - override var type: TvType?, - override var posterUrl: String?, - override var id: Int?, - override var quality: SearchQuality? = null - ) : SearchResponse - - private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse { - return SyncSearchResultSearchResponse( - this.name, - this.url, - this.syncApiName, - null, - this.posterUrl, - null, //this.id.hashCode() - ) + onGoingSearch = search(query, providersActive, ignoreSettings) } fun updateHistory() = viewModelScope.launch { @@ -89,7 +64,6 @@ class SearchViewModel : ViewModel() { private fun search( query: String, - isMainApis: Boolean = true, providersActive: Set, ignoreSettings: Boolean = false ) = @@ -118,23 +92,12 @@ class SearchViewModel : ViewModel() { _currentSearch.postValue(ArrayList()) withContext(Dispatchers.IO) { // This interrupts UI otherwise - if (isMainApis) { - repos.filter { a -> - ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name)) - }.apmap { a -> // Parallel - val search = a.search(query) - currentList.add(OnGoingSearch(a.name, search)) - _currentSearch.postValue(currentList) - } - } else { - syncApis.apmap { a -> - val search = safeApiCall { - a.search(query)?.map { it.toSearchResponse() } - ?: throw ErrorLoadingException() - } - - currentList.add(OnGoingSearch(a.name, search)) - } + repos.filter { a -> + ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name)) + }.apmap { a -> // Parallel + val search = a.search(query) + currentList.add(OnGoingSearch(a.name, search)) + _currentSearch.postValue(currentList) } } _currentSearch.postValue(currentList) 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 new file mode 100644 index 00000000..5185e8a2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt @@ -0,0 +1,30 @@ +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.SyncAPI + +class SyncSearchViewModel { + data class SyncSearchResultSearchResponse( + override val name: String, + override val url: String, + override val apiName: String, + override var type: TvType?, + override var posterUrl: String?, + override var id: Int?, + override var quality: SearchQuality? = null + ) : SearchResponse + + private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse { + return SyncSearchResultSearchResponse( + this.name, + this.url, + this.syncApiName, + null, + this.posterUrl, + null, //this.id.hashCode() + ) + } + +} \ No newline at end of file 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 d5c4fc4e..8a2a6443 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 @@ -88,7 +88,7 @@ class SettingsFragment : PreferenceFragmentCompat() { return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION } - private const val accountEnabled = false + const val accountEnabled = false } private var beneneCount = 0 @@ -155,7 +155,11 @@ class SettingsFragment : PreferenceFragmentCompat() { val dialog = builder.show() dialog.findViewById(R.id.account_add)?.setOnClickListener { - api.authenticate() + try { + api.authenticate() + } catch (e: Exception) { + logError(e) + } } val ogIndex = api.accountIndex @@ -325,10 +329,9 @@ class SettingsFragment : PreferenceFragmentCompat() { val syncApis = listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi)) - for (sync in syncApis) { - getPref(sync.first)?.apply { + for ((key, api) in syncApis) { + getPref(key)?.apply { isVisible = accountEnabled - val api = sync.second title = getString(R.string.login_format).format(api.name, getString(R.string.account)) setOnPreferenceClickListener { _ -> @@ -336,7 +339,11 @@ class SettingsFragment : PreferenceFragmentCompat() { if (info != null) { showLoginInfo(api, info) } else { - api.authenticate() + try { + api.authenticate() + } catch (e: Exception) { + logError(e) + } } return@setOnPreferenceClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 142cc93a..43eff14d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.utils import android.annotation.SuppressLint import android.app.Activity -import android.content.ComponentName import android.content.ContentValues import android.content.Context import android.content.Intent @@ -23,6 +22,7 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.annotation.WorkerThread import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat import androidx.tvprovider.media.tv.PreviewChannelHelper import androidx.tvprovider.media.tv.TvContractCompat import androidx.tvprovider.media.tv.WatchNextProgram @@ -33,7 +33,10 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.wrappers.Wrappers -import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -196,17 +199,10 @@ object AppUtils { fun Context.openBrowser(url: String) { try { - val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java)) val intent = Intent(Intent.ACTION_VIEW) intent.data = Uri.parse(url) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - startActivity( - Intent.createChooser(intent, null) - .putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components) - ) - else - startActivity(intent) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + ContextCompat.startActivity(this, intent, null) } catch (e: Exception) { logError(e) } diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index 3aaccfb6..b21846ce 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -7,9 +7,10 @@ android:layout_height="match_parent"> @@ -38,8 +39,9 @@ app:tint="?attr/textColor" /> @@ -64,10 +67,10 @@ android:layout_gravity="end|center_vertical" android:contentDescription="@string/result_share" app:tint="?attr/textColor" /> - - - + android:layout_height="match_parent" + android:layout_gravity="center" + android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt b/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt index be95d2be..d8438ce2 100644 --- a/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt +++ b/app/src/test/java/com/lagradost/cloudstream3/ProviderTests.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3 +import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SubtitleHelper @@ -9,9 +10,7 @@ import org.junit.Test class ProviderTests { private fun getAllProviders(): List { - val allApis = APIHolder.apis - allApis.addAll(APIHolder.restrictedApis) - return allApis.filter { !it.usesWebView } + return allProviders.filter { !it.usesWebView } } private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {