forked from recloudstream/cloudstream
		
	added more recommendations
This commit is contained in:
		
							parent
							
								
									ed6c16c780
								
							
						
					
					
						commit
						1d8300341e
					
				
					 16 changed files with 339 additions and 117 deletions
				
			
		|  | @ -36,7 +36,7 @@ android { | |||
|         targetSdkVersion 30 | ||||
| 
 | ||||
|         versionCode 45 | ||||
|         versionName "2.9.18" | ||||
|         versionName "2.9.19" | ||||
| 
 | ||||
|         resValue "string", "app_version", | ||||
|                 "${defaultConfig.versionName}${versionNameSuffix ?: ""}" | ||||
|  | @ -96,8 +96,8 @@ dependencies { | |||
|     implementation 'androidx.appcompat:appcompat:1.4.1' | ||||
|     implementation 'com.google.android.material:material:1.5.0' | ||||
|     implementation 'androidx.constraintlayout:constraintlayout:2.1.3' | ||||
|     implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha03' | ||||
|     implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha03' | ||||
|     implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha04' | ||||
|     implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha04' | ||||
|     implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' | ||||
|     implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' | ||||
|     testImplementation 'junit:junit:4.13.2' | ||||
|  | @ -126,6 +126,8 @@ dependencies { | |||
|     implementation 'com.google.android.exoplayer:exoplayer:2.16.1' | ||||
|     implementation 'com.google.android.exoplayer:extension-cast:2.16.1' | ||||
|     implementation "com.google.android.exoplayer:extension-mediasession:2.16.1" | ||||
|     implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1' | ||||
| 
 | ||||
|     //implementation "com.google.android.exoplayer:extension-leanback:2.14.0" | ||||
| 
 | ||||
|     // Bug reports | ||||
|  | @ -154,7 +156,6 @@ dependencies { | |||
|     // Networking | ||||
|     implementation "com.squareup.okhttp3:okhttp:4.9.2" | ||||
|     implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1" | ||||
|     implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1' | ||||
| 
 | ||||
|     // Util to skip the URI file fuckery 🙏 | ||||
|     implementation "com.github.tachiyomiorg:unifile:17bec43" | ||||
|  |  | |||
|  | @ -197,7 +197,8 @@ class NineAnimeProvider : MainAPI() { | |||
|     ) | ||||
| 
 | ||||
|     override suspend fun load(url: String): LoadResponse? { | ||||
|         val doc = app.get(url).document | ||||
|         val validUrl = url.replace("https://9anime.to", mainUrl) | ||||
|         val doc = app.get(validUrl).document | ||||
|         val animeid = doc.selectFirst("div.player-wrapper.watchpage").attr("data-id") ?: return null | ||||
|         val animeidencoded = encode(getVrf(animeid) ?: return null) | ||||
|         val poster = doc.selectFirst("aside.main div.thumb div img").attr("src") | ||||
|  | @ -233,7 +234,7 @@ class NineAnimeProvider : MainAPI() { | |||
|             else null | ||||
|         val tags = doc.select("div.info .meta .col1 div:contains(Genre) a").map { it.text() } | ||||
| 
 | ||||
|         return newAnimeLoadResponse(title, url, tvType) { | ||||
|         return newAnimeLoadResponse(title, validUrl, tvType) { | ||||
|             this.posterUrl = poster | ||||
|             this.plot = description | ||||
|             this.recommendations = recommendations | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| package com.lagradost.cloudstream3.metaproviders | ||||
| 
 | ||||
| import com.lagradost.cloudstream3.ErrorLoadingException | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi | ||||
| import com.lagradost.cloudstream3.utils.SyncUtil | ||||
| 
 | ||||
| object SyncRedirector { | ||||
|     val syncApis = SyncApis | ||||
| 
 | ||||
|     suspend fun redirect(url: String, preferredUrl: String): String { | ||||
|         for (api in syncApis) { | ||||
|             if (url.contains(api.mainUrl)) { | ||||
|                 val otherApi = when (api.name) { | ||||
|                     aniListApi.name -> "anilist" | ||||
|                     malApi.name -> "myanimelist" | ||||
|                     else -> return url | ||||
|                 } | ||||
| 
 | ||||
|                 return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl -> | ||||
|                     realUrl.contains(preferredUrl) | ||||
|                 } ?: run { | ||||
|                     throw ErrorLoadingException("Page does not exist on $preferredUrl") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return url | ||||
|     } | ||||
| } | ||||
|  | @ -4,13 +4,26 @@ import com.lagradost.cloudstream3.* | |||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId | ||||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
| import com.lagradost.cloudstream3.syncproviders.providers.AniListApi | ||||
| import com.lagradost.cloudstream3.syncproviders.providers.MALApi | ||||
| import com.lagradost.cloudstream3.utils.SyncUtil | ||||
| 
 | ||||
| // wont be implemented | ||||
| class MultiAnimeProvider : MainAPI() { | ||||
|     override var name = "MultiAnime" | ||||
|     override val lang = "en" | ||||
|     override val usesWebView = true | ||||
|     override val supportedTypes = setOf(TvType.Anime) | ||||
|     private val syncApi = OAuth2API.aniListApi | ||||
|     private val syncApi: SyncAPI = OAuth2API.aniListApi | ||||
| 
 | ||||
|     private val syncUtilType by lazy { | ||||
|         when (syncApi) { | ||||
|             is AniListApi -> "anilist" | ||||
|             is MALApi -> "myanimelist" | ||||
|             else -> throw ErrorLoadingException("Invalid Api") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private val validApis by lazy { | ||||
|         APIHolder.apis.filter { | ||||
|  | @ -32,13 +45,25 @@ class MultiAnimeProvider : MainAPI() { | |||
| 
 | ||||
|     override suspend fun load(url: String): LoadResponse? { | ||||
|         return syncApi.getResult(url)?.let { res -> | ||||
|             newAnimeLoadResponse(res.title!!, url, TvType.Anime) { | ||||
|             val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).apmap { url -> | ||||
|                 validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url) | ||||
|             }.filterNotNull() | ||||
| 
 | ||||
|             val type = | ||||
|                 if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime | ||||
| 
 | ||||
|             newAnimeLoadResponse( | ||||
|                 res.title ?: throw ErrorLoadingException("No Title found"), | ||||
|                 url, | ||||
|                 type | ||||
|             ) { | ||||
|                 posterUrl = res.posterUrl | ||||
|                 plot = res.synopsis | ||||
|                 tags = res.genres | ||||
|                 rating = res.publicScore | ||||
|                 addTrailer(res.trailerUrl) | ||||
|                 addAniListId(res.id.toIntOrNull()) | ||||
|                 recommendations = res.recommendations | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package com.lagradost.cloudstream3.syncproviders | ||||
| 
 | ||||
| import com.lagradost.cloudstream3.ActorData | ||||
| import com.lagradost.cloudstream3.ShowStatus | ||||
| import com.lagradost.cloudstream3.* | ||||
| 
 | ||||
| interface SyncAPI : OAuth2API { | ||||
|     val icon: Int | ||||
|  | @ -24,13 +23,19 @@ interface SyncAPI : OAuth2API { | |||
| 
 | ||||
|     suspend fun search(name: String): List<SyncSearchResult>? | ||||
| 
 | ||||
|     fun getIdFromUrl(url : String) : String | ||||
| 
 | ||||
|     data class SyncSearchResult( | ||||
|         val name: String, | ||||
|         val syncApiName: String, | ||||
|         val id: String, | ||||
|         val url: String, | ||||
|         val posterUrl: String?, | ||||
|     ) | ||||
|         override val name: String, | ||||
|         override val apiName: String, | ||||
|         var syncId: String, | ||||
|         override val url: String, | ||||
|         override var posterUrl: String?, | ||||
|         override var type: TvType? = null, | ||||
|         override var quality: SearchQuality? = null, | ||||
|         override var posterHeaders: Map<String, String>? = null, | ||||
|         override var id: Int? = null, | ||||
|     ) : SearchResponse | ||||
| 
 | ||||
|     data class SyncNextAiring( | ||||
|         val episode: Int, | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ class SyncRepo(private val repo: SyncAPI) { | |||
|     val idPrefix = repo.idPrefix | ||||
|     val name = repo.name | ||||
|     val icon = repo.icon | ||||
|     val mainUrl = repo.mainUrl | ||||
| 
 | ||||
|     suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { | ||||
|         return safeApiCall { repo.score(id, status) } | ||||
|  | @ -29,4 +30,6 @@ class SyncRepo(private val repo: SyncAPI) { | |||
|     fun hasAccount() : Boolean { | ||||
|         return normalSafeApiCall { repo.loginInfo() != null } ?: false | ||||
|     } | ||||
| 
 | ||||
|     fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url) | ||||
| } | ||||
|  | @ -70,6 +70,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         return user != null | ||||
|     } | ||||
| 
 | ||||
|     override fun getIdFromUrl(url : String): String { | ||||
|         return url.removePrefix("$mainUrl/anime/").removeSuffix("/") | ||||
|     } | ||||
| 
 | ||||
|     private fun getUrlFromId(id: Int): String { | ||||
|         return "$mainUrl/anime/$id" | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { | ||||
|         val data = searchShows(name) ?: return null | ||||
|         return data.data?.Page?.media?.map { | ||||
|  | @ -77,7 +85,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 it.title.romaji ?: return null, | ||||
|                 this.name, | ||||
|                 it.id.toString(), | ||||
|                 "$mainUrl/anime/${it.id}", | ||||
|                 getUrlFromId(it.id), | ||||
|                 it.bannerImage | ||||
|             ) | ||||
|         } | ||||
|  | @ -126,7 +134,16 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                 ) | ||||
|             }, | ||||
|             publicScore = season.averageScore?.times(100), | ||||
|             //recommendations = season. | ||||
|             recommendations = season.recommendations?.edges?.mapNotNull { rec -> | ||||
|                 val recMedia = rec.node.mediaRecommendation | ||||
|                 SyncAPI.SyncSearchResult( | ||||
|                     name = recMedia.title?.userPreferred ?: return@mapNotNull null, | ||||
|                     this.name, | ||||
|                     recMedia.id?.toString() ?: return@mapNotNull null, | ||||
|                     getUrlFromId(recMedia.id), | ||||
|                     recMedia.coverImage?.large ?: recMedia.coverImage?.medium | ||||
|                 ) | ||||
|             } | ||||
|             //TODO REST | ||||
|         ) | ||||
|     } | ||||
|  | @ -335,10 +352,10 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                            color | ||||
|                        } | ||||
|                        title { | ||||
|                             romaji | ||||
|                             english | ||||
|                             native | ||||
|                             userPreferred | ||||
|                            romaji | ||||
|                            english | ||||
|                            native | ||||
|                            userPreferred | ||||
|                        } | ||||
|                        duration | ||||
|                        episodes | ||||
|  | @ -382,23 +399,44 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|                            thumbnail | ||||
|                        } | ||||
|                        relations { | ||||
|                             edges { | ||||
|                                  id | ||||
|                                  relationType(version: 2) | ||||
|                                  node { | ||||
|                                       id | ||||
|                                       coverImage { | ||||
|                                           extraLarge | ||||
|                                           large | ||||
|                                           medium | ||||
|                                           color | ||||
|                                       } | ||||
|                                  } | ||||
|                             } | ||||
|                            edges { | ||||
|                                 id | ||||
|                                 relationType(version: 2) | ||||
|                                 node { | ||||
|                                      id | ||||
|                                      coverImage { | ||||
|                                          extraLarge | ||||
|                                          large | ||||
|                                          medium | ||||
|                                          color | ||||
|                                      } | ||||
|                                 } | ||||
|                            } | ||||
|                        } | ||||
|                        recommendations { | ||||
|                            edges { | ||||
|                                node { | ||||
|                                    mediaRecommendation { | ||||
|                                        id | ||||
|                                        coverImage { | ||||
|                                            extraLarge | ||||
|                                            large | ||||
|                                            medium | ||||
|                                            color | ||||
|                                        } | ||||
|                                        title { | ||||
|                                            romaji | ||||
|                                            english | ||||
|                                            native | ||||
|                                            userPreferred | ||||
|                                        } | ||||
|                                    } | ||||
|                                } | ||||
|                            } | ||||
|                        } | ||||
|                        nextAiringEpisode { | ||||
|                             timeUntilAiring | ||||
|                             episode | ||||
|                            timeUntilAiring | ||||
|                            episode | ||||
|                        } | ||||
|                        format | ||||
|                    } | ||||
|  | @ -772,6 +810,21 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         @JsonProperty("trailer") val trailer: MediaTrailer?, | ||||
|         @JsonProperty("description") val description: String?, | ||||
|         @JsonProperty("characters") val characters: CharacterConnection?, | ||||
|         @JsonProperty("recommendations") val recommendations: RecommendationConnection?, | ||||
|     ) | ||||
| 
 | ||||
|     data class RecommendationConnection( | ||||
|         @JsonProperty("edges") val edges: List<RecommendationEdge> = emptyList(), | ||||
|         @JsonProperty("nodes") val nodes: List<Recommendation> = emptyList(), | ||||
|         //@JsonProperty("pageInfo") val pageInfo: PageInfo, | ||||
|     ) | ||||
| 
 | ||||
|     data class RecommendationEdge( | ||||
|         //@JsonProperty("rating") val rating: Int, | ||||
|         @JsonProperty("node") val node: Recommendation, | ||||
|     ) | ||||
|     data class Recommendation( | ||||
|         @JsonProperty("mediaRecommendation") val mediaRecommendation: SeasonMedia, | ||||
|     ) | ||||
| 
 | ||||
|     data class CharacterName( | ||||
|  | @ -955,13 +1008,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         @JsonProperty("data") val data: LikeData?, | ||||
|     ) | ||||
| 
 | ||||
|     data class Recommendation( | ||||
|         @JsonProperty("title") val title: String?, | ||||
|         @JsonProperty("idMal") val idMal: Int?, | ||||
|         @JsonProperty("poster") val poster: String?, | ||||
|         @JsonProperty("averageScore") val averageScore: Int? | ||||
|     ) | ||||
| 
 | ||||
|     data class AniListTitleHolder( | ||||
|         @JsonProperty("title") val title: Title?, | ||||
|         @JsonProperty("isFavourite") val isFavourite: Boolean?, | ||||
|  |  | |||
|  | @ -81,6 +81,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun getIdFromUrl(url: String): String { | ||||
|         return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() | ||||
|     } | ||||
| 
 | ||||
|     override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { | ||||
|         return setScoreRequest( | ||||
|             id.toIntOrNull() ?: return false, | ||||
|  | @ -173,8 +177,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { | |||
|     private fun toSearchResult(node: Node?): SyncAPI.SyncSearchResult? { | ||||
|         return SyncAPI.SyncSearchResult( | ||||
|             name = node?.title ?: return null, | ||||
|             syncApiName = this.name, | ||||
|             id = node.id.toString(), | ||||
|             apiName = this.name, | ||||
|             syncId = node.id.toString(), | ||||
|             url = "https://myanimelist.net/anime/${node.id}", | ||||
|             posterUrl = node.main_picture?.large | ||||
|         ) | ||||
|  |  | |||
|  | @ -93,6 +93,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName | |||
| import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename | ||||
| import kotlinx.android.synthetic.main.fragment_result.* | ||||
| import kotlinx.android.synthetic.main.fragment_result_swipe.* | ||||
| import kotlinx.android.synthetic.main.result_recommendations.* | ||||
| import kotlinx.android.synthetic.main.result_sync.* | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.Job | ||||
|  | @ -572,14 +573,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|         setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) | ||||
|     } | ||||
| 
 | ||||
|     private fun setMalSync(id: Int?): Boolean { | ||||
|         return syncModel.setMalId(id?.toString()) | ||||
|     } | ||||
| 
 | ||||
|     private fun setAniListSync(id: Int?): Boolean { | ||||
|         return syncModel.setAniListId(id?.toString()) | ||||
|     } | ||||
| 
 | ||||
|     private fun setActors(actors: List<ActorData>?) { | ||||
|         if (actors.isNullOrEmpty()) { | ||||
|             result_cast_text?.isVisible = false | ||||
|  | @ -601,23 +594,47 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun setRecommendations(rec: List<SearchResponse>?) { | ||||
|     private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) { | ||||
|         val isInvalid = rec.isNullOrEmpty() | ||||
|         result_recommendations?.isGone = isInvalid | ||||
|         result_recommendations_btt?.isGone = isInvalid | ||||
|         result_recommendations_btt?.setOnClickListener { | ||||
|             if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) { | ||||
|                 result_recommendations_btt?.nextFocusDownId = R.id.result_recommendations | ||||
|             val nextFocusDown = if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) { | ||||
|                 result_overlapping_panels?.openEndPanel() | ||||
|                 R.id.result_recommendations | ||||
|             } else { | ||||
|                 result_recommendations_btt?.nextFocusDownId = R.id.result_description | ||||
|                 result_overlapping_panels?.closePanels() | ||||
|                 R.id.result_description | ||||
|             } | ||||
| 
 | ||||
|             result_recommendations_btt?.nextFocusDownId = nextFocusDown | ||||
|             result_search?.nextFocusDownId = nextFocusDown | ||||
|             result_open_in_browser?.nextFocusDownId = nextFocusDown | ||||
|             result_share?.nextFocusDownId = nextFocusDown | ||||
|         } | ||||
|         result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) | ||||
| 
 | ||||
|         val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName | ||||
|         rec?.map { it.apiName }?.distinct()?.let { apiNames -> | ||||
|             // very dirty selection | ||||
|             result_recommendations_filter_button?.isVisible = apiNames.size > 1 | ||||
|             result_recommendations_filter_button?.text = matchAgainst | ||||
|             result_recommendations_filter_button?.setOnClickListener { _ -> | ||||
|                 activity?.showBottomDialog( | ||||
|                     apiNames, | ||||
|                     apiNames.indexOf(matchAgainst), | ||||
|                     getString(R.string.home_change_provider_img_des), false, {} | ||||
|                 ) { | ||||
|                     setRecommendations(rec, apiNames[it]) | ||||
|                 } | ||||
|             } | ||||
|         } ?: run { | ||||
|             result_recommendations_filter_button?.isVisible = false | ||||
|         } | ||||
| 
 | ||||
|         result_recommendations?.post { | ||||
|             rec?.let { list -> | ||||
|                 (result_recommendations?.adapter as SearchAdapter?)?.updateList(list) | ||||
|                 (result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst }) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -1086,7 +1103,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|                 ACTION_PLAY_EPISODE_IN_PLAYER -> { | ||||
|                     viewModel.getGenerator(episodeClick.data) | ||||
|                         ?.let { generator -> | ||||
|                             println("LANUCJ:::: $syncdata") | ||||
|                             activity?.navigate( | ||||
|                                 R.id.global_to_navigation_player, | ||||
|                                 GeneratorPlayer.newInstance( | ||||
|  | @ -1641,7 +1657,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|                     setDuration(d.duration) | ||||
|                     setYear(d.year) | ||||
|                     setRating(d.rating) | ||||
|                     setRecommendations(d.recommendations) | ||||
|                     setRecommendations(d.recommendations, null) | ||||
|                     setActors(d.actors) | ||||
| 
 | ||||
|                     if (SettingsFragment.accountEnabled) { | ||||
|  | @ -1950,7 +1966,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         result_recommendations.adapter = recAdapter | ||||
|         result_recommendations?.adapter = recAdapter | ||||
| 
 | ||||
|         context?.let { ctx -> | ||||
|             result_bookmark_button?.isVisible = ctx.isTvSettings() | ||||
|  |  | |||
|  | @ -11,6 +11,9 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull | |||
| import com.lagradost.cloudstream3.APIHolder.getId | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||
| import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer | ||||
| import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider | ||||
| import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider | ||||
| import com.lagradost.cloudstream3.metaproviders.SyncRedirector | ||||
| import com.lagradost.cloudstream3.mvvm.Resource | ||||
| import com.lagradost.cloudstream3.mvvm.safeApiCall | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
|  | @ -127,6 +130,17 @@ class ResultViewModel : ViewModel() { | |||
|             addTrailer(meta.trailerUrl) | ||||
|             posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl | ||||
|             actors = actors ?: meta.actors | ||||
| 
 | ||||
|             val realRecommendations = ArrayList<SearchResponse>() | ||||
|             val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name) | ||||
|             meta.recommendations?.forEach { rec -> | ||||
|                 apiNames.forEach { name -> | ||||
|                     realRecommendations.add(rec.copy(apiName = name)) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             recommendations = recommendations?.union(realRecommendations)?.toList() | ||||
|                 ?: realRecommendations | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -296,10 +310,9 @@ class ResultViewModel : ViewModel() { | |||
|     } | ||||
| 
 | ||||
|     fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch { | ||||
|         _resultResponse.postValue(Resource.Loading(url)) | ||||
|         _publicEpisodes.postValue(Resource.Loading()) | ||||
|         _resultResponse.postValue(Resource.Loading(url)) | ||||
| 
 | ||||
|         _apiName.postValue(apiName) | ||||
|         val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url) | ||||
|         if (api == null) { | ||||
|             _resultResponse.postValue( | ||||
|  | @ -312,9 +325,31 @@ class ResultViewModel : ViewModel() { | |||
|             ) | ||||
|             return@launch | ||||
|         } | ||||
| 
 | ||||
|         val validUrlResource = safeApiCall { | ||||
|             SyncRedirector.redirect( | ||||
|                 url, | ||||
|                 api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime") | ||||
|                     .replace(GogoanimeProvider().mainUrl, "gogoanime") | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         if (validUrlResource !is Resource.Success) { | ||||
|             if (validUrlResource is Resource.Failure) { | ||||
|                 _resultResponse.postValue(validUrlResource) | ||||
|             } | ||||
| 
 | ||||
|             return@launch | ||||
|         } | ||||
|         val validUrl = validUrlResource.value | ||||
| 
 | ||||
|         _resultResponse.postValue(Resource.Loading(validUrl)) | ||||
| 
 | ||||
|         _apiName.postValue(apiName) | ||||
| 
 | ||||
|         repo = APIRepository(api) | ||||
| 
 | ||||
|         val data = repo?.load(url) ?: return@launch | ||||
|         val data = repo?.load(validUrl) ?: return@launch | ||||
| 
 | ||||
|         _resultResponse.postValue(data) | ||||
| 
 | ||||
|  | @ -331,7 +366,7 @@ class ResultViewModel : ViewModel() { | |||
|                     mainId.toString(), | ||||
|                     VideoDownloadHelper.DownloadHeaderCached( | ||||
|                         apiName, | ||||
|                         url, | ||||
|                         validUrl, | ||||
|                         d.type, | ||||
|                         d.name, | ||||
|                         d.posterUrl, | ||||
|  |  | |||
|  | @ -82,16 +82,16 @@ class SyncViewModel : ViewModel() { | |||
|         var isValid = false | ||||
| 
 | ||||
|         map?.forEach { (prefix, id) -> | ||||
|             isValid = isValid || addSync(prefix, id) | ||||
|             isValid = addSync(prefix, id) || isValid | ||||
|         } | ||||
|         return isValid | ||||
|     } | ||||
| 
 | ||||
|     fun setMalId(id: String?): Boolean { | ||||
|     private fun setMalId(id: String?): Boolean { | ||||
|         return addSync(malApi.idPrefix, id ?: return false) | ||||
|     } | ||||
| 
 | ||||
|     fun setAniListId(id: String?): Boolean { | ||||
|     private fun setAniListId(id: String?): Boolean { | ||||
|         return addSync(aniListApi.idPrefix, id ?: return false) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import com.lagradost.cloudstream3.SearchQuality | |||
| import com.lagradost.cloudstream3.SearchResponse | ||||
| import com.lagradost.cloudstream3.TvType | ||||
| import com.lagradost.cloudstream3.syncproviders.OAuth2API | ||||
| import com.lagradost.cloudstream3.syncproviders.SyncAPI | ||||
| 
 | ||||
| class SyncSearchViewModel { | ||||
|     private val repos = OAuth2API.SyncApis | ||||
|  | @ -20,15 +19,4 @@ class SyncSearchViewModel { | |||
|         override var posterHeaders: Map<String, String>? = null, | ||||
|     ) : SearchResponse | ||||
| 
 | ||||
|     private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse { | ||||
|         return SyncSearchResultSearchResponse( | ||||
|             this.name, | ||||
|             this.url, | ||||
|             this.syncApiName, | ||||
|             null, | ||||
|             this.posterUrl, | ||||
|             null, //this.id.hashCode() | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -7,6 +7,7 @@ import com.google.android.gms.cast.framework.CastSession | |||
| import com.google.android.gms.cast.framework.media.RemoteMediaClient | ||||
| import com.google.android.gms.common.api.PendingResult | ||||
| import com.google.android.gms.common.images.WebImage | ||||
| import com.lagradost.cloudstream3.mvvm.logError | ||||
| import com.lagradost.cloudstream3.ui.MetadataHolder | ||||
| import com.lagradost.cloudstream3.ui.player.SubtitleData | ||||
| import com.lagradost.cloudstream3.ui.result.ResultEpisode | ||||
|  | @ -64,7 +65,10 @@ object CastHelper { | |||
|         return builder.build() | ||||
|     } | ||||
| 
 | ||||
|     fun awaitLinks(pending: PendingResult<RemoteMediaClient.MediaChannelResult>?, callback: (Boolean) -> Unit) { | ||||
|     fun awaitLinks( | ||||
|         pending: PendingResult<RemoteMediaClient.MediaChannelResult>?, | ||||
|         callback: (Boolean) -> Unit | ||||
|     ) { | ||||
|         if (pending == null) return | ||||
|         main { | ||||
|             val res = withContext(Dispatchers.IO) { pending.await() } | ||||
|  | @ -90,27 +94,15 @@ object CastHelper { | |||
|         startIndex: Int? = null, | ||||
|         startTime: Long? = null, | ||||
|     ): Boolean { | ||||
|         if (this == null) return false | ||||
|         if (episodes.isEmpty()) return false | ||||
|         if (currentEpisodeIndex >= episodes.size) return false | ||||
|         try { | ||||
|             if (this == null) return false | ||||
|             if (episodes.isEmpty()) return false | ||||
|             if (currentEpisodeIndex >= episodes.size) return false | ||||
| 
 | ||||
|         val epData = episodes[currentEpisodeIndex] | ||||
|             val epData = episodes[currentEpisodeIndex] | ||||
| 
 | ||||
|         val holder = | ||||
|             MetadataHolder(apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks, subtitles) | ||||
| 
 | ||||
|         val index = if (startIndex == null || startIndex < 0) 0 else startIndex | ||||
| 
 | ||||
|         val mediaItem = | ||||
|             getMediaInfo(epData, holder, index, JSONObject(holder.toJson()), subtitles) | ||||
| 
 | ||||
|         awaitLinks( | ||||
|             this.remoteMediaClient?.load( | ||||
|                 MediaLoadRequestData.Builder().setMediaInfo(mediaItem).setCurrentTime(startTime ?: 0L).build() | ||||
|             ) | ||||
|         ) { | ||||
|             if (currentLinks.size > index + 1) | ||||
|                 startCast( | ||||
|             val holder = | ||||
|                 MetadataHolder( | ||||
|                     apiName, | ||||
|                     isMovie, | ||||
|                     title, | ||||
|  | @ -118,11 +110,38 @@ object CastHelper { | |||
|                     currentEpisodeIndex, | ||||
|                     episodes, | ||||
|                     currentLinks, | ||||
|                     subtitles, | ||||
|                     index + 1, | ||||
|                     startTime | ||||
|                     subtitles | ||||
|                 ) | ||||
| 
 | ||||
|             val index = if (startIndex == null || startIndex < 0) 0 else startIndex | ||||
| 
 | ||||
|             val mediaItem = | ||||
|                 getMediaInfo(epData, holder, index, JSONObject(holder.toJson()), subtitles) | ||||
| 
 | ||||
|             awaitLinks( | ||||
|                 this.remoteMediaClient?.load( | ||||
|                     MediaLoadRequestData.Builder().setMediaInfo(mediaItem) | ||||
|                         .setCurrentTime(startTime ?: 0L).build() | ||||
|                 ) | ||||
|             ) { | ||||
|                 if (currentLinks.size > index + 1) | ||||
|                     startCast( | ||||
|                         apiName, | ||||
|                         isMovie, | ||||
|                         title, | ||||
|                         poster, | ||||
|                         currentEpisodeIndex, | ||||
|                         episodes, | ||||
|                         currentLinks, | ||||
|                         subtitles, | ||||
|                         index + 1, | ||||
|                         startTime | ||||
|                     ) | ||||
|             } | ||||
|             return true | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             return false | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| } | ||||
|  | @ -53,7 +53,7 @@ object SyncUtil { | |||
|      * valid sites are: Gogoanime, Twistmoe and 9anime*/ | ||||
|     private suspend fun getIdsFromSlug( | ||||
|         slug: String, | ||||
|         site: String = "GogoanimeGogoanime" | ||||
|         site: String = "Gogoanime" | ||||
|     ): Pair<String?, String?>? { | ||||
|         Log.i(TAG, "getIdsFromSlug $slug $site") | ||||
|         try { | ||||
|  | @ -76,6 +76,28 @@ object SyncUtil { | |||
|         return null | ||||
|     } | ||||
| 
 | ||||
|     suspend fun getUrlsFromId(id: String, type: String = "anilist") : List<String> { | ||||
|         val url = | ||||
|             "https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json" | ||||
|         val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).mapped<SyncPage>() | ||||
|         val pages = response.pages ?: return emptyList() | ||||
|         return pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url } | ||||
|     } | ||||
| 
 | ||||
|     data class SyncPage( | ||||
|         @JsonProperty("Pages") val pages: SyncPages?, | ||||
|     ) | ||||
| 
 | ||||
|     data class SyncPages( | ||||
|         @JsonProperty("9anime") val nineanime: Map<String, ProviderPage> = emptyMap(), | ||||
|         @JsonProperty("Gogoanime") val gogoanime: Map<String, ProviderPage> = emptyMap(), | ||||
|         @JsonProperty("Twistmoe") val twistmoe: Map<String, ProviderPage> = emptyMap(), | ||||
|     ) | ||||
| 
 | ||||
|     data class ProviderPage( | ||||
|         @JsonProperty("url") val url: String?, | ||||
|     ) | ||||
| 
 | ||||
|     data class MalSyncPage( | ||||
|         @JsonProperty("identifier") val identifier: String?, | ||||
|         @JsonProperty("type") val type: String?, | ||||
|  |  | |||
|  | @ -180,17 +180,7 @@ | |||
|                     android:layout_height="match_parent" | ||||
|                     android:layout_gravity="end"> | ||||
| 
 | ||||
|                 <com.lagradost.cloudstream3.ui.AutofitRecyclerView | ||||
|                         android:descendantFocusability="afterDescendants" | ||||
| 
 | ||||
|                         android:background="?attr/primaryBlackBackground" | ||||
|                         android:layout_width="match_parent" | ||||
|                         android:layout_height="match_parent" | ||||
|                         android:clipToPadding="false" | ||||
|                         app:spanCount="3" | ||||
|                         android:id="@+id/result_recommendations" | ||||
|                         tools:listitem="@layout/search_result_grid" | ||||
|                         android:orientation="vertical" /> | ||||
|                 <include layout="@layout/result_recommendations" /> | ||||
|             </FrameLayout> | ||||
|         </com.discord.panels.OverlappingPanelsLayout> | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										37
									
								
								app/src/main/res/layout/result_recommendations.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/src/main/res/layout/result_recommendations.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|         xmlns:tools="http://schemas.android.com/tools" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <LinearLayout | ||||
|             android:orientation="vertical" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content"> | ||||
| 
 | ||||
|         <com.google.android.material.button.MaterialButton | ||||
|                 style="@style/BlackButton" | ||||
|                 android:layout_gravity="center_vertical|end" | ||||
|                 android:text="GogoAnime" | ||||
|                 android:id="@+id/result_recommendations_filter_button" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_marginTop="10dp" | ||||
|                 android:layout_marginBottom="10dp" | ||||
|                 android:layout_marginStart="0dp" | ||||
|                 android:layout_marginEnd="0dp" | ||||
|                 app:layout_constraintBottom_toTopOf="@id/result_recommendations" /> | ||||
| 
 | ||||
|         <com.lagradost.cloudstream3.ui.AutofitRecyclerView | ||||
|                 android:descendantFocusability="afterDescendants" | ||||
| 
 | ||||
|                 android:background="?attr/primaryBlackBackground" | ||||
|                 android:layout_width="match_parent" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:clipToPadding="false" | ||||
|                 app:spanCount="3" | ||||
|                 android:id="@+id/result_recommendations" | ||||
|                 tools:listitem="@layout/search_result_grid" | ||||
|                 android:orientation="vertical" /> | ||||
|     </LinearLayout> | ||||
| </androidx.core.widget.NestedScrollView> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue