forked from recloudstream/cloudstream
		
	sync stuff
This commit is contained in:
		
							parent
							
								
									e41542bef4
								
							
						
					
					
						commit
						2a27c0360d
					
				
					 16 changed files with 729 additions and 542 deletions
				
			
		|  | @ -266,7 +266,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { | ||||||
|             if (str.contains(appString)) { |             if (str.contains(appString)) { | ||||||
|                 for (api in OAuth2Apis) { |                 for (api in OAuth2Apis) { | ||||||
|                     if (str.contains("/${api.redirectUrl}")) { |                     if (str.contains("/${api.redirectUrl}")) { | ||||||
|  |                         try { | ||||||
|                             api.handleRedirect(str) |                             api.handleRedirect(str) | ||||||
|  |                         } catch (e : Exception) { | ||||||
|  |                             logError(e) | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|  |  | ||||||
|  | @ -43,8 +43,8 @@ interface OAuth2API { | ||||||
| 
 | 
 | ||||||
|         // used for active syncing |         // used for active syncing | ||||||
|         val SyncApis |         val SyncApis | ||||||
|             get() = listOf<SyncAPI>( |             get() = listOf( | ||||||
|                 malApi, aniListApi |                 SyncRepo(malApi), SyncRepo(aniListApi) | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         const val appString = "cloudstreamapp" |         const val appString = "cloudstreamapp" | ||||||
|  |  | ||||||
|  | @ -3,6 +3,26 @@ package com.lagradost.cloudstream3.syncproviders | ||||||
| import com.lagradost.cloudstream3.ShowStatus | import com.lagradost.cloudstream3.ShowStatus | ||||||
| 
 | 
 | ||||||
| interface SyncAPI : OAuth2API { | 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<SyncSearchResult>? | ||||||
|  | 
 | ||||||
|     data class SyncSearchResult( |     data class SyncSearchResult( | ||||||
|         val name: String, |         val name: String, | ||||||
|         val syncApiName: String, |         val syncApiName: String, | ||||||
|  | @ -48,7 +68,7 @@ interface SyncAPI : OAuth2API { | ||||||
|         var synopsis: String? = null, |         var synopsis: String? = null, | ||||||
|         var airStatus: ShowStatus? = null, |         var airStatus: ShowStatus? = null, | ||||||
|         var nextAiring: SyncNextAiring? = null, |         var nextAiring: SyncNextAiring? = null, | ||||||
|         var studio: String? = null, |         var studio: List<String>? = null, | ||||||
|         var genres: List<String>? = null, |         var genres: List<String>? = null, | ||||||
|         var trailerUrl: String? = null, |         var trailerUrl: String? = null, | ||||||
| 
 | 
 | ||||||
|  | @ -62,24 +82,4 @@ interface SyncAPI : OAuth2API { | ||||||
|         var actors: List<SyncActor>? = null, |         var actors: List<SyncActor>? = null, | ||||||
|         var characters: List<SyncCharacter>? = null, |         var characters: List<SyncCharacter>? = null, | ||||||
|     ) |     ) | ||||||
| 
 |  | ||||||
|     val icon: Int |  | ||||||
| 
 |  | ||||||
|     val mainUrl: String |  | ||||||
|     suspend fun search(name: String): List<SyncSearchResult>? |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|     -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? |  | ||||||
| } | } | ||||||
|  | @ -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<Boolean> { | ||||||
|  |         return safeApiCall { repo.score(id, status) } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus>  { | ||||||
|  |         return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult>  { | ||||||
|  |         return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> { | ||||||
|  |         return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -55,7 +55,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun handleRedirect(url: String) { |     override fun handleRedirect(url: String) { | ||||||
|         try { |  | ||||||
|         val sanitizer = |         val sanitizer = | ||||||
|             splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR |             splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR | ||||||
|         val token = sanitizer["access_token"]!! |         val token = sanitizer["access_token"]!! | ||||||
|  | @ -70,9 +69,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         ioSafe { |         ioSafe { | ||||||
|             getUser() |             getUser() | ||||||
|         } |         } | ||||||
|         } catch (e: Exception) { |  | ||||||
|             e.printStackTrace() |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { |     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { | ||||||
|  | @ -338,7 +334,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun initGetUser() { |     fun initGetUser() { | ||||||
|         if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return |         if (getAuth() == null) return | ||||||
|         ioSafe { |         ioSafe { | ||||||
|             getUser() |             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 = |         val q = | ||||||
|             """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) |             """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) |                 Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) | ||||||
|  | @ -369,18 +365,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             }""" |             }""" | ||||||
|         try { | 
 | ||||||
|             val data = postApi("https://graphql.anilist.co", q, true) |         val data = postApi(q, true) | ||||||
|             var d: GetDataRoot? = null |         val d = mapper.readValue<GetDataRoot>(data ?: return null) | ||||||
|             try { |  | ||||||
|                 d = mapper.readValue<GetDataRoot>(data) |  | ||||||
|             } catch (e: Exception) { |  | ||||||
|                 logError(e) |  | ||||||
|                 println("AniList json failed") |  | ||||||
|             } |  | ||||||
|             if (d == null) { |  | ||||||
|                 return null |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|         val main = d.data.Media |         val main = d.data.Media | ||||||
|         if (main.mediaListEntry != null) { |         if (main.mediaListEntry != null) { | ||||||
|  | @ -404,24 +391,22 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                 type = AniListStatusType.None, |                 type = AniListStatusType.None, | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|         } catch (e: Exception) { | 
 | ||||||
|             logError(e) |  | ||||||
|             return null |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun postApi(url: String, q: String, cache: Boolean = false): String { |     private fun getAuth(): String? { | ||||||
|         return try { |         return getKey( | ||||||
|             if (!checkToken()) { |             accountId, | ||||||
|                 // println("VARS_ " + vars) |             ANILIST_TOKEN_KEY | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private suspend fun postApi(q: String, cache: Boolean = false): String? { | ||||||
|  |         return if (!checkToken()) { | ||||||
|             app.post( |             app.post( | ||||||
|                 "https://graphql.anilist.co/", |                 "https://graphql.anilist.co/", | ||||||
|                 headers = mapOf( |                 headers = mapOf( | ||||||
|                         "Authorization" to "Bearer " + getKey( |                     "Authorization" to "Bearer " + (getAuth() ?: return null), | ||||||
|                             accountId, |  | ||||||
|                             ANILIST_TOKEN_KEY, |  | ||||||
|                             "" |  | ||||||
|                         )!!, |  | ||||||
|                     if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" |                     if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" | ||||||
|                 ), |                 ), | ||||||
|                 cacheTime = 0, |                 cacheTime = 0, | ||||||
|  | @ -429,11 +414,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                 timeout = 5 // REASONABLE TIMEOUT |                 timeout = 5 // REASONABLE TIMEOUT | ||||||
|             ).text.replace("\\/", "/") |             ).text.replace("\\/", "/") | ||||||
|         } else { |         } else { | ||||||
|                 "" |             null | ||||||
|             } |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             logError(e) |  | ||||||
|             "" |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -515,12 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suspend fun getAnilistAnimeListSmart(): Array<Lists>? { |     suspend fun getAnilistAnimeListSmart(): Array<Lists>? { | ||||||
|         if (getKey<String>( |         if (getAuth() == null) return null | ||||||
|                 accountId, |  | ||||||
|                 ANILIST_TOKEN_KEY, |  | ||||||
|                 null |  | ||||||
|             ) == null |  | ||||||
|         ) return null |  | ||||||
| 
 | 
 | ||||||
|         if (checkToken()) return null |         if (checkToken()) return null | ||||||
|         return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { |         return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { | ||||||
|  | @ -536,7 +512,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun getFullAnilistList(): FullAnilistList? { |     private suspend fun getFullAnilistList(): FullAnilistList? { | ||||||
|         try { |  | ||||||
|         var userID: Int? = null |         var userID: Int? = null | ||||||
|         /** WARNING ASSUMES ONE USER! **/ |         /** WARNING ASSUMES ONE USER! **/ | ||||||
|         getKeys(ANILIST_USER_KEY)?.forEach { key -> |         getKeys(ANILIST_USER_KEY)?.forEach { key -> | ||||||
|  | @ -588,13 +563,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                     } |                     } | ||||||
|                     } |                     } | ||||||
|             """ |             """ | ||||||
|             val text = postApi("https://graphql.anilist.co", query) |         val text = postApi(query) | ||||||
|             return text.toKotlinObject() |         return text?.toKotlinObject() | ||||||
| 
 |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             logError(e) |  | ||||||
|             return null |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suspend fun toggleLike(id: Int): Boolean { |     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 != "" |         return data != "" | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -620,7 +590,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         score: Int?, |         score: Int?, | ||||||
|         progress: Int? |         progress: Int? | ||||||
|     ): Boolean { |     ): Boolean { | ||||||
|         try { |  | ||||||
|         val q = |         val q = | ||||||
|             """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ |             """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ | ||||||
|                 aniListStatusString[maxOf( |                 aniListStatusString[maxOf( | ||||||
|  | @ -635,12 +604,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                     score |                     score | ||||||
|                 } |                 } | ||||||
|                 }""" |                 }""" | ||||||
|             val data = postApi("https://graphql.anilist.co", q) |         val data = postApi(q) | ||||||
|         return data != "" |         return data != "" | ||||||
|         } catch (e: Exception) { |  | ||||||
|             logError(e) |  | ||||||
|             return false |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun getUser(setSettings: Boolean = true): AniListUser? { |     private suspend fun getUser(setSettings: Boolean = true): AniListUser? { | ||||||
|  | @ -661,10 +626,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                         } |                         } | ||||||
|   					} |   					} | ||||||
| 				}""" | 				}""" | ||||||
|         try { |         val data = postApi(q) | ||||||
|             val data = postApi("https://graphql.anilist.co", q) |  | ||||||
|         if (data == "") return null |         if (data == "") return null | ||||||
|             val userData = mapper.readValue<AniListRoot>(data) |         val userData = mapper.readValue<AniListRoot>(data ?: return null) | ||||||
|         val u = userData.data.Viewer |         val u = userData.data.Viewer | ||||||
|         val user = AniListUser( |         val user = AniListUser( | ||||||
|             u.id, |             u.id, | ||||||
|  | @ -680,10 +644,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|             println("FFAV:" + i.id) |             println("FFAV:" + i.id) | ||||||
|         }*/ |         }*/ | ||||||
|         return user |         return user | ||||||
|         } catch (e: java.lang.Exception) { |  | ||||||
|             logError(e) |  | ||||||
|             return null |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suspend fun getAllSeasons(id: Int): List<SeasonResponse?> { |     suspend fun getAllSeasons(id: Int): List<SeasonResponse?> { | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser | import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
| import com.lagradost.cloudstream3.R | import com.lagradost.cloudstream3.R | ||||||
|  | import com.lagradost.cloudstream3.ShowStatus | ||||||
| import com.lagradost.cloudstream3.app | import com.lagradost.cloudstream3.app | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.syncproviders.AccountManager | import com.lagradost.cloudstream3.syncproviders.AccountManager | ||||||
|  | @ -45,20 +46,28 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     override fun loginInfo(): OAuth2API.LoginInfo? { |     override fun loginInfo(): OAuth2API.LoginInfo? { | ||||||
|         //getMalUser(true)? |         //getMalUser(true)? | ||||||
|         getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> |         getKey<MalUser>(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 |         return null | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> { |     private fun getAuth(): String? { | ||||||
|         val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" |         return getKey( | ||||||
|         val auth = getKey<String>( |  | ||||||
|             accountId, |             accountId, | ||||||
|             MAL_TOKEN_KEY |             MAL_TOKEN_KEY | ||||||
|         ) ?: return emptyList() |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> { | ||||||
|  |         val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" | ||||||
|  |         val auth = getAuth() ?: return emptyList() | ||||||
|         val res = app.get( |         val res = app.get( | ||||||
|             url, headers = mapOf( |             url, headers = mapOf( | ||||||
|                 "Authorization" to "Bearer " + auth, |                 "Authorization" to "Bearer $auth", | ||||||
|             ), cacheTime = 0 |             ), cacheTime = 0 | ||||||
|         ).text |         ).text | ||||||
|         return mapper.readValue<MalSearch>(res).data.map { |         return mapper.readValue<MalSearch>(res).data.map { | ||||||
|  | @ -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<Genres>, | ||||||
|  |         @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<MainPicture>, | ||||||
|  |         @JsonProperty("background") val background: String?, | ||||||
|  |         @JsonProperty("related_anime") val relatedAnime: ArrayList<RelatedAnime>, | ||||||
|  |         @JsonProperty("related_manga") val relatedManga: ArrayList<String>, | ||||||
|  |         @JsonProperty("recommendations") val recommendations: ArrayList<Recommendations>, | ||||||
|  |         @JsonProperty("studios") val studios: ArrayList<Studios>, | ||||||
|  |         @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? { |     override suspend fun getResult(id: String): SyncAPI.SyncResult? { | ||||||
|         val internalId = id.toIntOrNull() ?: return null |         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<MalAnime>(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? { |     override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { | ||||||
|         val internalId = id.toIntOrNull() ?: return null |         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( |         return SyncAPI.SyncStatus( | ||||||
|             score = data.score, |             score = data?.score, | ||||||
|             status = malStatusAsString.indexOf(data.status), |             status = malStatusAsString.indexOf(data?.status), | ||||||
|             isFavorite = null, |             isFavorite = null, | ||||||
|             watchedEpisodes = data.num_episodes_watched, |             watchedEpisodes = data?.num_episodes_watched, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     companion object { |     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_USER_KEY: String = "mal_user" // user data like profile | ||||||
|         const val MAL_CACHED_LIST: String = "mal_cached_list" |         const val MAL_CACHED_LIST: String = "mal_cached_list" | ||||||
|  | @ -111,7 +251,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun handleRedirect(url: String) { |     override fun handleRedirect(url: String) { | ||||||
|         try { |  | ||||||
|         val sanitizer = |         val sanitizer = | ||||||
|             splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR |             splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR | ||||||
|         val state = sanitizer["state"]!! |         val state = sanitizer["state"]!! | ||||||
|  | @ -142,9 +281,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         } catch (e: Exception) { |  | ||||||
|             e.printStackTrace() |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun authenticate() { |     override fun authenticate() { | ||||||
|  | @ -277,30 +413,23 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         @JsonProperty("start_time") val start_time: String? |         @JsonProperty("start_time") val start_time: String? | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     fun getMalAnimeListCached(): Array<Data>? { |     private fun getMalAnimeListCached(): Array<Data>? { | ||||||
|         return getKey(MAL_CACHED_LIST) as? Array<Data> |         return getKey(MAL_CACHED_LIST) as? Array<Data> | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suspend fun getMalAnimeListSmart(): Array<Data>? { |     suspend fun getMalAnimeListSmart(): Array<Data>? { | ||||||
|         if (getKey<String>( |         if (getAuth() == null) return null | ||||||
|                 accountId, |  | ||||||
|                 MAL_TOKEN_KEY |  | ||||||
|             ) == null |  | ||||||
|         ) return null |  | ||||||
|         return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { |         return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { | ||||||
|             val list = getMalAnimeList() |             val list = getMalAnimeList() | ||||||
|             if (list != null) { |  | ||||||
|             setKey(MAL_CACHED_LIST, list) |             setKey(MAL_CACHED_LIST, list) | ||||||
|             setKey(MAL_SHOULD_UPDATE_LIST, false) |             setKey(MAL_SHOULD_UPDATE_LIST, false) | ||||||
|             } |  | ||||||
|             list |             list | ||||||
|         } else { |         } else { | ||||||
|             getMalAnimeListCached() |             getMalAnimeListCached() | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun getMalAnimeList(): Array<Data>? { |     private suspend fun getMalAnimeList(): Array<Data> { | ||||||
|         return try { |  | ||||||
|         checkMalToken() |         checkMalToken() | ||||||
|         var offset = 0 |         var offset = 0 | ||||||
|         val fullList = mutableListOf<Data>() |         val fullList = mutableListOf<Data>() | ||||||
|  | @ -308,13 +437,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         while (true) { |         while (true) { | ||||||
|             val data: MalList = getMalAnimeListSlice(offset) ?: break |             val data: MalList = getMalAnimeListSlice(offset) ?: break | ||||||
|             fullList.addAll(data.data) |             fullList.addAll(data.data) | ||||||
|                 offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break |             offset = | ||||||
|             } |                 data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } | ||||||
|             fullList.toTypedArray() |                     ?: break | ||||||
|             //mapper.readValue<MalAnime>(res) |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             null |  | ||||||
|         } |         } | ||||||
|  |         return fullList.toTypedArray() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun convertToStatus(string: String): MalStatusType { |     fun convertToStatus(string: String): MalStatusType { | ||||||
|  | @ -323,11 +450,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
| 
 | 
 | ||||||
|     private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { |     private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { | ||||||
|         val user = "@me" |         val user = "@me" | ||||||
|         val auth = getKey<String>( |         val auth = getAuth() ?: return null | ||||||
|             accountId, |  | ||||||
|             MAL_TOKEN_KEY |  | ||||||
|         ) ?: return null |  | ||||||
|         return try { |  | ||||||
|         // Very lackluster docs |         // Very lackluster docs | ||||||
|         // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get |         // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get | ||||||
|         val url = |         val url = | ||||||
|  | @ -337,29 +460,20 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                 "Authorization" to "Bearer $auth", |                 "Authorization" to "Bearer $auth", | ||||||
|             ), cacheTime = 0 |             ), cacheTime = 0 | ||||||
|         ).text |         ).text | ||||||
|             res.toKotlinObject() |         return res.toKotlinObject() | ||||||
|         } catch (e: Exception) { |  | ||||||
|             logError(e) |  | ||||||
|             null |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun getDataAboutMalId(id: Int): MalAnime? { |     private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? { | ||||||
|         return try { |  | ||||||
|         // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get |         // 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 url = | ||||||
|  |             "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" | ||||||
|         val res = app.get( |         val res = app.get( | ||||||
|             url, headers = mapOf( |             url, headers = mapOf( | ||||||
|                     "Authorization" to "Bearer " + getKey<String>( |                 "Authorization" to "Bearer " + (getAuth() ?: return null) | ||||||
|                         accountId, |  | ||||||
|                         MAL_TOKEN_KEY |  | ||||||
|                     )!! |  | ||||||
|             ), cacheTime = 0 |             ), cacheTime = 0 | ||||||
|         ).text |         ).text | ||||||
|             mapper.readValue<MalAnime>(res) | 
 | ||||||
|         } catch (e: Exception) { |         return mapper.readValue<SmallMalAnime>(res) | ||||||
|             null |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     suspend fun setAllMalData() { |     suspend fun setAllMalData() { | ||||||
|  | @ -372,14 +486,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|             val res = app.get( |             val res = app.get( | ||||||
|                 "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}", |                 "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}", | ||||||
|                 headers = mapOf( |                 headers = mapOf( | ||||||
|                     "Authorization" to "Bearer " + getKey<String>( |                     "Authorization" to "Bearer " + (getAuth() ?: return) | ||||||
|                         accountId, |  | ||||||
|                         MAL_TOKEN_KEY |  | ||||||
|                     )!! |  | ||||||
|                 ), cacheTime = 0 |                 ), cacheTime = 0 | ||||||
|             ).text |             ).text | ||||||
|             val values = mapper.readValue<MalRoot>(res) |             val values = mapper.readValue<MalRoot>(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) { |             for (t in titles) { | ||||||
|                 allTitles[t.id] = t |                 allTitles[t.id] = t | ||||||
|             } |             } | ||||||
|  | @ -389,7 +501,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { |     fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { | ||||||
|         try { |  | ||||||
|         // No time remaining if the show has already ended |         // No time remaining if the show has already ended | ||||||
|         try { |         try { | ||||||
|             endDate?.let { |             endDate?.let { | ||||||
|  | @ -412,7 +523,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
| 
 | 
 | ||||||
|         val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") |         val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") | ||||||
|         dateFormat.timeZone = TimeZone.getTimeZone("Japan") |         dateFormat.timeZone = TimeZone.getTimeZone("Japan") | ||||||
|             val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null |         val parsedDate = | ||||||
|  |             dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null | ||||||
|         val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000 |         val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000 | ||||||
| 
 | 
 | ||||||
|         // if it has already aired this week add a week to the timer |         // if it has already aired this week add a week to the timer | ||||||
|  | @ -420,10 +532,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|             if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff |             if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff | ||||||
|         return secondsToReadable(updatedTimeDiff.toInt(), "Now") |         return secondsToReadable(updatedTimeDiff.toInt(), "Now") | ||||||
| 
 | 
 | ||||||
|         } catch (e: Exception) { |  | ||||||
|             logError(e) |  | ||||||
|         } |  | ||||||
|         return null |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun checkMalToken() { |     private suspend fun checkMalToken() { | ||||||
|  | @ -438,14 +546,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
| 
 | 
 | ||||||
|     private suspend fun getMalUser(setSettings: Boolean = true): MalUser? { |     private suspend fun getMalUser(setSettings: Boolean = true): MalUser? { | ||||||
|         checkMalToken() |         checkMalToken() | ||||||
|         return try { |  | ||||||
|         val res = app.get( |         val res = app.get( | ||||||
|             "https://api.myanimelist.net/v2/users/@me", |             "https://api.myanimelist.net/v2/users/@me", | ||||||
|             headers = mapOf( |             headers = mapOf( | ||||||
|                     "Authorization" to "Bearer " + getKey<String>( |                 "Authorization" to "Bearer " + (getAuth() ?: return null) | ||||||
|                         accountId, |  | ||||||
|                         MAL_TOKEN_KEY |  | ||||||
|                     )!! |  | ||||||
|             ), cacheTime = 0 |             ), cacheTime = 0 | ||||||
|         ).text |         ).text | ||||||
| 
 | 
 | ||||||
|  | @ -454,11 +558,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|             setKey(accountId, MAL_USER_KEY, user) |             setKey(accountId, MAL_USER_KEY, user) | ||||||
|             registerAccount() |             registerAccount() | ||||||
|         } |         } | ||||||
|             user |         return user | ||||||
|         } catch (e: Exception) { |  | ||||||
|             e.printStackTrace() |  | ||||||
|             null |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     enum class MalStatusType(var value: Int) { |     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, |         id: Int, | ||||||
|         status: MalStatusType? = null, |         status: MalStatusType? = null, | ||||||
|         score: Int? = null, |         score: Int? = null, | ||||||
|  | @ -495,8 +595,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|             score, |             score, | ||||||
|             num_watched_episodes |             num_watched_episodes | ||||||
|         ) |         ) | ||||||
|         if (res != "") { | 
 | ||||||
|             return try { |         return if (res.isNullOrBlank()) { | ||||||
|  |             false | ||||||
|  |         } else { | ||||||
|             val malStatus = mapper.readValue<MalStatus>(res) |             val malStatus = mapper.readValue<MalStatus>(res) | ||||||
|             if (allTitles.containsKey(id)) { |             if (allTitles.containsKey(id)) { | ||||||
|                 val currentTitle = allTitles[id]!! |                 val currentTitle = allTitles[id]!! | ||||||
|  | @ -505,12 +607,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                 allTitles[id] = MalTitleHolder(malStatus, id, "") |                 allTitles[id] = MalTitleHolder(malStatus, id, "") | ||||||
|             } |             } | ||||||
|             true |             true | ||||||
|             } catch (e: Exception) { |  | ||||||
|                 logError(e) |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             return false |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -519,15 +615,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|         status: String? = null, |         status: String? = null, | ||||||
|         score: Int? = null, |         score: Int? = null, | ||||||
|         num_watched_episodes: Int? = null, |         num_watched_episodes: Int? = null, | ||||||
|     ): String { |     ): String? { | ||||||
|         return try { |         return app.put( | ||||||
|             app.put( |  | ||||||
|             "https://api.myanimelist.net/v2/anime/$id/my_list_status", |             "https://api.myanimelist.net/v2/anime/$id/my_list_status", | ||||||
|             headers = mapOf( |             headers = mapOf( | ||||||
|                     "Authorization" to "Bearer " + getKey<String>( |                 "Authorization" to "Bearer " + (getAuth() ?: return null) | ||||||
|                         accountId, |  | ||||||
|                         MAL_TOKEN_KEY |  | ||||||
|                     )!! |  | ||||||
|             ), |             ), | ||||||
|             data = mapOf( |             data = mapOf( | ||||||
|                 "status" to status, |                 "status" to status, | ||||||
|  | @ -535,10 +627,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|                 "num_watched_episodes" to num_watched_episodes?.toString() |                 "num_watched_episodes" to num_watched_episodes?.toString() | ||||||
|             ) |             ) | ||||||
|         ).text |         ).text | ||||||
|         } catch (e: Exception) { |  | ||||||
|             e.printStackTrace() |  | ||||||
|             return "" |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -591,7 +679,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     // Used for getDataAboutId() |     // Used for getDataAboutId() | ||||||
|     data class MalAnime( |     data class SmallMalAnime( | ||||||
|         @JsonProperty("id") val id: Int, |         @JsonProperty("id") val id: Int, | ||||||
|         @JsonProperty("title") val title: String?, |         @JsonProperty("title") val title: String?, | ||||||
|         @JsonProperty("num_episodes") val num_episodes: Int, |         @JsonProperty("num_episodes") val num_episodes: Int, | ||||||
|  |  | ||||||
|  | @ -12,8 +12,7 @@ import androidx.fragment.app.Fragment | ||||||
| import androidx.fragment.app.activityViewModels | import androidx.fragment.app.activityViewModels | ||||||
| import androidx.recyclerview.widget.GridLayoutManager | import androidx.recyclerview.widget.GridLayoutManager | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
| import com.lagradost.cloudstream3.APIHolder.apis | import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia | ||||||
| import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings |  | ||||||
| import com.lagradost.cloudstream3.HomePageList | import com.lagradost.cloudstream3.HomePageList | ||||||
| import com.lagradost.cloudstream3.R | import com.lagradost.cloudstream3.R | ||||||
| import com.lagradost.cloudstream3.mvvm.Resource | 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.mvvm.observe | ||||||
| import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList | import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList | ||||||
| import com.lagradost.cloudstream3.ui.home.ParentItemAdapter | 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.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 | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.navigate | 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 kotlinx.android.synthetic.main.quick_search.* | ||||||
| import java.util.concurrent.locks.ReentrantLock | import java.util.concurrent.locks.ReentrantLock | ||||||
| 
 | 
 | ||||||
| class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | class QuickSearchFragment : Fragment() { | ||||||
|     companion object { |     companion object { | ||||||
|         fun pushSearch(activity: Activity?, autoSearch: String? = null) { |         fun pushSearch(activity: Activity?, autoSearch: String? = null) { | ||||||
|             activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { |             activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { | ||||||
|                 putBoolean("mainapi", true) |  | ||||||
|                 autoSearch?.let { |                 autoSearch?.let { | ||||||
|                     putString( |                     putString( | ||||||
|                         "autosearch", |                         "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 |         var clickCallback: ((SearchClickCallback) -> Unit)? = null | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -87,10 +76,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | ||||||
|         super.onViewCreated(view, savedInstanceState) |         super.onViewCreated(view, savedInstanceState) | ||||||
|         context?.fixPaddingStatusbar(quick_search_root) |         context?.fixPaddingStatusbar(quick_search_root) | ||||||
| 
 | 
 | ||||||
|         arguments?.getBoolean("mainapi", true)?.let { |  | ||||||
|             isMainApis = it |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         val listLock = ReentrantLock() |         val listLock = ReentrantLock() | ||||||
|         observe(searchViewModel.currentSearch) { list -> |         observe(searchViewModel.currentSearch) { list -> | ||||||
|             try { |             try { | ||||||
|  | @ -114,45 +99,39 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { | ||||||
| 
 | 
 | ||||||
|         val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = |         val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = | ||||||
|             ParentItemAdapter(mutableListOf(), { callback -> |             ParentItemAdapter(mutableListOf(), { callback -> | ||||||
|                 when (callback.action) { |  | ||||||
|                     SEARCH_ACTION_LOAD -> { |  | ||||||
|                         if (isMainApis) { |  | ||||||
|                             activity?.popCurrentPage() |  | ||||||
| 
 |  | ||||||
|                 SearchHelper.handleSearchClickCallback(activity, callback) |                 SearchHelper.handleSearchClickCallback(activity, callback) | ||||||
|                         } else { |                 //when (callback.action) { | ||||||
|                             clickCallback?.invoke(callback) |                     //SEARCH_ACTION_LOAD -> { | ||||||
|                         } |                     //    clickCallback?.invoke(callback) | ||||||
|                     } |                     //} | ||||||
|                     else -> SearchHelper.handleSearchClickCallback(activity, callback) |                 //    else -> SearchHelper.handleSearchClickCallback(activity, callback) | ||||||
|                 } |                 //} | ||||||
|             }, { item -> |             }, { item -> | ||||||
|                 activity?.loadHomepageList(item) |                 activity?.loadHomepageList(item) | ||||||
|             }) |             }) | ||||||
| 
 | 
 | ||||||
|         val searchExitIcon = |         val searchExitIcon = | ||||||
|             quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) |             quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) | ||||||
|  | 
 | ||||||
|         val searchMagIcon = |         val searchMagIcon = | ||||||
|             quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon) |             quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon) | ||||||
| 
 | 
 | ||||||
|         searchMagIcon?.scaleX = 0.65f |         searchMagIcon?.scaleX = 0.65f | ||||||
|         searchMagIcon?.scaleY = 0.65f |         searchMagIcon?.scaleY = 0.65f | ||||||
|  | 
 | ||||||
|         quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { |         quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { | ||||||
|             override fun onQueryTextSubmit(query: String): Boolean { |             override fun onQueryTextSubmit(query: String): Boolean { | ||||||
|                 val active = if (isMainApis) { |                 context?.filterProviderByPreferredMedia(hasHomePageIsRequired = false) | ||||||
|                     val langs = context?.getApiProviderLangSettings() |                     ?.map { it.name }?.toSet()?.let { active -> | ||||||
|                     apis.filter { langs?.contains(it.lang) == true }.map { it.name }.toSet() |  | ||||||
|                 } else emptySet() |  | ||||||
| 
 |  | ||||||
|                     searchViewModel.searchAndCancel( |                     searchViewModel.searchAndCancel( | ||||||
|                         query = query, |                         query = query, | ||||||
|                     isMainApis = isMainApis, |  | ||||||
|                         ignoreSettings = false, |                         ignoreSettings = false, | ||||||
|                         providersActive = active |                         providersActive = active | ||||||
|                     ) |                     ) | ||||||
|                     quick_search?.let { |                     quick_search?.let { | ||||||
|                         UIHelper.hideKeyboard(it) |                         UIHelper.hideKeyboard(it) | ||||||
|                     } |                     } | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 return true |                 return true | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -47,8 +47,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
| import com.lagradost.cloudstream3.CommonActivity.getCastSession | import com.lagradost.cloudstream3.CommonActivity.getCastSession | ||||||
| import com.lagradost.cloudstream3.CommonActivity.showToast | import com.lagradost.cloudstream3.CommonActivity.showToast | ||||||
| import com.lagradost.cloudstream3.mvvm.* | 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.WatchType | ||||||
| import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD | import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD | ||||||
| import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO | 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.quicksearch.QuickSearchFragment | ||||||
| import com.lagradost.cloudstream3.ui.search.SearchAdapter | import com.lagradost.cloudstream3.ui.search.SearchAdapter | ||||||
| import com.lagradost.cloudstream3.ui.search.SearchHelper | 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.isTrueTvSettings | ||||||
| import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings | ||||||
| import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 | 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.Coroutines.main | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.getFolderName | import com.lagradost.cloudstream3.utils.DataStore.getFolderName | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.setKey | 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.DataStoreHelper.getViewPos | ||||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog | import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog | ||||||
| import com.lagradost.cloudstream3.utils.UIHelper.checkWrite | 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 com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename | ||||||
| import kotlinx.android.synthetic.main.fragment_result.* | import kotlinx.android.synthetic.main.fragment_result.* | ||||||
| import kotlinx.android.synthetic.main.fragment_result_swipe.* | import kotlinx.android.synthetic.main.fragment_result_swipe.* | ||||||
|  | import kotlinx.android.synthetic.main.result_sync.* | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
| import kotlinx.coroutines.Job | import kotlinx.coroutines.Job | ||||||
| import kotlinx.coroutines.withContext | import kotlinx.coroutines.withContext | ||||||
|  | @ -372,6 +370,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | ||||||
|     private var currentLoadingCount = |     private var currentLoadingCount = | ||||||
|         0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED |         0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED | ||||||
|     private lateinit var viewModel: ResultViewModel //by activityViewModels() |     private lateinit var viewModel: ResultViewModel //by activityViewModels() | ||||||
|  |     private lateinit var syncModel: SyncViewModel | ||||||
|     private var currentHeaderName: String? = null |     private var currentHeaderName: String? = null | ||||||
|     private var currentType: TvType? = null |     private var currentType: TvType? = null | ||||||
|     private var currentEpisodes: List<ResultEpisode>? = null |     private var currentEpisodes: List<ResultEpisode>? = null | ||||||
|  | @ -384,6 +383,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | ||||||
|     ): View? { |     ): View? { | ||||||
|         viewModel = |         viewModel = | ||||||
|             ViewModelProvider(this)[ResultViewModel::class.java] |             ViewModelProvider(this)[ResultViewModel::class.java] | ||||||
|  |         syncModel = | ||||||
|  |             ViewModelProvider(this)[SyncViewModel::class.java] | ||||||
|  | 
 | ||||||
|         return inflater.inflate(R.layout.fragment_result_swipe, container, false) |         return inflater.inflate(R.layout.fragment_result_swipe, container, false) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -469,16 +471,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | ||||||
|     var startAction: Int? = null |     var startAction: Int? = null | ||||||
|     private var startValue: Int? = null |     private var startValue: Int? = null | ||||||
| 
 | 
 | ||||||
|     private fun updateSync(id: Int) { |  | ||||||
|         val syncList = getSync(id, SyncApis.map { it.idPrefix }) ?: return |  | ||||||
|         val list = ArrayList<Pair<SyncAPI, String>>() |  | ||||||
|         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?) { |     private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { | ||||||
|         // java.util.IllegalFormatConversionException: f != java.lang.Integer |         // java.util.IllegalFormatConversionException: f != java.lang.Integer | ||||||
|         // This can fail with malformed formatting |         // 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)) |         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<ActorData>?) { |     private fun setActors(actors: List<ActorData>?) { | ||||||
|         if (actors.isNullOrEmpty()) { |         if (actors.isNullOrEmpty()) { | ||||||
|             result_cast_text?.isVisible = false |             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 -> |         observe(viewModel.episodes) { episodeList -> | ||||||
|             lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this |             lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this | ||||||
|             var isSeriesVisible = false |             var isSeriesVisible = false | ||||||
|  | @ -1297,7 +1338,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         result_episode_select.setOnClickListener { |         result_episode_select?.setOnClickListener { | ||||||
|             val ranges = episodeRanges |             val ranges = episodeRanges | ||||||
|             if (ranges != null) { |             if (ranges != null) { | ||||||
|                 it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) } |                 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 -> |         observe(viewModel.publicEpisodesCount) { count -> | ||||||
|             if (count < 0) { |             if (count < 0) { | ||||||
|                 result_episodes_text?.isVisible = false |                 result_episodes_text?.isVisible = false | ||||||
|  | @ -1321,19 +1369,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | ||||||
|             currentId = it |             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 -> |         observe(viewModel.resultResponse) { data -> | ||||||
|             when (data) { |             when (data) { | ||||||
|                 is Resource.Success -> { |                 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) { |                         val showStatus = when (d) { | ||||||
|                             is TvSeriesLoadResponse -> d.showStatus |                             is TvSeriesLoadResponse -> d.showStatus | ||||||
|                             is AnimeLoadResponse -> d.showStatus |                             is AnimeLoadResponse -> d.showStatus | ||||||
|                             else -> null |                             else -> null | ||||||
|                         } |                         } | ||||||
|  | 
 | ||||||
|                         setShow(showStatus) |                         setShow(showStatus) | ||||||
|                         setDuration(d.duration) |                         setDuration(d.duration) | ||||||
|                         setYear(d.year) |                         setYear(d.year) | ||||||
|  | @ -1427,6 +1447,18 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | ||||||
|                         setRecommendations(d.recommendations) |                         setRecommendations(d.recommendations) | ||||||
|                         setActors(d.actors) |                         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 |                         result_meta_site?.text = d.apiName | ||||||
| 
 | 
 | ||||||
|                         val posterImageLink = d.posterUrl |                         val posterImageLink = d.posterUrl | ||||||
|  |  | ||||||
|  | @ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.getId | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||||
| import com.lagradost.cloudstream3.mvvm.Resource | import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.mvvm.safeApiCall | import com.lagradost.cloudstream3.mvvm.safeApiCall | ||||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI |  | ||||||
| import com.lagradost.cloudstream3.ui.APIRepository | import com.lagradost.cloudstream3.ui.APIRepository | ||||||
| import com.lagradost.cloudstream3.ui.WatchType | import com.lagradost.cloudstream3.ui.WatchType | ||||||
| import com.lagradost.cloudstream3.ui.player.IGenerator | import com.lagradost.cloudstream3.ui.player.IGenerator | ||||||
|  | @ -79,9 +78,6 @@ class ResultViewModel : ViewModel() { | ||||||
|     private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() |     private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() | ||||||
|     val watchStatus: LiveData<WatchType> get() = _watchStatus |     val watchStatus: LiveData<WatchType> get() = _watchStatus | ||||||
| 
 | 
 | ||||||
|     private val _sync: MutableLiveData<List<Resource<SyncAPI.SyncResult?>>> = MutableLiveData() |  | ||||||
|     val sync: LiveData<List<Resource<SyncAPI.SyncResult?>>> get() = _sync |  | ||||||
| 
 |  | ||||||
|     fun updateWatchStatus(status: WatchType) = viewModelScope.launch { |     fun updateWatchStatus(status: WatchType) = viewModelScope.launch { | ||||||
|         val currentId = id.value ?: return@launch |         val currentId = id.value ?: return@launch | ||||||
|         _watchStatus.postValue(status) |         _watchStatus.postValue(status) | ||||||
|  | @ -233,17 +229,6 @@ class ResultViewModel : ViewModel() { | ||||||
|         return generator |         return generator | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun updateSync(context: Context?, sync: List<Pair<SyncAPI, String>>) = viewModelScope.launch { |  | ||||||
|         if (context == null) return@launch |  | ||||||
| 
 |  | ||||||
|         val list = ArrayList<Resource<SyncAPI.SyncResult?>>() |  | ||||||
|         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<ResultEpisode>, selection: Int?) { |     private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) { | ||||||
|         _episodes.postValue(list) |         _episodes.postValue(list) | ||||||
|         generator = RepoLinkGenerator(list) |         generator = RepoLinkGenerator(list) | ||||||
|  |  | ||||||
|  | @ -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<Resource<SyncAPI.SyncResult>> = | ||||||
|  |         MutableLiveData() | ||||||
|  | 
 | ||||||
|  |     val metadata: LiveData<Resource<SyncAPI.SyncResult>> get() = _metaResponse | ||||||
|  | 
 | ||||||
|  |     private val _statusResponse: MutableLiveData<Resource<SyncAPI.SyncStatus>?> = | ||||||
|  |         MutableLiveData(null) | ||||||
|  | 
 | ||||||
|  |     val status: LiveData<Resource<SyncAPI.SyncStatus>?> get() = _statusResponse | ||||||
|  | 
 | ||||||
|  |     // prefix, id | ||||||
|  |     private val syncIds = hashMapOf<String, String>() | ||||||
|  | 
 | ||||||
|  |     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<SyncAPI.SyncStatus> = 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<SyncAPI.SyncResult> = 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) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -4,15 +4,13 @@ import androidx.lifecycle.LiveData | ||||||
| import androidx.lifecycle.MutableLiveData | import androidx.lifecycle.MutableLiveData | ||||||
| import androidx.lifecycle.ViewModel | import androidx.lifecycle.ViewModel | ||||||
| import androidx.lifecycle.viewModelScope | import androidx.lifecycle.viewModelScope | ||||||
| import com.lagradost.cloudstream3.* |  | ||||||
| import com.lagradost.cloudstream3.APIHolder.apis | import com.lagradost.cloudstream3.APIHolder.apis | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys | import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys | ||||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | 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.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.ui.APIRepository | ||||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||||
| import kotlinx.coroutines.Dispatchers | import kotlinx.coroutines.Dispatchers | ||||||
|  | @ -39,7 +37,6 @@ class SearchViewModel : ViewModel() { | ||||||
|     val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory |     val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory | ||||||
| 
 | 
 | ||||||
|     private val repos = apis.map { APIRepository(it) } |     private val repos = apis.map { APIRepository(it) } | ||||||
|     private val syncApis = SyncApis |  | ||||||
| 
 | 
 | ||||||
|     fun clearSearch() { |     fun clearSearch() { | ||||||
|         _searchResponse.postValue(Resource.Success(ArrayList())) |         _searchResponse.postValue(Resource.Success(ArrayList())) | ||||||
|  | @ -49,33 +46,11 @@ class SearchViewModel : ViewModel() { | ||||||
|     private var onGoingSearch: Job? = null |     private var onGoingSearch: Job? = null | ||||||
|     fun searchAndCancel( |     fun searchAndCancel( | ||||||
|         query: String, |         query: String, | ||||||
|         isMainApis: Boolean = true, |  | ||||||
|         providersActive: Set<String> = setOf(), |         providersActive: Set<String> = setOf(), | ||||||
|         ignoreSettings: Boolean = false |         ignoreSettings: Boolean = false | ||||||
|     ) { |     ) { | ||||||
|         onGoingSearch?.cancel() |         onGoingSearch?.cancel() | ||||||
|         onGoingSearch = search(query, isMainApis, providersActive, ignoreSettings) |         onGoingSearch = search(query, 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() |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fun updateHistory() = viewModelScope.launch { |     fun updateHistory() = viewModelScope.launch { | ||||||
|  | @ -89,7 +64,6 @@ class SearchViewModel : ViewModel() { | ||||||
| 
 | 
 | ||||||
|     private fun search( |     private fun search( | ||||||
|         query: String, |         query: String, | ||||||
|         isMainApis: Boolean = true, |  | ||||||
|         providersActive: Set<String>, |         providersActive: Set<String>, | ||||||
|         ignoreSettings: Boolean = false |         ignoreSettings: Boolean = false | ||||||
|     ) = |     ) = | ||||||
|  | @ -118,7 +92,6 @@ class SearchViewModel : ViewModel() { | ||||||
|             _currentSearch.postValue(ArrayList()) |             _currentSearch.postValue(ArrayList()) | ||||||
| 
 | 
 | ||||||
|             withContext(Dispatchers.IO) { // This interrupts UI otherwise |             withContext(Dispatchers.IO) { // This interrupts UI otherwise | ||||||
|                 if (isMainApis) { |  | ||||||
|                 repos.filter { a -> |                 repos.filter { a -> | ||||||
|                     ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name)) |                     ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name)) | ||||||
|                 }.apmap { a -> // Parallel |                 }.apmap { a -> // Parallel | ||||||
|  | @ -126,16 +99,6 @@ class SearchViewModel : ViewModel() { | ||||||
|                     currentList.add(OnGoingSearch(a.name, search)) |                     currentList.add(OnGoingSearch(a.name, search)) | ||||||
|                     _currentSearch.postValue(currentList) |                     _currentSearch.postValue(currentList) | ||||||
|                 } |                 } | ||||||
|                 } else { |  | ||||||
|                     syncApis.apmap { a -> |  | ||||||
|                         val search = safeApiCall { |  | ||||||
|                             a.search(query)?.map { it.toSearchResponse() } |  | ||||||
|                                 ?: throw ErrorLoadingException() |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         currentList.add(OnGoingSearch(a.name, search)) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|             _currentSearch.postValue(currentList) |             _currentSearch.postValue(currentList) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -88,7 +88,7 @@ class SettingsFragment : PreferenceFragmentCompat() { | ||||||
|             return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION |             return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         private const val accountEnabled = false |         const val accountEnabled = false | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private var beneneCount = 0 |     private var beneneCount = 0 | ||||||
|  | @ -155,7 +155,11 @@ class SettingsFragment : PreferenceFragmentCompat() { | ||||||
|         val dialog = builder.show() |         val dialog = builder.show() | ||||||
| 
 | 
 | ||||||
|         dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener { |         dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener { | ||||||
|  |             try { | ||||||
|                 api.authenticate() |                 api.authenticate() | ||||||
|  |             } catch (e: Exception) { | ||||||
|  |                 logError(e) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         val ogIndex = api.accountIndex |         val ogIndex = api.accountIndex | ||||||
|  | @ -325,10 +329,9 @@ class SettingsFragment : PreferenceFragmentCompat() { | ||||||
| 
 | 
 | ||||||
|         val syncApis = |         val syncApis = | ||||||
|             listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi)) |             listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi)) | ||||||
|         for (sync in syncApis) { |         for ((key, api) in syncApis) { | ||||||
|             getPref(sync.first)?.apply { |             getPref(key)?.apply { | ||||||
|                 isVisible = accountEnabled |                 isVisible = accountEnabled | ||||||
|                 val api = sync.second |  | ||||||
|                 title = |                 title = | ||||||
|                     getString(R.string.login_format).format(api.name, getString(R.string.account)) |                     getString(R.string.login_format).format(api.name, getString(R.string.account)) | ||||||
|                 setOnPreferenceClickListener { _ -> |                 setOnPreferenceClickListener { _ -> | ||||||
|  | @ -336,7 +339,11 @@ class SettingsFragment : PreferenceFragmentCompat() { | ||||||
|                     if (info != null) { |                     if (info != null) { | ||||||
|                         showLoginInfo(api, info) |                         showLoginInfo(api, info) | ||||||
|                     } else { |                     } else { | ||||||
|  |                         try { | ||||||
|                             api.authenticate() |                             api.authenticate() | ||||||
|  |                         } catch (e: Exception) { | ||||||
|  |                             logError(e) | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                     return@setOnPreferenceClickListener true |                     return@setOnPreferenceClickListener true | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ package com.lagradost.cloudstream3.utils | ||||||
| 
 | 
 | ||||||
| import android.annotation.SuppressLint | import android.annotation.SuppressLint | ||||||
| import android.app.Activity | import android.app.Activity | ||||||
| import android.content.ComponentName |  | ||||||
| import android.content.ContentValues | import android.content.ContentValues | ||||||
| import android.content.Context | import android.content.Context | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
|  | @ -23,6 +22,7 @@ import android.util.Log | ||||||
| import androidx.annotation.RequiresApi | import androidx.annotation.RequiresApi | ||||||
| import androidx.annotation.WorkerThread | import androidx.annotation.WorkerThread | ||||||
| import androidx.appcompat.app.AppCompatActivity | import androidx.appcompat.app.AppCompatActivity | ||||||
|  | import androidx.core.content.ContextCompat | ||||||
| import androidx.tvprovider.media.tv.PreviewChannelHelper | import androidx.tvprovider.media.tv.PreviewChannelHelper | ||||||
| import androidx.tvprovider.media.tv.TvContractCompat | import androidx.tvprovider.media.tv.TvContractCompat | ||||||
| import androidx.tvprovider.media.tv.WatchNextProgram | 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.ConnectionResult | ||||||
| import com.google.android.gms.common.GoogleApiAvailability | import com.google.android.gms.common.GoogleApiAvailability | ||||||
| import com.google.android.gms.common.wrappers.Wrappers | 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.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.ui.result.ResultFragment | import com.lagradost.cloudstream3.ui.result.ResultFragment | ||||||
| import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | import com.lagradost.cloudstream3.utils.Coroutines.ioSafe | ||||||
|  | @ -196,17 +199,10 @@ object AppUtils { | ||||||
| 
 | 
 | ||||||
|     fun Context.openBrowser(url: String) { |     fun Context.openBrowser(url: String) { | ||||||
|         try { |         try { | ||||||
|             val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java)) |  | ||||||
|             val intent = Intent(Intent.ACTION_VIEW) |             val intent = Intent(Intent.ACTION_VIEW) | ||||||
|             intent.data = Uri.parse(url) |             intent.data = Uri.parse(url) | ||||||
| 
 |             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) |             ContextCompat.startActivity(this, intent, null) | ||||||
|                 startActivity( |  | ||||||
|                     Intent.createChooser(intent, null) |  | ||||||
|                         .putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components) |  | ||||||
|                 ) |  | ||||||
|             else |  | ||||||
|                 startActivity(intent) |  | ||||||
|         } catch (e: Exception) { |         } catch (e: Exception) { | ||||||
|             logError(e) |             logError(e) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -7,9 +7,10 @@ | ||||||
|         android:layout_height="match_parent"> |         android:layout_height="match_parent"> | ||||||
| 
 | 
 | ||||||
|     <LinearLayout |     <LinearLayout | ||||||
|  |             android:visibility="gone" | ||||||
|             android:padding="16dp" |             android:padding="16dp" | ||||||
|             android:orientation="vertical" |             android:orientation="vertical" | ||||||
|             android:id="@+id/sync_holder" |             android:id="@+id/result_sync_holder" | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             android:layout_height="match_parent"> |             android:layout_height="match_parent"> | ||||||
| 
 | 
 | ||||||
|  | @ -38,8 +39,9 @@ | ||||||
|                     app:tint="?attr/textColor" /> |                     app:tint="?attr/textColor" /> | ||||||
| 
 | 
 | ||||||
|             <EditText |             <EditText | ||||||
|  |                     android:id="@+id/result_sync_current_episodes" | ||||||
|                     style="@style/AppEditStyle" |                     style="@style/AppEditStyle" | ||||||
|                     android:hint="20" |                     tools:hint="20" | ||||||
|                     android:textSize="20sp" |                     android:textSize="20sp" | ||||||
|                     android:inputType="number" |                     android:inputType="number" | ||||||
|                     android:layout_width="wrap_content" |                     android:layout_width="wrap_content" | ||||||
|  | @ -47,11 +49,12 @@ | ||||||
|                     tools:ignore="LabelFor" /> |                     tools:ignore="LabelFor" /> | ||||||
| 
 | 
 | ||||||
|             <TextView |             <TextView | ||||||
|  |                     android:id="@+id/result_sync_max_episodes" | ||||||
|                     android:layout_gravity="center_vertical" |                     android:layout_gravity="center_vertical" | ||||||
|                     android:paddingBottom="1dp" |                     android:paddingBottom="1dp" | ||||||
|                     android:textSize="20sp" |                     android:textSize="20sp" | ||||||
|                     android:textColor="?attr/textColor" |                     android:textColor="?attr/textColor" | ||||||
|                     android:text="/30" |                     tools:text="/30" | ||||||
|                     android:layout_width="wrap_content" |                     android:layout_width="wrap_content" | ||||||
|                     android:layout_height="wrap_content" /> |                     android:layout_height="wrap_content" /> | ||||||
| 
 | 
 | ||||||
|  | @ -64,10 +67,10 @@ | ||||||
|                     android:layout_gravity="end|center_vertical" |                     android:layout_gravity="end|center_vertical" | ||||||
|                     android:contentDescription="@string/result_share" |                     android:contentDescription="@string/result_share" | ||||||
|                     app:tint="?attr/textColor" /> |                     app:tint="?attr/textColor" /> | ||||||
| 
 |  | ||||||
|         </LinearLayout> |         </LinearLayout> | ||||||
| 
 | 
 | ||||||
|         <androidx.core.widget.ContentLoadingProgressBar |         <androidx.core.widget.ContentLoadingProgressBar | ||||||
|  |                 android:id="@+id/result_sync_episodes" | ||||||
|                 android:padding="10dp" |                 android:padding="10dp" | ||||||
|                 android:layout_width="match_parent" |                 android:layout_width="match_parent" | ||||||
|                 android:layout_height="20dp" |                 android:layout_height="20dp" | ||||||
|  | @ -192,6 +195,7 @@ | ||||||
|         </LinearLayout> |         </LinearLayout> | ||||||
| 
 | 
 | ||||||
|         <com.google.android.material.slider.Slider |         <com.google.android.material.slider.Slider | ||||||
|  |                 android:id="@+id/result_sync_rating" | ||||||
|                 android:valueFrom="0" |                 android:valueFrom="0" | ||||||
|                 android:valueTo="10" |                 android:valueTo="10" | ||||||
|                 android:value="4" |                 android:value="4" | ||||||
|  | @ -350,6 +354,7 @@ | ||||||
|                 style="@style/WhiteButton" |                 style="@style/WhiteButton" | ||||||
|                 android:text="@string/type_watching" /> |                 android:text="@string/type_watching" /> | ||||||
|         <com.google.android.material.button.MaterialButton |         <com.google.android.material.button.MaterialButton | ||||||
|  |                 android:id="@+id/result_sync_set_score" | ||||||
|                 android:layout_marginTop="10dp" |                 android:layout_marginTop="10dp" | ||||||
|                 android:layout_width="match_parent" |                 android:layout_width="match_parent" | ||||||
|                 style="@style/BlackButton" |                 style="@style/BlackButton" | ||||||
|  | @ -357,13 +362,48 @@ | ||||||
|                 android:text="@string/upload_sync" /> |                 android:text="@string/upload_sync" /> | ||||||
|     </LinearLayout> |     </LinearLayout> | ||||||
| 
 | 
 | ||||||
| 
 |     <com.facebook.shimmer.ShimmerFrameLayout | ||||||
|     <com.google.android.material.button.MaterialButton |             android:id="@+id/result_sync_loading_shimmer" | ||||||
|             android:visibility="gone" |             app:shimmer_base_alpha="0.2" | ||||||
|             android:id="@+id/sync_add_tracking" |             app:shimmer_highlight_alpha="0.3" | ||||||
|             android:layout_gravity="center" |             app:shimmer_duration="@integer/loading_time" | ||||||
|  |             app:shimmer_auto_start="true" | ||||||
|  |             android:padding="15dp" | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             style="@style/SyncButton" |             android:layout_height="match_parent" | ||||||
|             app:icon="@drawable/ic_baseline_add_24" |             android:layout_gravity="center" | ||||||
|             android:text="@string/add_sync" /> |             android:orientation="vertical"> | ||||||
|  | 
 | ||||||
|  |         <LinearLayout | ||||||
|  |                 android:layout_width="match_parent" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:orientation="vertical"> | ||||||
|  |             <Space | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="30dp"/> | ||||||
|  |             <include layout="@layout/loading_line_short" /> | ||||||
|  | 
 | ||||||
|  |             <include layout="@layout/loading_line" /> | ||||||
|  |             <Space | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="30dp"/> | ||||||
|  |             <include layout="@layout/loading_line_short" /> | ||||||
|  |             <include layout="@layout/loading_line" /> | ||||||
|  |             <Space | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="30dp"/> | ||||||
|  |             <include layout="@layout/loading_line_short" /> | ||||||
|  |             <include layout="@layout/loading_line_short" /> | ||||||
|  |             <include layout="@layout/loading_line_short" /> | ||||||
|  |             <include layout="@layout/loading_line_short" /> | ||||||
|  |             <include layout="@layout/loading_line_short" /> | ||||||
|  |             <Space | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="30dp"/> | ||||||
|  |             <include layout="@layout/loading_line" /> | ||||||
|  |             <include layout="@layout/loading_line" /> | ||||||
|  | 
 | ||||||
|  |         </LinearLayout> | ||||||
|  |     </com.facebook.shimmer.ShimmerFrameLayout> | ||||||
|  | 
 | ||||||
| </FrameLayout> | </FrameLayout> | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| package com.lagradost.cloudstream3 | package com.lagradost.cloudstream3 | ||||||
| 
 | 
 | ||||||
|  | import com.lagradost.cloudstream3.APIHolder.allProviders | ||||||
| import com.lagradost.cloudstream3.mvvm.logError | import com.lagradost.cloudstream3.mvvm.logError | ||||||
| import com.lagradost.cloudstream3.utils.Qualities | import com.lagradost.cloudstream3.utils.Qualities | ||||||
| import com.lagradost.cloudstream3.utils.SubtitleHelper | import com.lagradost.cloudstream3.utils.SubtitleHelper | ||||||
|  | @ -9,9 +10,7 @@ import org.junit.Test | ||||||
| 
 | 
 | ||||||
| class ProviderTests { | class ProviderTests { | ||||||
|     private fun getAllProviders(): List<MainAPI> { |     private fun getAllProviders(): List<MainAPI> { | ||||||
|         val allApis = APIHolder.apis |         return allProviders.filter { !it.usesWebView } | ||||||
|         allApis.addAll(APIHolder.restrictedApis) |  | ||||||
|         return allApis.filter { !it.usesWebView } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private suspend fun loadLinks(api: MainAPI, url: String?): Boolean { |     private suspend fun loadLinks(api: MainAPI, url: String?): Boolean { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue