sync stuff

This commit is contained in:
LagradOst 2022-04-01 22:05:34 +02:00
parent e41542bef4
commit 2a27c0360d
16 changed files with 729 additions and 542 deletions

View file

@ -266,7 +266,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (str.contains(appString)) { if (str.contains(appString)) {
for (api in OAuth2Apis) { for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) { if (str.contains("/${api.redirectUrl}")) {
try {
api.handleRedirect(str) api.handleRedirect(str)
} catch (e : Exception) {
logError(e)
}
} }
} }
} else { } else {

View file

@ -10,7 +10,7 @@ interface OAuth2API {
val redirectUrl: String val redirectUrl: String
// don't change this as all keys depend on it // don't change this as all keys depend on it
val idPrefix : String val idPrefix: String
fun handleRedirect(url: String) fun handleRedirect(url: String)
fun authenticate() fun authenticate()
@ -43,8 +43,8 @@ interface OAuth2API {
// used for active syncing // used for active syncing
val SyncApis val SyncApis
get() = listOf<SyncAPI>( get() = listOf(
malApi, aniListApi SyncRepo(malApi), SyncRepo(aniListApi)
) )
const val appString = "cloudstreamapp" const val appString = "cloudstreamapp"

View file

@ -3,6 +3,26 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.ShowStatus
interface SyncAPI : OAuth2API { interface SyncAPI : OAuth2API {
val icon: Int
val mainUrl: String
/**
-1 -> None
0 -> Watching
1 -> Completed
2 -> OnHold
3 -> Dropped
4 -> PlanToWatch
5 -> ReWatching
*/
suspend fun score(id: String, status: SyncStatus): Boolean
suspend fun getStatus(id: String): SyncStatus?
suspend fun getResult(id: String): SyncResult?
suspend fun search(name: String): List<SyncSearchResult>?
data class SyncSearchResult( data class SyncSearchResult(
val name: String, val name: String,
val syncApiName: String, val syncApiName: String,
@ -48,7 +68,7 @@ interface SyncAPI : OAuth2API {
var synopsis: String? = null, var synopsis: String? = null,
var airStatus: ShowStatus? = null, var airStatus: ShowStatus? = null,
var nextAiring: SyncNextAiring? = null, var nextAiring: SyncNextAiring? = null,
var studio: String? = null, var studio: List<String>? = null,
var genres: List<String>? = null, var genres: List<String>? = null,
var trailerUrl: String? = null, var trailerUrl: String? = null,
@ -62,24 +82,4 @@ interface SyncAPI : OAuth2API {
var actors: List<SyncActor>? = null, var actors: List<SyncActor>? = null,
var characters: List<SyncCharacter>? = null, var characters: List<SyncCharacter>? = null,
) )
val icon: Int
val mainUrl: String
suspend fun search(name: String): List<SyncSearchResult>?
/**
-1 -> None
0 -> Watching
1 -> Completed
2 -> OnHold
3 -> Dropped
4 -> PlanToWatch
5 -> ReWatching
*/
suspend fun score(id: String, status: SyncStatus): Boolean
suspend fun getStatus(id: String): SyncStatus?
suspend fun getResult(id: String): SyncResult?
} }

View file

@ -0,0 +1,25 @@
package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
class SyncRepo(private val repo: SyncAPI) {
val idPrefix get() = repo.idPrefix
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
return safeApiCall { repo.score(id, status) }
}
suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus> {
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
}
suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult> {
return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") }
}
suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> {
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
}
}

View file

@ -55,7 +55,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
override fun handleRedirect(url: String) { override fun handleRedirect(url: String) {
try {
val sanitizer = val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val token = sanitizer["access_token"]!! val token = sanitizer["access_token"]!!
@ -70,9 +69,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
ioSafe { ioSafe {
getUser() getUser()
} }
} catch (e: Exception) {
e.printStackTrace()
}
} }
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
@ -338,7 +334,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
fun initGetUser() { fun initGetUser() {
if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return if (getAuth() == null) return
ioSafe { ioSafe {
getUser() getUser()
} }
@ -351,7 +347,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)!! )!!
} }
suspend fun getDataAboutId(id: Int): AniListTitleHolder? { private suspend fun getDataAboutId(id: Int): AniListTitleHolder? {
val q = val q =
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) """query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query) Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
@ -369,18 +365,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
}""" }"""
try {
val data = postApi("https://graphql.anilist.co", q, true) val data = postApi(q, true)
var d: GetDataRoot? = null val d = mapper.readValue<GetDataRoot>(data ?: return null)
try {
d = mapper.readValue<GetDataRoot>(data)
} catch (e: Exception) {
logError(e)
println("AniList json failed")
}
if (d == null) {
return null
}
val main = d.data.Media val main = d.data.Media
if (main.mediaListEntry != null) { if (main.mediaListEntry != null) {
@ -404,24 +391,22 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
type = AniListStatusType.None, type = AniListStatusType.None,
) )
} }
} catch (e: Exception) {
logError(e)
return null
}
} }
private suspend fun postApi(url: String, q: String, cache: Boolean = false): String { private fun getAuth(): String? {
return try { return getKey(
if (!checkToken()) { accountId,
// println("VARS_ " + vars) ANILIST_TOKEN_KEY
)
}
private suspend fun postApi(q: String, cache: Boolean = false): String? {
return if (!checkToken()) {
app.post( app.post(
"https://graphql.anilist.co/", "https://graphql.anilist.co/",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + getKey( "Authorization" to "Bearer " + (getAuth() ?: return null),
accountId,
ANILIST_TOKEN_KEY,
""
)!!,
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
), ),
cacheTime = 0, cacheTime = 0,
@ -429,11 +414,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
timeout = 5 // REASONABLE TIMEOUT timeout = 5 // REASONABLE TIMEOUT
).text.replace("\\/", "/") ).text.replace("\\/", "/")
} else { } else {
"" null
}
} catch (e: Exception) {
logError(e)
""
} }
} }
@ -515,12 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
suspend fun getAnilistAnimeListSmart(): Array<Lists>? { suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
if (getKey<String>( if (getAuth() == null) return null
accountId,
ANILIST_TOKEN_KEY,
null
) == null
) return null
if (checkToken()) return null if (checkToken()) return null
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
@ -536,7 +512,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
private suspend fun getFullAnilistList(): FullAnilistList? { private suspend fun getFullAnilistList(): FullAnilistList? {
try {
var userID: Int? = null var userID: Int? = null
/** WARNING ASSUMES ONE USER! **/ /** WARNING ASSUMES ONE USER! **/
getKeys(ANILIST_USER_KEY)?.forEach { key -> getKeys(ANILIST_USER_KEY)?.forEach { key ->
@ -588,13 +563,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
""" """
val text = postApi("https://graphql.anilist.co", query) val text = postApi(query)
return text.toKotlinObject() return text?.toKotlinObject()
} catch (e: Exception) {
logError(e)
return null
}
} }
suspend fun toggleLike(id: Int): Boolean { suspend fun toggleLike(id: Int): Boolean {
@ -610,7 +580,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
}""" }"""
val data = postApi("https://graphql.anilist.co", q) val data = postApi(q)
return data != "" return data != ""
} }
@ -620,7 +590,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
score: Int?, score: Int?,
progress: Int? progress: Int?
): Boolean { ): Boolean {
try {
val q = val q =
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
aniListStatusString[maxOf( aniListStatusString[maxOf(
@ -635,12 +604,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
score score
} }
}""" }"""
val data = postApi("https://graphql.anilist.co", q) val data = postApi(q)
return data != "" return data != ""
} catch (e: Exception) {
logError(e)
return false
}
} }
private suspend fun getUser(setSettings: Boolean = true): AniListUser? { private suspend fun getUser(setSettings: Boolean = true): AniListUser? {
@ -661,10 +626,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
}""" }"""
try { val data = postApi(q)
val data = postApi("https://graphql.anilist.co", q)
if (data == "") return null if (data == "") return null
val userData = mapper.readValue<AniListRoot>(data) val userData = mapper.readValue<AniListRoot>(data ?: return null)
val u = userData.data.Viewer val u = userData.data.Viewer
val user = AniListUser( val user = AniListUser(
u.id, u.id,
@ -680,10 +644,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
println("FFAV:" + i.id) println("FFAV:" + i.id)
}*/ }*/
return user return user
} catch (e: java.lang.Exception) {
logError(e)
return null
}
} }
suspend fun getAllSeasons(id: Int): List<SeasonResponse?> { suspend fun getAllSeasons(id: Int): List<SeasonResponse?> {

View file

@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
@ -45,20 +46,28 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override fun loginInfo(): OAuth2API.LoginInfo? { override fun loginInfo(): OAuth2API.LoginInfo? {
//getMalUser(true)? //getMalUser(true)?
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return OAuth2API.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex) return OAuth2API.LoginInfo(
profilePicture = user.picture,
name = user.name,
accountIndex = accountIndex
)
} }
return null return null
} }
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> { private fun getAuth(): String? {
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT" return getKey(
val auth = getKey<String>(
accountId, accountId,
MAL_TOKEN_KEY MAL_TOKEN_KEY
) ?: return emptyList() )
}
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> {
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
val auth = getAuth() ?: return emptyList()
val res = app.get( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
"Authorization" to "Bearer " + auth, "Authorization" to "Bearer $auth",
), cacheTime = 0 ), cacheTime = 0
).text ).text
return mapper.readValue<MalSearch>(res).data.map { return mapper.readValue<MalSearch>(res).data.map {
@ -73,7 +82,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
override suspend fun score(id: String, status : SyncAPI.SyncStatus): Boolean { override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
return setScoreRequest( return setScoreRequest(
id.toIntOrNull() ?: return false, id.toIntOrNull() ?: return false,
fromIntToAnimeStatus(status.status), fromIntToAnimeStatus(status.status),
@ -82,25 +91,156 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
) )
} }
data class MalAnime(
@JsonProperty("id") val id: Int?,
@JsonProperty("title") val title: String?,
@JsonProperty("main_picture") val mainPicture: MainPicture?,
@JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?,
@JsonProperty("start_date") val startDate: String?,
@JsonProperty("end_date") val endDate: String?,
@JsonProperty("synopsis") val synopsis: String?,
@JsonProperty("mean") val mean: Double?,
@JsonProperty("rank") val rank: Int?,
@JsonProperty("popularity") val popularity: Int?,
@JsonProperty("num_list_users") val numListUsers: Int?,
@JsonProperty("num_scoring_users") val numScoringUsers: Int?,
@JsonProperty("nsfw") val nsfw: String?,
@JsonProperty("created_at") val createdAt: String?,
@JsonProperty("updated_at") val updatedAt: String?,
@JsonProperty("media_type") val mediaType: String?,
@JsonProperty("status") val status: String?,
@JsonProperty("genres") val genres: ArrayList<Genres>,
@JsonProperty("my_list_status") val myListStatus: MyListStatus?,
@JsonProperty("num_episodes") val numEpisodes: Int?,
@JsonProperty("start_season") val startSeason: StartSeason?,
@JsonProperty("broadcast") val broadcast: Broadcast?,
@JsonProperty("source") val source: String?,
@JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?,
@JsonProperty("rating") val rating: String?,
@JsonProperty("pictures") val pictures: ArrayList<MainPicture>,
@JsonProperty("background") val background: String?,
@JsonProperty("related_anime") val relatedAnime: ArrayList<RelatedAnime>,
@JsonProperty("related_manga") val relatedManga: ArrayList<String>,
@JsonProperty("recommendations") val recommendations: ArrayList<Recommendations>,
@JsonProperty("studios") val studios: ArrayList<Studios>,
@JsonProperty("statistics") val statistics: Statistics?,
)
data class Recommendations(
@JsonProperty("node") val node: Node? = null,
@JsonProperty("num_recommendations") val numRecommendations: Int? = null
)
data class Studios(
@JsonProperty("id") val id: Int? = null,
@JsonProperty("name") val name: String? = null
)
data class MyListStatus(
@JsonProperty("status") val status: String? = null,
@JsonProperty("score") val score: Int? = null,
@JsonProperty("num_episodes_watched") val numEpisodesWatched: Int? = null,
@JsonProperty("is_rewatching") val isRewatching: Boolean? = null,
@JsonProperty("updated_at") val updatedAt: String? = null
)
data class RelatedAnime(
@JsonProperty("node") val node: Node? = null,
@JsonProperty("relation_type") val relationType: String? = null,
@JsonProperty("relation_type_formatted") val relationTypeFormatted: String? = null
)
data class Status(
@JsonProperty("watching") val watching: String? = null,
@JsonProperty("completed") val completed: String? = null,
@JsonProperty("on_hold") val onHold: String? = null,
@JsonProperty("dropped") val dropped: String? = null,
@JsonProperty("plan_to_watch") val planToWatch: String? = null
)
data class Statistics(
@JsonProperty("status") val status: Status? = null,
@JsonProperty("num_list_users") val numListUsers: Int? = null
)
private fun parseDate(string: String?): Long? {
return try {
SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time
} catch (e: Exception) {
null
}
}
private fun toSearchResult(node : Node?) : SyncAPI.SyncSearchResult? {
return SyncAPI.SyncSearchResult(
name = node?.title ?: return null,
syncApiName = this.name,
id = node.id.toString(),
url = "https://myanimelist.net/anime/${node.id}",
posterUrl = node.main_picture?.large
)
}
override suspend fun getResult(id: String): SyncAPI.SyncResult? { override suspend fun getResult(id: String): SyncAPI.SyncResult? {
val internalId = id.toIntOrNull() ?: return null val internalId = id.toIntOrNull() ?: return null
TODO("Not yet implemented") val url =
"https://api.myanimelist.net/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null)
)
).text
return mapper.readValue<MalAnime>(res).let { malAnime ->
SyncAPI.SyncResult(
id = malAnime.id?.toString()!!,
totalEpisodes = malAnime.numEpisodes,
title = malAnime.title,
publicScore = malAnime.mean?.toFloat()?.times(100)?.toInt(),
duration = malAnime.averageEpisodeDuration,
synopsis = malAnime.synopsis,
airStatus = when (malAnime.status) {
"finished_airing" -> ShowStatus.Completed
"airing" -> ShowStatus.Ongoing
else -> null
},
nextAiring = null,
studio = malAnime.studios.mapNotNull { it.name },
genres = malAnime.genres.map { it.name },
trailerUrl = null,
startDate = parseDate(malAnime.startDate),
endDate = parseDate(malAnime.endDate),
recommendations = malAnime.recommendations.mapNotNull { rec ->
val node = rec.node ?: return@mapNotNull null
toSearchResult(node)
},
nextSeason = malAnime.relatedAnime.firstOrNull {
return@firstOrNull it.relationType == "sequel"
}?.let { toSearchResult(it.node) },
prevSeason = malAnime.relatedAnime.firstOrNull {
return@firstOrNull it.relationType == "prequel"
}?.let { toSearchResult(it.node) },
actors = null,
characters = null,
)
}
} }
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
val internalId = id.toIntOrNull() ?: return null val internalId = id.toIntOrNull() ?: return null
val data = getDataAboutMalId(internalId)?.my_list_status ?: return null val data = getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
return SyncAPI.SyncStatus( return SyncAPI.SyncStatus(
score = data.score, score = data?.score,
status = malStatusAsString.indexOf(data.status), status = malStatusAsString.indexOf(data?.status),
isFavorite = null, isFavorite = null,
watchedEpisodes = data.num_episodes_watched, watchedEpisodes = data?.num_episodes_watched,
) )
} }
companion object { companion object {
private val malStatusAsString = arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch") private val malStatusAsString =
arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch")
const val MAL_USER_KEY: String = "mal_user" // user data like profile const val MAL_USER_KEY: String = "mal_user" // user data like profile
const val MAL_CACHED_LIST: String = "mal_cached_list" const val MAL_CACHED_LIST: String = "mal_cached_list"
@ -111,7 +251,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
override fun handleRedirect(url: String) { override fun handleRedirect(url: String) {
try {
val sanitizer = val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val state = sanitizer["state"]!! val state = sanitizer["state"]!!
@ -142,9 +281,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
} }
} catch (e: Exception) {
e.printStackTrace()
}
} }
override fun authenticate() { override fun authenticate() {
@ -277,30 +413,23 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("start_time") val start_time: String? @JsonProperty("start_time") val start_time: String?
) )
fun getMalAnimeListCached(): Array<Data>? { private fun getMalAnimeListCached(): Array<Data>? {
return getKey(MAL_CACHED_LIST) as? Array<Data> return getKey(MAL_CACHED_LIST) as? Array<Data>
} }
suspend fun getMalAnimeListSmart(): Array<Data>? { suspend fun getMalAnimeListSmart(): Array<Data>? {
if (getKey<String>( if (getAuth() == null) return null
accountId,
MAL_TOKEN_KEY
) == null
) return null
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
val list = getMalAnimeList() val list = getMalAnimeList()
if (list != null) {
setKey(MAL_CACHED_LIST, list) setKey(MAL_CACHED_LIST, list)
setKey(MAL_SHOULD_UPDATE_LIST, false) setKey(MAL_SHOULD_UPDATE_LIST, false)
}
list list
} else { } else {
getMalAnimeListCached() getMalAnimeListCached()
} }
} }
private suspend fun getMalAnimeList(): Array<Data>? { private suspend fun getMalAnimeList(): Array<Data> {
return try {
checkMalToken() checkMalToken()
var offset = 0 var offset = 0
val fullList = mutableListOf<Data>() val fullList = mutableListOf<Data>()
@ -308,13 +437,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
while (true) { while (true) {
val data: MalList = getMalAnimeListSlice(offset) ?: break val data: MalList = getMalAnimeListSlice(offset) ?: break
fullList.addAll(data.data) fullList.addAll(data.data)
offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break offset =
} data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() }
fullList.toTypedArray() ?: break
//mapper.readValue<MalAnime>(res)
} catch (e: Exception) {
null
} }
return fullList.toTypedArray()
} }
fun convertToStatus(string: String): MalStatusType { fun convertToStatus(string: String): MalStatusType {
@ -323,11 +450,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
val user = "@me" val user = "@me"
val auth = getKey<String>( val auth = getAuth() ?: return null
accountId,
MAL_TOKEN_KEY
) ?: return null
return try {
// Very lackluster docs // Very lackluster docs
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
val url = val url =
@ -337,29 +460,20 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
"Authorization" to "Bearer $auth", "Authorization" to "Bearer $auth",
), cacheTime = 0 ), cacheTime = 0
).text ).text
res.toKotlinObject() return res.toKotlinObject()
} catch (e: Exception) {
logError(e)
null
}
} }
private suspend fun getDataAboutMalId(id: Int): MalAnime? { private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
return try {
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get // https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" val url =
"https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
val res = app.get( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
"Authorization" to "Bearer " + getKey<String>( "Authorization" to "Bearer " + (getAuth() ?: return null)
accountId,
MAL_TOKEN_KEY
)!!
), cacheTime = 0 ), cacheTime = 0
).text ).text
mapper.readValue<MalAnime>(res)
} catch (e: Exception) { return mapper.readValue<SmallMalAnime>(res)
null
}
} }
suspend fun setAllMalData() { suspend fun setAllMalData() {
@ -372,14 +486,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val res = app.get( val res = app.get(
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}", "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + getKey<String>( "Authorization" to "Bearer " + (getAuth() ?: return)
accountId,
MAL_TOKEN_KEY
)!!
), cacheTime = 0 ), cacheTime = 0
).text ).text
val values = mapper.readValue<MalRoot>(res) val values = mapper.readValue<MalRoot>(res)
val titles = values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } val titles =
values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
for (t in titles) { for (t in titles) {
allTitles[t.id] = t allTitles[t.id] = t
} }
@ -389,7 +501,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
try {
// No time remaining if the show has already ended // No time remaining if the show has already ended
try { try {
endDate?.let { endDate?.let {
@ -412,7 +523,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
dateFormat.timeZone = TimeZone.getTimeZone("Japan") dateFormat.timeZone = TimeZone.getTimeZone("Japan")
val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null val parsedDate =
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000 val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000
// if it has already aired this week add a week to the timer // if it has already aired this week add a week to the timer
@ -420,10 +532,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff
return secondsToReadable(updatedTimeDiff.toInt(), "Now") return secondsToReadable(updatedTimeDiff.toInt(), "Now")
} catch (e: Exception) {
logError(e)
}
return null
} }
private suspend fun checkMalToken() { private suspend fun checkMalToken() {
@ -438,14 +546,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? { private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
checkMalToken() checkMalToken()
return try {
val res = app.get( val res = app.get(
"https://api.myanimelist.net/v2/users/@me", "https://api.myanimelist.net/v2/users/@me",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + getKey<String>( "Authorization" to "Bearer " + (getAuth() ?: return null)
accountId,
MAL_TOKEN_KEY
)!!
), cacheTime = 0 ), cacheTime = 0
).text ).text
@ -454,11 +558,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
setKey(accountId, MAL_USER_KEY, user) setKey(accountId, MAL_USER_KEY, user)
registerAccount() registerAccount()
} }
user return user
} catch (e: Exception) {
e.printStackTrace()
null
}
} }
enum class MalStatusType(var value: Int) { enum class MalStatusType(var value: Int) {
@ -483,7 +583,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
suspend fun setScoreRequest( private suspend fun setScoreRequest(
id: Int, id: Int,
status: MalStatusType? = null, status: MalStatusType? = null,
score: Int? = null, score: Int? = null,
@ -495,8 +595,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
score, score,
num_watched_episodes num_watched_episodes
) )
if (res != "") {
return try { return if (res.isNullOrBlank()) {
false
} else {
val malStatus = mapper.readValue<MalStatus>(res) val malStatus = mapper.readValue<MalStatus>(res)
if (allTitles.containsKey(id)) { if (allTitles.containsKey(id)) {
val currentTitle = allTitles[id]!! val currentTitle = allTitles[id]!!
@ -505,12 +607,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
allTitles[id] = MalTitleHolder(malStatus, id, "") allTitles[id] = MalTitleHolder(malStatus, id, "")
} }
true true
} catch (e: Exception) {
logError(e)
false
}
} else {
return false
} }
} }
@ -519,15 +615,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
status: String? = null, status: String? = null,
score: Int? = null, score: Int? = null,
num_watched_episodes: Int? = null, num_watched_episodes: Int? = null,
): String { ): String? {
return try { return app.put(
app.put(
"https://api.myanimelist.net/v2/anime/$id/my_list_status", "https://api.myanimelist.net/v2/anime/$id/my_list_status",
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + getKey<String>( "Authorization" to "Bearer " + (getAuth() ?: return null)
accountId,
MAL_TOKEN_KEY
)!!
), ),
data = mapOf( data = mapOf(
"status" to status, "status" to status,
@ -535,10 +627,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
"num_watched_episodes" to num_watched_episodes?.toString() "num_watched_episodes" to num_watched_episodes?.toString()
) )
).text ).text
} catch (e: Exception) {
e.printStackTrace()
return ""
}
} }
@ -591,7 +679,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
) )
// Used for getDataAboutId() // Used for getDataAboutId()
data class MalAnime( data class SmallMalAnime(
@JsonProperty("id") val id: Int, @JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String?, @JsonProperty("title") val title: String?,
@JsonProperty("num_episodes") val num_episodes: Int, @JsonProperty("num_episodes") val num_episodes: Int,

View file

@ -12,8 +12,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
@ -21,8 +20,11 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.search.SearchViewModel
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
@ -31,11 +33,10 @@ import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.quick_search.* import kotlinx.android.synthetic.main.quick_search.*
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() { class QuickSearchFragment : Fragment() {
companion object { companion object {
fun pushSearch(activity: Activity?, autoSearch: String? = null) { fun pushSearch(activity: Activity?, autoSearch: String? = null) {
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply { activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
putBoolean("mainapi", true)
autoSearch?.let { autoSearch?.let {
putString( putString(
"autosearch", "autosearch",
@ -49,18 +50,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
}) })
} }
fun pushSync(
activity: Activity?,
autoSearch: String? = null,
callback: (SearchClickCallback) -> Unit
) {
clickCallback = callback
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
putBoolean("mainapi", false)
putString("autosearch", autoSearch)
})
}
var clickCallback: ((SearchClickCallback) -> Unit)? = null var clickCallback: ((SearchClickCallback) -> Unit)? = null
} }
@ -87,10 +76,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(quick_search_root) context?.fixPaddingStatusbar(quick_search_root)
arguments?.getBoolean("mainapi", true)?.let {
isMainApis = it
}
val listLock = ReentrantLock() val listLock = ReentrantLock()
observe(searchViewModel.currentSearch) { list -> observe(searchViewModel.currentSearch) { list ->
try { try {
@ -114,45 +99,39 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
ParentItemAdapter(mutableListOf(), { callback -> ParentItemAdapter(mutableListOf(), { callback ->
when (callback.action) {
SEARCH_ACTION_LOAD -> {
if (isMainApis) {
activity?.popCurrentPage()
SearchHelper.handleSearchClickCallback(activity, callback) SearchHelper.handleSearchClickCallback(activity, callback)
} else { //when (callback.action) {
clickCallback?.invoke(callback) //SEARCH_ACTION_LOAD -> {
} // clickCallback?.invoke(callback)
} //}
else -> SearchHelper.handleSearchClickCallback(activity, callback) // else -> SearchHelper.handleSearchClickCallback(activity, callback)
} //}
}, { item -> }, { item ->
activity?.loadHomepageList(item) activity?.loadHomepageList(item)
}) })
val searchExitIcon = val searchExitIcon =
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchMagIcon = val searchMagIcon =
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon) quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
searchMagIcon?.scaleX = 0.65f searchMagIcon?.scaleX = 0.65f
searchMagIcon?.scaleY = 0.65f searchMagIcon?.scaleY = 0.65f
quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
val active = if (isMainApis) { context?.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
val langs = context?.getApiProviderLangSettings() ?.map { it.name }?.toSet()?.let { active ->
apis.filter { langs?.contains(it.lang) == true }.map { it.name }.toSet()
} else emptySet()
searchViewModel.searchAndCancel( searchViewModel.searchAndCancel(
query = query, query = query,
isMainApis = isMainApis,
ignoreSettings = false, ignoreSettings = false,
providersActive = active providersActive = active
) )
quick_search?.let { quick_search?.let {
UIHelper.hideKeyboard(it) UIHelper.hideKeyboard(it)
} }
}
return true return true
} }

View file

@ -47,8 +47,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.getCastSession
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
@ -60,6 +58,7 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1 import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
@ -73,8 +72,6 @@ import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.addSync
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSync
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
@ -93,6 +90,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -372,6 +370,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
private var currentLoadingCount = private var currentLoadingCount =
0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED 0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED
private lateinit var viewModel: ResultViewModel //by activityViewModels() private lateinit var viewModel: ResultViewModel //by activityViewModels()
private lateinit var syncModel: SyncViewModel
private var currentHeaderName: String? = null private var currentHeaderName: String? = null
private var currentType: TvType? = null private var currentType: TvType? = null
private var currentEpisodes: List<ResultEpisode>? = null private var currentEpisodes: List<ResultEpisode>? = null
@ -384,6 +383,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
): View? { ): View? {
viewModel = viewModel =
ViewModelProvider(this)[ResultViewModel::class.java] ViewModelProvider(this)[ResultViewModel::class.java]
syncModel =
ViewModelProvider(this)[SyncViewModel::class.java]
return inflater.inflate(R.layout.fragment_result_swipe, container, false) return inflater.inflate(R.layout.fragment_result_swipe, container, false)
} }
@ -469,16 +471,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
var startAction: Int? = null var startAction: Int? = null
private var startValue: Int? = null private var startValue: Int? = null
private fun updateSync(id: Int) {
val syncList = getSync(id, SyncApis.map { it.idPrefix }) ?: return
val list = ArrayList<Pair<SyncAPI, String>>()
for (i in 0 until SyncApis.count()) {
val res = syncList[i] ?: continue
list.add(Pair(SyncApis[i], res))
}
viewModel.updateSync(context, list)
}
private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) { private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) {
// java.util.IllegalFormatConversionException: f != java.lang.Integer // java.util.IllegalFormatConversionException: f != java.lang.Integer
// This can fail with malformed formatting // This can fail with malformed formatting
@ -525,6 +517,16 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
} }
private fun setMalSync(id: String?): Boolean {
syncModel.setMalId(id ?: return false)
return true
}
private fun setAniListSync(id: String?): Boolean {
syncModel.setAniListId(id ?: return false)
return true
}
private fun setActors(actors: List<ActorData>?) { private fun setActors(actors: List<ActorData>?) {
if (actors.isNullOrEmpty()) { if (actors.isNullOrEmpty()) {
result_cast_text?.isVisible = false result_cast_text?.isVisible = false
@ -1147,6 +1149,45 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
} }
observe(syncModel.status) { status ->
var closed = false
when (status) {
is Resource.Failure -> {
result_sync_loading_shimmer?.stopShimmer()
result_sync_loading_shimmer?.isVisible = false
result_sync_holder?.isVisible = false
}
is Resource.Loading -> {
result_sync_loading_shimmer?.startShimmer()
result_sync_loading_shimmer?.isVisible = true
result_sync_holder?.isVisible = false
}
is Resource.Success -> {
result_sync_loading_shimmer?.stopShimmer()
result_sync_loading_shimmer?.isVisible = false
result_sync_holder?.isVisible = true
val d = status.value
result_sync_rating?.value = d.score?.toFloat() ?: 0.0f
/*when(d.status) {
-1 -> None
0 -> Watching
1 -> Completed
2 -> OnHold
3 -> Dropped
4 -> PlanToWatch
5 -> ReWatching
}*/
//d.status
}
null -> {
closed = false
}
}
result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED)
}
observe(viewModel.episodes) { episodeList -> observe(viewModel.episodes) { episodeList ->
lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
var isSeriesVisible = false var isSeriesVisible = false
@ -1262,7 +1303,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
observe(viewModel.dubSubSelections) { range -> observe(viewModel.dubSubSelections) { range ->
dubRange = range dubRange = range
if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true){ if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true) {
viewModel.changeDubStatus(DubStatus.Dubbed) viewModel.changeDubStatus(DubStatus.Dubbed)
} }
@ -1297,7 +1338,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
} }
result_episode_select.setOnClickListener { result_episode_select?.setOnClickListener {
val ranges = episodeRanges val ranges = episodeRanges
if (ranges != null) { if (ranges != null) {
it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) } it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) }
@ -1307,6 +1348,13 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
} }
result_sync_set_score?.setOnClickListener {
// TODO set score
//syncModel.setScore(SyncAPI.SyncStatus(
// status =
//))
}
observe(viewModel.publicEpisodesCount) { count -> observe(viewModel.publicEpisodesCount) { count ->
if (count < 0) { if (count < 0) {
result_episodes_text?.isVisible = false result_episodes_text?.isVisible = false
@ -1321,19 +1369,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
currentId = it currentId = it
} }
observe(viewModel.sync) { sync ->
for (s in sync) {
when (s) {
is Resource.Success -> {
val d = s.value ?: continue
setDuration(d.duration)
setRating(d.publicScore)
}
else -> Unit
}
}
}
observe(viewModel.resultResponse) { data -> observe(viewModel.resultResponse) { data ->
when (data) { when (data) {
is Resource.Success -> { is Resource.Success -> {
@ -1399,27 +1434,12 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
} }
updateSync(d.getId())
result_add_sync?.setOnClickListener {
QuickSearchFragment.pushSync(activity, d.name) { click ->
addSync(d.getId(), click.card.apiName, click.card.url)
showToast(
activity,
context?.getString(R.string.added_sync_format)
?.format(click.card.name),
Toast.LENGTH_SHORT
)
updateSync(d.getId())
}
}
val showStatus = when (d) { val showStatus = when (d) {
is TvSeriesLoadResponse -> d.showStatus is TvSeriesLoadResponse -> d.showStatus
is AnimeLoadResponse -> d.showStatus is AnimeLoadResponse -> d.showStatus
else -> null else -> null
} }
setShow(showStatus) setShow(showStatus)
setDuration(d.duration) setDuration(d.duration)
setYear(d.year) setYear(d.year)
@ -1427,6 +1447,18 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setRecommendations(d.recommendations) setRecommendations(d.recommendations)
setActors(d.actors) setActors(d.actors)
if (SettingsFragment.accountEnabled)
if (d is AnimeLoadResponse) {
if (
setMalSync(d.malId?.toString())
||
setAniListSync(d.anilistId?.toString())
) {
syncModel.updateMetadata()
syncModel.updateStatus()
}
}
result_meta_site?.text = d.apiName result_meta_site?.text = d.apiName
val posterImageLink = d.posterUrl val posterImageLink = d.posterUrl

View file

@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.IGenerator import com.lagradost.cloudstream3.ui.player.IGenerator
@ -79,9 +78,6 @@ class ResultViewModel : ViewModel() {
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData() private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
val watchStatus: LiveData<WatchType> get() = _watchStatus val watchStatus: LiveData<WatchType> get() = _watchStatus
private val _sync: MutableLiveData<List<Resource<SyncAPI.SyncResult?>>> = MutableLiveData()
val sync: LiveData<List<Resource<SyncAPI.SyncResult?>>> get() = _sync
fun updateWatchStatus(status: WatchType) = viewModelScope.launch { fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
val currentId = id.value ?: return@launch val currentId = id.value ?: return@launch
_watchStatus.postValue(status) _watchStatus.postValue(status)
@ -233,17 +229,6 @@ class ResultViewModel : ViewModel() {
return generator return generator
} }
fun updateSync(context: Context?, sync: List<Pair<SyncAPI, String>>) = viewModelScope.launch {
if (context == null) return@launch
val list = ArrayList<Resource<SyncAPI.SyncResult?>>()
for (s in sync) {
val result = safeApiCall { s.first.getResult(s.second) }
list.add(result)
_sync.postValue(list)
}
}
private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) { private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) {
_episodes.postValue(list) _episodes.postValue(list)
generator = RepoLinkGenerator(list) generator = RepoLinkGenerator(list)

View file

@ -0,0 +1,79 @@
package com.lagradost.cloudstream3.ui.result
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import kotlinx.coroutines.launch
class SyncViewModel : ViewModel() {
private val repos = SyncApis
private val _metaResponse: MutableLiveData<Resource<SyncAPI.SyncResult>> =
MutableLiveData()
val metadata: LiveData<Resource<SyncAPI.SyncResult>> get() = _metaResponse
private val _statusResponse: MutableLiveData<Resource<SyncAPI.SyncStatus>?> =
MutableLiveData(null)
val status: LiveData<Resource<SyncAPI.SyncStatus>?> get() = _statusResponse
// prefix, id
private val syncIds = hashMapOf<String, String>()
fun setMalId(id: String) {
syncIds[malApi.idPrefix] = id
}
fun setAniListId(id: String) {
syncIds[aniListApi.idPrefix] = id
}
fun setScore(status: SyncAPI.SyncStatus) = viewModelScope.launch {
for ((prefix, id) in syncIds) {
repos.firstOrNull { it.idPrefix == prefix }?.score(id, status)
}
updateStatus()
}
fun updateStatus() = viewModelScope.launch {
_statusResponse.postValue(Resource.Loading())
var lastError: Resource<SyncAPI.SyncStatus> = Resource.Failure(false, null, null, "No data")
for ((prefix, id) in syncIds) {
repos.firstOrNull { it.idPrefix == prefix }?.let {
val result = it.getStatus(id)
if (result is Resource.Success) {
_statusResponse.postValue(result)
return@launch
} else if (result is Resource.Failure) {
lastError = result
}
}
}
_statusResponse.postValue(lastError)
}
fun updateMetadata() = viewModelScope.launch {
_metaResponse.postValue(Resource.Loading())
var lastError: Resource<SyncAPI.SyncResult> = Resource.Failure(false, null, null, "No data")
for ((prefix, id) in syncIds) {
repos.firstOrNull { it.idPrefix == prefix }?.let {
val result = it.getResult(id)
if (result is Resource.Success) {
_metaResponse.postValue(result)
return@launch
} else if (result is Resource.Failure) {
lastError = result
}
}
}
_metaResponse.postValue(lastError)
}
}

View file

@ -4,15 +4,13 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -39,7 +37,6 @@ class SearchViewModel : ViewModel() {
val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory
private val repos = apis.map { APIRepository(it) } private val repos = apis.map { APIRepository(it) }
private val syncApis = SyncApis
fun clearSearch() { fun clearSearch() {
_searchResponse.postValue(Resource.Success(ArrayList())) _searchResponse.postValue(Resource.Success(ArrayList()))
@ -49,33 +46,11 @@ class SearchViewModel : ViewModel() {
private var onGoingSearch: Job? = null private var onGoingSearch: Job? = null
fun searchAndCancel( fun searchAndCancel(
query: String, query: String,
isMainApis: Boolean = true,
providersActive: Set<String> = setOf(), providersActive: Set<String> = setOf(),
ignoreSettings: Boolean = false ignoreSettings: Boolean = false
) { ) {
onGoingSearch?.cancel() onGoingSearch?.cancel()
onGoingSearch = search(query, isMainApis, providersActive, ignoreSettings) onGoingSearch = search(query, providersActive, ignoreSettings)
}
data class SyncSearchResultSearchResponse(
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override var id: Int?,
override var quality: SearchQuality? = null
) : SearchResponse
private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse {
return SyncSearchResultSearchResponse(
this.name,
this.url,
this.syncApiName,
null,
this.posterUrl,
null, //this.id.hashCode()
)
} }
fun updateHistory() = viewModelScope.launch { fun updateHistory() = viewModelScope.launch {
@ -89,7 +64,6 @@ class SearchViewModel : ViewModel() {
private fun search( private fun search(
query: String, query: String,
isMainApis: Boolean = true,
providersActive: Set<String>, providersActive: Set<String>,
ignoreSettings: Boolean = false ignoreSettings: Boolean = false
) = ) =
@ -118,7 +92,6 @@ class SearchViewModel : ViewModel() {
_currentSearch.postValue(ArrayList()) _currentSearch.postValue(ArrayList())
withContext(Dispatchers.IO) { // This interrupts UI otherwise withContext(Dispatchers.IO) { // This interrupts UI otherwise
if (isMainApis) {
repos.filter { a -> repos.filter { a ->
ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name)) ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))
}.apmap { a -> // Parallel }.apmap { a -> // Parallel
@ -126,16 +99,6 @@ class SearchViewModel : ViewModel() {
currentList.add(OnGoingSearch(a.name, search)) currentList.add(OnGoingSearch(a.name, search))
_currentSearch.postValue(currentList) _currentSearch.postValue(currentList)
} }
} else {
syncApis.apmap { a ->
val search = safeApiCall {
a.search(query)?.map { it.toSearchResponse() }
?: throw ErrorLoadingException()
}
currentList.add(OnGoingSearch(a.name, search))
}
}
} }
_currentSearch.postValue(currentList) _currentSearch.postValue(currentList)

View file

@ -0,0 +1,30 @@
package com.lagradost.cloudstream3.ui.search
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.SyncAPI
class SyncSearchViewModel {
data class SyncSearchResultSearchResponse(
override val name: String,
override val url: String,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override var id: Int?,
override var quality: SearchQuality? = null
) : SearchResponse
private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse {
return SyncSearchResultSearchResponse(
this.name,
this.url,
this.syncApiName,
null,
this.posterUrl,
null, //this.id.hashCode()
)
}
}

View file

@ -88,7 +88,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
} }
private const val accountEnabled = false const val accountEnabled = false
} }
private var beneneCount = 0 private var beneneCount = 0
@ -155,7 +155,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
val dialog = builder.show() val dialog = builder.show()
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener { dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
try {
api.authenticate() api.authenticate()
} catch (e: Exception) {
logError(e)
}
} }
val ogIndex = api.accountIndex val ogIndex = api.accountIndex
@ -325,10 +329,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
val syncApis = val syncApis =
listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi)) listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi))
for (sync in syncApis) { for ((key, api) in syncApis) {
getPref(sync.first)?.apply { getPref(key)?.apply {
isVisible = accountEnabled isVisible = accountEnabled
val api = sync.second
title = title =
getString(R.string.login_format).format(api.name, getString(R.string.account)) getString(R.string.login_format).format(api.name, getString(R.string.account))
setOnPreferenceClickListener { _ -> setOnPreferenceClickListener { _ ->
@ -336,7 +339,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
if (info != null) { if (info != null) {
showLoginInfo(api, info) showLoginInfo(api, info)
} else { } else {
try {
api.authenticate() api.authenticate()
} catch (e: Exception) {
logError(e)
}
} }
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }

View file

@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ComponentName
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -23,6 +22,7 @@ import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.tvprovider.media.tv.PreviewChannelHelper import androidx.tvprovider.media.tv.PreviewChannelHelper
import androidx.tvprovider.media.tv.TvContractCompat import androidx.tvprovider.media.tv.TvContractCompat
import androidx.tvprovider.media.tv.WatchNextProgram import androidx.tvprovider.media.tv.WatchNextProgram
@ -33,7 +33,10 @@ import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.wrappers.Wrappers import com.google.android.gms.common.wrappers.Wrappers
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.isMovieType
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -196,17 +199,10 @@ object AppUtils {
fun Context.openBrowser(url: String) { fun Context.openBrowser(url: String) {
try { try {
val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java))
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url) intent.data = Uri.parse(url)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) ContextCompat.startActivity(this, intent, null)
startActivity(
Intent.createChooser(intent, null)
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components)
)
else
startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }

View file

@ -7,9 +7,10 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:visibility="gone"
android:padding="16dp" android:padding="16dp"
android:orientation="vertical" android:orientation="vertical"
android:id="@+id/sync_holder" android:id="@+id/result_sync_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -38,8 +39,9 @@
app:tint="?attr/textColor" /> app:tint="?attr/textColor" />
<EditText <EditText
android:id="@+id/result_sync_current_episodes"
style="@style/AppEditStyle" style="@style/AppEditStyle"
android:hint="20" tools:hint="20"
android:textSize="20sp" android:textSize="20sp"
android:inputType="number" android:inputType="number"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -47,11 +49,12 @@
tools:ignore="LabelFor" /> tools:ignore="LabelFor" />
<TextView <TextView
android:id="@+id/result_sync_max_episodes"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:paddingBottom="1dp" android:paddingBottom="1dp"
android:textSize="20sp" android:textSize="20sp"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:text="/30" tools:text="/30"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
@ -64,10 +67,10 @@
android:layout_gravity="end|center_vertical" android:layout_gravity="end|center_vertical"
android:contentDescription="@string/result_share" android:contentDescription="@string/result_share"
app:tint="?attr/textColor" /> app:tint="?attr/textColor" />
</LinearLayout> </LinearLayout>
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/result_sync_episodes"
android:padding="10dp" android:padding="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="20dp" android:layout_height="20dp"
@ -192,6 +195,7 @@
</LinearLayout> </LinearLayout>
<com.google.android.material.slider.Slider <com.google.android.material.slider.Slider
android:id="@+id/result_sync_rating"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="10" android:valueTo="10"
android:value="4" android:value="4"
@ -350,6 +354,7 @@
style="@style/WhiteButton" style="@style/WhiteButton"
android:text="@string/type_watching" /> android:text="@string/type_watching" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/result_sync_set_score"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
style="@style/BlackButton" style="@style/BlackButton"
@ -357,13 +362,48 @@
android:text="@string/upload_sync" /> android:text="@string/upload_sync" />
</LinearLayout> </LinearLayout>
<com.facebook.shimmer.ShimmerFrameLayout
<com.google.android.material.button.MaterialButton android:id="@+id/result_sync_loading_shimmer"
android:visibility="gone" app:shimmer_base_alpha="0.2"
android:id="@+id/sync_add_tracking" app:shimmer_highlight_alpha="0.3"
android:layout_gravity="center" app:shimmer_duration="@integer/loading_time"
app:shimmer_auto_start="true"
android:padding="15dp"
android:layout_width="match_parent" android:layout_width="match_parent"
style="@style/SyncButton" android:layout_height="match_parent"
app:icon="@drawable/ic_baseline_add_24" android:layout_gravity="center"
android:text="@string/add_sync" /> android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<Space
android:layout_width="match_parent"
android:layout_height="30dp"/>
<include layout="@layout/loading_line_short" />
<include layout="@layout/loading_line" />
<Space
android:layout_width="match_parent"
android:layout_height="30dp"/>
<include layout="@layout/loading_line_short" />
<include layout="@layout/loading_line" />
<Space
android:layout_width="match_parent"
android:layout_height="30dp"/>
<include layout="@layout/loading_line_short" />
<include layout="@layout/loading_line_short" />
<include layout="@layout/loading_line_short" />
<include layout="@layout/loading_line_short" />
<include layout="@layout/loading_line_short" />
<Space
android:layout_width="match_parent"
android:layout_height="30dp"/>
<include layout="@layout/loading_line" />
<include layout="@layout/loading_line" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
</FrameLayout> </FrameLayout>

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
@ -9,9 +10,7 @@ import org.junit.Test
class ProviderTests { class ProviderTests {
private fun getAllProviders(): List<MainAPI> { private fun getAllProviders(): List<MainAPI> {
val allApis = APIHolder.apis return allProviders.filter { !it.usesWebView }
allApis.addAll(APIHolder.restrictedApis)
return allApis.filter { !it.usesWebView }
} }
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean { private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {