added more recommendations

This commit is contained in:
LagradOst 2022-04-18 02:26:13 +02:00
parent ed6c16c780
commit 1d8300341e
16 changed files with 339 additions and 117 deletions

View File

@ -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"

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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?,

View File

@ -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
)

View File

@ -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()

View File

@ -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,

View File

@ -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)
}

View File

@ -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()
)
}
}

View File

@ -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
}
}

View File

@ -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?,

View File

@ -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>

View 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>