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}")) {
api.handleRedirect(str) try {
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,23 +55,19 @@ 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"]!! val expiresIn = sanitizer["expires_in"]!!
val expiresIn = sanitizer["expires_in"]!!
val endTime = unixTime + expiresIn.toLong() val endTime = unixTime + expiresIn.toLong()
switchToNewAccount() switchToNewAccount()
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime) setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
setKey(accountId, ANILIST_TOKEN_KEY, token) setKey(accountId, ANILIST_TOKEN_KEY, token)
setKey(ANILIST_SHOULD_UPDATE_LIST, true) setKey(ANILIST_SHOULD_UPDATE_LIST, true)
ioSafe { ioSafe {
getUser() getUser()
}
} catch (e: Exception) {
e.printStackTrace()
} }
} }
@ -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,71 +365,56 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
}""" }"""
try {
val data = postApi("https://graphql.anilist.co", q, true)
var d: GetDataRoot? = 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 data = postApi(q, true)
if (main.mediaListEntry != null) { val d = mapper.readValue<GetDataRoot>(data ?: return null)
return AniListTitleHolder(
title = main.title, val main = d.data.Media
id = id, if (main.mediaListEntry != null) {
isFavourite = main.isFavourite, return AniListTitleHolder(
progress = main.mediaListEntry.progress, title = main.title,
episodes = main.episodes, id = id,
score = main.mediaListEntry.score, isFavourite = main.isFavourite,
type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)), progress = main.mediaListEntry.progress,
) episodes = main.episodes,
} else { score = main.mediaListEntry.score,
return AniListTitleHolder( type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)),
title = main.title, )
id = id, } else {
isFavourite = main.isFavourite, return AniListTitleHolder(
progress = 0, title = main.title,
episodes = main.episodes, id = id,
score = 0, isFavourite = main.isFavourite,
type = AniListStatusType.None, progress = 0,
) episodes = main.episodes,
} score = 0,
} catch (e: Exception) { type = AniListStatusType.None,
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
app.post( )
"https://graphql.anilist.co/", }
headers = mapOf(
"Authorization" to "Bearer " + getKey( private suspend fun postApi(q: String, cache: Boolean = false): String? {
accountId, return if (!checkToken()) {
ANILIST_TOKEN_KEY, app.post(
"" "https://graphql.anilist.co/",
)!!, headers = mapOf(
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" "Authorization" to "Bearer " + (getAuth() ?: return null),
), if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
cacheTime = 0, ),
data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) cacheTime = 0,
timeout = 5 // REASONABLE TIMEOUT data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
).text.replace("\\/", "/") timeout = 5 // REASONABLE TIMEOUT
} else { ).text.replace("\\/", "/")
"" } 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,19 +512,18 @@ 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 -> getKey<AniListUser>(key, null)?.let {
getKey<AniListUser>(key, null)?.let { userID = it.id
userID = it.id
}
} }
}
val fixedUserID = userID ?: return null val fixedUserID = userID ?: return null
val mediaType = "ANIME" val mediaType = "ANIME"
val query = """ val query = """
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) { query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
lists { lists {
@ -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,14 +590,13 @@ 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( 0,
0, type.value
type.value )]
)] }, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) { SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
id id
status status
@ -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,29 +626,24 @@ 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 ?: return null)
val userData = mapper.readValue<AniListRoot>(data) val u = userData.data.Viewer
val u = userData.data.Viewer val user = AniListUser(
val user = AniListUser( u.id,
u.id, u.name,
u.name, u.avatar.large,
u.avatar.large, )
) if (setSettings) {
if (setSettings) { setKey(accountId, ANILIST_USER_KEY, user)
setKey(accountId, ANILIST_USER_KEY, user) registerAccount()
registerAccount()
}
/* // TODO FIX FAVS
for(i in u.favourites.anime.nodes) {
println("FFAV:" + i.id)
}*/
return user
} catch (e: java.lang.Exception) {
logError(e)
return null
} }
/* // TODO FIX FAVS
for(i in u.favourites.anime.nodes) {
println("FFAV:" + i.id)
}*/
return user
} }
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,39 +251,35 @@ 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"]!! if (state == "RequestID$requestId") {
if (state == "RequestID$requestId") { val currentCode = sanitizer["code"]!!
val currentCode = sanitizer["code"]!! ioSafe {
ioSafe { var res = ""
var res = "" try {
try { //println("cc::::: " + codeVerifier)
//println("cc::::: " + codeVerifier) res = app.post(
res = app.post( "https://myanimelist.net/v1/oauth2/token",
"https://myanimelist.net/v1/oauth2/token", data = mapOf(
data = mapOf( "client_id" to key,
"client_id" to key, "code" to currentCode,
"code" to currentCode, "code_verifier" to codeVerifier,
"code_verifier" to codeVerifier, "grant_type" to "authorization_code"
"grant_type" to "authorization_code" )
) ).text
).text } catch (e: Exception) {
} catch (e: Exception) { e.printStackTrace()
e.printStackTrace() }
}
if (res != "") { if (res != "") {
switchToNewAccount() switchToNewAccount()
storeToken(res) storeToken(res)
getMalUser() getMalUser()
setKey(MAL_SHOULD_UPDATE_LIST, true) setKey(MAL_SHOULD_UPDATE_LIST, true)
}
} }
} }
} catch (e: Exception) {
e.printStackTrace()
} }
} }
@ -277,44 +413,35 @@ 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>() val offsetRegex = Regex("""offset=(\d+)""")
val offsetRegex = Regex("""offset=(\d+)""") 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 =
offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() }
} ?: break
fullList.toTypedArray()
//mapper.readValue<MalAnime>(res)
} catch (e: Exception) {
null
} }
return fullList.toTypedArray()
} }
fun convertToStatus(string: String): MalStatusType { fun convertToStatus(string: String): MalStatusType {
@ -323,43 +450,30 @@ 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, // Very lackluster docs
MAL_TOKEN_KEY // https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
) ?: return null val url =
return try { "https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
// Very lackluster docs val res = app.get(
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get url, headers = mapOf(
val url = "Authorization" to "Bearer $auth",
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset" ), cacheTime = 0
val res = app.get( ).text
url, headers = mapOf( return res.toKotlinObject()
"Authorization" to "Bearer $auth",
), cacheTime = 0
).text
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 =
val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" "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, ), cacheTime = 0
MAL_TOKEN_KEY ).text
)!!
), cacheTime = 0 return mapper.readValue<SmallMalAnime>(res)
).text
mapper.readValue<MalAnime>(res)
} catch (e: Exception) {
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,41 +501,37 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
// No time remaining if the show has already ended
try { try {
// No time remaining if the show has already ended endDate?.let {
try { if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
endDate?.let {
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
}
} catch (e: ParseException) {
logError(e)
} }
} catch (e: ParseException) {
// Unparseable date: "2021 7 4 other null"
// Weekday: other, date: null
if (date.contains("null") || date.contains("other")) {
return null
}
val currentDate = Calendar.getInstance()
val currentMonth = currentDate.get(Calendar.MONTH) + 1
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
val currentYear = currentDate.get(Calendar.YEAR)
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000
// if it has already aired this week add a week to the timer
val updatedTimeDiff =
if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff
return secondsToReadable(updatedTimeDiff.toInt(), "Now")
} catch (e: Exception) {
logError(e) logError(e)
} }
return null
// Unparseable date: "2021 7 4 other null"
// Weekday: other, date: null
if (date.contains("null") || date.contains("other")) {
return null
}
val currentDate = Calendar.getInstance()
val currentMonth = currentDate.get(Calendar.MONTH) + 1
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
val currentYear = currentDate.get(Calendar.YEAR)
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
val parsedDate =
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000
// if it has already aired this week add a week to the timer
val updatedTimeDiff =
if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff
return secondsToReadable(updatedTimeDiff.toInt(), "Now")
} }
private suspend fun checkMalToken() { private suspend fun checkMalToken() {
@ -438,27 +546,19 @@ 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 " + (getAuth() ?: return null)
"Authorization" to "Bearer " + getKey<String>( ), cacheTime = 0
accountId, ).text
MAL_TOKEN_KEY
)!!
), cacheTime = 0
).text
val user = mapper.readValue<MalUser>(res) val user = mapper.readValue<MalUser>(res)
if (setSettings) { if (setSettings) {
setKey(accountId, MAL_USER_KEY, user) setKey(accountId, MAL_USER_KEY, user)
registerAccount() registerAccount()
}
user
} catch (e: Exception) {
e.printStackTrace()
null
} }
return user
} }
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,22 +595,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
score, score,
num_watched_episodes num_watched_episodes
) )
if (res != "") {
return try { return if (res.isNullOrBlank()) {
val malStatus = mapper.readValue<MalStatus>(res) false
if (allTitles.containsKey(id)) {
val currentTitle = allTitles[id]!!
allTitles[id] = MalTitleHolder(malStatus, id, currentTitle.name)
} else {
allTitles[id] = MalTitleHolder(malStatus, id, "")
}
true
} catch (e: Exception) {
logError(e)
false
}
} else { } else {
return false val malStatus = mapper.readValue<MalStatus>(res)
if (allTitles.containsKey(id)) {
val currentTitle = allTitles[id]!!
allTitles[id] = MalTitleHolder(malStatus, id, currentTitle.name)
} else {
allTitles[id] = MalTitleHolder(malStatus, id, "")
}
true
} }
} }
@ -519,26 +615,18 @@ 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 " + (getAuth() ?: return null)
"Authorization" to "Bearer " + getKey<String>( ),
accountId, data = mapOf(
MAL_TOKEN_KEY "status" to status,
)!! "score" to score?.toString(),
), "num_watched_episodes" to num_watched_episodes?.toString()
data = mapOf( )
"status" to status, ).text
"score" to score?.toString(),
"num_watched_episodes" to num_watched_episodes?.toString()
)
).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,44 +99,38 @@ 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) { SearchHelper.handleSearchClickCallback(activity, callback)
SEARCH_ACTION_LOAD -> { //when (callback.action) {
if (isMainApis) { //SEARCH_ACTION_LOAD -> {
activity?.popCurrentPage() // clickCallback?.invoke(callback)
//}
SearchHelper.handleSearchClickCallback(activity, callback) // else -> SearchHelper.handleSearchClickCallback(activity, callback)
} else { //}
clickCallback?.invoke(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() searchViewModel.searchAndCancel(
} else emptySet() query = query,
ignoreSettings = false,
searchViewModel.searchAndCancel( providersActive = active
query = query, )
isMainApis = isMainApis, quick_search?.let {
ignoreSettings = false, UIHelper.hideKeyboard(it)
providersActive = active }
)
quick_search?.let {
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,23 +92,12 @@ 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 val search = a.search(query)
val search = a.search(query) 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 {
api.authenticate() try {
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 {
api.authenticate() try {
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 {