mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
sync stuff
This commit is contained in:
parent
e41542bef4
commit
2a27c0360d
16 changed files with 729 additions and 542 deletions
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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?
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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?> {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue