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)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
api.handleRedirect(str)
try {
api.handleRedirect(str)
} catch (e : Exception) {
logError(e)
}
}
}
} else {

View File

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

View File

@ -3,6 +3,26 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.ShowStatus
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(
val name: String,
val syncApiName: String,
@ -48,7 +68,7 @@ interface SyncAPI : OAuth2API {
var synopsis: String? = null,
var airStatus: ShowStatus? = null,
var nextAiring: SyncNextAiring? = null,
var studio: String? = null,
var studio: List<String>? = null,
var genres: List<String>? = null,
var trailerUrl: String? = null,
@ -62,24 +82,4 @@ interface SyncAPI : OAuth2API {
var actors: List<SyncActor>? = 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) {
try {
val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val token = sanitizer["access_token"]!!
val expiresIn = sanitizer["expires_in"]!!
val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val token = sanitizer["access_token"]!!
val expiresIn = sanitizer["expires_in"]!!
val endTime = unixTime + expiresIn.toLong()
val endTime = unixTime + expiresIn.toLong()
switchToNewAccount()
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
setKey(accountId, ANILIST_TOKEN_KEY, token)
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
ioSafe {
getUser()
}
} catch (e: Exception) {
e.printStackTrace()
switchToNewAccount()
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
setKey(accountId, ANILIST_TOKEN_KEY, token)
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
ioSafe {
getUser()
}
}
@ -338,7 +334,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
fun initGetUser() {
if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return
if (getAuth() == null) return
ioSafe {
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 =
"""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)
@ -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
if (main.mediaListEntry != null) {
return AniListTitleHolder(
title = main.title,
id = id,
isFavourite = main.isFavourite,
progress = main.mediaListEntry.progress,
episodes = main.episodes,
score = main.mediaListEntry.score,
type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)),
)
} else {
return AniListTitleHolder(
title = main.title,
id = id,
isFavourite = main.isFavourite,
progress = 0,
episodes = main.episodes,
score = 0,
type = AniListStatusType.None,
)
}
} catch (e: Exception) {
logError(e)
return null
val data = postApi(q, true)
val d = mapper.readValue<GetDataRoot>(data ?: return null)
val main = d.data.Media
if (main.mediaListEntry != null) {
return AniListTitleHolder(
title = main.title,
id = id,
isFavourite = main.isFavourite,
progress = main.mediaListEntry.progress,
episodes = main.episodes,
score = main.mediaListEntry.score,
type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)),
)
} else {
return AniListTitleHolder(
title = main.title,
id = id,
isFavourite = main.isFavourite,
progress = 0,
episodes = main.episodes,
score = 0,
type = AniListStatusType.None,
)
}
}
private suspend fun postApi(url: String, q: String, cache: Boolean = false): String {
return try {
if (!checkToken()) {
// println("VARS_ " + vars)
app.post(
"https://graphql.anilist.co/",
headers = mapOf(
"Authorization" to "Bearer " + getKey(
accountId,
ANILIST_TOKEN_KEY,
""
)!!,
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))
timeout = 5 // REASONABLE TIMEOUT
).text.replace("\\/", "/")
} else {
""
}
} catch (e: Exception) {
logError(e)
""
private fun getAuth(): String? {
return getKey(
accountId,
ANILIST_TOKEN_KEY
)
}
private suspend fun postApi(q: String, cache: Boolean = false): String? {
return if (!checkToken()) {
app.post(
"https://graphql.anilist.co/",
headers = mapOf(
"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))
timeout = 5 // REASONABLE TIMEOUT
).text.replace("\\/", "/")
} else {
null
}
}
@ -515,12 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
if (getKey<String>(
accountId,
ANILIST_TOKEN_KEY,
null
) == null
) return null
if (getAuth() == null) return null
if (checkToken()) return null
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? {
try {
var userID: Int? = null
/** WARNING ASSUMES ONE USER! **/
getKeys(ANILIST_USER_KEY)?.forEach { key ->
getKey<AniListUser>(key, null)?.let {
userID = it.id
}
var userID: Int? = null
/** WARNING ASSUMES ONE USER! **/
getKeys(ANILIST_USER_KEY)?.forEach { key ->
getKey<AniListUser>(key, null)?.let {
userID = it.id
}
}
val fixedUserID = userID ?: return null
val mediaType = "ANIME"
val fixedUserID = userID ?: return null
val mediaType = "ANIME"
val query = """
val query = """
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
lists {
@ -588,13 +563,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
"""
val text = postApi("https://graphql.anilist.co", query)
return text.toKotlinObject()
} catch (e: Exception) {
logError(e)
return null
}
val text = postApi(query)
return text?.toKotlinObject()
}
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 != ""
}
@ -620,14 +590,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
score: Int?,
progress: Int?
): Boolean {
try {
val q =
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
aniListStatusString[maxOf(
0,
type.value
)]
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
val q =
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
aniListStatusString[maxOf(
0,
type.value
)]
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
id
status
@ -635,12 +604,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
score
}
}"""
val data = postApi("https://graphql.anilist.co", q)
return data != ""
} catch (e: Exception) {
logError(e)
return false
}
val data = postApi(q)
return data != ""
}
private suspend fun getUser(setSettings: Boolean = true): AniListUser? {
@ -661,29 +626,24 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
}
}
}"""
try {
val data = postApi("https://graphql.anilist.co", q)
if (data == "") return null
val userData = mapper.readValue<AniListRoot>(data)
val u = userData.data.Viewer
val user = AniListUser(
u.id,
u.name,
u.avatar.large,
)
if (setSettings) {
setKey(accountId, ANILIST_USER_KEY, user)
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
val data = postApi(q)
if (data == "") return null
val userData = mapper.readValue<AniListRoot>(data ?: return null)
val u = userData.data.Viewer
val user = AniListUser(
u.id,
u.name,
u.avatar.large,
)
if (setSettings) {
setKey(accountId, ANILIST_USER_KEY, user)
registerAccount()
}
/* // TODO FIX FAVS
for(i in u.favourites.anime.nodes) {
println("FFAV:" + i.id)
}*/
return user
}
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.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
@ -45,20 +46,28 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override fun loginInfo(): OAuth2API.LoginInfo? {
//getMalUser(true)?
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
}
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 = getKey<String>(
private fun getAuth(): String? {
return getKey(
accountId,
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(
url, headers = mapOf(
"Authorization" to "Bearer " + auth,
"Authorization" to "Bearer $auth",
), cacheTime = 0
).text
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(
id.toIntOrNull() ?: return false,
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? {
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? {
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(
score = data.score,
status = malStatusAsString.indexOf(data.status),
score = data?.score,
status = malStatusAsString.indexOf(data?.status),
isFavorite = null,
watchedEpisodes = data.num_episodes_watched,
watchedEpisodes = data?.num_episodes_watched,
)
}
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_CACHED_LIST: String = "mal_cached_list"
@ -111,39 +251,35 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
override fun handleRedirect(url: String) {
try {
val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val state = sanitizer["state"]!!
if (state == "RequestID$requestId") {
val currentCode = sanitizer["code"]!!
ioSafe {
var res = ""
try {
//println("cc::::: " + codeVerifier)
res = app.post(
"https://myanimelist.net/v1/oauth2/token",
data = mapOf(
"client_id" to key,
"code" to currentCode,
"code_verifier" to codeVerifier,
"grant_type" to "authorization_code"
)
).text
} catch (e: Exception) {
e.printStackTrace()
}
val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val state = sanitizer["state"]!!
if (state == "RequestID$requestId") {
val currentCode = sanitizer["code"]!!
ioSafe {
var res = ""
try {
//println("cc::::: " + codeVerifier)
res = app.post(
"https://myanimelist.net/v1/oauth2/token",
data = mapOf(
"client_id" to key,
"code" to currentCode,
"code_verifier" to codeVerifier,
"grant_type" to "authorization_code"
)
).text
} catch (e: Exception) {
e.printStackTrace()
}
if (res != "") {
switchToNewAccount()
storeToken(res)
getMalUser()
setKey(MAL_SHOULD_UPDATE_LIST, true)
}
if (res != "") {
switchToNewAccount()
storeToken(res)
getMalUser()
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?
)
fun getMalAnimeListCached(): Array<Data>? {
private fun getMalAnimeListCached(): Array<Data>? {
return getKey(MAL_CACHED_LIST) as? Array<Data>
}
suspend fun getMalAnimeListSmart(): Array<Data>? {
if (getKey<String>(
accountId,
MAL_TOKEN_KEY
) == null
) return null
if (getAuth() == null) return null
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
val list = getMalAnimeList()
if (list != null) {
setKey(MAL_CACHED_LIST, list)
setKey(MAL_SHOULD_UPDATE_LIST, false)
}
setKey(MAL_CACHED_LIST, list)
setKey(MAL_SHOULD_UPDATE_LIST, false)
list
} else {
getMalAnimeListCached()
}
}
private suspend fun getMalAnimeList(): Array<Data>? {
return try {
checkMalToken()
var offset = 0
val fullList = mutableListOf<Data>()
val offsetRegex = Regex("""offset=(\d+)""")
while (true) {
val data: MalList = getMalAnimeListSlice(offset) ?: break
fullList.addAll(data.data)
offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break
}
fullList.toTypedArray()
//mapper.readValue<MalAnime>(res)
} catch (e: Exception) {
null
private suspend fun getMalAnimeList(): Array<Data> {
checkMalToken()
var offset = 0
val fullList = mutableListOf<Data>()
val offsetRegex = Regex("""offset=(\d+)""")
while (true) {
val data: MalList = getMalAnimeListSlice(offset) ?: break
fullList.addAll(data.data)
offset =
data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() }
?: break
}
return fullList.toTypedArray()
}
fun convertToStatus(string: String): MalStatusType {
@ -323,43 +450,30 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
val user = "@me"
val auth = getKey<String>(
accountId,
MAL_TOKEN_KEY
) ?: return null
return try {
// Very lackluster docs
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
val url =
"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"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $auth",
), cacheTime = 0
).text
res.toKotlinObject()
} catch (e: Exception) {
logError(e)
null
}
val auth = getAuth() ?: return null
// Very lackluster docs
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
val url =
"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"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $auth",
), cacheTime = 0
).text
return res.toKotlinObject()
}
private suspend fun getDataAboutMalId(id: Int): MalAnime? {
return try {
// 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 res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer " + getKey<String>(
accountId,
MAL_TOKEN_KEY
)!!
), cacheTime = 0
).text
mapper.readValue<MalAnime>(res)
} catch (e: Exception) {
null
}
private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
// 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 res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null)
), cacheTime = 0
).text
return mapper.readValue<SmallMalAnime>(res)
}
suspend fun setAllMalData() {
@ -372,14 +486,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val res = app.get(
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
headers = mapOf(
"Authorization" to "Bearer " + getKey<String>(
accountId,
MAL_TOKEN_KEY
)!!
"Authorization" to "Bearer " + (getAuth() ?: return)
), cacheTime = 0
).text
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) {
allTitles[t.id] = t
}
@ -389,41 +501,37 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
// No time remaining if the show has already ended
try {
// No time remaining if the show has already ended
try {
endDate?.let {
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
}
} catch (e: ParseException) {
logError(e)
endDate?.let {
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining 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")
} catch (e: Exception) {
} catch (e: ParseException) {
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() {
@ -438,27 +546,19 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
checkMalToken()
return try {
val res = app.get(
"https://api.myanimelist.net/v2/users/@me",
headers = mapOf(
"Authorization" to "Bearer " + getKey<String>(
accountId,
MAL_TOKEN_KEY
)!!
), cacheTime = 0
).text
val res = app.get(
"https://api.myanimelist.net/v2/users/@me",
headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null)
), cacheTime = 0
).text
val user = mapper.readValue<MalUser>(res)
if (setSettings) {
setKey(accountId, MAL_USER_KEY, user)
registerAccount()
}
user
} catch (e: Exception) {
e.printStackTrace()
null
val user = mapper.readValue<MalUser>(res)
if (setSettings) {
setKey(accountId, MAL_USER_KEY, user)
registerAccount()
}
return user
}
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,
status: MalStatusType? = null,
score: Int? = null,
@ -495,22 +595,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
score,
num_watched_episodes
)
if (res != "") {
return try {
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
} catch (e: Exception) {
logError(e)
false
}
return if (res.isNullOrBlank()) {
false
} 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,
score: Int? = null,
num_watched_episodes: Int? = null,
): String {
return try {
app.put(
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
headers = mapOf(
"Authorization" to "Bearer " + getKey<String>(
accountId,
MAL_TOKEN_KEY
)!!
),
data = mapOf(
"status" to status,
"score" to score?.toString(),
"num_watched_episodes" to num_watched_episodes?.toString()
)
).text
} catch (e: Exception) {
e.printStackTrace()
return ""
}
): String? {
return app.put(
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
headers = mapOf(
"Authorization" to "Bearer " + (getAuth() ?: return null)
),
data = mapOf(
"status" to status,
"score" to score?.toString(),
"num_watched_episodes" to num_watched_episodes?.toString()
)
).text
}
@ -591,7 +679,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
)
// Used for getDataAboutId()
data class MalAnime(
data class SmallMalAnime(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String?,
@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.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.R
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.ui.home.HomeFragment.Companion.loadHomepageList
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.SearchHelper
import com.lagradost.cloudstream3.ui.search.SearchViewModel
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
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 java.util.concurrent.locks.ReentrantLock
class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
class QuickSearchFragment : Fragment() {
companion object {
fun pushSearch(activity: Activity?, autoSearch: String? = null) {
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
putBoolean("mainapi", true)
autoSearch?.let {
putString(
"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
}
@ -87,10 +76,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(quick_search_root)
arguments?.getBoolean("mainapi", true)?.let {
isMainApis = it
}
val listLock = ReentrantLock()
observe(searchViewModel.currentSearch) { list ->
try {
@ -114,44 +99,38 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
ParentItemAdapter(mutableListOf(), { callback ->
when (callback.action) {
SEARCH_ACTION_LOAD -> {
if (isMainApis) {
activity?.popCurrentPage()
SearchHelper.handleSearchClickCallback(activity, callback)
} else {
clickCallback?.invoke(callback)
}
}
else -> SearchHelper.handleSearchClickCallback(activity, callback)
}
SearchHelper.handleSearchClickCallback(activity, callback)
//when (callback.action) {
//SEARCH_ACTION_LOAD -> {
// clickCallback?.invoke(callback)
//}
// else -> SearchHelper.handleSearchClickCallback(activity, callback)
//}
}, { item ->
activity?.loadHomepageList(item)
})
val searchExitIcon =
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchMagIcon =
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
searchMagIcon?.scaleX = 0.65f
searchMagIcon?.scaleY = 0.65f
quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
val active = if (isMainApis) {
val langs = context?.getApiProviderLangSettings()
apis.filter { langs?.contains(it.lang) == true }.map { it.name }.toSet()
} else emptySet()
searchViewModel.searchAndCancel(
query = query,
isMainApis = isMainApis,
ignoreSettings = false,
providersActive = active
)
quick_search?.let {
UIHelper.hideKeyboard(it)
context?.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
?.map { it.name }?.toSet()?.let { active ->
searchViewModel.searchAndCancel(
query = query,
ignoreSettings = false,
providersActive = active
)
quick_search?.let {
UIHelper.hideKeyboard(it)
}
}
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.showToast
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.download.DOWNLOAD_ACTION_DOWNLOAD
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.search.SearchAdapter
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.isTvSettings
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.DataStore.getFolderName
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.SingleSelectionHelper.showBottomDialog
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 kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
@ -372,6 +370,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
private var currentLoadingCount =
0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED
private lateinit var viewModel: ResultViewModel //by activityViewModels()
private lateinit var syncModel: SyncViewModel
private var currentHeaderName: String? = null
private var currentType: TvType? = null
private var currentEpisodes: List<ResultEpisode>? = null
@ -384,6 +383,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
): View? {
viewModel =
ViewModelProvider(this)[ResultViewModel::class.java]
syncModel =
ViewModelProvider(this)[SyncViewModel::class.java]
return inflater.inflate(R.layout.fragment_result_swipe, container, false)
}
@ -469,16 +471,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
var startAction: 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?) {
// java.util.IllegalFormatConversionException: f != java.lang.Integer
// 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))
}
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>?) {
if (actors.isNullOrEmpty()) {
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 ->
lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
var isSeriesVisible = false
@ -1262,7 +1303,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
observe(viewModel.dubSubSelections) { range ->
dubRange = range
if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true){
if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true) {
viewModel.changeDubStatus(DubStatus.Dubbed)
}
@ -1297,7 +1338,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
}
}
result_episode_select.setOnClickListener {
result_episode_select?.setOnClickListener {
val ranges = episodeRanges
if (ranges != null) {
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 ->
if (count < 0) {
result_episodes_text?.isVisible = false
@ -1321,19 +1369,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
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 ->
when (data) {
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) {
is TvSeriesLoadResponse -> d.showStatus
is AnimeLoadResponse -> d.showStatus
else -> null
}
setShow(showStatus)
setDuration(d.duration)
setYear(d.year)
@ -1427,6 +1447,18 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setRecommendations(d.recommendations)
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
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.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.IGenerator
@ -79,9 +78,6 @@ class ResultViewModel : ViewModel() {
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
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 {
val currentId = id.value ?: return@launch
_watchStatus.postValue(status)
@ -233,17 +229,6 @@ class ResultViewModel : ViewModel() {
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?) {
_episodes.postValue(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.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
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.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.utils.Coroutines.ioSafe
import kotlinx.coroutines.Dispatchers
@ -39,7 +37,6 @@ class SearchViewModel : ViewModel() {
val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory
private val repos = apis.map { APIRepository(it) }
private val syncApis = SyncApis
fun clearSearch() {
_searchResponse.postValue(Resource.Success(ArrayList()))
@ -49,33 +46,11 @@ class SearchViewModel : ViewModel() {
private var onGoingSearch: Job? = null
fun searchAndCancel(
query: String,
isMainApis: Boolean = true,
providersActive: Set<String> = setOf(),
ignoreSettings: Boolean = false
) {
onGoingSearch?.cancel()
onGoingSearch = search(query, isMainApis, 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()
)
onGoingSearch = search(query, providersActive, ignoreSettings)
}
fun updateHistory() = viewModelScope.launch {
@ -89,7 +64,6 @@ class SearchViewModel : ViewModel() {
private fun search(
query: String,
isMainApis: Boolean = true,
providersActive: Set<String>,
ignoreSettings: Boolean = false
) =
@ -118,23 +92,12 @@ class SearchViewModel : ViewModel() {
_currentSearch.postValue(ArrayList())
withContext(Dispatchers.IO) { // This interrupts UI otherwise
if (isMainApis) {
repos.filter { a ->
ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))
}.apmap { a -> // Parallel
val search = a.search(query)
currentList.add(OnGoingSearch(a.name, search))
_currentSearch.postValue(currentList)
}
} else {
syncApis.apmap { a ->
val search = safeApiCall {
a.search(query)?.map { it.toSearchResponse() }
?: throw ErrorLoadingException()
}
currentList.add(OnGoingSearch(a.name, search))
}
repos.filter { a ->
ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))
}.apmap { a -> // Parallel
val search = a.search(query)
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
}
private const val accountEnabled = false
const val accountEnabled = false
}
private var beneneCount = 0
@ -155,7 +155,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
val dialog = builder.show()
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
api.authenticate()
try {
api.authenticate()
} catch (e: Exception) {
logError(e)
}
}
val ogIndex = api.accountIndex
@ -325,10 +329,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
val syncApis =
listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi))
for (sync in syncApis) {
getPref(sync.first)?.apply {
for ((key, api) in syncApis) {
getPref(key)?.apply {
isVisible = accountEnabled
val api = sync.second
title =
getString(R.string.login_format).format(api.name, getString(R.string.account))
setOnPreferenceClickListener { _ ->
@ -336,7 +339,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
if (info != null) {
showLoginInfo(api, info)
} else {
api.authenticate()
try {
api.authenticate()
} catch (e: Exception) {
logError(e)
}
}
return@setOnPreferenceClickListener true
}

View File

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

View File

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

View File

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