mirror of
				https://github.com/recloudstream/cloudstream.git
				synced 2024-08-15 01:53:11 +00:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master'
This commit is contained in:
		
						commit
						2f69fffe87
					
				
					 18 changed files with 349 additions and 134 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -218,31 +218,26 @@ class NginxProvider : MainAPI() {
 | 
			
		|||
 | 
			
		||||
                            if (isMovieType) {
 | 
			
		||||
                                val movieName = nfoContent.select("title").text()
 | 
			
		||||
 | 
			
		||||
                                val posterUrl = mediaRootUrl + "poster.jpg"
 | 
			
		||||
 | 
			
		||||
                                return@mapNotNull MovieSearchResponse(
 | 
			
		||||
                                return@mapNotNull newMovieSearchResponse(
 | 
			
		||||
                                    movieName,
 | 
			
		||||
                                    mediaRootUrl,
 | 
			
		||||
                                    this.name,
 | 
			
		||||
                                    TvType.Movie,
 | 
			
		||||
                                    posterUrl,
 | 
			
		||||
                                    null,
 | 
			
		||||
                                )
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    addPoster(posterUrl, authHeader)
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {  // tv serie
 | 
			
		||||
                                val serieName = nfoContent.select("title").text()
 | 
			
		||||
 | 
			
		||||
                                val posterUrl = mediaRootUrl + "poster.jpg"
 | 
			
		||||
 | 
			
		||||
                                TvSeriesSearchResponse(
 | 
			
		||||
                                newTvSeriesSearchResponse(
 | 
			
		||||
                                    serieName,
 | 
			
		||||
                                    nfoPath,
 | 
			
		||||
                                    this.name,
 | 
			
		||||
                                    TvType.TvSeries,
 | 
			
		||||
                                    posterUrl,
 | 
			
		||||
                                    null,
 | 
			
		||||
                                    null,
 | 
			
		||||
                                )
 | 
			
		||||
                                ) {
 | 
			
		||||
                                    addPoster(posterUrl, authHeader)
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -605,14 +605,12 @@ open class SflixProvider : MainAPI() {
 | 
			
		|||
                    M3u8Helper().m3u8Generation(M3u8Helper.M3u8Stream(this.file, null), true)
 | 
			
		||||
                        .map { stream ->
 | 
			
		||||
                            //println("stream: ${stream.quality} at ${stream.streamUrl}")
 | 
			
		||||
                            val qualityString = if ((stream.quality ?: 0) == 0) label
 | 
			
		||||
                                ?: "" else "${stream.quality}p"
 | 
			
		||||
                            ExtractorLink(
 | 
			
		||||
                                caller.name,
 | 
			
		||||
                                "${caller.name} $qualityString $name",
 | 
			
		||||
                                "${caller.name} $name",
 | 
			
		||||
                                stream.streamUrl,
 | 
			
		||||
                                caller.mainUrl,
 | 
			
		||||
                                getQualityFromName(stream.quality.toString()),
 | 
			
		||||
                                getQualityFromName(stream.quality?.toString()),
 | 
			
		||||
                                true,
 | 
			
		||||
                                extractorData = extractorData
 | 
			
		||||
                            )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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